vbguard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/package.json +35 -0
- package/src/cli.js +62 -0
- package/src/index.js +135 -0
- package/src/reporter.js +78 -0
- package/src/scanners/dangerous-defaults.js +166 -0
- package/src/scanners/dangerous-functions.js +172 -0
- package/src/scanners/dependencies.js +162 -0
- package/src/scanners/exposed-frontend.js +134 -0
- package/src/scanners/gitignore.js +88 -0
- package/src/scanners/permissive-configs.js +151 -0
- package/src/scanners/secrets.js +178 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vibeguard contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# ⚡ vibeguard
|
|
2
|
+
|
|
3
|
+
**Security scanner built for AI-generated code. Catches what traditional scanners miss.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/vibeguard)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
Vibe coding is fast. But 45% of AI-generated code ships with known vulnerabilities. The Moltbook breach, the pickle exploits, the hardcoded Supabase keys — all caused by patterns that traditional scanners weren't designed to catch.
|
|
11
|
+
|
|
12
|
+
**vibeguard** scans your codebase for the security mistakes that AI coding tools (Cursor, Claude Code, Copilot, Lovable, Bolt, Replit) introduce most often.
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx vibeguard .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That's it. No config, no account, no API key.
|
|
21
|
+
|
|
22
|
+
## What It Catches
|
|
23
|
+
|
|
24
|
+
| Category | Examples | Severity |
|
|
25
|
+
|----------|----------|----------|
|
|
26
|
+
| **Hardcoded Secrets** | API keys, DB connection strings, JWTs, private keys inline in code | Critical |
|
|
27
|
+
| **Frontend-Exposed Secrets** | Stripe secret keys, service role tokens, DB URLs in client-side code | Critical |
|
|
28
|
+
| **Dangerous Functions** | `pickle.loads()`, `eval()` with user input, SQL injection via f-strings/template literals | Critical |
|
|
29
|
+
| **Missing Auth** | Express/Flask/FastAPI servers with no authentication middleware | High |
|
|
30
|
+
| **Permissive Configs** | `cors(*)`, `debug=True`, Firebase rules `allow: if true`, Supabase without RLS | High |
|
|
31
|
+
| **No Rate Limiting** | HTTP servers without rate limiting middleware | High |
|
|
32
|
+
| **Dangerous Dependencies** | Compromised packages (event-stream, faker), deprecated libs AI still suggests | Medium |
|
|
33
|
+
| **Missing .gitignore** | `.env` files not gitignored, secrets about to be committed | Critical |
|
|
34
|
+
| **Docker Misconfigs** | Running as root, copying `.env` into images, exposed DB ports | Medium-High |
|
|
35
|
+
|
|
36
|
+
## Supported Languages
|
|
37
|
+
|
|
38
|
+
- **JavaScript / TypeScript** — Express, Fastify, Next.js, React, Vue, Svelte
|
|
39
|
+
- **Python** — Flask, FastAPI, Django
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Scan current directory
|
|
45
|
+
vibeguard .
|
|
46
|
+
|
|
47
|
+
# Scan a specific project
|
|
48
|
+
vibeguard ./my-app
|
|
49
|
+
|
|
50
|
+
# Only show high and critical issues
|
|
51
|
+
vibeguard . --severity=high
|
|
52
|
+
|
|
53
|
+
# Output as JSON (for CI/CD)
|
|
54
|
+
vibeguard . --json
|
|
55
|
+
|
|
56
|
+
# Hide fix suggestions
|
|
57
|
+
vibeguard . --no-fix
|
|
58
|
+
|
|
59
|
+
# Ignore specific directories
|
|
60
|
+
vibeguard . --ignore=tests,scripts
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## CI/CD Integration
|
|
64
|
+
|
|
65
|
+
### GitHub Actions
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
name: Security Scan
|
|
69
|
+
on: [push, pull_request]
|
|
70
|
+
|
|
71
|
+
jobs:
|
|
72
|
+
vibeguard:
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/checkout@v4
|
|
76
|
+
- uses: actions/setup-node@v4
|
|
77
|
+
with:
|
|
78
|
+
node-version: '20'
|
|
79
|
+
- run: npx vibeguard . --severity=high
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
vibeguard exits with code 1 if critical or high severity issues are found, making it easy to block deploys.
|
|
83
|
+
|
|
84
|
+
### Pre-commit Hook
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# .husky/pre-commit
|
|
88
|
+
npx vibeguard . --severity=high
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Example Output
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
⚡ vibeguard v0.1.0
|
|
95
|
+
Security scanner for AI-generated code
|
|
96
|
+
|
|
97
|
+
Scanning: /Users/dev/my-vibe-app
|
|
98
|
+
|
|
99
|
+
🚨 CRITICAL (3)
|
|
100
|
+
|
|
101
|
+
▸ secret/openai-api-key
|
|
102
|
+
src/api/chat.ts:5
|
|
103
|
+
Hardcoded OpenAI API Key detected. AI tools commonly inline
|
|
104
|
+
credentials — this is a top cause of breaches in vibe-coded apps.
|
|
105
|
+
💡 Fix: Move to environment variable OPENAI_API_KEY.
|
|
106
|
+
|
|
107
|
+
▸ frontend/stripe-secret-key-in-client
|
|
108
|
+
src/components/Checkout.tsx:12
|
|
109
|
+
Stripe Secret Key in Client found in client-side code. This will
|
|
110
|
+
be visible to anyone who opens browser DevTools.
|
|
111
|
+
💡 Fix: Stripe secret keys must NEVER be in frontend code.
|
|
112
|
+
|
|
113
|
+
▸ dangerous/pickle-deserialization
|
|
114
|
+
api/data.py:23
|
|
115
|
+
pickle.load() allows arbitrary code execution when deserializing
|
|
116
|
+
untrusted data.
|
|
117
|
+
💡 Fix: Use json.loads() for data exchange.
|
|
118
|
+
|
|
119
|
+
🔴 HIGH (2)
|
|
120
|
+
|
|
121
|
+
▸ defaults/no-rate-limiting
|
|
122
|
+
src/api/server.ts
|
|
123
|
+
No rate limiting detected on HTTP server.
|
|
124
|
+
💡 Fix: Add express-rate-limit.
|
|
125
|
+
|
|
126
|
+
▸ defaults/permissive-cors
|
|
127
|
+
src/api/server.ts:8
|
|
128
|
+
CORS is set to allow all origins (*).
|
|
129
|
+
💡 Fix: Set specific origin: cors({ origin: 'https://yourdomain.com' })
|
|
130
|
+
|
|
131
|
+
─────────────────────────────────────────
|
|
132
|
+
5 issues found: 3 critical, 2 high
|
|
133
|
+
Scanned 24 files in 12ms
|
|
134
|
+
|
|
135
|
+
⚠ Fix critical and high severity issues before deploying!
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Why Not Just Use Snyk / Semgrep / SonarQube?
|
|
139
|
+
|
|
140
|
+
Those tools are great for traditional code. But they weren't designed for AI-generated code patterns:
|
|
141
|
+
|
|
142
|
+
- **Snyk** focuses on dependency vulnerabilities, not hardcoded secrets or missing middleware
|
|
143
|
+
- **Semgrep** requires writing custom rules — vibeguard ships with AI-specific patterns out of the box
|
|
144
|
+
- **SonarQube** is enterprise-heavy and takes hours to configure
|
|
145
|
+
|
|
146
|
+
vibeguard is opinionated, zero-config, and runs in milliseconds. It's built specifically for the patterns that Cursor, Claude Code, Copilot, Lovable, and Bolt introduce.
|
|
147
|
+
|
|
148
|
+
## How It Works
|
|
149
|
+
|
|
150
|
+
vibeguard uses pattern matching (regex + structural analysis) against a curated ruleset of AI-specific vulnerability patterns. No AI, no API calls, no data leaves your machine. It runs entirely locally.
|
|
151
|
+
|
|
152
|
+
The ruleset is based on real-world breaches and academic research:
|
|
153
|
+
- The Moltbook breach (Supabase misconfiguration)
|
|
154
|
+
- Tenzai's 2025 study (69 vulnerabilities across 5 AI coding tools)
|
|
155
|
+
- Escape.tech's scan of 5,600 vibe-coded apps
|
|
156
|
+
- Georgia Tech's Vibe Security Radar (tracking AI-generated CVEs)
|
|
157
|
+
|
|
158
|
+
## Contributing
|
|
159
|
+
|
|
160
|
+
Contributions welcome. If you've found a vulnerability pattern that AI tools commonly introduce, open a PR to add it to the scanner.
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
src/scanners/
|
|
164
|
+
secrets.js # Hardcoded API keys, tokens, connection strings
|
|
165
|
+
dangerous-defaults.js # Missing auth, rate limiting, CORS, headers
|
|
166
|
+
dangerous-functions.js # eval, pickle, SQL injection, XSS
|
|
167
|
+
exposed-frontend.js # Server secrets in client-side code
|
|
168
|
+
permissive-configs.js # Supabase, Firebase, Docker misconfigs
|
|
169
|
+
dependencies.js # Compromised/deprecated packages
|
|
170
|
+
gitignore.js # Missing .gitignore entries
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vbguard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Security scanner for AI-generated code. Catches what traditional scanners miss — hardcoded secrets, dangerous defaults, exposed keys, and more.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vbguard": "./src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node src/cli.js",
|
|
11
|
+
"test": "node test/test.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"security",
|
|
15
|
+
"vibe-coding",
|
|
16
|
+
"ai-code",
|
|
17
|
+
"scanner",
|
|
18
|
+
"vulnerability",
|
|
19
|
+
"cli",
|
|
20
|
+
"vibeguard",
|
|
21
|
+
"sast",
|
|
22
|
+
"secrets",
|
|
23
|
+
"ai-security"
|
|
24
|
+
],
|
|
25
|
+
"author": "",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=16.0.0"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src/**/*",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
]
|
|
35
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { scanDirectory } = require('./index');
|
|
3
|
+
const { formatReport } = require('./reporter');
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
|
|
7
|
+
const HELP = `
|
|
8
|
+
vbguard - Security scanner for AI-generated code
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
vbguard [directory] [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--json Output results as JSON
|
|
15
|
+
--severity=X Minimum severity (low, medium, high, critical)
|
|
16
|
+
--no-fix Hide fix suggestions
|
|
17
|
+
--ignore=X Comma-separated patterns to ignore
|
|
18
|
+
-h, --help Show this help
|
|
19
|
+
-v, --version Show version
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
function parseArgs(args) {
|
|
23
|
+
const opts = { dir: '.', json: false, severity: 'low', showFix: true, ignore: [] };
|
|
24
|
+
for (const arg of args) {
|
|
25
|
+
if (arg === '-h' || arg === '--help') { console.log(HELP); process.exit(0); }
|
|
26
|
+
if (arg === '-v' || arg === '--version') { console.log('0.1.0'); process.exit(0); }
|
|
27
|
+
if (arg === '--json') opts.json = true;
|
|
28
|
+
else if (arg === '--no-fix') opts.showFix = false;
|
|
29
|
+
else if (arg.startsWith('--severity=')) opts.severity = arg.split('=')[1];
|
|
30
|
+
else if (arg.startsWith('--ignore=')) opts.ignore = arg.split('=')[1].split(',');
|
|
31
|
+
else if (!arg.startsWith('-')) opts.dir = arg;
|
|
32
|
+
}
|
|
33
|
+
return opts;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function main() {
|
|
37
|
+
const opts = parseArgs(args);
|
|
38
|
+
const targetDir = path.resolve(opts.dir);
|
|
39
|
+
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log(' \x1b[1m\x1b[35m\u26a1 vbguard\x1b[0m \x1b[2mv0.1.0\x1b[0m');
|
|
42
|
+
console.log(' \x1b[2mSecurity scanner for AI-generated code\x1b[0m');
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log(` \x1b[2mScanning:\x1b[0m ${targetDir}`);
|
|
45
|
+
console.log('');
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const results = await scanDirectory(targetDir, opts);
|
|
49
|
+
if (opts.json) {
|
|
50
|
+
console.log(JSON.stringify(results, null, 2));
|
|
51
|
+
} else {
|
|
52
|
+
formatReport(results, opts);
|
|
53
|
+
}
|
|
54
|
+
const hasCritical = results.findings.some(f => f.severity === 'critical' || f.severity === 'high');
|
|
55
|
+
process.exit(hasCritical ? 1 : 0);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error(`\x1b[31m Error: ${err.message}\x1b[0m`);
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main();
|
package/src/index.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const { scanSecrets } = require('./scanners/secrets');
|
|
5
|
+
const { scanDangerousDefaults } = require('./scanners/dangerous-defaults');
|
|
6
|
+
const { scanExposedFrontend } = require('./scanners/exposed-frontend');
|
|
7
|
+
const { scanMissingGitignore } = require('./scanners/gitignore');
|
|
8
|
+
const { scanDangerousFunctions } = require('./scanners/dangerous-functions');
|
|
9
|
+
const { scanPermissiveConfigs } = require('./scanners/permissive-configs');
|
|
10
|
+
const { scanDependencies } = require('./scanners/dependencies');
|
|
11
|
+
|
|
12
|
+
const IGNORE_DIRS = new Set([
|
|
13
|
+
'node_modules', '.git', '.next', '__pycache__', '.venv', 'venv',
|
|
14
|
+
'env', '.env', 'dist', 'build', '.cache', 'coverage', '.nyc_output',
|
|
15
|
+
'.pytest_cache', 'egg-info', '.tox', '.mypy_cache',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const SCAN_EXTENSIONS = new Set([
|
|
19
|
+
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
|
|
20
|
+
'.py', '.pyw',
|
|
21
|
+
'.json', '.yaml', '.yml', '.toml',
|
|
22
|
+
'.env', '.env.local', '.env.production', '.env.development',
|
|
23
|
+
'.html', '.htm', '.vue', '.svelte',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function shouldScanFile(filePath) {
|
|
27
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
28
|
+
const basename = path.basename(filePath);
|
|
29
|
+
|
|
30
|
+
// Always scan dotenv files
|
|
31
|
+
if (basename.startsWith('.env')) return true;
|
|
32
|
+
// Always scan config files
|
|
33
|
+
if (['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml',
|
|
34
|
+
'firestore.rules', 'database.rules.json', 'storage.rules',
|
|
35
|
+
'firebase.json'].includes(basename)) return true;
|
|
36
|
+
|
|
37
|
+
return SCAN_EXTENSIONS.has(ext);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function walkDir(dir, ignore = []) {
|
|
41
|
+
const files = [];
|
|
42
|
+
|
|
43
|
+
function walk(currentDir) {
|
|
44
|
+
let entries;
|
|
45
|
+
try {
|
|
46
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
47
|
+
} catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
53
|
+
const relativePath = path.relative(dir, fullPath);
|
|
54
|
+
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
57
|
+
if (ignore.some((pattern) => relativePath.includes(pattern))) continue;
|
|
58
|
+
walk(fullPath);
|
|
59
|
+
} else if (entry.isFile()) {
|
|
60
|
+
if (shouldScanFile(fullPath)) {
|
|
61
|
+
files.push(fullPath);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
walk(dir);
|
|
68
|
+
return files;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function scanDirectory(dir, opts = {}) {
|
|
72
|
+
const startTime = Date.now();
|
|
73
|
+
|
|
74
|
+
if (!fs.existsSync(dir)) {
|
|
75
|
+
throw new Error(`Directory not found: ${dir}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const files = walkDir(dir, opts.ignore || []);
|
|
79
|
+
const findings = [];
|
|
80
|
+
let filesScanned = 0;
|
|
81
|
+
|
|
82
|
+
// Per-file scanners
|
|
83
|
+
for (const filePath of files) {
|
|
84
|
+
let content;
|
|
85
|
+
try {
|
|
86
|
+
const stat = fs.statSync(filePath);
|
|
87
|
+
// Skip files > 1MB
|
|
88
|
+
if (stat.size > 1_000_000) continue;
|
|
89
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
90
|
+
} catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
filesScanned++;
|
|
95
|
+
const relativePath = path.relative(dir, filePath);
|
|
96
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
97
|
+
const basename = path.basename(filePath);
|
|
98
|
+
|
|
99
|
+
const ctx = { filePath, relativePath, content, ext, basename };
|
|
100
|
+
|
|
101
|
+
findings.push(...scanSecrets(ctx));
|
|
102
|
+
findings.push(...scanDangerousDefaults(ctx));
|
|
103
|
+
findings.push(...scanExposedFrontend(ctx));
|
|
104
|
+
findings.push(...scanDangerousFunctions(ctx));
|
|
105
|
+
findings.push(...scanPermissiveConfigs(ctx));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Project-level scanners
|
|
109
|
+
findings.push(...scanMissingGitignore(dir));
|
|
110
|
+
findings.push(...(await scanDependencies(dir)));
|
|
111
|
+
|
|
112
|
+
// Filter by severity
|
|
113
|
+
const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
114
|
+
const minSeverity = severityOrder[opts.severity || 'low'] || 1;
|
|
115
|
+
const filtered = findings.filter(
|
|
116
|
+
(f) => (severityOrder[f.severity] || 0) >= minSeverity
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const elapsed = Date.now() - startTime;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
findings: filtered,
|
|
123
|
+
summary: {
|
|
124
|
+
filesScanned,
|
|
125
|
+
totalFindings: filtered.length,
|
|
126
|
+
critical: filtered.filter((f) => f.severity === 'critical').length,
|
|
127
|
+
high: filtered.filter((f) => f.severity === 'high').length,
|
|
128
|
+
medium: filtered.filter((f) => f.severity === 'medium').length,
|
|
129
|
+
low: filtered.filter((f) => f.severity === 'low').length,
|
|
130
|
+
elapsed,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = { scanDirectory };
|
package/src/reporter.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const SEVERITY_COLORS = {
|
|
2
|
+
critical: '\x1b[41m\x1b[37m', // white on red bg
|
|
3
|
+
high: '\x1b[31m', // red
|
|
4
|
+
medium: '\x1b[33m', // yellow
|
|
5
|
+
low: '\x1b[36m', // cyan
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const SEVERITY_ICONS = {
|
|
9
|
+
critical: '🚨',
|
|
10
|
+
high: '🔴',
|
|
11
|
+
medium: '🟡',
|
|
12
|
+
low: '🔵',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const RESET = '\x1b[0m';
|
|
16
|
+
const DIM = '\x1b[2m';
|
|
17
|
+
const BOLD = '\x1b[1m';
|
|
18
|
+
|
|
19
|
+
function formatReport(results, opts = {}) {
|
|
20
|
+
const { findings, summary } = results;
|
|
21
|
+
|
|
22
|
+
if (findings.length === 0) {
|
|
23
|
+
console.log(' \x1b[32m✓ No security issues found!\x1b[0m');
|
|
24
|
+
console.log(` ${DIM}Scanned ${summary.filesScanned} files in ${summary.elapsed}ms${RESET}`);
|
|
25
|
+
console.log('');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Group by severity
|
|
30
|
+
const grouped = { critical: [], high: [], medium: [], low: [] };
|
|
31
|
+
for (const f of findings) {
|
|
32
|
+
if (grouped[f.severity]) {
|
|
33
|
+
grouped[f.severity].push(f);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const severity of ['critical', 'high', 'medium', 'low']) {
|
|
38
|
+
const items = grouped[severity];
|
|
39
|
+
if (items.length === 0) continue;
|
|
40
|
+
|
|
41
|
+
const color = SEVERITY_COLORS[severity];
|
|
42
|
+
const icon = SEVERITY_ICONS[severity];
|
|
43
|
+
|
|
44
|
+
console.log(` ${color}${BOLD}${icon} ${severity.toUpperCase()} (${items.length})${RESET}`);
|
|
45
|
+
console.log('');
|
|
46
|
+
|
|
47
|
+
for (const finding of items) {
|
|
48
|
+
console.log(` ${color} ▸ ${finding.rule}${RESET}`);
|
|
49
|
+
console.log(` ${DIM}${finding.file}${finding.line ? `:${finding.line}` : ''}${RESET}`);
|
|
50
|
+
console.log(` ${finding.message}`);
|
|
51
|
+
|
|
52
|
+
if (opts.showFix !== false && finding.fix) {
|
|
53
|
+
console.log(` ${DIM}💡 Fix: ${finding.fix}${RESET}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log('');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Summary bar
|
|
61
|
+
console.log(' ─────────────────────────────────────────');
|
|
62
|
+
const parts = [];
|
|
63
|
+
if (summary.critical > 0) parts.push(`\x1b[31m${summary.critical} critical${RESET}`);
|
|
64
|
+
if (summary.high > 0) parts.push(`\x1b[31m${summary.high} high${RESET}`);
|
|
65
|
+
if (summary.medium > 0) parts.push(`\x1b[33m${summary.medium} medium${RESET}`);
|
|
66
|
+
if (summary.low > 0) parts.push(`\x1b[36m${summary.low} low${RESET}`);
|
|
67
|
+
|
|
68
|
+
console.log(` ${BOLD}${summary.totalFindings} issues${RESET} found: ${parts.join(', ')}`);
|
|
69
|
+
console.log(` ${DIM}Scanned ${summary.filesScanned} files in ${summary.elapsed}ms${RESET}`);
|
|
70
|
+
console.log('');
|
|
71
|
+
|
|
72
|
+
if (summary.critical > 0 || summary.high > 0) {
|
|
73
|
+
console.log(` \x1b[31m${BOLD}⚠ Fix critical and high severity issues before deploying!${RESET}`);
|
|
74
|
+
console.log('');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { formatReport };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// AI-generated code frequently ships without basic security middleware
|
|
2
|
+
// These checks catch the most common omissions
|
|
3
|
+
|
|
4
|
+
function scanDangerousDefaults(ctx) {
|
|
5
|
+
const { content, relativePath, ext, basename } = ctx;
|
|
6
|
+
const findings = [];
|
|
7
|
+
|
|
8
|
+
const isJS = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(ext);
|
|
9
|
+
const isPy = ['.py', '.pyw'].includes(ext);
|
|
10
|
+
const isServerFile = /(?:server|app|index|main|api)\.(js|ts|py)$/i.test(basename) ||
|
|
11
|
+
/(?:server|api|backend|routes)/i.test(relativePath);
|
|
12
|
+
|
|
13
|
+
if (!isServerFile) return findings;
|
|
14
|
+
|
|
15
|
+
// === Express / Node.js checks ===
|
|
16
|
+
if (isJS) {
|
|
17
|
+
const hasExpress = /require\s*\(\s*['"]express['"]\s*\)|from\s+['"]express['"]/i.test(content);
|
|
18
|
+
const hasFastify = /require\s*\(\s*['"]fastify['"]\s*\)|from\s+['"]fastify['"]/i.test(content);
|
|
19
|
+
const hasKoa = /require\s*\(\s*['"]koa['"]\s*\)|from\s+['"]koa['"]/i.test(content);
|
|
20
|
+
const isHttpServer = hasExpress || hasFastify || hasKoa;
|
|
21
|
+
|
|
22
|
+
if (isHttpServer) {
|
|
23
|
+
// No rate limiting
|
|
24
|
+
if (!/rate.?limit|express-rate-limit|@fastify\/rate-limit|bottleneck|express-slow-down/i.test(content)) {
|
|
25
|
+
findings.push({
|
|
26
|
+
rule: 'defaults/no-rate-limiting',
|
|
27
|
+
severity: 'high',
|
|
28
|
+
file: relativePath,
|
|
29
|
+
line: null,
|
|
30
|
+
message: 'No rate limiting detected on HTTP server. AI-generated servers almost never include rate limiting, making them vulnerable to brute force and DDoS.',
|
|
31
|
+
fix: 'Add express-rate-limit: app.use(rateLimit({ windowMs: 15*60*1000, max: 100 }))',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// No helmet / security headers
|
|
36
|
+
if (!/helmet|security.?headers|x-content-type|x-frame-options|strict-transport/i.test(content)) {
|
|
37
|
+
findings.push({
|
|
38
|
+
rule: 'defaults/no-security-headers',
|
|
39
|
+
severity: 'medium',
|
|
40
|
+
file: relativePath,
|
|
41
|
+
line: null,
|
|
42
|
+
message: 'No security headers middleware (helmet) detected. Missing headers like X-Frame-Options, CSP, HSTS.',
|
|
43
|
+
fix: 'Add helmet: app.use(helmet()). Install with npm install helmet.',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Permissive CORS
|
|
48
|
+
const corsMatch = content.match(/cors\(\s*\{?\s*origin\s*:\s*['"`]\*['"`]|cors\(\s*\)/);
|
|
49
|
+
if (corsMatch) {
|
|
50
|
+
const lineNum = content.substring(0, corsMatch.index).split('\n').length;
|
|
51
|
+
findings.push({
|
|
52
|
+
rule: 'defaults/permissive-cors',
|
|
53
|
+
severity: 'high',
|
|
54
|
+
file: relativePath,
|
|
55
|
+
line: lineNum,
|
|
56
|
+
message: 'CORS is set to allow all origins (*). AI tools default to permissive CORS. Restrict to your actual frontend domain.',
|
|
57
|
+
fix: "Set specific origin: cors({ origin: 'https://yourdomain.com' })",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// No input validation
|
|
62
|
+
if (!/zod|joi|yup|express-validator|class-validator|ajv|superstruct/i.test(content)) {
|
|
63
|
+
// Check if there are POST/PUT route handlers
|
|
64
|
+
if (/\.(post|put|patch)\s*\(/i.test(content)) {
|
|
65
|
+
findings.push({
|
|
66
|
+
rule: 'defaults/no-input-validation',
|
|
67
|
+
severity: 'medium',
|
|
68
|
+
file: relativePath,
|
|
69
|
+
line: null,
|
|
70
|
+
message: 'No input validation library detected but POST/PUT handlers exist. AI-generated APIs rarely validate input, enabling injection attacks.',
|
|
71
|
+
fix: 'Add zod or joi for request body validation on all mutation endpoints.',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// No auth middleware
|
|
77
|
+
if (/\.(get|post|put|patch|delete)\s*\(/i.test(content)) {
|
|
78
|
+
if (!/auth|jwt|passport|clerk|supabase.*auth|next-auth|lucia|session|bearer/i.test(content)) {
|
|
79
|
+
findings.push({
|
|
80
|
+
rule: 'defaults/no-auth-middleware',
|
|
81
|
+
severity: 'high',
|
|
82
|
+
file: relativePath,
|
|
83
|
+
line: null,
|
|
84
|
+
message: 'Route handlers detected but no authentication middleware found. AI tools frequently skip auth, leaving all endpoints publicly accessible.',
|
|
85
|
+
fix: 'Add authentication middleware to protected routes. Use passport, clerk, or JWT verification.',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// === Python / Flask / FastAPI / Django checks ===
|
|
93
|
+
if (isPy) {
|
|
94
|
+
const hasFlask = /from\s+flask\s+import|import\s+flask/i.test(content);
|
|
95
|
+
const hasFastAPI = /from\s+fastapi\s+import|import\s+fastapi/i.test(content);
|
|
96
|
+
const hasDjango = /from\s+django/i.test(content);
|
|
97
|
+
const isPyServer = hasFlask || hasFastAPI || hasDjango;
|
|
98
|
+
|
|
99
|
+
if (isPyServer) {
|
|
100
|
+
// Debug mode in production
|
|
101
|
+
const debugMatch = content.match(/debug\s*=\s*True|DEBUG\s*=\s*True|app\.run\([^)]*debug\s*=\s*True/i);
|
|
102
|
+
if (debugMatch) {
|
|
103
|
+
const lineNum = content.substring(0, debugMatch.index).split('\n').length;
|
|
104
|
+
findings.push({
|
|
105
|
+
rule: 'defaults/debug-mode-enabled',
|
|
106
|
+
severity: 'critical',
|
|
107
|
+
file: relativePath,
|
|
108
|
+
line: lineNum,
|
|
109
|
+
message: 'Debug mode enabled. AI tools always set debug=True. This exposes stack traces, allows code execution (Flask debugger), and leaks secrets in production.',
|
|
110
|
+
fix: 'Set debug=False or use environment variable: debug=os.environ.get("DEBUG", "False") == "True"',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Flask secret key hardcoded
|
|
115
|
+
const secretKeyMatch = content.match(/(?:secret_key|SECRET_KEY)\s*=\s*['"`]([^'"`]{1,100})['"`]/i);
|
|
116
|
+
if (secretKeyMatch) {
|
|
117
|
+
const lineNum = content.substring(0, secretKeyMatch.index).split('\n').length;
|
|
118
|
+
const key = secretKeyMatch[1];
|
|
119
|
+
if (!/os\.environ|os\.getenv|environ/i.test(content.split('\n')[lineNum - 1] || '')) {
|
|
120
|
+
findings.push({
|
|
121
|
+
rule: 'defaults/hardcoded-secret-key',
|
|
122
|
+
severity: 'critical',
|
|
123
|
+
file: relativePath,
|
|
124
|
+
line: lineNum,
|
|
125
|
+
message: 'Hardcoded SECRET_KEY. AI tools generate a static secret key. This compromises session security and CSRF protection.',
|
|
126
|
+
fix: "Use os.environ.get('SECRET_KEY') with a randomly generated value.",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// No CORS restriction (FastAPI)
|
|
132
|
+
if (hasFastAPI) {
|
|
133
|
+
const corsAll = content.match(/allow_origins\s*=\s*\[\s*['"`]\*['"`]\s*\]/);
|
|
134
|
+
if (corsAll) {
|
|
135
|
+
const lineNum = content.substring(0, corsAll.index).split('\n').length;
|
|
136
|
+
findings.push({
|
|
137
|
+
rule: 'defaults/permissive-cors',
|
|
138
|
+
severity: 'high',
|
|
139
|
+
file: relativePath,
|
|
140
|
+
line: lineNum,
|
|
141
|
+
message: 'FastAPI CORS allows all origins. Restrict to your frontend domain.',
|
|
142
|
+
fix: "Set allow_origins=['https://yourdomain.com']",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// No rate limiting
|
|
148
|
+
if (!/slowapi|flask.?limiter|ratelimit|throttle/i.test(content)) {
|
|
149
|
+
if (/@app\.(?:route|get|post|put|delete)|@router\./i.test(content)) {
|
|
150
|
+
findings.push({
|
|
151
|
+
rule: 'defaults/no-rate-limiting',
|
|
152
|
+
severity: 'high',
|
|
153
|
+
file: relativePath,
|
|
154
|
+
line: null,
|
|
155
|
+
message: 'No rate limiting on Python server. Add slowapi (FastAPI) or flask-limiter (Flask).',
|
|
156
|
+
fix: 'Install and configure rate limiting: pip install slowapi',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return findings;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { scanDangerousDefaults };
|