Skip to content

Commit 375ef71

Browse files
committed
unsafe route checker
1 parent e579821 commit 375ef71

16 files changed

+841
-50
lines changed

README.md

+14-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ VibeSafe helps developers quickly check their projects for common security issue
88

99
* **Secret Scanning:** Detects potential secrets (API keys, credentials) using regex patterns and entropy analysis.
1010
* **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.
1114
* **Multiple Output Formats:** Provides results via console output (with colors!), JSON (`--output`), or a Markdown report (`--report`).
1215
* **AI-Powered Suggestions (Optional):** Generates fix suggestions in the Markdown report using OpenAI (requires API key).
1316
* **Filtering:** Focus on high-impact issues using `--high-only`.
@@ -45,7 +48,11 @@ vibesafe scan -o scan-results.json
4548
**Generate Markdown Report:**
4649

4750
```bash
48-
vibesafe scan -r scan-report.md
51+
# Generate report with a specific name
52+
vibesafe scan -r my-report.md
53+
54+
# Generate report with the default name (VIBESAFE-REPORT.md in the scanned directory)
55+
vibesafe scan -r
4956
```
5057

5158
**Generate AI Report (Requires API Key):**
@@ -59,7 +66,11 @@ To generate fix suggestions in the Markdown report, you need an OpenAI API key.
5966
```
6067
3. Run the scan with the report flag:
6168
```bash
62-
vibesafe scan -r ai-report.md
69+
# Use default name VIBESAFE-REPORT.md
70+
vibesafe scan -r
71+
72+
# Or specify a name
73+
vibesafe scan -r vibesafe-ai-report.md
6374
```
6475
6576
**Show Only High/Critical Issues:**
@@ -72,7 +83,7 @@ vibesafe scan --high-only
7283

7384
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`.
7485

75-
**Example `.vibesafeignore`:**
86+
**Example `.vibesafeignore**:**
7687

7788
```
7889
# Ignore all test data

instructions.md

+6-9
Original file line numberDiff line numberDiff line change
@@ -93,18 +93,15 @@
9393

9494
### Phase 6: Additional Common Checks
9595
1. **Insecure Default Configurations**
96-
- [ ] Scan config files (JSON/YAML) for flags like `DEBUG=true`, `devMode`, or permissive CORS (`*` origins)
96+
- [x] Scan config files (JSON/YAML) for flags like `DEBUG=true`, `devMode`, or permissive CORS (`*` origins)
9797
2. **Unvalidated File Uploads**
98-
- [ ] Detect code handling file uploads (e.g., multer, busboy) without size/type restrictions
98+
- [x] Detect code handling file uploads (e.g., multer, busboy, formidable, express-fileupload, generic patterns) without size/type restrictions
9999
3. **Exposed Debug/Admin Endpoints**
100-
- [ ] Search for routes named `/debug`, `/admin`, `/console`
101-
- [ ] Flag those without authentication or middleware checks
100+
- [x] Search for routes named `/debug`, `/admin`, `/status`, `/info`, etc. using framework patterns or string literals
101+
- [ ] Flag those without authentication or middleware checks // (Future enhancement - complex)
102102
4. **Lack of Rate‑Limiting**
103103
- [ ] Identify HTTP handlers or clients missing rate‑limiter middleware (e.g., express-rate-limit)
104-
- [ ] Flag missing throttle/retry settings in HTTP client code
105-
5. **Insufficient Logging & Error Sanitization**
106-
- [ ] Find logging of full error objects or stack traces (e.g., `console.error(err)`)
107-
- [ ] Detect logging of PII or sensitive data in plain text
104+
- [ ] Flag missing throttle/retry settings in HTTP client code
108105

109106
## 6. Risks & Mitigations
110107

@@ -124,4 +121,4 @@
124121
**Next Steps:**
125122
1. Tackle Phase 6 atomic tasks in order.
126123
2. Validate each check against representative repos.
127-
3. Prepare to expand into Most Dangerous vulnerability scans once Phase 6 is done.
124+
3. Prepare to expand into "Most Dangerous" vulnerability scans once Phase 6 is done.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525
"typescript": "^5.8.3"
2626
},
2727
"dependencies": {
28+
"@types/js-yaml": "^4.0.9",
2829
"axios": "^1.8.4",
2930
"chalk": "^4.1.2",
3031
"commander": "^13.1.0",
3132
"dotenv": "^16.5.0",
3233
"ignore": "^7.0.3",
34+
"js-yaml": "^4.1.0",
3335
"openai": "^4.95.0",
3436
"ora": "^5.4.1"
3537
}

src/index.ts

