security-mcp 1.1.4 → 1.3.3

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 (158) hide show
  1. package/README.md +341 -1018
  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/cloud-controls/aws.json +10712 -0
  9. package/defaults/cloud-controls/azure.json +7201 -0
  10. package/defaults/cloud-controls/gcp.json +4061 -0
  11. package/defaults/control-catalog.json +24 -0
  12. package/defaults/security-policy.json +2 -2
  13. package/dist/ci/pr-gate.js +22 -5
  14. package/dist/cli/index.js +73 -2
  15. package/dist/cli/install.js +4 -55
  16. package/dist/cli/onboarding.js +18 -10
  17. package/dist/gate/baseline.js +82 -7
  18. package/dist/gate/catalog.js +10 -2
  19. package/dist/gate/checks/agentic-instructions.js +515 -0
  20. package/dist/gate/checks/ai-governance.js +132 -0
  21. package/dist/gate/checks/ai.js +757 -39
  22. package/dist/gate/checks/auth-deep.js +920 -216
  23. package/dist/gate/checks/business-logic.js +751 -0
  24. package/dist/gate/checks/ci-pipeline.js +399 -4
  25. package/dist/gate/checks/cloud-controls.js +69 -0
  26. package/dist/gate/checks/crypto.js +423 -2
  27. package/dist/gate/checks/data-platform.js +954 -0
  28. package/dist/gate/checks/dependencies.js +582 -15
  29. package/dist/gate/checks/docker-deep.js +1236 -0
  30. package/dist/gate/checks/gitops.js +724 -0
  31. package/dist/gate/checks/graphql.js +201 -19
  32. package/dist/gate/checks/iac.js +1230 -0
  33. package/dist/gate/checks/infra.js +246 -1
  34. package/dist/gate/checks/injection-deep.js +827 -184
  35. package/dist/gate/checks/k8s.js +955 -2
  36. package/dist/gate/checks/mobile-android.js +917 -3
  37. package/dist/gate/checks/mobile-ios.js +797 -5
  38. package/dist/gate/checks/required-artifacts.js +194 -0
  39. package/dist/gate/checks/runtime.js +178 -0
  40. package/dist/gate/checks/secrets.js +256 -13
  41. package/dist/gate/checks/supply-chain-deep.js +787 -0
  42. package/dist/gate/checks/web-nextjs.js +572 -48
  43. package/dist/gate/cloud-controls/apply.js +115 -0
  44. package/dist/gate/cloud-controls/bicep.js +36 -0
  45. package/dist/gate/cloud-controls/cfn.js +125 -0
  46. package/dist/gate/cloud-controls/detect.js +104 -0
  47. package/dist/gate/cloud-controls/hcl.js +140 -0
  48. package/dist/gate/cloud-controls/types.js +87 -0
  49. package/dist/gate/diff.js +17 -5
  50. package/dist/gate/evidence.js +8 -1
  51. package/dist/gate/exceptions.js +202 -9
  52. package/dist/gate/findings.js +15 -2
  53. package/dist/gate/policy.js +316 -130
  54. package/dist/gate/threat-intel.js +6 -0
  55. package/dist/mcp/audit-chain.js +131 -28
  56. package/dist/mcp/auth.js +169 -0
  57. package/dist/mcp/learning.js +129 -4
  58. package/dist/mcp/model-router.js +161 -24
  59. package/dist/mcp/orchestration.js +377 -89
  60. package/dist/mcp/server.js +460 -69
  61. package/dist/mcp/tool-audit.js +193 -0
  62. package/dist/repo/fs.js +37 -1
  63. package/dist/repo/search.js +31 -6
  64. package/dist/review/store.js +56 -3
  65. package/dist/tests/run.js +124 -1
  66. package/package.json +9 -9
  67. package/skills/_TEMPLATE/SKILL.md +99 -0
  68. package/skills/advanced-dos-tester/SKILL.md +118 -0
  69. package/skills/agentic-instruction-auditor/SKILL.md +111 -0
  70. package/skills/agentic-loop-exploiter/SKILL.md +377 -0
  71. package/skills/ai-llm-redteam/SKILL.md +113 -0
  72. package/skills/ai-model-supply-chain-agent/SKILL.md +112 -0
  73. package/skills/algorithm-implementation-reviewer/SKILL.md +107 -0
  74. package/skills/android-penetration-tester/SKILL.md +464 -46
  75. package/skills/anti-replay-tester/SKILL.md +115 -0
  76. package/skills/appsec-code-auditor/SKILL.md +94 -0
  77. package/skills/artifact-integrity-analyst/SKILL.md +450 -0
  78. package/skills/attack-navigator/SKILL.md +476 -8
  79. package/skills/auth-session-hacker/SKILL.md +111 -0
  80. package/skills/aws-penetration-tester/SKILL.md +510 -0
  81. package/skills/azure-penetration-tester/SKILL.md +542 -3
  82. package/skills/binary-auth-validator/SKILL.md +120 -0
  83. package/skills/bot-detection-specialist/SKILL.md +118 -0
  84. package/skills/business-logic-attacker/SKILL.md +240 -0
  85. package/skills/capec-code-mapper/SKILL.md +93 -0
  86. package/skills/cert-pin-rotation-specialist/SKILL.md +121 -0
  87. package/skills/cicd-pipeline-hijacker/SKILL.md +414 -0
  88. package/skills/ciso-orchestrator/SKILL.md +465 -43
  89. package/skills/cloud-infra-specialist/SKILL.md +127 -0
  90. package/skills/compliance-gap-analyst/SKILL.md +431 -0
  91. package/skills/compliance-grc/SKILL.md +94 -0
  92. package/skills/compliance-lifecycle-tracker/SKILL.md +93 -0
  93. package/skills/container-hardening-auditor/SKILL.md +125 -0
  94. package/skills/credential-stuffing-specialist/SKILL.md +111 -0
  95. package/skills/crypto-pki-specialist/SKILL.md +96 -0
  96. package/skills/csa-ccm-mapper/SKILL.md +93 -0
  97. package/skills/csf2-governance-mapper/SKILL.md +93 -0
  98. package/skills/data-platform-auditor/SKILL.md +125 -0
  99. package/skills/deep-link-fuzzer/SKILL.md +118 -0
  100. package/skills/dependency-confusion-attacker/SKILL.md +424 -0
  101. package/skills/device-integrity-aggregator/SKILL.md +117 -0
  102. package/skills/dos-resilience-tester/SKILL.md +106 -0
  103. package/skills/dread-scorer/SKILL.md +93 -0
  104. package/skills/egress-policy-enforcer/SKILL.md +108 -0
  105. package/skills/evidence-collector/SKILL.md +107 -0
  106. package/skills/file-upload-attacker/SKILL.md +118 -0
  107. package/skills/gcp-penetration-tester/SKILL.md +510 -2
  108. package/skills/git-history-secret-scanner/SKILL.md +115 -0
  109. package/skills/gitops-delivery-auditor/SKILL.md +120 -0
  110. package/skills/iac-security-auditor/SKILL.md +125 -0
  111. package/skills/iam-privesc-graph-builder/SKILL.md +161 -0
  112. package/skills/incident-responder/SKILL.md +120 -0
  113. package/skills/injection-specialist/SKILL.md +111 -0
  114. package/skills/ios-security-auditor/SKILL.md +291 -0
  115. package/skills/json-ambiguity-tester/SKILL.md +145 -0
  116. package/skills/k8s-container-escaper/SKILL.md +406 -0
  117. package/skills/key-management-lifecycle-analyst/SKILL.md +107 -0
  118. package/skills/kill-switch-engineer/SKILL.md +111 -0
  119. package/skills/linddun-privacy-analyst/SKILL.md +111 -0
  120. package/skills/logic-race-fuzzer/SKILL.md +452 -0
  121. package/skills/mobile-api-network-attacker/SKILL.md +430 -0
  122. package/skills/mobile-binary-hardener/SKILL.md +111 -0
  123. package/skills/mobile-security-specialist/SKILL.md +94 -0
  124. package/skills/mobile-webview-auditor/SKILL.md +105 -0
  125. package/skills/model-extraction-attacker/SKILL.md +228 -0
  126. package/skills/multipart-abuse-tester/SKILL.md +93 -0
  127. package/skills/oauth-pkce-specialist/SKILL.md +113 -0
  128. package/skills/parser-exhaustion-tester/SKILL.md +151 -0
  129. package/skills/pentest-infra/SKILL.md +107 -0
  130. package/skills/pentest-social/SKILL.md +210 -0
  131. package/skills/pentest-team/SKILL.md +96 -0
  132. package/skills/pentest-web-api/SKILL.md +107 -0
  133. package/skills/privacy-flow-analyst/SKILL.md +243 -0
  134. package/skills/prompt-injection-specialist/SKILL.md +403 -0
  135. package/skills/quantum-migration-planner/SKILL.md +105 -0
  136. package/skills/rag-poisoning-specialist/SKILL.md +367 -0
  137. package/skills/registry-mirror-enforcer/SKILL.md +93 -0
  138. package/skills/rotation-validation-agent/SKILL.md +121 -0
  139. package/skills/samm-assessor/SKILL.md +94 -0
  140. package/skills/secrets-mask-bypass-tester/SKILL.md +109 -0
  141. package/skills/senior-security-engineer/SKILL.md +178 -0
  142. package/skills/serialization-memory-attacker/SKILL.md +341 -0
  143. package/skills/session-timeout-tester/SKILL.md +170 -0
  144. package/skills/slsa-level3-enforcer/SKILL.md +121 -0
  145. package/skills/slsa-provenance-enforcer/SKILL.md +111 -0
  146. package/skills/ssrf-detection-validator/SKILL.md +117 -0
  147. package/skills/step-up-auth-enforcer/SKILL.md +93 -0
  148. package/skills/stride-pasta-analyst/SKILL.md +429 -0
  149. package/skills/supply-chain-devsecops/SKILL.md +107 -0
  150. package/skills/threat-infrastructure-analyst/SKILL.md +93 -0
  151. package/skills/threat-modeler/SKILL.md +94 -0
  152. package/skills/tls-certificate-auditor/SKILL.md +582 -18
  153. package/skills/token-reuse-detector/SKILL.md +104 -0
  154. package/skills/trike-risk-modeler/SKILL.md +93 -0
  155. package/skills/unicode-homograph-tester/SKILL.md +93 -0
  156. package/skills/waf-rule-lifecycle-agent/SKILL.md +106 -0
  157. package/skills/webhook-security-tester/SKILL.md +111 -0
  158. package/skills/zero-trust-architect/SKILL.md +118 -0
