security-mcp 1.1.3 → 1.3.1
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 +164 -185
- package/defaults/checklists/ai.json +20 -1
- package/defaults/checklists/api.json +35 -1
- package/defaults/checklists/infra.json +34 -1
- package/defaults/checklists/mobile.json +23 -1
- package/defaults/checklists/payments.json +15 -1
- package/defaults/checklists/web.json +11 -1
- package/defaults/control-catalog.json +200 -0
- package/defaults/security-policy.json +2 -2
- package/dist/cli/index.js +82 -5
- package/dist/cli/install.js +36 -6
- package/dist/cli/onboarding.js +6 -0
- package/dist/gate/baseline.js +82 -7
- package/dist/gate/catalog.js +10 -2
- package/dist/gate/checks/ai.js +757 -39
- package/dist/gate/checks/auth-deep.js +935 -0
- package/dist/gate/checks/business-logic.js +751 -0
- package/dist/gate/checks/ci-pipeline.js +399 -4
- package/dist/gate/checks/crypto.js +423 -2
- package/dist/gate/checks/dependencies.js +571 -15
- package/dist/gate/checks/graphql.js +201 -19
- package/dist/gate/checks/infra.js +246 -1
- package/dist/gate/checks/injection-deep.js +848 -0
- package/dist/gate/checks/k8s.js +114 -1
- package/dist/gate/checks/mobile-android.js +917 -3
- package/dist/gate/checks/mobile-ios.js +797 -5
- package/dist/gate/checks/required-artifacts.js +194 -0
- package/dist/gate/checks/runtime.js +178 -0
- package/dist/gate/checks/secrets.js +244 -13
- package/dist/gate/checks/supply-chain-deep.js +787 -0
- package/dist/gate/checks/web-nextjs.js +572 -48
- package/dist/gate/diff.js +17 -5
- package/dist/gate/evidence.js +8 -1
- package/dist/gate/exceptions.js +131 -9
- package/dist/gate/policy.js +282 -129
- package/dist/mcp/audit-chain.js +122 -28
- package/dist/mcp/auth.js +169 -0
- package/dist/mcp/learning.js +129 -4
- package/dist/mcp/model-router.js +158 -21
- package/dist/mcp/orchestration.js +186 -51
- package/dist/mcp/server.js +608 -94
- package/dist/repo/fs.js +24 -1
- package/dist/repo/search.js +31 -6
- package/dist/review/store.js +52 -1
- package/package.json +7 -7
- package/prompts/SECURITY_PROMPT.md +73 -0
- package/skills/_TEMPLATE/SKILL.md +99 -0
- package/skills/advanced-dos-tester/SKILL.md +109 -0
- package/skills/agentic-loop-exploiter/SKILL.md +368 -0
- package/skills/ai-llm-redteam/SKILL.md +104 -0
- package/skills/ai-model-supply-chain-agent/SKILL.md +103 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +98 -0
- package/skills/android-penetration-tester/SKILL.md +455 -46
- package/skills/anti-replay-tester/SKILL.md +106 -0
- package/skills/appsec-code-auditor/SKILL.md +120 -0
- package/skills/artifact-integrity-analyst/SKILL.md +441 -0
- package/skills/attack-navigator/SKILL.md +467 -8
- package/skills/auth-session-hacker/SKILL.md +128 -0
- package/skills/aws-penetration-tester/SKILL.md +456 -0
- package/skills/azure-penetration-tester/SKILL.md +490 -3
- package/skills/binary-auth-validator/SKILL.md +111 -0
- package/skills/bot-detection-specialist/SKILL.md +109 -0
- package/skills/business-logic-attacker/SKILL.md +231 -0
- package/skills/capec-code-mapper/SKILL.md +84 -0
- package/skills/cert-pin-rotation-specialist/SKILL.md +112 -0
- package/skills/cicd-pipeline-hijacker/SKILL.md +405 -0
- package/skills/ciso-orchestrator/SKILL.md +454 -43
- package/skills/cloud-infra-specialist/SKILL.md +118 -0
- package/skills/compliance-gap-analyst/SKILL.md +422 -0
- package/skills/compliance-grc/SKILL.md +85 -0
- package/skills/compliance-lifecycle-tracker/SKILL.md +84 -0
- package/skills/credential-stuffing-specialist/SKILL.md +102 -0
- package/skills/crypto-pki-specialist/SKILL.md +87 -0
- package/skills/csa-ccm-mapper/SKILL.md +84 -0
- package/skills/csf2-governance-mapper/SKILL.md +84 -0
- package/skills/deep-link-fuzzer/SKILL.md +109 -0
- package/skills/dependency-confusion-attacker/SKILL.md +415 -0
- package/skills/device-integrity-aggregator/SKILL.md +108 -0
- package/skills/dos-resilience-tester/SKILL.md +97 -0
- package/skills/dread-scorer/SKILL.md +84 -0
- package/skills/egress-policy-enforcer/SKILL.md +99 -0
- package/skills/evidence-collector/SKILL.md +98 -0
- package/skills/file-upload-attacker/SKILL.md +109 -0
- package/skills/gcp-penetration-tester/SKILL.md +459 -2
- package/skills/git-history-secret-scanner/SKILL.md +106 -0
- package/skills/iam-privesc-graph-builder/SKILL.md +152 -0
- package/skills/incident-responder/SKILL.md +111 -0
- package/skills/injection-specialist/SKILL.md +131 -0
- package/skills/ios-security-auditor/SKILL.md +282 -0
- package/skills/json-ambiguity-tester/SKILL.md +0 -0
- package/skills/k8s-container-escaper/SKILL.md +384 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +98 -0
- package/skills/kill-switch-engineer/SKILL.md +102 -0
- package/skills/linddun-privacy-analyst/SKILL.md +102 -0
- package/skills/logic-race-fuzzer/SKILL.md +443 -0
- package/skills/mobile-api-network-attacker/SKILL.md +421 -0
- package/skills/mobile-binary-hardener/SKILL.md +102 -0
- package/skills/mobile-security-specialist/SKILL.md +85 -0
- package/skills/mobile-webview-auditor/SKILL.md +96 -0
- package/skills/model-extraction-attacker/SKILL.md +219 -0
- package/skills/multipart-abuse-tester/SKILL.md +84 -0
- package/skills/oauth-pkce-specialist/SKILL.md +104 -0
- package/skills/parser-exhaustion-tester/SKILL.md +142 -0
- package/skills/pentest-infra/SKILL.md +141 -0
- package/skills/pentest-social/SKILL.md +201 -0
- package/skills/pentest-team/SKILL.md +134 -0
- package/skills/pentest-web-api/SKILL.md +151 -0
- package/skills/privacy-flow-analyst/SKILL.md +234 -0
- package/skills/prompt-injection-specialist/SKILL.md +394 -0
- package/skills/quantum-migration-planner/SKILL.md +96 -0
- package/skills/rag-poisoning-specialist/SKILL.md +358 -0
- package/skills/registry-mirror-enforcer/SKILL.md +84 -0
- package/skills/rotation-validation-agent/SKILL.md +112 -0
- package/skills/samm-assessor/SKILL.md +85 -0
- package/skills/secrets-mask-bypass-tester/SKILL.md +100 -0
- package/skills/senior-security-engineer/SKILL.md +370 -2
- package/skills/serialization-memory-attacker/SKILL.md +332 -0
- package/skills/session-timeout-tester/SKILL.md +161 -0
- package/skills/slsa-level3-enforcer/SKILL.md +112 -0
- package/skills/slsa-provenance-enforcer/SKILL.md +102 -0
- package/skills/ssrf-detection-validator/SKILL.md +108 -0
- package/skills/step-up-auth-enforcer/SKILL.md +84 -0
- package/skills/stride-pasta-analyst/SKILL.md +420 -0
- package/skills/supply-chain-devsecops/SKILL.md +98 -0
- package/skills/threat-infrastructure-analyst/SKILL.md +84 -0
- package/skills/threat-modeler/SKILL.md +85 -0
- package/skills/tls-certificate-auditor/SKILL.md +573 -18
- package/skills/token-reuse-detector/SKILL.md +95 -0
- package/skills/trike-risk-modeler/SKILL.md +84 -0
- package/skills/unicode-homograph-tester/SKILL.md +84 -0
- package/skills/waf-rule-lifecycle-agent/SKILL.md +97 -0
- package/skills/webhook-security-tester/SKILL.md +102 -0
- package/skills/zero-trust-architect/SKILL.md +109 -0
|
@@ -26,6 +26,381 @@ function checkPbkdf2Iterations(hits) {
|
|
|
26
26
|
}
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
29
|
+
async function checkAesCbcUnauthenticated() {
|
|
30
|
+
const findings = [];
|
|
31
|
+
// Primary: string literal match
|
|
32
|
+
const cbcLiteralHits = await searchRepo({
|
|
33
|
+
query: String.raw `createCipheriv\s*\(\s*['"]aes-(?:128|192|256)-cbc['"]`,
|
|
34
|
+
isRegex: true,
|
|
35
|
+
maxMatches: 200
|
|
36
|
+
});
|
|
37
|
+
// Secondary: detect concatenated or dynamic AES-CBC strings that evade the
|
|
38
|
+
// string-literal regex (e.g. 'aes-' + '256-cbc', `aes-${bits}-cbc`).
|
|
39
|
+
// CWE-327 evasion via string concatenation is a documented bypass technique.
|
|
40
|
+
const cbcConcatHits = await searchRepo({
|
|
41
|
+
query: String.raw `createCipheriv\s*\([^)]*['"\x60][^)]*-cbc['"\x60]|['"]aes-['"].*cbc|['"\x60].*-cbc['"\x60].*createCipheriv`,
|
|
42
|
+
isRegex: true,
|
|
43
|
+
maxMatches: 200
|
|
44
|
+
});
|
|
45
|
+
const cbcHits = [
|
|
46
|
+
...cbcLiteralHits,
|
|
47
|
+
...cbcConcatHits.filter((h) => !cbcLiteralHits.some((l) => l.file === h.file && l.line === h.line))
|
|
48
|
+
];
|
|
49
|
+
if (cbcHits.length === 0)
|
|
50
|
+
return findings;
|
|
51
|
+
// Check for HMAC authentication near AES-CBC usage
|
|
52
|
+
const hmacHits = await searchRepo({
|
|
53
|
+
query: String.raw `createHmac|hmac\.digest|crypto\.sign|authenticate`,
|
|
54
|
+
isRegex: true,
|
|
55
|
+
maxMatches: 200
|
|
56
|
+
});
|
|
57
|
+
// If AES-CBC is used and no HMAC found anywhere nearby, flag it
|
|
58
|
+
const hmacFiles = new Set(hmacHits.map((m) => m.file));
|
|
59
|
+
const unauthenticated = cbcHits.filter((m) => !hmacFiles.has(m.file));
|
|
60
|
+
if (unauthenticated.length > 0) {
|
|
61
|
+
findings.push({
|
|
62
|
+
id: "CRYPTO_AES_CBC_NO_AUTH",
|
|
63
|
+
title: "AES-CBC without HMAC authentication is vulnerable to padding oracle attacks. Use AES-256-GCM instead.",
|
|
64
|
+
severity: "CRITICAL",
|
|
65
|
+
evidence: unauthenticated.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
66
|
+
files: [...new Set(unauthenticated.slice(0, 10).map((m) => m.file))],
|
|
67
|
+
requiredActions: [
|
|
68
|
+
"Replace createCipheriv('aes-256-cbc') with createCipheriv('aes-256-gcm') and use the GCM authentication tag"
|
|
69
|
+
]
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return findings;
|
|
73
|
+
}
|
|
74
|
+
async function checkGcmNonceReuse() {
|
|
75
|
+
const findings = [];
|
|
76
|
+
const gcmHits = await searchRepo({
|
|
77
|
+
query: String.raw `createCipheriv\s*\(\s*['"]aes-(?:128|192|256)-gcm['"]`,
|
|
78
|
+
isRegex: true,
|
|
79
|
+
maxMatches: 200
|
|
80
|
+
});
|
|
81
|
+
if (gcmHits.length === 0)
|
|
82
|
+
return findings;
|
|
83
|
+
// Check for nonce reuse patterns
|
|
84
|
+
const nonceReuseHits = await searchRepo({
|
|
85
|
+
query: String.raw `(?:let|var)\s+(?:iv|nonce|counter)\s*=|iv\+\+|nonce\+\+|counter\+\+|iv\s*\+=|nonce\s*\+=|Date\.now\(\)|new Date\(\)|performance\.now`,
|
|
86
|
+
isRegex: true,
|
|
87
|
+
maxMatches: 200
|
|
88
|
+
});
|
|
89
|
+
const gcmFiles = new Set(gcmHits.map((m) => m.file));
|
|
90
|
+
const reuseInGcmFiles = nonceReuseHits.filter((m) => gcmFiles.has(m.file));
|
|
91
|
+
if (reuseInGcmFiles.length > 0) {
|
|
92
|
+
findings.push({
|
|
93
|
+
id: "CRYPTO_GCM_NONCE_REUSE_RISK",
|
|
94
|
+
title: "GCM nonce reuse risk detected — mutable or time-based IV/nonce near AES-GCM cipher",
|
|
95
|
+
severity: "CRITICAL",
|
|
96
|
+
evidence: reuseInGcmFiles.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
97
|
+
files: [...new Set(reuseInGcmFiles.slice(0, 10).map((m) => m.file))],
|
|
98
|
+
requiredActions: [
|
|
99
|
+
"Generate a fresh random 12-byte nonce for every encryption with crypto.randomBytes(12).",
|
|
100
|
+
"GCM nonce reuse completely breaks confidentiality and authentication."
|
|
101
|
+
]
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Check for missing crypto.randomBytes near GCM usage
|
|
105
|
+
const randomBytesHits = await searchRepo({
|
|
106
|
+
query: String.raw `crypto\.randomBytes`,
|
|
107
|
+
isRegex: true,
|
|
108
|
+
maxMatches: 200
|
|
109
|
+
});
|
|
110
|
+
const randomBytesFiles = new Set(randomBytesHits.map((m) => m.file));
|
|
111
|
+
const gcmWithoutRandom = gcmHits.filter((m) => !randomBytesFiles.has(m.file));
|
|
112
|
+
if (gcmWithoutRandom.length > 0) {
|
|
113
|
+
findings.push({
|
|
114
|
+
id: "CRYPTO_GCM_NO_RANDOM_NONCE",
|
|
115
|
+
title: "AES-GCM used without crypto.randomBytes for nonce generation",
|
|
116
|
+
severity: "HIGH",
|
|
117
|
+
evidence: gcmWithoutRandom.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
118
|
+
files: [...new Set(gcmWithoutRandom.slice(0, 10).map((m) => m.file))],
|
|
119
|
+
requiredActions: [
|
|
120
|
+
"Use crypto.randomBytes(12) to generate a random 12-byte nonce for each AES-GCM encryption.",
|
|
121
|
+
"Never use a fixed, sequential, or time-based nonce with GCM."
|
|
122
|
+
]
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// Check for module-level (top-scope) nonce/iv assigned from randomBytes — reused across calls.
|
|
126
|
+
// Pattern: const/let iv = (crypto.)randomBytes(...) appearing at module scope (not inside a function).
|
|
127
|
+
// Heuristic: the assignment is not indented (or indented only by whitespace without a function keyword
|
|
128
|
+
// on the same line), combined with GCM usage in the same file.
|
|
129
|
+
const moduleLevelNonceHits = await searchRepo({
|
|
130
|
+
query: String.raw `^(?:const|let|var)\s+(?:iv|nonce|counter)\s*=\s*(?:crypto\.)?randomBytes\s*\(`,
|
|
131
|
+
isRegex: true,
|
|
132
|
+
maxMatches: 200
|
|
133
|
+
});
|
|
134
|
+
const moduleLevelInGcmFiles = moduleLevelNonceHits.filter((m) => gcmFiles.has(m.file));
|
|
135
|
+
if (moduleLevelInGcmFiles.length > 0) {
|
|
136
|
+
findings.push({
|
|
137
|
+
id: "CRYPTO_GCM_MODULE_LEVEL_NONCE",
|
|
138
|
+
title: "AES-GCM nonce generated at module scope — nonce is reused across all encrypt calls",
|
|
139
|
+
severity: "CRITICAL",
|
|
140
|
+
evidence: moduleLevelInGcmFiles.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
141
|
+
files: [...new Set(moduleLevelInGcmFiles.slice(0, 10).map((m) => m.file))],
|
|
142
|
+
requiredActions: [
|
|
143
|
+
"Move crypto.randomBytes(12) inside the encryption function so a fresh nonce is generated per call.",
|
|
144
|
+
"A module-level nonce is initialised once and reused — GCM nonce reuse completely breaks confidentiality and authentication (CWE-329)."
|
|
145
|
+
]
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return findings;
|
|
149
|
+
}
|
|
150
|
+
async function checkRsaPaddingScheme() {
|
|
151
|
+
const findings = [];
|
|
152
|
+
const rsaHits = await searchRepo({
|
|
153
|
+
query: String.raw `crypto\.publicEncrypt|crypto\.privateDecrypt`,
|
|
154
|
+
isRegex: true,
|
|
155
|
+
maxMatches: 200
|
|
156
|
+
});
|
|
157
|
+
if (rsaHits.length === 0)
|
|
158
|
+
return findings;
|
|
159
|
+
// Check for explicit OAEP padding
|
|
160
|
+
const oaepHits = await searchRepo({
|
|
161
|
+
query: String.raw `RSA_PKCS1_OAEP_PADDING|oaepHash`,
|
|
162
|
+
isRegex: true,
|
|
163
|
+
maxMatches: 200
|
|
164
|
+
});
|
|
165
|
+
// Check for explicit PKCS1 v1.5 padding
|
|
166
|
+
const pkcs1Hits = await searchRepo({
|
|
167
|
+
query: String.raw `RSA_PKCS1_PADDING|'pkcs1'|padding.*PKCS1`,
|
|
168
|
+
isRegex: true,
|
|
169
|
+
maxMatches: 200
|
|
170
|
+
});
|
|
171
|
+
const oaepFiles = new Set(oaepHits.map((m) => m.file));
|
|
172
|
+
const rsaWithoutOaep = rsaHits.filter((m) => !oaepFiles.has(m.file));
|
|
173
|
+
if (rsaWithoutOaep.length > 0 || pkcs1Hits.length > 0) {
|
|
174
|
+
const allEvidence = [...rsaWithoutOaep, ...pkcs1Hits].slice(0, 10);
|
|
175
|
+
findings.push({
|
|
176
|
+
id: "CRYPTO_RSA_PKCS1_PADDING",
|
|
177
|
+
title: "RSA PKCS#1 v1.5 padding is vulnerable to Bleichenbacher attacks. Use RSA-OAEP padding: { key, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }",
|
|
178
|
+
severity: "HIGH",
|
|
179
|
+
evidence: allEvidence.map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
180
|
+
files: [...new Set(allEvidence.map((m) => m.file))],
|
|
181
|
+
requiredActions: [
|
|
182
|
+
"Pass an options object with padding: crypto.constants.RSA_PKCS1_OAEP_PADDING to publicEncrypt/privateDecrypt.",
|
|
183
|
+
"The default RSA padding (PKCS#1 v1.5) is vulnerable to adaptive chosen-ciphertext attacks."
|
|
184
|
+
]
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return findings;
|
|
188
|
+
}
|
|
189
|
+
async function checkShaUsedForPassword(weakHashHits) {
|
|
190
|
+
const findings = [];
|
|
191
|
+
// Detect SHA-256/384/512 used in password context
|
|
192
|
+
const shaPasswordHits = await searchRepo({
|
|
193
|
+
query: String.raw `createHash\s*\(\s*['"]sha(?:256|384|512|2)['"]`,
|
|
194
|
+
isRegex: true,
|
|
195
|
+
maxMatches: 200
|
|
196
|
+
});
|
|
197
|
+
if (shaPasswordHits.length === 0)
|
|
198
|
+
return findings;
|
|
199
|
+
const passwordContextRe = /password|passwd|pwd|credential/i;
|
|
200
|
+
const shaPasswordContext = shaPasswordHits.filter((m) => passwordContextRe.test(m.preview));
|
|
201
|
+
// Also search for direct pattern: createHash('sha256').update(password
|
|
202
|
+
const directPatternHits = await searchRepo({
|
|
203
|
+
query: String.raw `createHash\s*\(\s*['"]sha(?:256|384|512)['"]\s*\)\.update\s*\(\s*(?:password|passwd|pwd)`,
|
|
204
|
+
isRegex: true,
|
|
205
|
+
maxMatches: 200
|
|
206
|
+
});
|
|
207
|
+
const combined = [...shaPasswordContext, ...directPatternHits];
|
|
208
|
+
const unique = combined.filter((m, i, arr) => arr.findIndex((x) => x.file === m.file && x.line === m.line) === i);
|
|
209
|
+
if (unique.length > 0) {
|
|
210
|
+
findings.push({
|
|
211
|
+
id: "CRYPTO_SHA_USED_FOR_PASSWORD",
|
|
212
|
+
title: "SHA-256/SHA-512 are fast hash functions unsuitable for password storage. Use bcrypt, argon2, or scrypt.",
|
|
213
|
+
severity: "HIGH",
|
|
214
|
+
evidence: unique.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
215
|
+
files: [...new Set(unique.slice(0, 10).map((m) => m.file))],
|
|
216
|
+
requiredActions: [
|
|
217
|
+
"Replace SHA-based password hashing with bcrypt (cost ≥ 12), argon2id, or scrypt.",
|
|
218
|
+
"Fast hash functions allow billions of guesses per second with GPU hardware."
|
|
219
|
+
]
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return findings;
|
|
223
|
+
}
|
|
224
|
+
async function checkHardcodedSalt() {
|
|
225
|
+
const findings = [];
|
|
226
|
+
const hardcodedSaltHits = await searchRepo({
|
|
227
|
+
query: String.raw `pbkdf2(?:Sync)?\s*\([^,]+,\s*(?:['"][^'"]{1,}['"]|Buffer\.from\s*\(\s*['"][^'"]+['"]\s*\))`,
|
|
228
|
+
isRegex: true,
|
|
229
|
+
maxMatches: 200
|
|
230
|
+
});
|
|
231
|
+
if (hardcodedSaltHits.length > 0) {
|
|
232
|
+
findings.push({
|
|
233
|
+
id: "CRYPTO_PBKDF2_HARDCODED_SALT",
|
|
234
|
+
title: "Hardcoded salt makes PBKDF2 equivalent to an unsalted hash. Generate a unique random salt per user with crypto.randomBytes(32).",
|
|
235
|
+
severity: "HIGH",
|
|
236
|
+
evidence: hardcodedSaltHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
237
|
+
files: [...new Set(hardcodedSaltHits.slice(0, 10).map((m) => m.file))],
|
|
238
|
+
requiredActions: [
|
|
239
|
+
"Replace the hardcoded salt with crypto.randomBytes(32) generated uniquely per user.",
|
|
240
|
+
"Store the random salt alongside the hash in the database."
|
|
241
|
+
]
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return findings;
|
|
245
|
+
}
|
|
246
|
+
async function checkTlsConfig() {
|
|
247
|
+
const findings = [];
|
|
248
|
+
// Check for weak TLS minimum version
|
|
249
|
+
const weakTlsHits = await searchRepo({
|
|
250
|
+
query: String.raw `minVersion\s*:\s*['"]TLSv1(?:\.[01])?['"]|secureProtocol\s*:\s*['"](?:SSLv3|TLSv1)_method['"]`,
|
|
251
|
+
isRegex: true,
|
|
252
|
+
maxMatches: 200
|
|
253
|
+
});
|
|
254
|
+
if (weakTlsHits.length > 0) {
|
|
255
|
+
findings.push({
|
|
256
|
+
id: "TLS_WEAK_MIN_VERSION",
|
|
257
|
+
title: "TLS 1.0/1.1 or SSL configured as minimum version — insecure protocol",
|
|
258
|
+
severity: "HIGH",
|
|
259
|
+
evidence: weakTlsHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
260
|
+
files: [...new Set(weakTlsHits.slice(0, 10).map((m) => m.file))],
|
|
261
|
+
requiredActions: [
|
|
262
|
+
"Set minVersion: 'TLSv1.2' or 'TLSv1.3' in TLS/HTTPS server configuration.",
|
|
263
|
+
"TLS 1.0 and 1.1 are deprecated by RFC 8996 and prohibited by PCI DSS 4.0."
|
|
264
|
+
]
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// Check for disabled certificate verification
|
|
268
|
+
const rejectUnauthorizedHits = await searchRepo({
|
|
269
|
+
query: String.raw `rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]0['"]`,
|
|
270
|
+
isRegex: true,
|
|
271
|
+
maxMatches: 200
|
|
272
|
+
});
|
|
273
|
+
if (rejectUnauthorizedHits.length > 0) {
|
|
274
|
+
findings.push({
|
|
275
|
+
id: "TLS_REJECT_UNAUTHORIZED_DISABLED",
|
|
276
|
+
title: "rejectUnauthorized: false disables TLS certificate verification, enabling MITM attacks.",
|
|
277
|
+
severity: "HIGH",
|
|
278
|
+
evidence: rejectUnauthorizedHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
279
|
+
files: [...new Set(rejectUnauthorizedHits.slice(0, 10).map((m) => m.file))],
|
|
280
|
+
requiredActions: [
|
|
281
|
+
"Remove rejectUnauthorized: false and fix the underlying certificate issue.",
|
|
282
|
+
"If using a self-signed cert, add it via the ca option rather than disabling verification."
|
|
283
|
+
]
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return findings;
|
|
287
|
+
}
|
|
288
|
+
async function checkZeroFilledIv() {
|
|
289
|
+
const findings = [];
|
|
290
|
+
const zeroIvHits = await searchRepo({
|
|
291
|
+
query: String.raw `(?:Buffer\.alloc\s*\(\s*(?:8|12|16|24|32)\s*\)|new\s+Uint8Array\s*\(\s*(?:8|12|16|24|32)\s*\))[^\n]*(?:iv|IV|nonce|Nonce)`,
|
|
292
|
+
isRegex: true,
|
|
293
|
+
maxMatches: 200
|
|
294
|
+
});
|
|
295
|
+
const zeroIvAssignHits = await searchRepo({
|
|
296
|
+
query: String.raw `(?:iv|nonce)\s*=\s*Buffer\.alloc\s*\(`,
|
|
297
|
+
isRegex: true,
|
|
298
|
+
maxMatches: 200
|
|
299
|
+
});
|
|
300
|
+
const combined = [
|
|
301
|
+
...zeroIvHits,
|
|
302
|
+
...zeroIvAssignHits.filter((h) => !zeroIvHits.some((l) => l.file === h.file && l.line === h.line))
|
|
303
|
+
];
|
|
304
|
+
if (combined.length > 0) {
|
|
305
|
+
findings.push({
|
|
306
|
+
id: "CRYPTO_ZERO_IV",
|
|
307
|
+
title: "Zero-filled IV or nonce (Buffer.alloc creates all-zeros) — deterministic IV breaks cipher security (CWE-330)",
|
|
308
|
+
severity: "CRITICAL",
|
|
309
|
+
evidence: combined.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
310
|
+
files: [...new Set(combined.slice(0, 10).map((m) => m.file))],
|
|
311
|
+
requiredActions: [
|
|
312
|
+
"Replace Buffer.alloc(n) with crypto.randomBytes(n) for IV/nonce generation.",
|
|
313
|
+
"A zero-filled IV is equivalent to a hardcoded IV — every encryption with the same key produces the same ciphertext."
|
|
314
|
+
]
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return findings;
|
|
318
|
+
}
|
|
319
|
+
async function checkWeakRsaKeySize() {
|
|
320
|
+
const findings = [];
|
|
321
|
+
const weakRsaHits = await searchRepo({
|
|
322
|
+
query: String.raw `modulusLength\s*:\s*(?:512|768|1536)`,
|
|
323
|
+
isRegex: true,
|
|
324
|
+
maxMatches: 200
|
|
325
|
+
});
|
|
326
|
+
if (weakRsaHits.length > 0) {
|
|
327
|
+
findings.push({
|
|
328
|
+
id: "CRYPTO_RSA_WEAK_KEY",
|
|
329
|
+
title: "RSA key size 512/768/1536 bits — sub-2048 keys factorable with commodity hardware (CWE-326)",
|
|
330
|
+
severity: "CRITICAL",
|
|
331
|
+
evidence: weakRsaHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
332
|
+
files: [...new Set(weakRsaHits.slice(0, 10).map((m) => m.file))],
|
|
333
|
+
requiredActions: [
|
|
334
|
+
"Use a minimum modulusLength of 2048; prefer 4096 for long-lived keys.",
|
|
335
|
+
"Keys below 2048 bits can be factored with commodity hardware and are prohibited by NIST SP 800-131A Rev 2."
|
|
336
|
+
]
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return findings;
|
|
340
|
+
}
|
|
341
|
+
async function checkWeakDhParams() {
|
|
342
|
+
const findings = [];
|
|
343
|
+
const weakDhSizeHits = await searchRepo({
|
|
344
|
+
query: String.raw `createDiffieHellman\s*\(\s*(?:[0-9]{1,3}|1[0-9]{3}|[5-9][0-9]{2})\s*[,)]`,
|
|
345
|
+
isRegex: true,
|
|
346
|
+
maxMatches: 200
|
|
347
|
+
});
|
|
348
|
+
const weakDhGroupHits = await searchRepo({
|
|
349
|
+
query: String.raw `createDiffieHellmanGroup\s*\(\s*['"]modp(?:1|2|5)['"]`,
|
|
350
|
+
isRegex: true,
|
|
351
|
+
maxMatches: 200
|
|
352
|
+
});
|
|
353
|
+
const combined = [
|
|
354
|
+
...weakDhSizeHits,
|
|
355
|
+
...weakDhGroupHits.filter((h) => !weakDhSizeHits.some((l) => l.file === h.file && l.line === h.line))
|
|
356
|
+
];
|
|
357
|
+
if (combined.length > 0) {
|
|
358
|
+
findings.push({
|
|
359
|
+
id: "CRYPTO_WEAK_DH_PARAMS",
|
|
360
|
+
title: "DH parameters below 2048 bits or weak group (modp1/2/5) — vulnerable to Logjam precomputation (CWE-326)",
|
|
361
|
+
severity: "HIGH",
|
|
362
|
+
evidence: combined.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
363
|
+
files: [...new Set(combined.slice(0, 10).map((m) => m.file))],
|
|
364
|
+
requiredActions: [
|
|
365
|
+
"Use createDiffieHellmanGroup('modp14') or higher (modp14 = 2048-bit), or prefer ECDH with P-256 or P-384.",
|
|
366
|
+
"modp1/2/5 and DH groups below 2048 bits are broken by Logjam-style precomputation attacks."
|
|
367
|
+
]
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return findings;
|
|
371
|
+
}
|
|
372
|
+
async function checkMissingForwardSecrecy() {
|
|
373
|
+
const findings = [];
|
|
374
|
+
const weakCipherSuiteHits = await searchRepo({
|
|
375
|
+
query: String.raw `ciphers\s*:\s*['"][^'"]*(?:TLS_RSA_WITH|RC4|NULL|EXPORT|!ECDHE|!DHE)[^'"]*['"]`,
|
|
376
|
+
isRegex: true,
|
|
377
|
+
maxMatches: 200
|
|
378
|
+
});
|
|
379
|
+
const honorCipherOrderHits = await searchRepo({
|
|
380
|
+
query: String.raw `honorCipherOrder\s*:\s*false`,
|
|
381
|
+
isRegex: true,
|
|
382
|
+
maxMatches: 200
|
|
383
|
+
});
|
|
384
|
+
const combined = [
|
|
385
|
+
...weakCipherSuiteHits,
|
|
386
|
+
...honorCipherOrderHits.filter((h) => !weakCipherSuiteHits.some((l) => l.file === h.file && l.line === h.line))
|
|
387
|
+
];
|
|
388
|
+
if (combined.length > 0) {
|
|
389
|
+
findings.push({
|
|
390
|
+
id: "CRYPTO_NO_FORWARD_SECRECY",
|
|
391
|
+
title: "TLS cipher suite config without forward secrecy (no ECDHE/DHE) — retroactive decryption possible (PCI DSS 4.0)",
|
|
392
|
+
severity: "HIGH",
|
|
393
|
+
evidence: combined.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
394
|
+
files: [...new Set(combined.slice(0, 10).map((m) => m.file))],
|
|
395
|
+
requiredActions: [
|
|
396
|
+
"Configure ciphers to prefer ECDHE or DHE key exchange (e.g. 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256').",
|
|
397
|
+
"Set honorCipherOrder: true so the server's cipher preference (which should list ECDHE first) takes effect.",
|
|
398
|
+
"Without forward secrecy, a compromised private key retroactively decrypts all recorded sessions (PCI DSS 4.0 requirement 4.2.1)."
|
|
399
|
+
]
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return findings;
|
|
403
|
+
}
|
|
29
404
|
export async function checkCrypto(_opts) {
|
|
30
405
|
const findings = [];
|
|
31
406
|
try {
|
|
@@ -48,6 +423,9 @@ export async function checkCrypto(_opts) {
|
|
|
48
423
|
]
|
|
49
424
|
});
|
|
50
425
|
}
|
|
426
|
+
// SHA-256/512 used for password hashing (extends weak hash check)
|
|
427
|
+
const shaPasswordFindings = await checkShaUsedForPassword(weakHashHits);
|
|
428
|
+
findings.push(...shaPasswordFindings);
|
|
51
429
|
// 2. Weak symmetric ciphers
|
|
52
430
|
const weakCipherHits = await searchRepo({
|
|
53
431
|
query: String.raw `createCipheriv\s*\(\s*['"](?:des|rc4|rc2|blowfish|3des|des-ede)['"]\)|Cipher\.getInstance\(['"](?:DES|RC4|RC2|Blowfish)['"]`,
|
|
@@ -67,19 +445,20 @@ export async function checkCrypto(_opts) {
|
|
|
67
445
|
]
|
|
68
446
|
});
|
|
69
447
|
}
|
|
70
|
-
// 3. Insecure random
|
|
448
|
+
// 3. Insecure random — security-specific contexts (CRITICAL)
|
|
71
449
|
const insecureRandomHits = await searchRepo({
|
|
72
450
|
query: String.raw `Math\.random\(\)|random\.random\(\)|rand\(\)|srand\(`,
|
|
73
451
|
isRegex: true,
|
|
74
452
|
maxMatches: 200
|
|
75
453
|
});
|
|
76
454
|
const securityContextRe = /token|key|secret|password|nonce|salt|csrf|session/i;
|
|
455
|
+
const identifierContextRe = /id|path|url|upload|order|invoice|coupon|code|ref|link|hash/i;
|
|
77
456
|
const insecureSecRandom = insecureRandomHits.filter((m) => securityContextRe.test(m.preview));
|
|
78
457
|
if (insecureSecRandom.length > 0) {
|
|
79
458
|
findings.push({
|
|
80
459
|
id: "CRYPTO_INSECURE_RANDOM",
|
|
81
460
|
title: "Non-cryptographic random used in security-sensitive context",
|
|
82
|
-
severity: "
|
|
461
|
+
severity: "CRITICAL",
|
|
83
462
|
evidence: insecureSecRandom.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
84
463
|
files: [...new Set(insecureSecRandom.slice(0, 10).map((m) => m.file))],
|
|
85
464
|
requiredActions: [
|
|
@@ -88,6 +467,21 @@ export async function checkCrypto(_opts) {
|
|
|
88
467
|
]
|
|
89
468
|
});
|
|
90
469
|
}
|
|
470
|
+
// Insecure random — identifier/path contexts (HIGH)
|
|
471
|
+
const insecureIdentifierRandom = insecureRandomHits.filter((m) => !securityContextRe.test(m.preview) && identifierContextRe.test(m.preview));
|
|
472
|
+
if (insecureIdentifierRandom.length > 0) {
|
|
473
|
+
findings.push({
|
|
474
|
+
id: "CRYPTO_INSECURE_RANDOM_IDENTIFIER",
|
|
475
|
+
title: "Non-cryptographic random used to generate identifiers or paths — predictable IDs enable enumeration attacks",
|
|
476
|
+
severity: "HIGH",
|
|
477
|
+
evidence: insecureIdentifierRandom.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
478
|
+
files: [...new Set(insecureIdentifierRandom.slice(0, 10).map((m) => m.file))],
|
|
479
|
+
requiredActions: [
|
|
480
|
+
"Use crypto.randomBytes() or crypto.randomUUID() for generating IDs, paths, and codes.",
|
|
481
|
+
"Predictable identifiers enable IDOR and enumeration attacks."
|
|
482
|
+
]
|
|
483
|
+
});
|
|
484
|
+
}
|
|
91
485
|
// 4. Weak JWT algorithm
|
|
92
486
|
const weakJwtHits = await searchRepo({
|
|
93
487
|
query: String.raw `algorithm\s*[:=]\s*['"]HS(?:256|384|512)['"]|sign\(.*['"]HS256['"]`,
|
|
@@ -116,6 +510,9 @@ export async function checkCrypto(_opts) {
|
|
|
116
510
|
const pbkdf2Finding = checkPbkdf2Iterations(pbkdf2Hits);
|
|
117
511
|
if (pbkdf2Finding)
|
|
118
512
|
findings.push(pbkdf2Finding);
|
|
513
|
+
// Hardcoded PBKDF2 salt
|
|
514
|
+
const hardcodedSaltFindings = await checkHardcodedSalt();
|
|
515
|
+
findings.push(...hardcodedSaltFindings);
|
|
119
516
|
// 6. Hardcoded IV/nonce
|
|
120
517
|
const hardcodedIvHits = await searchRepo({
|
|
121
518
|
query: String.raw `iv\s*[:=]\s*(?:Buffer\.from\(['"][0-9a-fA-F]+['"]\)|['"][0-9a-fA-F]{16,}['"])`,
|
|
@@ -214,6 +611,30 @@ export async function checkCrypto(_opts) {
|
|
|
214
611
|
]
|
|
215
612
|
});
|
|
216
613
|
}
|
|
614
|
+
// 11. AES-CBC without authentication (padding oracle)
|
|
615
|
+
const aesCbcFindings = await checkAesCbcUnauthenticated();
|
|
616
|
+
findings.push(...aesCbcFindings);
|
|
617
|
+
// 12. GCM nonce reuse
|
|
618
|
+
const gcmNonceFindings = await checkGcmNonceReuse();
|
|
619
|
+
findings.push(...gcmNonceFindings);
|
|
620
|
+
// 13. RSA PKCS#1 v1.5 padding
|
|
621
|
+
const rsaPaddingFindings = await checkRsaPaddingScheme();
|
|
622
|
+
findings.push(...rsaPaddingFindings);
|
|
623
|
+
// 14. TLS configuration weaknesses
|
|
624
|
+
const tlsFindings = await checkTlsConfig();
|
|
625
|
+
findings.push(...tlsFindings);
|
|
626
|
+
// 15. Zero-filled IV/nonce
|
|
627
|
+
const zeroIvFindings = await checkZeroFilledIv();
|
|
628
|
+
findings.push(...zeroIvFindings);
|
|
629
|
+
// 16. Weak RSA key sizes (512/768/1536)
|
|
630
|
+
const weakRsaKeyFindings = await checkWeakRsaKeySize();
|
|
631
|
+
findings.push(...weakRsaKeyFindings);
|
|
632
|
+
// 17. Weak DH parameters or named groups
|
|
633
|
+
const weakDhFindings = await checkWeakDhParams();
|
|
634
|
+
findings.push(...weakDhFindings);
|
|
635
|
+
// 18. Missing forward secrecy in TLS cipher config
|
|
636
|
+
const forwardSecrecyFindings = await checkMissingForwardSecrecy();
|
|
637
|
+
findings.push(...forwardSecrecyFindings);
|
|
217
638
|
}
|
|
218
639
|
catch (err) {
|
|
219
640
|
console.warn("[checkCrypto] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
|