+139-15
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import { generateMarkdownReport } from './reporting/markdown';
1111
import path from 'path';
1212
import fs from 'fs';
1313
import chalk from 'chalk';
14+
import { scanConfigFile, ConfigFinding } from './scanners/configuration';
15+
import { scanForUnvalidatedUploads, UploadFinding } from './scanners/uploads';
16+
import { scanForExposedEndpoints, EndpointFinding } from './scanners/endpoints';
1417

1518
// Define a combined finding type if needed later
1619

1720
// Helper for coloring severities
18-
function colorSeverity(severity: FindingSeverity | SecretFinding['severity']): string {
21+
function colorSeverity(severity: FindingSeverity | SecretFinding['severity'] | UploadFinding['severity']): string {
1922
switch (severity) {
2023
case 'Critical': return chalk.red.bold(severity);
2124
case 'High': return chalk.red(severity);
@@ -37,7 +40,7 @@ program.command('scan')
3740
.description('Scan a directory for potential security issues.')
3841
.argument('[directory]', 'Directory to scan', '.')
3942
.option('-o, --output <file>', 'Specify JSON output file path (e.g., report.json)')
40-
.option('-r, --report <file>', 'Specify Markdown report file path (e.g., report.md)')
43+
.option('-r, --report [file]', 'Specify Markdown report file path (defaults to VIBESAFE-REPORT.md)')
4144
.option('--high-only', 'Only report high severity issues')
4245
.action(async (directory, options) => {
4346
const rootDir = path.resolve(directory);
@@ -46,10 +49,21 @@ program.command('scan')
4649
console.log('(--high-only flag detected)');
4750
}
4851
if (options.output) {
49-
console.log(`Output will be written to: ${options.output}`);
52+
console.log(`JSON output will be written to: ${options.output}`);
5053
}
51-
if (options.report) {
52-
console.log(`Markdown report will be written to: ${options.report}`);
54+
55+
// Determine report path based on options
56+
let reportPath: string | null = null;
57+
if (options.report) { // Check if -r or --report was used
58+
if (typeof options.report === 'string') {
59+
// User provided a specific filename
60+
reportPath = path.resolve(options.report);
61+
console.log(`Markdown report will be written to: ${reportPath}`);
62+
} else {
63+
// User used the flag without a filename, use default
64+
reportPath = path.join(rootDir, 'VIBESAFE-REPORT.md');
65+
console.log(`Markdown report will be written to default location: ${reportPath}`);
66+
}
5367
}
5468

5569
// --- Moved: Check .gitignore Status ---
@@ -59,9 +73,13 @@ program.command('scan')
5973
// --- Findings Aggregation ---
6074
let allSecretFindings: SecretFinding[] = [];
6175
let allDependencyFindings: DependencyFinding[] = [];
76+
let allConfigFindings: ConfigFinding[] = [];
77+
let allUploadFindings: UploadFinding[] = [];
78+
let allEndpointFindings: EndpointFinding[] = [];
6279

6380
// --- File Traversal (Phase 2.2) ---
6481
const filesToScan = getFilesToScan(directory);
82+
const configFilesToScan = filesToScan.filter(f => /\.(json|ya?ml)$/i.test(f));
6583

6684
// --- Detect Package Manager (Phase 3.1) ---
6785
const detectedManagers = detectPackageManagers(filesToScan, rootDir);
@@ -93,6 +111,48 @@ program.command('scan')
93111
console.log('Skipping CVE lookup as no dependencies were parsed.');
94112
}
95113

114+
// --- Configuration Scan (Phase 6.1) ---
115+
console.log(`Scanning ${configFilesToScan.length} potential config files...`);
116+
configFilesToScan.forEach(filePath => {
117+
const findings = scanConfigFile(filePath);
118+
const relativeFindings = findings.map(f => ({ ...f, file: path.relative(rootDir, f.file) }));
119+
allConfigFindings = allConfigFindings.concat(relativeFindings);
120+
});
121+
122+
// --- Upload Scan (Phase 6.2) ---
123+
// Define file extensions relevant for upload checks
124+
const UPLOAD_SCAN_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.vue', '.html']);
125+
const filesForUploadScan = filesToScan.filter(f => UPLOAD_SCAN_EXTENSIONS.has(path.extname(f).toLowerCase()));
126+
console.log(`Scanning ${filesForUploadScan.length} files for potential upload issues...`);
127+
filesForUploadScan.forEach(filePath => {
128+
try {
129+
const content = fs.readFileSync(filePath, 'utf-8');
130+
const findings = scanForUnvalidatedUploads(filePath, content);
131+
const relativeFindings = findings.map(f => ({ ...f, file: path.relative(rootDir, f.file) }));
132+
allUploadFindings = allUploadFindings.concat(relativeFindings);
133+
} catch (error: any) {
134+
// Avoid crashing if a single file fails (e.g., read permission)
135+
console.warn(chalk.yellow(`Could not scan ${path.relative(rootDir, filePath)} for uploads: ${error.message}`));
136+
}
137+
});
138+
139+
// --- Endpoint Scan (Phase 6.3) ---
140+
// Define file extensions relevant for endpoint checks (JS/TS files)
141+
const ENDPOINT_SCAN_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx']);
142+
const filesForEndpointScan = filesToScan.filter(f => ENDPOINT_SCAN_EXTENSIONS.has(path.extname(f).toLowerCase()));
143+
console.log(`Scanning ${filesForEndpointScan.length} files for potentially exposed endpoints...`);
144+
filesForEndpointScan.forEach(filePath => {
145+
try {
146+
const content = fs.readFileSync(filePath, 'utf-8');
147+
const findings = scanForExposedEndpoints(filePath, content);
148+
const relativeFindings = findings.map(f => ({ ...f, file: path.relative(rootDir, f.file) }));
149+
allEndpointFindings = allEndpointFindings.concat(relativeFindings);
150+
} catch (error: any) {
151+
// Avoid crashing if a single file fails (e.g., read permission)
152+
console.warn(chalk.yellow(`Could not scan ${path.relative(rootDir, filePath)} for endpoints: ${error.message}`));
153+
}
154+
});
155+
96156
// Separate Info findings
97157
const infoSecretFindings = allSecretFindings.filter(f => f.severity === 'Info');
98158
const standardSecretFindings = allSecretFindings.filter(f => f.severity !== 'Info');
@@ -106,13 +166,31 @@ program.command('scan')
106166
? allDependencyFindings.filter(dep => (dep.maxSeverity === 'High' || dep.maxSeverity === 'Critical')) // Exclude errors when highOnly
107167
: allDependencyFindings.filter(dep => dep.vulnerabilities.length > 0 || dep.error);
108168

169+
// Filter config findings based on high-only flag if needed (e.g., only High CORS)
170+
const reportConfigFindings = options.highOnly
171+
? allConfigFindings.filter(f => f.severity === 'High' || f.severity === 'Critical')
172+
: allConfigFindings;
173+
174+
// Filter upload findings (adjust severity filtering as needed)
175+
const reportUploadFindings = options.highOnly
176+
? allUploadFindings.filter(f => f.severity === 'High' || f.severity === 'Critical' || f.severity === 'Medium') // Example: Include Medium for uploads even with --high-only?
177+
: allUploadFindings;
178+
179+
// Filter endpoint findings (e.g., keep Medium+ for high-only)
180+
const reportEndpointFindings = options.highOnly
181+
? allEndpointFindings.filter(f => f.severity === 'High' || f.severity === 'Critical' || f.severity === 'Medium')
182+
: allEndpointFindings;
183+
109184
// --- NOW Check Gitignore Status ---
110185
gitignoreWarnings = checkGitignoreStatus(rootDir);
111186

112187
// --- Output Generation ---
113188
const reportData = {
114-
secretFindings: reportSecretFindings, // Report only standard secrets
115-
dependencyFindings: reportDependencyFindings
189+
secretFindings: reportSecretFindings,
190+
dependencyFindings: reportDependencyFindings,
191+
configFindings: reportConfigFindings,
192+
uploadFindings: reportUploadFindings,
193+
endpointFindings: reportEndpointFindings
116194
};
117195

118196
// Generate JSON if requested
@@ -121,6 +199,9 @@ program.command('scan')
121199
const outputJsonData = {
122200
secrets: reportSecretFindings,
123201
dependencies: reportDependencyFindings,
202+
configuration: reportConfigFindings,
203+
uploads: reportUploadFindings,
204+
endpoints: reportEndpointFindings
124205
}
125206
fs.writeFileSync(options.output, JSON.stringify(outputJsonData, null, 2));
126207
console.log(`JSON results successfully written to ${options.output}`);
@@ -131,19 +212,19 @@ program.command('scan')
131212
}
132213

133214
// Generate Markdown Report if requested
134-
if (options.report) {
215+
if (reportPath) {
135216
try {
136217
// Await the async report generation
137218
const markdownContent = await generateMarkdownReport(reportData);
138-
fs.writeFileSync(options.report, markdownContent);
139-
console.log(`Markdown report successfully written to ${options.report}`);
219+
fs.writeFileSync(reportPath, markdownContent);
220+
console.log(`Markdown report successfully written to ${reportPath}`);
140221
} catch (error) {
141-
console.error(`Error writing Markdown report file ${options.report}:`, error);
222+
console.error(`Error writing Markdown report file ${reportPath}:`, error);
142223
}
143224
}
144225

145226
// Print to console ONLY if neither JSON nor Markdown output was specified
146-
if (!options.output && !options.report) {
227+
if (!options.output && !reportPath) {
147228

148229
// Print Configuration Warnings FIRST (after scans, before results)
149230
if (gitignoreWarnings.length > 0) {
@@ -164,11 +245,14 @@ program.command('scan')
164245
});
165246
}
166247

167-
// Check if any standard findings exist
248+
// Check if any standard findings exist (including config)
168249
const hasStandardSecrets = reportSecretFindings.length > 0;
169250
const hasDependencyIssues = reportDependencyFindings.length > 0;
251+
const hasConfigIssues = reportConfigFindings.length > 0;
252+
const hasUploadIssues = reportUploadFindings.length > 0;
253+
const hasEndpointIssues = reportEndpointFindings.length > 0;
170254

171-
if (hasStandardSecrets || hasDependencyIssues) {
255+
if (hasStandardSecrets || hasDependencyIssues || hasConfigIssues || hasUploadIssues || hasEndpointIssues) {
172256
// Print standard secrets to console if found
173257
if (hasStandardSecrets) {
174258
console.log(chalk.bold('\nPotential Secrets Found:'));
@@ -192,6 +276,43 @@ program.command('scan')
192276
}
193277
});
194278
}
279+
280+
// Print config findings to console if found
281+
if (hasConfigIssues) {
282+
console.log(chalk.bold('\nConfiguration Issues Found:'));
283+
reportConfigFindings.sort((a,b) => severityToSortOrder(b.severity) - severityToSortOrder(a.severity));
284+
reportConfigFindings.forEach(finding => {
285+
console.log(` - [${colorSeverity(finding.severity)}] ${finding.type}: ${chalk.cyan(finding.file)} - Key: ${chalk.magenta(finding.key)}, Value: ${chalk.yellow(JSON.stringify(finding.value))}`);
286+
console.log(chalk.dim(` > ${finding.message}`));
287+
});
288+
}
289+
290+
// Print upload findings to console if found
291+
if (hasUploadIssues) {
292+
console.log(chalk.bold('\nPotential Upload Issues Found:'));
293+
reportUploadFindings.sort((a,b) => severityToSortOrder(b.severity) - severityToSortOrder(a.severity));
294+
reportUploadFindings.forEach(finding => {
295+
// Customize console output for upload findings
296+
console.log(` - [${colorSeverity(finding.severity)}] ${finding.type} in ${chalk.cyan(finding.file)}:${chalk.yellow(String(finding.line))}`);
297+
console.log(chalk.dim(` > ${finding.message}`));
298+
if (finding.details) {
299+
console.log(chalk.dim(` ${finding.details}`));
300+
}
301+
});
302+
}
303+
304+
// Print endpoint findings to console if found
305+
if (hasEndpointIssues) {
306+
console.log(chalk.bold('\nPotentially Exposed Endpoints Found:'));
307+
reportEndpointFindings.sort((a,b) => severityToSortOrder(b.severity) - severityToSortOrder(a.severity));
308+
reportEndpointFindings.forEach(finding => {
309+
console.log(` - [${colorSeverity(finding.severity)}] ${finding.type} in ${chalk.cyan(finding.file)}:${chalk.yellow(String(finding.line))}`);
310+
console.log(chalk.dim(` > Path: ${chalk.magenta(finding.path)} - ${finding.message}`));
311+
if (finding.details) {
312+
console.log(chalk.dim(` Context: ${finding.details}`));
313+
}
314+
});
315+
}
195316
} else {
196317
// All Clear! Print positive message.
197318
// Check if we actually scanned for dependencies before saying no vulns found
@@ -210,8 +331,11 @@ program.command('scan')
210331
// Info findings should NOT affect exit code
211332
const highSeveritySecrets = reportSecretFindings.some(f => f.severity === 'High');
212333
const highSeverityDeps = reportDependencyFindings.some(d => d.maxSeverity === 'High' || d.maxSeverity === 'Critical');
334+
const highSeverityConfig = reportConfigFindings.some(f => f.severity === 'High' || f.severity === 'Critical');
335+
const highSeverityUploads = reportUploadFindings.some(f => f.severity === 'High' || f.severity === 'Critical' || f.severity === 'Medium');
336+
const highSeverityEndpoints = reportEndpointFindings.some(f => f.severity === 'High' || f.severity === 'Critical' || f.severity === 'Medium');
213337

214-
if (options.highOnly && (highSeveritySecrets || highSeverityDeps)) {
338+
if (options.highOnly && (highSeveritySecrets || highSeverityDeps || highSeverityConfig || highSeverityUploads || highSeverityEndpoints)) {
215339
console.log('Exiting with code 1 due to High/Critical severity findings (--high-only specified).');
216340
process.exit(1);
217341
}

0 commit comments

Comments
 (0)