security-mcp 1.1.4 → 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.
Files changed (129) hide show
  1. package/README.md +116 -264
  2. package/defaults/checklists/ai.json +20 -1
  3. package/defaults/checklists/api.json +35 -1
  4. package/defaults/checklists/infra.json +34 -1
  5. package/defaults/checklists/mobile.json +23 -1
  6. package/defaults/checklists/payments.json +15 -1
  7. package/defaults/checklists/web.json +11 -1
  8. package/defaults/security-policy.json +2 -2
  9. package/dist/cli/index.js +0 -0
  10. package/dist/gate/baseline.js +82 -7
  11. package/dist/gate/catalog.js +10 -2
  12. package/dist/gate/checks/ai.js +757 -39
  13. package/dist/gate/checks/auth-deep.js +920 -216
  14. package/dist/gate/checks/business-logic.js +751 -0
  15. package/dist/gate/checks/ci-pipeline.js +399 -4
  16. package/dist/gate/checks/crypto.js +423 -2
  17. package/dist/gate/checks/dependencies.js +571 -15
  18. package/dist/gate/checks/graphql.js +201 -19
  19. package/dist/gate/checks/infra.js +246 -1
  20. package/dist/gate/checks/injection-deep.js +827 -184
  21. package/dist/gate/checks/k8s.js +114 -1
  22. package/dist/gate/checks/mobile-android.js +917 -3
  23. package/dist/gate/checks/mobile-ios.js +797 -5
  24. package/dist/gate/checks/required-artifacts.js +194 -0
  25. package/dist/gate/checks/runtime.js +178 -0
  26. package/dist/gate/checks/secrets.js +244 -13
  27. package/dist/gate/checks/supply-chain-deep.js +787 -0
  28. package/dist/gate/checks/web-nextjs.js +572 -48
  29. package/dist/gate/diff.js +17 -5
  30. package/dist/gate/evidence.js +8 -1
  31. package/dist/gate/exceptions.js +131 -9
  32. package/dist/gate/policy.js +280 -131
  33. package/dist/mcp/audit-chain.js +122 -28
  34. package/dist/mcp/auth.js +169 -0
  35. package/dist/mcp/learning.js +129 -4
  36. package/dist/mcp/model-router.js +158 -21
  37. package/dist/mcp/orchestration.js +186 -51
  38. package/dist/mcp/server.js +337 -53
  39. package/dist/repo/fs.js +24 -1
  40. package/dist/repo/search.js +31 -6
  41. package/dist/review/store.js +52 -1
  42. package/package.json +7 -7
  43. package/skills/_TEMPLATE/SKILL.md +99 -0
  44. package/skills/advanced-dos-tester/SKILL.md +109 -0
  45. package/skills/agentic-loop-exploiter/SKILL.md +368 -0
  46. package/skills/ai-llm-redteam/SKILL.md +104 -0
  47. package/skills/ai-model-supply-chain-agent/SKILL.md +103 -0
  48. package/skills/algorithm-implementation-reviewer/SKILL.md +98 -0
  49. package/skills/android-penetration-tester/SKILL.md +455 -46
  50. package/skills/anti-replay-tester/SKILL.md +106 -0
  51. package/skills/appsec-code-auditor/SKILL.md +85 -0
  52. package/skills/artifact-integrity-analyst/SKILL.md +441 -0
  53. package/skills/attack-navigator/SKILL.md +467 -8
  54. package/skills/auth-session-hacker/SKILL.md +102 -0
  55. package/skills/aws-penetration-tester/SKILL.md +456 -0
  56. package/skills/azure-penetration-tester/SKILL.md +490 -3
  57. package/skills/binary-auth-validator/SKILL.md +111 -0
  58. package/skills/bot-detection-specialist/SKILL.md +109 -0
  59. package/skills/business-logic-attacker/SKILL.md +231 -0
  60. package/skills/capec-code-mapper/SKILL.md +84 -0
  61. package/skills/cert-pin-rotation-specialist/SKILL.md +112 -0
  62. package/skills/cicd-pipeline-hijacker/SKILL.md +405 -0
  63. package/skills/ciso-orchestrator/SKILL.md +454 -43
  64. package/skills/cloud-infra-specialist/SKILL.md +118 -0
  65. package/skills/compliance-gap-analyst/SKILL.md +422 -0
  66. package/skills/compliance-grc/SKILL.md +85 -0
  67. package/skills/compliance-lifecycle-tracker/SKILL.md +84 -0
  68. package/skills/credential-stuffing-specialist/SKILL.md +102 -0
  69. package/skills/crypto-pki-specialist/SKILL.md +87 -0
  70. package/skills/csa-ccm-mapper/SKILL.md +84 -0
  71. package/skills/csf2-governance-mapper/SKILL.md +84 -0
  72. package/skills/deep-link-fuzzer/SKILL.md +109 -0
  73. package/skills/dependency-confusion-attacker/SKILL.md +415 -0
  74. package/skills/device-integrity-aggregator/SKILL.md +108 -0
  75. package/skills/dos-resilience-tester/SKILL.md +97 -0
  76. package/skills/dread-scorer/SKILL.md +84 -0
  77. package/skills/egress-policy-enforcer/SKILL.md +99 -0
  78. package/skills/evidence-collector/SKILL.md +98 -0
  79. package/skills/file-upload-attacker/SKILL.md +109 -0
  80. package/skills/gcp-penetration-tester/SKILL.md +459 -2
  81. package/skills/git-history-secret-scanner/SKILL.md +106 -0
  82. package/skills/iam-privesc-graph-builder/SKILL.md +152 -0
  83. package/skills/incident-responder/SKILL.md +111 -0
  84. package/skills/injection-specialist/SKILL.md +102 -0
  85. package/skills/ios-security-auditor/SKILL.md +282 -0
  86. package/skills/json-ambiguity-tester/SKILL.md +0 -0
  87. package/skills/k8s-container-escaper/SKILL.md +384 -0
  88. package/skills/key-management-lifecycle-analyst/SKILL.md +98 -0
  89. package/skills/kill-switch-engineer/SKILL.md +102 -0
  90. package/skills/linddun-privacy-analyst/SKILL.md +102 -0
  91. package/skills/logic-race-fuzzer/SKILL.md +443 -0
  92. package/skills/mobile-api-network-attacker/SKILL.md +421 -0
  93. package/skills/mobile-binary-hardener/SKILL.md +102 -0
  94. package/skills/mobile-security-specialist/SKILL.md +85 -0
  95. package/skills/mobile-webview-auditor/SKILL.md +96 -0
  96. package/skills/model-extraction-attacker/SKILL.md +219 -0
  97. package/skills/multipart-abuse-tester/SKILL.md +84 -0
  98. package/skills/oauth-pkce-specialist/SKILL.md +104 -0
  99. package/skills/parser-exhaustion-tester/SKILL.md +142 -0
  100. package/skills/pentest-infra/SKILL.md +98 -0
  101. package/skills/pentest-social/SKILL.md +201 -0
  102. package/skills/pentest-team/SKILL.md +87 -0
  103. package/skills/pentest-web-api/SKILL.md +98 -0
  104. package/skills/privacy-flow-analyst/SKILL.md +234 -0
  105. package/skills/prompt-injection-specialist/SKILL.md +394 -0
  106. package/skills/quantum-migration-planner/SKILL.md +96 -0
  107. package/skills/rag-poisoning-specialist/SKILL.md +358 -0
  108. package/skills/registry-mirror-enforcer/SKILL.md +84 -0
  109. package/skills/rotation-validation-agent/SKILL.md +112 -0
  110. package/skills/samm-assessor/SKILL.md +85 -0
  111. package/skills/secrets-mask-bypass-tester/SKILL.md +100 -0
  112. package/skills/senior-security-engineer/SKILL.md +167 -0
  113. package/skills/serialization-memory-attacker/SKILL.md +332 -0
  114. package/skills/session-timeout-tester/SKILL.md +161 -0
  115. package/skills/slsa-level3-enforcer/SKILL.md +112 -0
  116. package/skills/slsa-provenance-enforcer/SKILL.md +102 -0
  117. package/skills/ssrf-detection-validator/SKILL.md +108 -0
  118. package/skills/step-up-auth-enforcer/SKILL.md +84 -0
  119. package/skills/stride-pasta-analyst/SKILL.md +420 -0
  120. package/skills/supply-chain-devsecops/SKILL.md +98 -0
  121. package/skills/threat-infrastructure-analyst/SKILL.md +84 -0
  122. package/skills/threat-modeler/SKILL.md +85 -0
  123. package/skills/tls-certificate-auditor/SKILL.md +573 -18
  124. package/skills/token-reuse-detector/SKILL.md +95 -0
  125. package/skills/trike-risk-modeler/SKILL.md +84 -0
  126. package/skills/unicode-homograph-tester/SKILL.md +84 -0
  127. package/skills/waf-rule-lifecycle-agent/SKILL.md +97 -0
  128. package/skills/webhook-security-tester/SKILL.md +102 -0
  129. 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 for security use
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: "HIGH",
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)));