Skip to content

Commit ef0e594

Browse files
committed
http scanner
1 parent 375ef71 commit ef0e594

15 files changed

+974
-21
lines changed

6-steps.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
Phase 6: Additional Common Checks
2+
6.3 Exposed Debug/Admin Endpoints
3+
Extend file traversal to include:
4+
5+
Express router files (src/routes/**)
6+
7+
Next.js API routes (pages/api/, app/api/)
8+
9+
Scan each .js/.ts file for sensitive-path definitions using regex or AST, e.g.
10+
app.(get|post|put|delete)('/(admin|debug|status|info)'
11+
router.(get|post)('/(admin|debug|status|info)'
12+
13+
Record each match as a JSON object:
14+
{ "file": "src/routes/admin.js", "line": 42, "path": "/admin" }
15+
16+
In each matched file, scan imports for auth libraries:
17+
18+
passport
19+
20+
express-session
21+
22+
jsonwebtoken
23+
24+
next-auth
25+
26+
@clerk/clerk-sdk-node
27+
28+
In the handler code, look for auth-usage patterns:
29+
30+
if (!req.user)
31+
32+
await getSession()
33+
34+
jwt.verify()
35+
36+
Flag endpoint if a sensitive route is found and no auth import or auth-usage pattern is detected
37+
38+
6.4 Lack of Rate‑Limiting in HTTP Clients
39+
Detect HTTP libraries in package.json:
40+
41+
axios
42+
43+
node-fetch
44+
45+
cross-fetch
46+
47+
got
48+
49+
superagent
50+
51+
AST scan each .js/.ts file for call expressions:
52+
53+
axios.<method>(url, config?)
54+
55+
fetch(url, options?)
56+
57+
For each call, inspect arguments:
58+
59+
axios: if only one argument or config missing timeout/signal, mark missing timeout
60+
61+
fetch: if no AbortController usage (new AbortController() or signal:), mark missing timeout
62+
63+
Emit JSON per finding: { "file": "src/api.js", "line": 27, "library": "axios", "call": "axios.get", "missing": ["timeout"] }
64+
65+
Add CLI flag (e.g. --check-timeouts) to include these findings in the report
66+
67+
6.5 Insufficient Logging & Error Sanitization
68+
AST scan for all CallExpression where callee is:
69+
70+
console.log
71+
72+
console.error
73+
74+
console.warn
75+
76+
console.debug
77+
78+
logger methods from known libs (e.g. winston, pino)
79+
80+
For each log call:
81+
82+
If single argument is an Identifier named err/error or a MemberExpression ending in .stack, flag as unsanitized-error
83+
84+
If any argument source text matches /password|email|token|ssn|secret/, flag as pii-logging
85+
86+
Emit JSON per finding: { "file": "src/app.js", "line": 123, "type": "pii-logging", "snippet": "console.error(User email: ${user.email})" }
87+
88+
Group and summarize findings by file in the final report

README.md

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ A CLI tool to scan your codebase for security vibes.
44

55
VibeSafe helps developers quickly check their projects for common security issues like exposed secrets, outdated dependencies with known vulnerabilities (CVEs), and generates helpful reports.
66

7-
## Features (MVP)
8-
9-
* **Secret Scanning:** Detects potential secrets (API keys, credentials) using regex patterns and entropy analysis.
10-
* **Dependency Scanning:** Parses package manifests (currently `package.json`) and checks dependencies against the OSV.dev vulnerability database.
11-
* **Configuration Scanning:** Checks configuration files (JSON, YAML) for common insecure settings (e.g., `DEBUG=true`, permissive CORS).
12-
* **Unvalidated Upload Detection:** Identifies potential missing file size/type restrictions in common upload libraries (e.g., `multer`, `formidable`) and generic patterns (`FormData`, `<input type="file">`).
13-
* **Exposed Endpoint Detection:** Flags potentially sensitive endpoints (e.g., `/admin`, `/debug`, `/status`) based on common patterns.
14-
* **Multiple Output Formats:** Provides results via console output (with colors!), JSON (`--output`), or a Markdown report (`--report`).
7+
## Features
8+
9+
* **Secret Scanning:** Detects potential secrets using regex patterns (AWS Keys, JWTs, SSH Keys, generic high-entropy strings) and specifically flags secrets found in `.env` files.
10+
* **Dependency Scanning:** Parses `package.json` (for npm/yarn projects) and checks dependencies against the OSV.dev vulnerability database for known CVEs.
11+
* **Configuration Scanning:** Checks JSON and YAML files for common insecure settings (e.g., `DEBUG = true`, `devMode = true`, permissive CORS like `origin: '*'`).
12+
* **HTTP Client Issues:** Detects potential missing timeout or cancellation configurations in calls using `axios`, `fetch`, `got`, and `request`. (*See Limitations below*).
13+
* **Unvalidated Upload Detection:** Identifies potential missing file size/type restrictions in common upload libraries (`multer`, `formidable`, `express-fileupload`, `busboy`) and generic patterns (`new FormData()`, `<input type="file">`).
14+
* **Exposed Endpoint Detection:** Flags potentially sensitive endpoints (e.g., `/admin`, `/debug`, `/status`, `/info`, `/metrics`) in Express/Node.js applications using common routing patterns or string literals.
15+
* **Rate Limit Check (Heuristic):** Suggests reviewing rate limiting if Express/Node.js routes are detected in a file without an `express-rate-limit` import.
16+
* **Improper Error Logging Detection:** Flags potential logging of full error objects (e.g., `console.error(err)`, `logger.error(e)`), which can leak stack traces.
17+
* **Multiple Output Formats:** Provides results via console output (with colors!), JSON (`--output`), or a Markdown report (`--report` with default `VIBESAFE-REPORT.md`).
1518
* **AI-Powered Suggestions (Optional):** Generates fix suggestions in the Markdown report using OpenAI (requires API key).
1619
* **Filtering:** Focus on high-impact issues using `--high-only`.
1720
* **Customizable Ignores:** Use a `.vibesafeignore` file (similar syntax to `.gitignore`) to exclude specific files or directories from the scan.
@@ -79,6 +82,13 @@ To generate fix suggestions in the Markdown report, you need an OpenAI API key.
7982
vibesafe scan --high-only
8083
```
8184

85+
## Limitations
86+
87+
* **Superagent Timeouts:** The check for missing timeouts in the `superagent` HTTP client library is currently disabled due to complexities in accurately detecting chained method calls (like `.timeout()`) using AST. Calls using `superagent` will not be flagged for missing timeouts at this time. This is planned for a future enhancement.
88+
* **Dynamic Configuration:** Checks rely on static analysis (AST parsing, regex). Timeouts or security settings configured dynamically (e.g., read from environment variables at runtime and passed into client options) may not be detected.
89+
* **Rate Limiting:** The check is a heuristic based on the presence of route definitions and the *absence* of a specific import (`express-rate-limit`). It does not guarantee that rate limiting is actually missing or insufficient if implemented differently.
90+
* **Authentication Checks:** Exposed endpoint detection does not currently verify if proper authentication or authorization middleware is applied to flagged routes.
91+
8292
## Ignoring Files (.vibesafeignore)
8393

8494
Create a `.vibesafeignore` file in the root of the directory being scanned. Add file paths or glob patterns (one per line) to exclude them from the scan. The syntax is the same as `.gitignore`.

instructions.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,13 @@
100100
- [x] Search for routes named `/debug`, `/admin`, `/status`, `/info`, etc. using framework patterns or string literals
101101
- [ ] Flag those without authentication or middleware checks // (Future enhancement - complex)
102102
4. **Lack of Rate‑Limiting**
103-
- [ ] Identify HTTP handlers or clients missing rate‑limiter middleware (e.g., express-rate-limit)
103+
- [x] Identify files with route definitions but missing `express-rate-limit` import (heuristic)
104104
- [ ] Flag missing throttle/retry settings in HTTP client code
105+
- [x] Detect missing timeout/cancellation in HTTP client calls (axios, fetch, got, request) // (Superagent check disabled due to AST complexity)
106+
- [ ] *TODO: Implement reliable `superagent` timeout detection via AST*
107+
5. **Insufficient Logging & Error Sanitization**
108+
- [x] Find logging of full error objects or stack traces (e.g., `console.error(err)`)
109+
- [ ] Detect logging of PII or sensitive data in plain text // (Future enhancement - complex)
105110

106111
## 6. Risks & Mitigations
107112

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"license": "SEE LICENSE IN LICENSE",
2222
"devDependencies": {
2323
"@types/node": "^22.14.1",
24+
"@typescript-eslint/typescript-estree": "^8.30.1",
2425
"ts-node": "^10.9.2",
2526
"typescript": "^5.8.3"
2627
},

src/index.ts

Lines changed: 127 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import chalk from 'chalk';
1414
import { scanConfigFile, ConfigFinding } from './scanners/configuration';
1515
import { scanForUnvalidatedUploads, UploadFinding } from './scanners/uploads';
1616
import { scanForExposedEndpoints, EndpointFinding } from './scanners/endpoints';
17+
import { scanForMissingRateLimit, RateLimitFinding } from './scanners/rateLimiting';
18+
import { scanForImproperErrorLogging, ErrorLoggingFinding } from './scanners/logging';
19+
import { scanForHttpClientIssues, HttpClientFinding } from './scanners/httpClient';
1720

1821
// Define a combined finding type if needed later
1922

@@ -76,6 +79,9 @@ program.command('scan')
7679
let allConfigFindings: ConfigFinding[] = [];
7780
let allUploadFindings: UploadFinding[] = [];
7881
let allEndpointFindings: EndpointFinding[] = [];
82+
let allRateLimitFindings: RateLimitFinding[] = [];
83+
let allErrorLoggingFindings: ErrorLoggingFinding[] = [];
84+
let allHttpClientFindings: HttpClientFinding[] = [];
7985

8086
// --- File Traversal (Phase 2.2) ---
8187
const filesToScan = getFilesToScan(directory);
@@ -153,6 +159,51 @@ program.command('scan')
153159
}
154160
});
155161

162+
// --- Rate Limit Scan (Phase 6.4) ---
163+
// Use the same JS/TS files as endpoint scan for checking rate limiting context
164+
console.log(`Scanning ${filesForEndpointScan.length} files for potential missing rate limiting...`);
165+
filesForEndpointScan.forEach(filePath => {
166+
try {
167+
const content = fs.readFileSync(filePath, 'utf-8');
168+
const findings = scanForMissingRateLimit(filePath, content);
169+
const relativeFindings = findings.map(f => ({ ...f, file: path.relative(rootDir, f.file) }));
170+
allRateLimitFindings = allRateLimitFindings.concat(relativeFindings);
171+
} catch (error: any) {
172+
// Avoid crashing if a single file fails
173+
console.warn(chalk.yellow(`Could not scan ${path.relative(rootDir, filePath)} for rate limiting: ${error.message}`));
174+
}
175+
});
176+
177+
// --- Error Logging Scan (Phase 6.5) ---
178+
// Use the same JS/TS files as endpoint/rate-limit scan
179+
console.log(`Scanning ${filesForEndpointScan.length} files for potential improper error logging...`);
180+
filesForEndpointScan.forEach(filePath => {
181+
try {
182+
const content = fs.readFileSync(filePath, 'utf-8');
183+
const findings = scanForImproperErrorLogging(filePath, content);
184+
const relativeFindings = findings.map(f => ({ ...f, file: path.relative(rootDir, f.file) }));
185+
allErrorLoggingFindings = allErrorLoggingFindings.concat(relativeFindings);
186+
} catch (error: any) {
187+
// Avoid crashing if a single file fails
188+
console.warn(chalk.yellow(`Could not scan ${path.relative(rootDir, filePath)} for logging issues: ${error.message}`));
189+
}
190+
});
191+
192+
// --- HTTP Client Scan (Phase 6.4.2) ---
193+
// Use the same JS/TS files as endpoint scan
194+
console.log(`Scanning ${filesForEndpointScan.length} files for potential HTTP client issues...`);
195+
filesForEndpointScan.forEach(filePath => {
196+
try {
197+
const content = fs.readFileSync(filePath, 'utf-8');
198+
const findings = scanForHttpClientIssues(filePath, content);
199+
const relativeFindings = findings.map(f => ({ ...f, file: path.relative(rootDir, f.file) }));
200+
allHttpClientFindings = allHttpClientFindings.concat(relativeFindings);
201+
} catch (error: any) {
202+
// Avoid crashing if a single file fails
203+
console.warn(chalk.yellow(`Could not scan ${path.relative(rootDir, filePath)} for HTTP client issues: ${error.message}`));
204+
}
205+
});
206+
156207
// Separate Info findings
157208
const infoSecretFindings = allSecretFindings.filter(f => f.severity === 'Info');
158209
const standardSecretFindings = allSecretFindings.filter(f => f.severity !== 'Info');
@@ -181,6 +232,21 @@ program.command('scan')
181232
? allEndpointFindings.filter(f => f.severity === 'High' || f.severity === 'Critical' || f.severity === 'Medium')
182233
: allEndpointFindings;
183234

235+
// Filter rate limit findings (These are 'Low' severity, so they likely won't show with --high-only)
236+
const reportRateLimitFindings = options.highOnly
237+
? allRateLimitFindings.filter(f => f.severity === 'High' || f.severity === 'Critical' || f.severity === 'Medium') // Will likely be empty
238+
: allRateLimitFindings;
239+
240+
// Filter error logging findings (Low severity)
241+
const reportErrorLoggingFindings = options.highOnly
242+
? allErrorLoggingFindings.filter(f => f.severity === 'High' || f.severity === 'Critical' || f.severity === 'Medium')
243+
: allErrorLoggingFindings;
244+
245+
// Filter HTTP client findings (Low severity)
246+
const reportHttpClientFindings = options.highOnly
247+
? allHttpClientFindings.filter(f => f.severity === 'High' || f.severity === 'Critical' || f.severity === 'Medium')
248+
: allHttpClientFindings;
249+
184250
// --- NOW Check Gitignore Status ---
185251
gitignoreWarnings = checkGitignoreStatus(rootDir);
186252

@@ -190,7 +256,10 @@ program.command('scan')
190256
dependencyFindings: reportDependencyFindings,
191257
configFindings: reportConfigFindings,
192258
uploadFindings: reportUploadFindings,
193-
endpointFindings: reportEndpointFindings
259+
endpointFindings: reportEndpointFindings,
260+
rateLimitFindings: reportRateLimitFindings,
261+
errorLoggingFindings: reportErrorLoggingFindings,
262+
httpClientFindings: reportHttpClientFindings
194263
};
195264

196265
// Generate JSON if requested
@@ -201,7 +270,10 @@ program.command('scan')
201270
dependencies: reportDependencyFindings,
202271
configuration: reportConfigFindings,
203272
uploads: reportUploadFindings,
204-
endpoints: reportEndpointFindings
273+
endpoints: reportEndpointFindings,
274+
rateLimiting: reportRateLimitFindings,
275+
errorLogging: reportErrorLoggingFindings,
276+
httpClients: reportHttpClientFindings
205277
}
206278
fs.writeFileSync(options.output, JSON.stringify(outputJsonData, null, 2));
207279
console.log(`JSON results successfully written to ${options.output}`);
@@ -251,8 +323,11 @@ program.command('scan')
251323
const hasConfigIssues = reportConfigFindings.length > 0;
252324
const hasUploadIssues = reportUploadFindings.length > 0;
253325
const hasEndpointIssues = reportEndpointFindings.length > 0;
326+
const hasRateLimitIssues = reportRateLimitFindings.length > 0;
327+
const hasErrorLoggingIssues = reportErrorLoggingFindings.length > 0;
328+
const hasHttpClientIssues = reportHttpClientFindings.length > 0;
254329

255-
if (hasStandardSecrets || hasDependencyIssues || hasConfigIssues || hasUploadIssues || hasEndpointIssues) {
330+
if (hasStandardSecrets || hasDependencyIssues || hasConfigIssues || hasUploadIssues || hasEndpointIssues || hasRateLimitIssues || hasErrorLoggingIssues || hasHttpClientIssues) {
256331
// Print standard secrets to console if found
257332
if (hasStandardSecrets) {
258333
console.log(chalk.bold('\nPotential Secrets Found:'));
@@ -313,6 +388,46 @@ program.command('scan')
313388
}
314389
});
315390
}
391+
392+
// Print rate limit findings to console if found
393+
if (hasRateLimitIssues) {
394+
console.log(chalk.bold('\nPotential Rate Limiting Issues Found:'));
395+
// Since findings are per-file, group them or just list them
396+
reportRateLimitFindings.sort((a,b) => severityToSortOrder(b.severity) - severityToSortOrder(a.severity)); // Although all are Low for now
397+
reportRateLimitFindings.forEach(finding => {
398+
console.log(` - [${colorSeverity(finding.severity)}] ${finding.type} in ${chalk.cyan(finding.file)} (around line ${chalk.yellow(String(finding.line))})`);
399+
console.log(chalk.dim(` > ${finding.message}`));
400+
if (finding.details) {
401+
console.log(chalk.dim(` ${finding.details}`));
402+
}
403+
});
404+
}
405+
406+
// Print error logging findings to console if found
407+
if (hasErrorLoggingIssues) {
408+
console.log(chalk.bold('\nPotential Unsanitized Error Logging Found:'));
409+
reportErrorLoggingFindings.sort((a,b) => severityToSortOrder(b.severity) - severityToSortOrder(a.severity)); // Although all are Low
410+
reportErrorLoggingFindings.forEach(finding => {
411+
console.log(` - [${colorSeverity(finding.severity)}] ${finding.type} in ${chalk.cyan(finding.file)}:${chalk.yellow(String(finding.line))}`);
412+
console.log(chalk.dim(` > ${finding.message}`));
413+
if (finding.details) {
414+
console.log(chalk.dim(` ${finding.details}`));
415+
}
416+
});
417+
}
418+
419+
// Print http client findings to console if found
420+
if (hasHttpClientIssues) {
421+
console.log(chalk.bold('\nPotential HTTP Client Issues Found:'));
422+
reportHttpClientFindings.sort((a,b) => severityToSortOrder(b.severity) - severityToSortOrder(a.severity)); // Although all are Low
423+
reportHttpClientFindings.forEach(finding => {
424+
console.log(` - [${colorSeverity(finding.severity)}] ${finding.type} (${finding.library}) in ${chalk.cyan(finding.file)}:${chalk.yellow(String(finding.line))}`);
425+
console.log(chalk.dim(` > ${finding.message}`));
426+
if (finding.details) {
427+
console.log(chalk.dim(` ${finding.details}`));
428+
}
429+
});
430+
}
316431
} else {
317432
// All Clear! Print positive message.
318433
// Check if we actually scanned for dependencies before saying no vulns found
@@ -334,8 +449,14 @@ program.command('scan')
334449
const highSeverityConfig = reportConfigFindings.some(f => f.severity === 'High' || f.severity === 'Critical');
335450
const highSeverityUploads = reportUploadFindings.some(f => f.severity === 'High' || f.severity === 'Critical' || f.severity === 'Medium');
336451
const highSeverityEndpoints = reportEndpointFindings.some(f => f.severity === 'High' || f.severity === 'Critical' || f.severity === 'Medium');
337-
338-
if (options.highOnly && (highSeveritySecrets || highSeverityDeps || highSeverityConfig || highSeverityUploads || highSeverityEndpoints)) {
452+
// Rate limit findings are currently Low, so they won't trigger exit code 1 with --high-only
453+
// const highSeverityRateLimit = reportRateLimitFindings.some(f => f.severity === 'High' || f.severity === 'Critical' || f.severity === 'Medium');
454+
// Error logging findings are Low, so they won't trigger exit code 1 with --high-only
455+
// const highSeverityErrorLogging = reportErrorLoggingFindings.some(f => f.severity === 'High' || f.severity === 'Critical' || f.severity === 'Medium');
456+
// HTTP Client findings are Low, so they won't trigger exit code 1 with --high-only
457+
// const highSeverityHttpClient = reportHttpClientFindings.some(f => f.severity === 'High' || f.severity === 'Critical' || f.severity === 'Medium');
458+
459+
if (options.highOnly && (highSeveritySecrets || highSeverityDeps || highSeverityConfig || highSeverityUploads || highSeverityEndpoints /*|| highSeverityRateLimit || highSeverityErrorLogging || highSeverityHttpClient*/)) {
339460
console.log('Exiting with code 1 due to High/Critical severity findings (--high-only specified).');
340461
process.exit(1);
341462
}
@@ -354,4 +475,4 @@ function severityToSortOrder(severity: FindingSeverity | SecretFinding['severity
354475
}
355476
}
356477

357-
program.parse(process.argv);
478+
program.parse(process.argv);

0 commit comments

Comments
 (0)