cyberaudit-skill 3.1.5 → 3.2.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/README.md +84 -25
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +552 -172
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +100 -0
- package/dist/mcp-server.d.ts +135 -1
- package/dist/mcp-server.js +185 -96
- package/dist/mcp-server.test.d.ts +1 -0
- package/dist/mcp-server.test.js +77 -0
- package/dist/report/generator.d.ts +2 -0
- package/dist/report/generator.js +260 -0
- package/dist/report/generator.test.d.ts +1 -0
- package/dist/report/generator.test.js +73 -0
- package/dist/report/types.d.ts +31 -0
- package/dist/report/types.js +1 -0
- package/dist/scanners/secrets.d.ts +32 -0
- package/dist/scanners/secrets.js +437 -0
- package/dist/scanners/secrets.test.d.ts +1 -0
- package/dist/scanners/secrets.test.js +96 -0
- package/dist/scanners/types.d.ts +22 -0
- package/dist/scanners/types.js +1 -0
- package/dist/scanners/web.d.ts +37 -0
- package/dist/scanners/web.js +327 -0
- package/dist/scanners/web.test.d.ts +1 -0
- package/dist/scanners/web.test.js +67 -0
- package/package.json +15 -3
- package/skills/cyberaudit/AGENT-BOOT.md +1 -1
- package/skills/cyberaudit/COMMANDS.md +11 -1
- package/skills/cyberaudit/INSTALL.md +7 -6
- package/skills/cyberaudit/README.md +20 -5
- package/skills/cyberaudit/SKILL.md +20 -5
- package/skills/cyberaudit/cloud/CLOUD-CHECKLIST.md +85 -0
- package/skills/cyberaudit/cloud/CLOUD-PHILOSOPHY.md +91 -0
- package/skills/cyberaudit/cloud/CLOUD-REMEDIATION-LIBRARY.md +145 -0
- package/skills/cyberaudit/commands/audit-angular.md +11 -0
- package/skills/cyberaudit/commands/audit-api-bola.md +11 -0
- package/skills/cyberaudit/commands/audit-api-bopla.md +11 -0
- package/skills/cyberaudit/commands/audit-api-graphql.md +11 -0
- package/skills/cyberaudit/commands/audit-api-inventory.md +11 -0
- package/skills/cyberaudit/commands/audit-api-rate-limit.md +11 -0
- package/skills/cyberaudit/commands/audit-api-rest.md +11 -0
- package/skills/cyberaudit/commands/audit-api-third-party.md +11 -0
- package/skills/cyberaudit/commands/audit-api-ws.md +11 -0
- package/skills/cyberaudit/commands/audit-auth-api.md +11 -0
- package/skills/cyberaudit/commands/audit-auth-mobile.md +11 -0
- package/skills/cyberaudit/commands/audit-auth.md +12 -0
- package/skills/cyberaudit/commands/audit-binary.md +11 -0
- package/skills/cyberaudit/commands/audit-cis.md +11 -0
- package/skills/cyberaudit/commands/audit-cloud-iam.md +11 -0
- package/skills/cyberaudit/commands/audit-cloud-network.md +11 -0
- package/skills/cyberaudit/commands/audit-cloud-s3.md +11 -0
- package/skills/cyberaudit/commands/audit-cloud.md +10 -0
- package/skills/cyberaudit/commands/audit-cors.md +11 -0
- package/skills/cyberaudit/commands/audit-crypto-mobile.md +11 -0
- package/skills/cyberaudit/commands/audit-crypto.md +10 -0
- package/skills/cyberaudit/commands/audit-csrf.md +11 -0
- package/skills/cyberaudit/commands/audit-deeplinks.md +11 -0
- package/skills/cyberaudit/commands/audit-deps.md +11 -0
- package/skills/cyberaudit/commands/audit-deserial.md +11 -0
- package/skills/cyberaudit/commands/audit-exec.md +11 -0
- package/skills/cyberaudit/commands/audit-expo.md +11 -0
- package/skills/cyberaudit/commands/audit-express.md +11 -0
- package/skills/cyberaudit/commands/audit-flutter.md +11 -0
- package/skills/cyberaudit/commands/audit-headers.md +11 -0
- package/skills/cyberaudit/commands/audit-hipaa.md +11 -0
- package/skills/cyberaudit/commands/audit-idor.md +11 -0
- package/skills/cyberaudit/commands/audit-injection.md +12 -0
- package/skills/cyberaudit/commands/audit-ionic.md +11 -0
- package/skills/cyberaudit/commands/audit-laravel.md +11 -0
- package/skills/cyberaudit/commands/audit-logic.md +11 -0
- package/skills/cyberaudit/commands/audit-masvs.md +11 -0
- package/skills/cyberaudit/commands/audit-nestjs.md +11 -0
- package/skills/cyberaudit/commands/audit-network.md +11 -0
- package/skills/cyberaudit/commands/audit-nextjs.md +11 -0
- package/skills/cyberaudit/commands/audit-owasp.md +11 -0
- package/skills/cyberaudit/commands/audit-pci.md +11 -0
- package/skills/cyberaudit/commands/audit-permissions.md +11 -0
- package/skills/cyberaudit/commands/audit-react-native.md +11 -0
- package/skills/cyberaudit/commands/audit-react.md +11 -0
- package/skills/cyberaudit/commands/audit-rgpd.md +11 -0
- package/skills/cyberaudit/commands/audit-runtime.md +11 -0
- package/skills/cyberaudit/commands/audit-secrets.md +11 -0
- package/skills/cyberaudit/commands/audit-ssrf.md +11 -0
- package/skills/cyberaudit/commands/audit-storage.md +11 -0
- package/skills/cyberaudit/commands/audit-vue.md +11 -0
- package/skills/cyberaudit/commands/audit-xss.md +11 -0
- package/skills/cyberaudit/commands/audit-xxe.md +11 -0
- package/skills/cyberaudit/reports/EXECUTIVE-SUMMARY-TEMPLATE.md +1 -1
- package/skills/cyberaudit/reports/REPORT-TEMPLATE-CLOUD.md +103 -0
- package/skills/cyberaudit/reports/REPORT-TEMPLATE-MOBILE.md +2 -2
- package/skills/cyberaudit/reports/REPORT-TEMPLATE-WEB.md +2 -2
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
const DEFAULT_IGNORE = [
|
|
4
|
+
'node_modules',
|
|
5
|
+
'.git',
|
|
6
|
+
'dist',
|
|
7
|
+
'build',
|
|
8
|
+
'.next',
|
|
9
|
+
'.nuxt',
|
|
10
|
+
'.output',
|
|
11
|
+
'coverage',
|
|
12
|
+
'.cache',
|
|
13
|
+
'.turbo',
|
|
14
|
+
'out',
|
|
15
|
+
'.vercel',
|
|
16
|
+
'.serverless',
|
|
17
|
+
'vendor',
|
|
18
|
+
];
|
|
19
|
+
function redactMiddle(s, keep = 4) {
|
|
20
|
+
if (s.length <= keep * 2)
|
|
21
|
+
return '*'.repeat(s.length);
|
|
22
|
+
return s.slice(0, keep) + '*'.repeat(s.length - keep * 2) + s.slice(-keep);
|
|
23
|
+
}
|
|
24
|
+
function redactSecret(match) {
|
|
25
|
+
// Keep first 4 and last 4 if long, otherwise mask 80%
|
|
26
|
+
const trimmed = match.trim();
|
|
27
|
+
if (trimmed.length < 12)
|
|
28
|
+
return '***REDACTED***';
|
|
29
|
+
// If it's key=value, redact value only
|
|
30
|
+
const eqIdx = trimmed.indexOf('=');
|
|
31
|
+
const colonIdx = trimmed.indexOf(':');
|
|
32
|
+
let sepIdx = -1;
|
|
33
|
+
if (eqIdx !== -1 && (colonIdx === -1 || eqIdx < colonIdx))
|
|
34
|
+
sepIdx = eqIdx;
|
|
35
|
+
else if (colonIdx !== -1)
|
|
36
|
+
sepIdx = colonIdx;
|
|
37
|
+
if (sepIdx !== -1 && sepIdx < trimmed.length - 2) {
|
|
38
|
+
const before = trimmed.slice(0, sepIdx + 1);
|
|
39
|
+
const after = trimmed.slice(sepIdx + 1).trim();
|
|
40
|
+
// strip quotes for redaction
|
|
41
|
+
const quote = after[0] === '"' || after[0] === "'" ? after[0] : '';
|
|
42
|
+
const core = after.replace(/^['"\s]+|['"\s]+$/g, '');
|
|
43
|
+
if (core.length > 8) {
|
|
44
|
+
return `${before} ${quote}${redactMiddle(core)}${quote}`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return redactMiddle(trimmed, 4);
|
|
48
|
+
}
|
|
49
|
+
const PATTERNS = [
|
|
50
|
+
{
|
|
51
|
+
id: 'AWS_ACCESS_KEY',
|
|
52
|
+
name: 'AWS Access Key ID',
|
|
53
|
+
regex: /AKIA[0-9A-Z]{16}/g,
|
|
54
|
+
severity: 'CRITICAL',
|
|
55
|
+
cvss: 9.1,
|
|
56
|
+
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N',
|
|
57
|
+
description: 'Hardcoded AWS Access Key ID found. Enables attacker to use AWS credentials if secret key also exposed.',
|
|
58
|
+
remediation: 'Move to env var AWS_ACCESS_KEY_ID via Secrets Manager / Parameter Store. Rotate key immediately. Use IAM roles for EC2/Lambda, not static keys.',
|
|
59
|
+
owasp: 'A07:2021 — Identification and Authentication Failures',
|
|
60
|
+
cwe: 'CWE-798',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'AWS_SECRET_KEY',
|
|
64
|
+
name: 'AWS Secret Access Key',
|
|
65
|
+
regex: /(?:aws_secret_access_key|aws_secret|AWS_SECRET)[\s]*[:=][\s]*['"]?([A-Za-z0-9/+=]{40})['"]?/gi,
|
|
66
|
+
severity: 'CRITICAL',
|
|
67
|
+
cvss: 9.8,
|
|
68
|
+
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H',
|
|
69
|
+
description: 'Hardcoded AWS Secret Access Key. Full account takeover possible if paired with access key.',
|
|
70
|
+
remediation: 'Store in AWS Secrets Manager, never commit. Rotate immediately. Prefer IAM roles.',
|
|
71
|
+
owasp: 'A07:2021',
|
|
72
|
+
cwe: 'CWE-798',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'GITHUB_PAT',
|
|
76
|
+
name: 'GitHub Personal Access Token',
|
|
77
|
+
regex: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
|
|
78
|
+
severity: 'CRITICAL',
|
|
79
|
+
cvss: 9.1,
|
|
80
|
+
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N',
|
|
81
|
+
description: 'GitHub PAT exposed. Allows repo access, code theft, supply chain injection.',
|
|
82
|
+
remediation: 'Revoke token in GitHub Settings > Developer settings. Use fine-grained tokens with expiry <30d, store in env.',
|
|
83
|
+
owasp: 'A07:2021',
|
|
84
|
+
cwe: 'CWE-798',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'STRIPE_SK',
|
|
88
|
+
name: 'Stripe Secret Key',
|
|
89
|
+
regex: /sk_(?:live|test)_[0-9a-zA-Z]{20,}/g,
|
|
90
|
+
severity: 'CRITICAL',
|
|
91
|
+
cvss: 9.1,
|
|
92
|
+
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N',
|
|
93
|
+
description: 'Stripe secret key hardcoded. Allows payment fraud, refunds, data theft.',
|
|
94
|
+
remediation: 'Move to STRIPE_SECRET_KEY env. Rotate in Stripe Dashboard. Use restricted keys, not secret keys in frontend.',
|
|
95
|
+
owasp: 'A07:2021',
|
|
96
|
+
cwe: 'CWE-798',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'PRIVATE_KEY',
|
|
100
|
+
name: 'Private Key (RSA/DSA/EC/OPENSSH)',
|
|
101
|
+
regex: /-----BEGIN (?:RSA |DSA |EC |OPENSSH )?PRIVATE KEY-----/g,
|
|
102
|
+
severity: 'CRITICAL',
|
|
103
|
+
cvss: 9.8,
|
|
104
|
+
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H',
|
|
105
|
+
description: 'Private key committed. Allows impersonation, decryption, signing.',
|
|
106
|
+
remediation: 'Remove from repo, purge git history (BFG). Store in KMS / Secrets Manager. Never commit.',
|
|
107
|
+
owasp: 'A07:2021',
|
|
108
|
+
cwe: 'CWE-798',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'SLACK_TOKEN',
|
|
112
|
+
name: 'Slack Token',
|
|
113
|
+
regex: /xox[baprs]-[0-9a-zA-Z\-]{10,}/g,
|
|
114
|
+
severity: 'HIGH',
|
|
115
|
+
cvss: 8.2,
|
|
116
|
+
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N',
|
|
117
|
+
description: 'Slack token exposed. Allows reading private channels, impersonation.',
|
|
118
|
+
remediation: 'Revoke in Slack API dashboard. Use env var, rotate.',
|
|
119
|
+
owasp: 'A07:2021',
|
|
120
|
+
cwe: 'CWE-798',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'JWT_SECRET',
|
|
124
|
+
name: 'JWT Secret hardcoded',
|
|
125
|
+
regex: /(?:jwt[_-]?secret|JWT_SECRET)\s*[:=]\s*['"][^'"]{8,}['"]/gi,
|
|
126
|
+
severity: 'HIGH',
|
|
127
|
+
cvss: 8.1,
|
|
128
|
+
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N',
|
|
129
|
+
description: 'JWT secret hardcoded. Allows forging arbitrary JWTs, auth bypass.',
|
|
130
|
+
remediation: 'Move to env var with 32+ chars random from crypto.randomBytes. Rotate, invalidate existing tokens.',
|
|
131
|
+
owasp: 'A02:2021 — Cryptographic Failures',
|
|
132
|
+
cwe: 'CWE-798',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'NEXT_PUBLIC_SECRET',
|
|
136
|
+
name: 'NEXT_PUBLIC_ with secret value',
|
|
137
|
+
regex: /NEXT_PUBLIC_(?:.*(?:SECRET|KEY|TOKEN|PASSWORD))?\s*=\s*['\"]?[^'"\s]{8,}['\"]?/gi,
|
|
138
|
+
severity: 'HIGH',
|
|
139
|
+
cvss: 7.5,
|
|
140
|
+
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N',
|
|
141
|
+
description: 'NEXT_PUBLIC_ env contains secret. NEXT_PUBLIC_ is bundled to client, exposed to browser.',
|
|
142
|
+
remediation: 'Remove NEXT_PUBLIC_ prefix for server-only secrets. Use server env only, not NEXT_PUBLIC_.',
|
|
143
|
+
owasp: 'A01:2021 — Broken Access Control',
|
|
144
|
+
cwe: 'CWE-200',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'DATABASE_URL',
|
|
148
|
+
name: 'Database URL with password',
|
|
149
|
+
regex: /(?:postgres|postgresql|mysql|mongodb(?:\+srv)?):\/\/[^:\s]+:([^@\s]+)@[^\s]+/gi,
|
|
150
|
+
severity: 'CRITICAL',
|
|
151
|
+
cvss: 9.0,
|
|
152
|
+
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L',
|
|
153
|
+
description: 'Database connection string with embedded password. DB compromise if repo leaked.',
|
|
154
|
+
remediation: 'Use separate DB_USER, DB_PASSWORD env vars via Secrets Manager. Rotate password. Restrict DB SG.',
|
|
155
|
+
owasp: 'A07:2021',
|
|
156
|
+
cwe: 'CWE-798',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'GENERIC_API_KEY',
|
|
160
|
+
name: 'Generic API Key assignment',
|
|
161
|
+
regex: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][0-9a-zA-Z\-_]{20,}['"]/gi,
|
|
162
|
+
severity: 'HIGH',
|
|
163
|
+
cvss: 7.5,
|
|
164
|
+
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N',
|
|
165
|
+
description: 'Generic API key hardcoded. Third-party abuse, quota theft.',
|
|
166
|
+
remediation: 'Move to env var, use Secrets Manager, rotate key.',
|
|
167
|
+
owasp: 'A07:2021',
|
|
168
|
+
cwe: 'CWE-798',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'GENERIC_SECRET_ASSIGNMENT',
|
|
172
|
+
name: 'Password / Token in code',
|
|
173
|
+
regex: /(?:password|passwd|secret|token)\s*[:=]\s*['"][^'"]{8,}['"]/gi,
|
|
174
|
+
severity: 'MEDIUM',
|
|
175
|
+
cvss: 6.5,
|
|
176
|
+
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N',
|
|
177
|
+
description: 'Potential hardcoded password or token. Context-dependent, needs verification.',
|
|
178
|
+
remediation: 'Verify if real secret. If yes, move to env var + Secrets Manager.',
|
|
179
|
+
owasp: 'A07:2021',
|
|
180
|
+
cwe: 'CWE-798',
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: 'ENV_FILE_COMMITTED',
|
|
184
|
+
name: '.env file with secrets (should not be committed)',
|
|
185
|
+
regex: /(?:^|\n)(?:[A-Z_]+_(?:KEY|SECRET|TOKEN|PASSWORD)=.+) /,
|
|
186
|
+
severity: 'INFO',
|
|
187
|
+
cvss: 0,
|
|
188
|
+
cvssVector: '',
|
|
189
|
+
description: 'Potential .env committed. Not a secret itself but file should be gitignored.',
|
|
190
|
+
remediation: 'Add .env to .gitignore, remove from repo history if contains real secrets.',
|
|
191
|
+
owasp: 'A01:2021',
|
|
192
|
+
cwe: 'CWE-200',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: 'OPENAI_KEY',
|
|
196
|
+
name: 'OpenAI API Key',
|
|
197
|
+
regex: /sk-(?:proj-|)[A-Za-z0-9]{20,}T3BlbkFJ[0-9A-Za-z]{20,}/g,
|
|
198
|
+
severity: 'HIGH',
|
|
199
|
+
cvss: 8.6,
|
|
200
|
+
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N',
|
|
201
|
+
description: 'OpenAI API key exposed. Allows usage on attacker behalf, cost fraud.',
|
|
202
|
+
remediation: 'Revoke in OpenAI dashboard, rotate, use env var.',
|
|
203
|
+
owasp: 'A07:2021',
|
|
204
|
+
cwe: 'CWE-798',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 'NPM_TOKEN',
|
|
208
|
+
name: 'NPM Token',
|
|
209
|
+
regex: /npm_[A-Za-z0-9]{36,}/g,
|
|
210
|
+
severity: 'HIGH',
|
|
211
|
+
cvss: 8.1,
|
|
212
|
+
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N',
|
|
213
|
+
description: 'NPM token exposed. Allows publishing malicious packages under your name.',
|
|
214
|
+
remediation: 'Revoke in npmjs.com, rotate, use fine-grained tokens.',
|
|
215
|
+
owasp: 'A07:2021',
|
|
216
|
+
cwe: 'CWE-798',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: 'HIGH_ENTROPY_STRING',
|
|
220
|
+
name: 'High entropy string (possible secret)',
|
|
221
|
+
regex: /['"][A-Za-z0-9/+]{32,}={0,2}['"]/g,
|
|
222
|
+
severity: 'LOW',
|
|
223
|
+
cvss: 3.7,
|
|
224
|
+
cvssVector: 'CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N',
|
|
225
|
+
description: 'High entropy string detected, might be secret. Low confidence, needs manual review.',
|
|
226
|
+
remediation: 'Verify if secret. If yes, move to env var.',
|
|
227
|
+
owasp: 'A07:2021',
|
|
228
|
+
cwe: 'CWE-798',
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
function shouldIgnore(filePath, ignoreList) {
|
|
232
|
+
const parts = filePath.split(/[\/\\]/);
|
|
233
|
+
return parts.some(p => ignoreList.includes(p));
|
|
234
|
+
}
|
|
235
|
+
function isProbablyTextFile(filePath, maxSize) {
|
|
236
|
+
try {
|
|
237
|
+
const stat = statSync(filePath);
|
|
238
|
+
if (!stat.isFile())
|
|
239
|
+
return false;
|
|
240
|
+
if (stat.size > maxSize)
|
|
241
|
+
return false;
|
|
242
|
+
// Quick binary check: extension
|
|
243
|
+
const ext = extname(filePath).toLowerCase();
|
|
244
|
+
// If extension is in deny list of binaries
|
|
245
|
+
const binaryExts = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.pdf', '.zip', '.tar', '.gz', '.mp4', '.mp3', '.woff', '.woff2', '.ttf', '.eot', '.exe', '.dll', '.so', '.dylib', '.class', '.pyc'];
|
|
246
|
+
if (binaryExts.includes(ext))
|
|
247
|
+
return false;
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function walkDir(dir, ignore, maxFileSize, files = []) {
|
|
255
|
+
try {
|
|
256
|
+
const entries = readdirSync(dir);
|
|
257
|
+
for (const entry of entries) {
|
|
258
|
+
if (ignore.includes(entry))
|
|
259
|
+
continue;
|
|
260
|
+
const full = join(dir, entry);
|
|
261
|
+
try {
|
|
262
|
+
const stat = statSync(full);
|
|
263
|
+
if (stat.isDirectory()) {
|
|
264
|
+
if (shouldIgnore(full, ignore))
|
|
265
|
+
continue;
|
|
266
|
+
walkDir(full, ignore, maxFileSize, files);
|
|
267
|
+
}
|
|
268
|
+
else if (stat.isFile()) {
|
|
269
|
+
if (isProbablyTextFile(full, maxFileSize)) {
|
|
270
|
+
files.push(full);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// ignore unreadable dirs
|
|
281
|
+
}
|
|
282
|
+
return files;
|
|
283
|
+
}
|
|
284
|
+
export function scanSecrets(targetPath, opts = {}) {
|
|
285
|
+
const ignore = [...DEFAULT_IGNORE, ...(opts.ignore || [])];
|
|
286
|
+
const maxFileSize = opts.maxFileSizeBytes || 1_000_000; // 1MB
|
|
287
|
+
const extensions = opts.extensions || [];
|
|
288
|
+
if (!existsSync(targetPath)) {
|
|
289
|
+
throw new Error(`Target path not found: ${targetPath}`);
|
|
290
|
+
}
|
|
291
|
+
const stat = statSync(targetPath);
|
|
292
|
+
let files = [];
|
|
293
|
+
if (stat.isFile()) {
|
|
294
|
+
files = [targetPath];
|
|
295
|
+
}
|
|
296
|
+
else if (stat.isDirectory()) {
|
|
297
|
+
files = walkDir(targetPath, ignore, maxFileSize);
|
|
298
|
+
// filter by extensions if provided
|
|
299
|
+
if (extensions.length > 0) {
|
|
300
|
+
files = files.filter(f => {
|
|
301
|
+
const ext = extname(f).toLowerCase();
|
|
302
|
+
const base = f.split('/').pop() || '';
|
|
303
|
+
// allow .env files regardless of ext logic
|
|
304
|
+
if (base.startsWith('.env'))
|
|
305
|
+
return true;
|
|
306
|
+
return extensions.includes(ext) || extensions.includes(base);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const findings = [];
|
|
311
|
+
for (const file of files) {
|
|
312
|
+
// Skip huge .env.example? Allow
|
|
313
|
+
let content;
|
|
314
|
+
try {
|
|
315
|
+
content = readFileSync(file, 'utf-8');
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
// Quick skip if file too large after read
|
|
321
|
+
if (content.length > maxFileSize)
|
|
322
|
+
continue;
|
|
323
|
+
// Special handling for .env file detection
|
|
324
|
+
const baseName = file.split('/').pop() || '';
|
|
325
|
+
if (baseName === '.env' || baseName.startsWith('.env.')) {
|
|
326
|
+
// Check if .env is committed (should be finding INFO if in repo and contains secrets)
|
|
327
|
+
// But actual secret detection will still run via patterns
|
|
328
|
+
}
|
|
329
|
+
const lines = content.split(/\r?\n/);
|
|
330
|
+
for (let i = 0; i < lines.length; i++) {
|
|
331
|
+
const line = lines[i];
|
|
332
|
+
// Skip long lines (minified)
|
|
333
|
+
if (line.length > 2000)
|
|
334
|
+
continue;
|
|
335
|
+
for (const pattern of PATTERNS) {
|
|
336
|
+
// Optimization: quick pre-check for keywords to avoid regex on every line
|
|
337
|
+
// Not applied for generic high entropy to keep simple
|
|
338
|
+
// Reset regex lastIndex for global
|
|
339
|
+
pattern.regex.lastIndex = 0;
|
|
340
|
+
let match;
|
|
341
|
+
while ((match = pattern.regex.exec(line)) !== null) {
|
|
342
|
+
const raw = match[0];
|
|
343
|
+
// Avoid duplicate findings same file+line+pattern+match
|
|
344
|
+
// Also skip obvious false positives
|
|
345
|
+
if (raw.length < 8)
|
|
346
|
+
continue;
|
|
347
|
+
// Skip example / placeholder
|
|
348
|
+
const lower = raw.toLowerCase();
|
|
349
|
+
if (lower.includes('example') || lower.includes('placeholder') || lower.includes('your_') || lower.includes('testkey') || lower.includes('xxx')) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
// Skip if in comment that says "example" and not CRITICAL (to reduce noise)
|
|
353
|
+
if (line.toLowerCase().includes('example') && pattern.severity !== 'CRITICAL') {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const redacted = redactSecret(raw);
|
|
357
|
+
findings.push({
|
|
358
|
+
id: `VULN-${pattern.id}`,
|
|
359
|
+
patternId: pattern.id,
|
|
360
|
+
severity: pattern.severity,
|
|
361
|
+
cvss: pattern.cvss,
|
|
362
|
+
cvssVector: pattern.cvssVector,
|
|
363
|
+
file: file,
|
|
364
|
+
line: i + 1,
|
|
365
|
+
column: match.index,
|
|
366
|
+
match: redacted,
|
|
367
|
+
fullMatch: raw,
|
|
368
|
+
description: pattern.description,
|
|
369
|
+
remediation: pattern.remediation,
|
|
370
|
+
owasp: pattern.owasp,
|
|
371
|
+
cwe: pattern.cwe,
|
|
372
|
+
});
|
|
373
|
+
// Prevent infinite loop for zero-length matches
|
|
374
|
+
if (match[0].length === 0)
|
|
375
|
+
pattern.regex.lastIndex++;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Deduplicate (file+line+patternId)
|
|
381
|
+
const seen = new Set();
|
|
382
|
+
const deduped = [];
|
|
383
|
+
for (const f of findings) {
|
|
384
|
+
const key = `${f.file}:${f.line}:${f.patternId}:${f.match}`;
|
|
385
|
+
if (!seen.has(key)) {
|
|
386
|
+
seen.add(key);
|
|
387
|
+
deduped.push(f);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Sort by severity: CRITICAL > HIGH > MEDIUM > LOW > INFO
|
|
391
|
+
const order = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3, INFO: 4 };
|
|
392
|
+
deduped.sort((a, b) => {
|
|
393
|
+
const oa = order[a.severity];
|
|
394
|
+
const ob = order[b.severity];
|
|
395
|
+
if (oa !== ob)
|
|
396
|
+
return oa - ob;
|
|
397
|
+
return a.file.localeCompare(b.file);
|
|
398
|
+
});
|
|
399
|
+
return deduped;
|
|
400
|
+
}
|
|
401
|
+
export function formatFindingsText(findings, target) {
|
|
402
|
+
if (findings.length === 0) {
|
|
403
|
+
return `✅ No secrets found in ${target}\nScanned with ${PATTERNS.length} patterns.\n`;
|
|
404
|
+
}
|
|
405
|
+
const bySeverity = {};
|
|
406
|
+
for (const f of findings) {
|
|
407
|
+
bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
|
|
408
|
+
}
|
|
409
|
+
let out = `🔍 Secrets scan results for ${target}\n`;
|
|
410
|
+
out += `Total findings: ${findings.length}\n`;
|
|
411
|
+
out += `Breakdown: ${Object.entries(bySeverity).map(([k, v]) => `${k}:${v}`).join(' ')}\n\n`;
|
|
412
|
+
for (const f of findings) {
|
|
413
|
+
const icon = f.severity === 'CRITICAL' ? '🔴' : f.severity === 'HIGH' ? '🟠' : f.severity === 'MEDIUM' ? '🟡' : f.severity === 'LOW' ? '🟢' : 'ℹ️';
|
|
414
|
+
out += `${icon} [${f.severity}] ${f.patternId} — ${f.file}:${f.line}\n`;
|
|
415
|
+
out += ` Match: ${f.match}\n`;
|
|
416
|
+
out += ` CVSS: ${f.cvss} ${f.cvssVector}\n`;
|
|
417
|
+
out += ` Fix: ${f.remediation.split('.')[0]}.\n\n`;
|
|
418
|
+
}
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
export function formatFindingsJson(findings) {
|
|
422
|
+
return findings.map(f => ({
|
|
423
|
+
id: f.id,
|
|
424
|
+
patternId: f.patternId,
|
|
425
|
+
severity: f.severity,
|
|
426
|
+
cvss: f.cvss,
|
|
427
|
+
cvssVector: f.cvssVector,
|
|
428
|
+
file: f.file,
|
|
429
|
+
line: f.line,
|
|
430
|
+
match: f.match,
|
|
431
|
+
description: f.description,
|
|
432
|
+
remediation: f.remediation,
|
|
433
|
+
owasp: f.owasp,
|
|
434
|
+
cwe: f.cwe,
|
|
435
|
+
}));
|
|
436
|
+
}
|
|
437
|
+
export { PATTERNS };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { scanSecrets, PATTERNS } from './secrets.js';
|
|
6
|
+
// Helpers to avoid literal secrets in source (GitHub secret scanning would block push)
|
|
7
|
+
// We build secrets at runtime via concatenation, so source doesn't contain contiguous secret
|
|
8
|
+
function buildSecret(parts) {
|
|
9
|
+
return parts.join('');
|
|
10
|
+
}
|
|
11
|
+
describe('Secrets Scanner', () => {
|
|
12
|
+
let tmp;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tmp = mkdtempSync(join(tmpdir(), 'cyberaudit-secrets-'));
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
it('should have at least 14 patterns', () => {
|
|
20
|
+
expect(PATTERNS.length).toBeGreaterThanOrEqual(14);
|
|
21
|
+
});
|
|
22
|
+
it('should detect AWS access key', () => {
|
|
23
|
+
const key = buildSecret(['AKIA', '1234567890ABCDEF']);
|
|
24
|
+
writeFileSync(join(tmp, 'test.js'), `const key = "${key}";`);
|
|
25
|
+
const findings = scanSecrets(tmp);
|
|
26
|
+
const aws = findings.find(f => f.patternId === 'AWS_ACCESS_KEY');
|
|
27
|
+
expect(aws).toBeDefined();
|
|
28
|
+
expect(aws?.severity).toBe('CRITICAL');
|
|
29
|
+
expect(aws?.match).toContain('*');
|
|
30
|
+
});
|
|
31
|
+
it('should detect GitHub PAT', () => {
|
|
32
|
+
const pat = buildSecret(['ghp_', '1234567890abcdefghij1234567890abcdefgh']);
|
|
33
|
+
writeFileSync(join(tmp, 'config.ts'), `token = "${pat}"`);
|
|
34
|
+
const findings = scanSecrets(tmp);
|
|
35
|
+
const gh = findings.find(f => f.patternId === 'GITHUB_PAT');
|
|
36
|
+
expect(gh).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
it('should detect Stripe secret key', () => {
|
|
39
|
+
const sk = buildSecret(['sk_live_', '1234567890abcdefghij12345']);
|
|
40
|
+
writeFileSync(join(tmp, 'payment.js'), `const stripe = "${sk}";`);
|
|
41
|
+
const findings = scanSecrets(tmp);
|
|
42
|
+
expect(findings.some(f => f.patternId === 'STRIPE_SK')).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it('should detect private key', () => {
|
|
45
|
+
const begin = buildSecret(['-----BEGIN ', 'RSA PRIVATE KEY-----']);
|
|
46
|
+
writeFileSync(join(tmp, 'key.pem'), `${begin}\nMIIE...`);
|
|
47
|
+
const findings = scanSecrets(tmp);
|
|
48
|
+
expect(findings.some(f => f.patternId === 'PRIVATE_KEY')).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it('should detect NEXT_PUBLIC_ secret', () => {
|
|
51
|
+
const val = buildSecret(['NEXT_PUBLIC_API_SECRET=sk-abc123supersecretvalue']);
|
|
52
|
+
writeFileSync(join(tmp, '.env.local'), val);
|
|
53
|
+
const findings = scanSecrets(tmp);
|
|
54
|
+
expect(findings.some(f => f.patternId === 'NEXT_PUBLIC_SECRET')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
it('should detect database URL with password', () => {
|
|
57
|
+
const url = buildSecret(['postgres://user:super', 'secret123@localhost/db']);
|
|
58
|
+
writeFileSync(join(tmp, 'db.js'), `const url = "${url}";`);
|
|
59
|
+
const findings = scanSecrets(tmp);
|
|
60
|
+
expect(findings.some(f => f.patternId === 'DATABASE_URL')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
it('should ignore node_modules', () => {
|
|
63
|
+
const nm = join(tmp, 'node_modules');
|
|
64
|
+
mkdirSync(nm);
|
|
65
|
+
const key = buildSecret(['AKIA', '1234567890ABCDEF']);
|
|
66
|
+
writeFileSync(join(nm, 'bad.js'), `const key = "${key}";`);
|
|
67
|
+
const findings = scanSecrets(tmp);
|
|
68
|
+
expect(findings.length).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
it('should return empty when no secrets', () => {
|
|
71
|
+
writeFileSync(join(tmp, 'clean.js'), 'const a = 1; console.log("hello world");');
|
|
72
|
+
const findings = scanSecrets(tmp);
|
|
73
|
+
expect(findings.length).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
it('should sort by severity CRITICAL first', () => {
|
|
76
|
+
const key = buildSecret(['AKIA', '1234567890ABCDEF']);
|
|
77
|
+
writeFileSync(join(tmp, 'mixed.js'), `
|
|
78
|
+
const a = "${key}";
|
|
79
|
+
const b = "some high entropy string maybe? 'ABCDE'";
|
|
80
|
+
const c = 'password = "supersecret123"';
|
|
81
|
+
`);
|
|
82
|
+
const findings = scanSecrets(tmp);
|
|
83
|
+
expect(findings.length).toBeGreaterThanOrEqual(2);
|
|
84
|
+
expect(findings[0].severity).toBe('CRITICAL');
|
|
85
|
+
});
|
|
86
|
+
it('should redact secrets in output', () => {
|
|
87
|
+
const key = buildSecret(['AKIA', '1234567890ABCDEF']);
|
|
88
|
+
writeFileSync(join(tmp, 'test.js'), `const key = "${key}";`);
|
|
89
|
+
const findings = scanSecrets(tmp);
|
|
90
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
91
|
+
const f = findings[0];
|
|
92
|
+
expect(f.fullMatch).toBe(key);
|
|
93
|
+
expect(f.match).not.toBe(f.fullMatch);
|
|
94
|
+
expect(f.match).toContain('*');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type Severity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' | 'INFO';
|
|
2
|
+
export interface SecretFinding {
|
|
3
|
+
id: string;
|
|
4
|
+
patternId: string;
|
|
5
|
+
severity: Severity;
|
|
6
|
+
cvss: number;
|
|
7
|
+
cvssVector: string;
|
|
8
|
+
file: string;
|
|
9
|
+
line: number;
|
|
10
|
+
column?: number;
|
|
11
|
+
match: string;
|
|
12
|
+
fullMatch: string;
|
|
13
|
+
description: string;
|
|
14
|
+
remediation: string;
|
|
15
|
+
owasp?: string;
|
|
16
|
+
cwe?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ScanOptions {
|
|
19
|
+
ignore?: string[];
|
|
20
|
+
maxFileSizeBytes?: number;
|
|
21
|
+
extensions?: string[];
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Severity } from './types.js';
|
|
2
|
+
export interface WebFinding {
|
|
3
|
+
id: string;
|
|
4
|
+
patternId: string;
|
|
5
|
+
severity: Severity;
|
|
6
|
+
cvss: number;
|
|
7
|
+
cvssVector: string;
|
|
8
|
+
file: string;
|
|
9
|
+
line: number;
|
|
10
|
+
column?: number;
|
|
11
|
+
match: string;
|
|
12
|
+
description: string;
|
|
13
|
+
remediation: string;
|
|
14
|
+
owasp: string;
|
|
15
|
+
cwe: string;
|
|
16
|
+
framework?: string;
|
|
17
|
+
}
|
|
18
|
+
type WebPattern = {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
regex?: RegExp;
|
|
22
|
+
fileRegex?: RegExp;
|
|
23
|
+
severity: Severity;
|
|
24
|
+
cvss: number;
|
|
25
|
+
cvssVector: string;
|
|
26
|
+
description: string;
|
|
27
|
+
remediation: string;
|
|
28
|
+
owasp: string;
|
|
29
|
+
cwe: string;
|
|
30
|
+
extensions?: string[];
|
|
31
|
+
mustContain?: string[];
|
|
32
|
+
falsePositiveIfContains?: string[];
|
|
33
|
+
};
|
|
34
|
+
declare const WEB_PATTERNS: WebPattern[];
|
|
35
|
+
export declare function scanWeb(targetPath: string): WebFinding[];
|
|
36
|
+
export declare function formatWebFindingsText(findings: WebFinding[], target: string): string;
|
|
37
|
+
export { WEB_PATTERNS };
|