@@ -58,6 +58,131 @@ async function fetchScorecardScore(dep) {
58
58
  return null;
59
59
  }
60
60
  }
61
+ // Known public registry scopes — scoped packages under these are NOT private
62
+ const KNOWN_PUBLIC_SCOPES = new Set([
63
+ "@types", "@babel", "@testing-library", "@jest", "@storybook", "@emotion",
64
+ "@mui", "@angular", "@vue", "@svelte", "@nestjs", "@aws-sdk", "@google-cloud",
65
+ "@azure", "@microsoft", "@graphql-codegen", "@typescript-eslint", "@eslint",
66
+ "@rollup", "@vitejs", "@vitest", "@remix-run", "@next", "@vercel", "@sentry",
67
+ "@opentelemetry", "@prisma", "@trpc", "@tanstack", "@radix-ui", "@headlessui",
68
+ "@tailwindcss", "@postcss", "@node-red", "@npmcli"
69
+ ]);
70
+ async function checkDependencyConfusion() {
71
+ const findings = [];
72
+ try {
73
+ let pkgRaw;
74
+ try {
75
+ pkgRaw = await readFile("package.json", "utf8");
76
+ }
77
+ catch {
78
+ return [];
79
+ }
80
+ const pkg = JSON.parse(pkgRaw);
81
+ const allDeps = {
82
+ ...pkg.dependencies,
83
+ ...pkg.devDependencies
84
+ };
85
+ // Read .npmrc for private registry scope routing
86
+ let npmrcContent = "";
87
+ try {
88
+ npmrcContent = await readFile(".npmrc", "utf8");
89
+ }
90
+ catch {
91
+ // .npmrc absent
92
+ }
93
+ const unprotectedScopes = [];
94
+ for (const name of Object.keys(allDeps)) {
95
+ if (!name.startsWith("@"))
96
+ continue;
97
+ const scope = name.split("/")[0]; // e.g. "@mycompany"
98
+ if (!scope)
99
+ continue;
100
+ if (KNOWN_PUBLIC_SCOPES.has(scope))
101
+ continue;
102
+ // Check if .npmrc has a registry entry for this scope
103
+ // e.g. @mycompany:registry=https://npm.mycompany.com
104
+ const escapedScope = scope.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
105
+ const scopeRegistryRe = new RegExp(`${escapedScope}\\s*:.*registry\\s*=|registry\\s*=.*${escapedScope}`, "i");
106
+ if (!scopeRegistryRe.test(npmrcContent)) {
107
+ unprotectedScopes.push(`${name} (scope ${scope} has no private registry entry in .npmrc)`);
108
+ }
109
+ }
110
+ // Also check for common typosquat vectors against critical ecosystem packages
111
+ const EXTENDED_TYPOSQUATS = {
112
+ "prism": "prisma",
113
+ "prismaa": "prisma",
114
+ "nextjs": "next",
115
+ "nextt": "next",
116
+ "nuxt3": "nuxt",
117
+ "vue3": "vue",
118
+ "sveltejs": "svelte",
119
+ "mongoosejs": "mongoose",
120
+ "sequelizejs": "sequelize",
121
+ "passportjs": "passport",
122
+ "jsonwebtoken-": "jsonwebtoken",
123
+ "bcryptjs-": "bcrypt",
124
+ "multerjs": "multer",
125
+ "axiosjs": "axios",
126
+ "socketio": "socket.io",
127
+ "redisjs": "redis",
128
+ "mysql-2": "mysql2",
129
+ "pgg": "pg",
130
+ "typeormjs": "typeorm",
131
+ "expressjs": "express",
132
+ "fastifyjs": "fastify",
133
+ "helmetjs": "helmet",
134
+ "corsjs": "cors"
135
+ };
136
+ const extendedHits = [];
137
+ for (const [name] of Object.entries(allDeps)) {
138
+ const normalized = name.toLowerCase();
139
+ if (EXTENDED_TYPOSQUATS[normalized]) {
140
+ extendedHits.push(`"${name}" (possible typo of "${EXTENDED_TYPOSQUATS[normalized]}")`);
141
+ }
142
+ }
143
+ if (unprotectedScopes.length > 0) {
144
+ findings.push({
145
+ id: "DEPENDENCY_CONFUSION_RISK",
146
+ title: `${unprotectedScopes.length} scoped package(s) lack a private registry entry in .npmrc — dependency confusion risk`,
147
+ severity: "HIGH",
148
+ evidence: unprotectedScopes.slice(0, 10),
149
+ requiredActions: [
150
+ "Scoped packages without a private registry mapping in .npmrc will resolve from the public npm registry, enabling dependency confusion attacks.",
151
+ "ATT&CK T1195.002 — an attacker can publish a higher-versioned package to the public registry under your private scope name.",
152
+ "Fix: add to .npmrc: @yourscope:registry=https://your-private-registry.example.com"
153
+ ]
154
+ });
155
+ }
156
+ if (extendedHits.length > 0) {
157
+ findings.push({
158
+ id: "DEP_TYPOSQUAT_EXTENDED",
159
+ title: "Possible typosquatted ecosystem package name(s) detected",
160
+ severity: "CRITICAL",
161
+ evidence: extendedHits.slice(0, 10),
162
+ requiredActions: [
163
+ "Verify each flagged package is the intended dependency — typosquatting replaces legitimate packages with malicious ones.",
164
+ "Remove the package, run `npm install` with the correctly-spelled name, and audit `package-lock.json`.",
165
+ "Use `npm audit` and review the package on npmjs.com before reinstalling."
166
+ ]
167
+ });
168
+ }
169
+ }
170
+ catch (err) {
171
+ console.warn("[checkDependencyConfusion] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
172
+ }
173
+ return findings;
174
+ }
175
+ // Malicious lifecycle script patterns
176
+ const MALICIOUS_SCRIPT_RES = [
177
+ /curl\s+[^\s]+\s*\|\s*(?:sh|bash)/,
178
+ /wget\s+[^\s]+\s*\|\s*(?:sh|bash)/,
179
+ /node\s+-e\s+['"`]\s*(?:eval|require|exec|spawn)/,
180
+ /base64\s+--decode\s*\|\s*(?:sh|bash)/,
181
+ /python\s+-c\s+['"`]\s*(?:import os|exec|eval)/,
182
+ ];
183
+ function isScriptMalicious(scriptValue) {
184
+ return MALICIOUS_SCRIPT_RES.some((re) => re.test(scriptValue));
185
+ }
61
186
  async function checkNpmProvenance() {
62
187
  const findings = [];
63
188
  try {
@@ -99,12 +224,38 @@ async function checkNpmProvenance() {
99
224
  catch {
100
225
  // npm not available or < v9 — skip gracefully
101
226
  }
102
- // 2. OpenSSF Scorecard for top 5 production deps
227
+ // 2. OpenSSF Scorecard check all prod deps and CI-executed dev deps, up to 20 total
103
228
  try {
104
229
  const pkgRaw = await readFile("package.json", "utf8");
105
230
  const pkg = JSON.parse(pkgRaw);
106
- const prodDeps = Object.keys(pkg.dependencies ?? {}).slice(0, 5);
107
- for (const dep of prodDeps) {
231
+ const prodDeps = Object.keys(pkg.dependencies ?? {});
232
+ // Determine which devDependencies are executed in CI scripts
233
+ const scriptText = Object.values(pkg.scripts ?? {}).join(" ");
234
+ const devDepsUsedInScripts = Object.keys(pkg.devDependencies ?? {}).filter((dep) => {
235
+ // Check if the dep name (or its binary form) is referenced in any script
236
+ const shortName = dep.replace(/^@[^/]+\//, "").replace(/-/g, "[-_]?");
237
+ try {
238
+ return new RegExp(shortName, "i").test(scriptText);
239
+ }
240
+ catch {
241
+ return false;
242
+ }
243
+ });
244
+ // Merge: prod deps first, then CI-executed dev deps, up to 20.
245
+ // CWE-200: never transmit private/internal package names to public
246
+ // endpoints (registry.npmjs.org / securityscorecards.dev). Skip any
247
+ // scoped package whose scope is not a known-public scope.
248
+ const depsToCheck = [...new Set([...prodDeps, ...devDepsUsedInScripts])]
249
+ .filter((dep) => {
250
+ const scope = dep.startsWith("@") ? dep.split("/")[0] : null;
251
+ return scope === null || KNOWN_PUBLIC_SCOPES.has(scope);
252
+ })
253
+ .slice(0, 20);
254
+ const totalAllDeps = prodDeps.length + Object.keys(pkg.devDependencies ?? {}).length;
255
+ // CWE-200: allow operators of private repos to disable all third-party
256
+ // network egress (scorecard/EPSS/registry lookups) with SECURITY_OFFLINE.
257
+ const offline = process.env["SECURITY_OFFLINE"] === "1" || process.env["SECURITY_OFFLINE"] === "true";
258
+ for (const dep of (offline ? [] : depsToCheck)) {
108
259
  const score = await fetchScorecardScore(dep);
109
260
  if (score !== null && score < 5.0) {
110
261
  findings.push({
@@ -119,6 +270,20 @@ async function checkNpmProvenance() {
119
270
  });
120
271
  }
121
272
  }
273
+ // Report partial coverage when total deps exceed the cap
274
+ if (totalAllDeps > 20) {
275
+ findings.push({
276
+ id: "SUPPLY_CHAIN_SCORECARD_PARTIAL",
277
+ title: `OpenSSF Scorecard checked ${depsToCheck.length} of ${totalAllDeps} total dependencies — coverage is partial`,
278
+ severity: "LOW",
279
+ evidence: [`Checked: ${depsToCheck.length} deps. Unchecked: ${totalAllDeps - depsToCheck.length} deps.`],
280
+ requiredActions: [
281
+ `${totalAllDeps - depsToCheck.length} dependencies were not checked against OpenSSF Scorecard due to the 20-dep cap.`,
282
+ "Run `npx ossf-scorecard` or review https://scorecard.dev for full coverage.",
283
+ "Consider using socket.dev or Snyk for continuous supply chain monitoring across all dependencies."
284
+ ]
285
+ });
286
+ }
122
287
  }
123
288
  catch {
124
289
  // package.json unreadable or API unavailable — skip
@@ -174,8 +339,18 @@ export async function checkDependencies(_) {
174
339
  findings.push(...transitive);
175
340
  const typosquat = await checkTyposquatting();
176
341
  findings.push(...typosquat);
342
+ const depConfusion = await checkDependencyConfusion();
343
+ findings.push(...depConfusion);
177
344
  const threatIntel = await checkCveExploitation();
178
345
  findings.push(...threatIntel);
346
+ const goSum = await checkGoSumMissing();
347
+ findings.push(...goSum);
348
+ const cargoLock = await checkCargoLockMissing();
349
+ findings.push(...cargoLock);
350
+ const lockfileSync = await checkLockfileSync();
351
+ findings.push(...lockfileSync);
352
+ const maintainerRisk = await checkMaintainerRisk();
353
+ findings.push(...maintainerRisk);
179
354
  return findings;
180
355
  }
181
356
  function extractCveIds(audit) {
@@ -265,35 +440,163 @@ function hasLifecycleScript(pkg) {
265
440
  function scanLockfilePackages(packages) {
266
441
  const scriptPkgs = [];
267
442
  const missingIntegrityPkgs = [];
443
+ const maliciousScriptPkgs = [];
268
444
  for (const [name, pkg] of Object.entries(packages)) {
269
445
  if (!name)
270
446
  continue; // skip root entry
271
447
  const pkgName = name.replace(/^node_modules\//, "");
272
- if (hasLifecycleScript(pkg))
448
+ if (hasLifecycleScript(pkg)) {
273
449
  scriptPkgs.push(pkgName);
450
+ // Check each lifecycle script VALUE for malicious patterns
451
+ for (const scriptKey of LIFECYCLE_SCRIPTS) {
452
+ const scriptVal = pkg.scripts?.[scriptKey];
453
+ if (scriptVal && isScriptMalicious(scriptVal)) {
454
+ maliciousScriptPkgs.push(`${pkgName} [${scriptKey}]: ${scriptVal.slice(0, 120)}`);
455
+ }
456
+ }
457
+ }
274
458
  if (pkg.version && !pkg.integrity)
275
459
  missingIntegrityPkgs.push(pkgName);
276
460
  }
277
- return { scriptPkgs, missingIntegrityPkgs };
461
+ return { scriptPkgs, missingIntegrityPkgs, maliciousScriptPkgs };
462
+ }
463
+ /**
464
+ * Scan yarn.lock content for lifecycle script values using a regex-based approach
465
+ * (yarn.lock is a custom format, not JSON/YAML, so we use heuristic line scanning).
466
+ * We look for `postinstall`, `preinstall`, or `install` key lines followed by a value
467
+ * in the same stanza, and check the value for malicious patterns.
468
+ */
469
+ function scanYarnLockForMaliciousScripts(content) {
470
+ const maliciousScriptPkgs = [];
471
+ const scriptPkgs = [];
472
+ const lines = content.split("\n");
473
+ let currentPkg = "";
474
+ for (const line of lines) {
475
+ // New stanza starts with a non-space character that ends with a colon (package header)
476
+ const pkgHeader = /^"?([^"#\s][^:]*)(?:@[^:]+)?":?\s*$/.exec(line);
477
+ if (pkgHeader) {
478
+ currentPkg = pkgHeader[1].trim();
479
+ continue;
480
+ }
481
+ // Lifecycle script lines inside a stanza look like: postinstall "cmd"
482
+ const scriptLine = /^\s+(postinstall|preinstall|install)\s+"([^"]+)"/.exec(line);
483
+ if (scriptLine && currentPkg) {
484
+ const scriptKey = scriptLine[1];
485
+ const scriptVal = scriptLine[2];
486
+ scriptPkgs.push(`${currentPkg} [${scriptKey}]`);
487
+ if (isScriptMalicious(scriptVal)) {
488
+ maliciousScriptPkgs.push(`${currentPkg} [${scriptKey}]: ${scriptVal.slice(0, 120)}`);
489
+ }
490
+ }
491
+ }
492
+ return { maliciousScriptPkgs, scriptPkgs };
493
+ }
494
+ /**
495
+ * Scan pnpm-lock.yaml content for lifecycle script values.
496
+ * pnpm-lock.yaml stores scripts in `requiresBuild: true` stanzas; the actual
497
+ * script content is in node_modules (not the lockfile itself). We flag any
498
+ * package that sets `requiresBuild: true` as having a lifecycle script,
499
+ * and also scan for inline `scripts:` blocks using a heuristic regex.
500
+ */
501
+ function scanPnpmLockForMaliciousScripts(content) {
502
+ const maliciousScriptPkgs = [];
503
+ const scriptPkgs = [];
504
+ const lines = content.split("\n");
505
+ let currentPkg = "";
506
+ for (const line of lines) {
507
+ // pnpm-lock.yaml package stanza: starts with exactly 2-space indent + "/" or name
508
+ const pkgHeader = /^ {2}\/?([^\s:][^:]+):$/.exec(line);
509
+ if (pkgHeader) {
510
+ currentPkg = pkgHeader[1].trim();
511
+ continue;
512
+ }
513
+ // requiresBuild: true means the package has a lifecycle script
514
+ if (/^\s+requiresBuild:\s*true/.test(line) && currentPkg) {
515
+ scriptPkgs.push(currentPkg);
516
+ }
517
+ // Inline script value (rare in pnpm-lock but possible in older format)
518
+ const scriptLine = /^\s+(?:postinstall|preinstall|install):\s+"?([^"]+)"?/.exec(line);
519
+ if (scriptLine && currentPkg) {
520
+ const scriptVal = scriptLine[1];
521
+ if (isScriptMalicious(scriptVal)) {
522
+ maliciousScriptPkgs.push(`${currentPkg}: ${scriptVal.slice(0, 120)}`);
523
+ }
524
+ }
525
+ }
526
+ return { maliciousScriptPkgs, scriptPkgs };
278
527
  }
279
528
  async function checkTransitiveDependencies() {
280
529
  const findings = [];
281
530
  try {
282
- let lockRaw;
531
+ // Try package-lock.json first (npm), then yarn.lock, then pnpm-lock.yaml.
532
+ // Each lockfile type has different structure; we use format-specific parsers.
533
+ let scriptPkgs = [];
534
+ let missingIntegrityPkgs = [];
535
+ let maliciousScriptPkgs = [];
536
+ let lockfileFound = false;
537
+ // ── npm package-lock.json ──────────────────────────────────────────────
283
538
  try {
284
- lockRaw = await readFile("package-lock.json", "utf8");
539
+ const lockRaw = await readFile("package-lock.json", "utf8");
540
+ let lock;
541
+ try {
542
+ lock = JSON.parse(lockRaw);
543
+ const result = scanLockfilePackages(lock.packages ?? {});
544
+ scriptPkgs = result.scriptPkgs;
545
+ missingIntegrityPkgs = result.missingIntegrityPkgs;
546
+ maliciousScriptPkgs = result.maliciousScriptPkgs;
547
+ lockfileFound = true;
548
+ }
549
+ catch {
550
+ // JSON parse failure — skip
551
+ }
285
552
  }
286
553
  catch {
287
- return [];
554
+ // package-lock.json not present — try alternatives
288
555
  }
289
- let lock;
290
- try {
291
- lock = JSON.parse(lockRaw);
556
+ // ── yarn.lock ──────────────────────────────────────────────────────────
557
+ if (!lockfileFound) {
558
+ try {
559
+ const yarnRaw = await readFile("yarn.lock", "utf8");
560
+ const result = scanYarnLockForMaliciousScripts(yarnRaw);
561
+ scriptPkgs = result.scriptPkgs;
562
+ maliciousScriptPkgs = result.maliciousScriptPkgs;
563
+ lockfileFound = true;
564
+ // yarn.lock does not encode integrity per entry the same way; skip integrity check
565
+ }
566
+ catch {
567
+ // yarn.lock not present
568
+ }
292
569
  }
293
- catch {
294
- return [];
570
+ // ── pnpm-lock.yaml ─────────────────────────────────────────────────────
571
+ if (!lockfileFound) {
572
+ try {
573
+ const pnpmRaw = await readFile("pnpm-lock.yaml", "utf8");
574
+ const result = scanPnpmLockForMaliciousScripts(pnpmRaw);
575
+ scriptPkgs = result.scriptPkgs;
576
+ maliciousScriptPkgs = result.maliciousScriptPkgs;
577
+ lockfileFound = true;
578
+ }
579
+ catch {
580
+ // pnpm-lock.yaml not present
581
+ }
582
+ }
583
+ if (!lockfileFound) {
584
+ return findings;
585
+ }
586
+ // Report malicious script patterns first — CRITICAL severity
587
+ if (maliciousScriptPkgs.length > 0) {
588
+ findings.push({
589
+ id: "DEPENDENCY_MALICIOUS_SCRIPT",
590
+ title: `${maliciousScriptPkgs.length} transitive dependency lifecycle script(s) contain malicious execution patterns`,
591
+ severity: "CRITICAL",
592
+ evidence: maliciousScriptPkgs.slice(0, 10),
593
+ requiredActions: [
594
+ "A transitive dependency has a lifecycle script matching known malicious patterns (download-and-execute, inline eval, base64 decode).",
595
+ "CWE-494 / ATT&CK T1195.002 — malicious postinstall scripts run automatically on every npm install.",
596
+ "Remove or replace these dependencies immediately. Treat the development environment as potentially compromised and rotate all secrets."
597
+ ]
598
+ });
295
599
  }
296
- const { scriptPkgs, missingIntegrityPkgs } = scanLockfilePackages(lock.packages ?? {});
297
600
  if (scriptPkgs.length > 0) {
298
601
  findings.push({
299
602
  id: "DEP_LIFECYCLE_SCRIPTS",
@@ -398,7 +701,31 @@ const KNOWN_TYPOSQUATS = {
398
701
  "debugg": "debug",
399
702
  "debu": "debug",
400
703
  "async-": "async",
401
- "asyncs": "async"
704
+ "asyncs": "async",
705
+ // Extended ecosystem packages
706
+ "prism": "prisma",
707
+ "prismaa": "prisma",
708
+ "nextjs": "next",
709
+ "nextt": "next",
710
+ "nuxt3": "nuxt",
711
+ "vue3": "vue",
712
+ "sveltejs": "svelte",
713
+ "mongoosejs": "mongoose",
714
+ "sequelizejs": "sequelize",
715
+ "passportjs": "passport",
716
+ "jsonwebtoken-": "jsonwebtoken",
717
+ "bcryptjs-": "bcrypt",
718
+ "multerjs": "multer",
719
+ "axiosjs": "axios",
720
+ "socketio": "socket.io",
721
+ "redisjs": "redis",
722
+ "mysql-2": "mysql2",
723
+ "pgg": "pg",
724
+ "typeormjs": "typeorm",
725
+ "expressjs": "express",
726
+ "fastifyjs": "fastify",
727
+ "helmetjs": "helmet",
728
+ "corsjs": "cors"
402
729
  };
403
730
  // Suspicious version patterns used in dependency confusion / version injection attacks
404
731
  const SUSPICIOUS_VERSION_RE = /^\^?999\.|^0\.0\.[01]$/;
@@ -462,3 +789,243 @@ async function checkTyposquatting() {
462
789
  }
463
790
  return findings;
464
791
  }
792
+ // ─── Go module integrity ────────────────────────────────────────────────────
793
+ async function checkGoSumMissing() {
794
+ const findings = [];
795
+ try {
796
+ const goModFiles = await fg(["**/go.mod"], {
797
+ dot: true,
798
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
799
+ });
800
+ const missing = [];
801
+ for (const goModPath of goModFiles) {
802
+ const dir = goModPath.replace(/\/go\.mod$/, "") || ".";
803
+ const goSumPath = dir === "." ? "go.sum" : `${dir}/go.sum`;
804
+ try {
805
+ const content = await readFileSafe(goSumPath);
806
+ if (!content) {
807
+ missing.push(goModPath);
808
+ }
809
+ }
810
+ catch {
811
+ missing.push(goModPath);
812
+ }
813
+ }
814
+ if (missing.length > 0) {
815
+ findings.push({
816
+ id: "GO_SUM_MISSING",
817
+ title: `${missing.length} go.mod file(s) present without a corresponding go.sum — Go module integrity unverified`,
818
+ severity: "HIGH",
819
+ evidence: missing.slice(0, 10),
820
+ requiredActions: [
821
+ "go.mod present without go.sum — Go module integrity unverified, compromised proxy can serve any content (ATT&CK T1195.001)",
822
+ "Run `go mod tidy` to generate go.sum, then commit it alongside go.mod.",
823
+ "Without go.sum, the Go toolchain cannot verify cryptographic hashes of downloaded modules."
824
+ ]
825
+ });
826
+ }
827
+ }
828
+ catch (err) {
829
+ console.warn("[checkGoSumMissing] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
830
+ }
831
+ return findings;
832
+ }
833
+ // ─── Cargo lock integrity ───────────────────────────────────────────────────
834
+ function isBinaryCrate(tomlContent) {
835
+ const hasBinSection = /^\[\[bin\]\]/m.test(tomlContent);
836
+ const hasLibSection = /^\[lib\]/m.test(tomlContent);
837
+ const hasPackageSection = /^\[package\]/m.test(tomlContent);
838
+ return hasBinSection || (hasPackageSection && !hasLibSection);
839
+ }
840
+ async function cargoLockMissingForToml(cargoTomlPath) {
841
+ let tomlContent = "";
842
+ try {
843
+ tomlContent = await readFileSafe(cargoTomlPath);
844
+ }
845
+ catch {
846
+ return false;
847
+ }
848
+ if (!tomlContent || !isBinaryCrate(tomlContent))
849
+ return false;
850
+ const dir = cargoTomlPath.replace(/\/Cargo\.toml$/, "") || ".";
851
+ const cargoLockPath = dir === "." ? "Cargo.lock" : `${dir}/Cargo.lock`;
852
+ try {
853
+ const lockContent = await readFileSafe(cargoLockPath);
854
+ return !lockContent;
855
+ }
856
+ catch {
857
+ return true;
858
+ }
859
+ }
860
+ async function checkCargoLockMissing() {
861
+ const findings = [];
862
+ try {
863
+ const cargoTomlFiles = await fg(["**/Cargo.toml"], {
864
+ dot: true,
865
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
866
+ });
867
+ const results = await Promise.all(cargoTomlFiles.map(async (p) => ({ path: p, missing: await cargoLockMissingForToml(p) })));
868
+ const missing = results.filter((r) => r.missing).map((r) => r.path);
869
+ if (missing.length > 0) {
870
+ findings.push({
871
+ id: "CARGO_LOCK_MISSING",
872
+ title: `${missing.length} Cargo.toml binary crate(s) present without Cargo.lock — Rust dependency resolution unverified`,
873
+ severity: "MEDIUM",
874
+ evidence: missing.slice(0, 10),
875
+ requiredActions: [
876
+ "Cargo.toml without Cargo.lock — Rust binary crate dependency resolution unverified (ATT&CK T1195.001)",
877
+ "Run `cargo generate-lockfile` to create Cargo.lock and commit it to version control.",
878
+ "Cargo.lock ensures reproducible builds and prevents silent dependency upgrades."
879
+ ]
880
+ });
881
+ }
882
+ }
883
+ catch (err) {
884
+ console.warn("[checkCargoLockMissing] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
885
+ }
886
+ return findings;
887
+ }
888
+ // ─── Lockfile sync check ────────────────────────────────────────────────────
889
+ function parsePkgDeps(content) {
890
+ try {
891
+ const pkg = JSON.parse(content);
892
+ return { ...pkg.dependencies, ...pkg.devDependencies };
893
+ }
894
+ catch {
895
+ return null;
896
+ }
897
+ }
898
+ function parseLockPackages(content) {
899
+ try {
900
+ const lock = JSON.parse(content);
901
+ return lock.packages ?? {};
902
+ }
903
+ catch {
904
+ return null;
905
+ }
906
+ }
907
+ function findOutOfSyncDeps(allDeps, lockPackages) {
908
+ const outOfSync = [];
909
+ for (const depName of Object.keys(allDeps)) {
910
+ // package-lock.json v2/v3 stores entries as "node_modules/<name>"
911
+ const key = `node_modules/${depName}`;
912
+ if (!(key in lockPackages) && !(depName in lockPackages)) {
913
+ outOfSync.push(depName);
914
+ }
915
+ }
916
+ return outOfSync;
917
+ }
918
+ async function checkLockfileSyncForPkg(pkgPath) {
919
+ const dir = pkgPath.replace(/\/package\.json$/, "") || ".";
920
+ const lockPath = dir === "." ? "package-lock.json" : `${dir}/package-lock.json`;
921
+ let pkgContent = "";
922
+ try {
923
+ pkgContent = await readFileSafe(pkgPath);
924
+ }
925
+ catch {
926
+ return [];
927
+ }
928
+ if (!pkgContent)
929
+ return [];
930
+ const allDeps = parsePkgDeps(pkgContent);
931
+ if (!allDeps || Object.keys(allDeps).length === 0)
932
+ return [];
933
+ let lockContent = "";
934
+ try {
935
+ lockContent = await readFileSafe(lockPath);
936
+ }
937
+ catch {
938
+ return [];
939
+ }
940
+ if (!lockContent)
941
+ return [];
942
+ const lockPackages = parseLockPackages(lockContent);
943
+ if (!lockPackages)
944
+ return [];
945
+ return findOutOfSyncDeps(allDeps, lockPackages);
946
+ }
947
+ async function checkLockfileSync() {
948
+ const findings = [];
949
+ try {
950
+ const pkgFiles = await fg(["**/package.json"], {
951
+ dot: true,
952
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
953
+ });
954
+ for (const pkgPath of pkgFiles) {
955
+ const outOfSync = await checkLockfileSyncForPkg(pkgPath);
956
+ if (outOfSync.length > 0) {
957
+ findings.push({
958
+ id: "LOCKFILE_OUT_OF_SYNC",
959
+ title: `${pkgPath}: ${outOfSync.length} dependency(ies) in package.json not present in package-lock.json`,
960
+ severity: "HIGH",
961
+ evidence: outOfSync.slice(0, 15),
962
+ requiredActions: [
963
+ "package.json has dependencies not present in package-lock.json — lockfile out of sync (ATT&CK T1195.001)",
964
+ "Run `npm install` to regenerate package-lock.json and commit the updated lockfile.",
965
+ "Use `npm ci` in CI/CD pipelines — it fails if package-lock.json is out of sync with package.json."
966
+ ]
967
+ });
968
+ }
969
+ }
970
+ }
971
+ catch (err) {
972
+ console.warn("[checkLockfileSync] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
973
+ }
974
+ return findings;
975
+ }
976
+ // ─── Known supply-chain incident packages ──────────────────────────────────
977
+ const KNOWN_INCIDENT_PACKAGES = new Set([
978
+ "node-ipc",
979
+ "event-stream",
980
+ "ua-parser-js",
981
+ "faker",
982
+ "colors",
983
+ "left-pad"
984
+ ]);
985
+ function flaggedIncidentDeps(allDeps) {
986
+ return Object.keys(allDeps).filter((name) => KNOWN_INCIDENT_PACKAGES.has(name));
987
+ }
988
+ async function maintainerRiskForPkg(pkgPath) {
989
+ let pkgContent = "";
990
+ try {
991
+ pkgContent = await readFileSafe(pkgPath);
992
+ }
993
+ catch {
994
+ return [];
995
+ }
996
+ if (!pkgContent)
997
+ return [];
998
+ const allDeps = parsePkgDeps(pkgContent);
999
+ if (!allDeps)
1000
+ return [];
1001
+ return flaggedIncidentDeps(allDeps);
1002
+ }
1003
+ async function checkMaintainerRisk() {
1004
+ const findings = [];
1005
+ try {
1006
+ const pkgFiles = await fg(["**/package.json"], {
1007
+ dot: true,
1008
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
1009
+ });
1010
+ for (const pkgPath of pkgFiles) {
1011
+ const flagged = await maintainerRiskForPkg(pkgPath);
1012
+ if (flagged.length > 0) {
1013
+ findings.push({
1014
+ id: "DEP_MAINTAINER_RISK",
1015
+ title: `${pkgPath}: ${flagged.length} dependency(ies) with known supply-chain incident history detected`,
1016
+ severity: "MEDIUM",
1017
+ evidence: flagged.slice(0, 10),
1018
+ requiredActions: [
1019
+ "Dependency with known supply-chain incident history detected — review and pin to safe version (ATT&CK T1195.001)",
1020
+ "Audit each flagged package: verify the current maintainer, review recent publish history on npmjs.com, and pin to a specific safe version.",
1021
+ "Consider replacing abandoned or historically-compromised packages with actively-maintained alternatives."
1022
+ ]
1023
+ });
1024
+ }
1025
+ }
1026
+ }
1027
+ catch (err) {
1028
+ console.warn("[checkMaintainerRisk] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
1029
+ }
1030
+ return findings;
1031
+ }