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.
Files changed (133) hide show
  1. package/README.md +164 -185
  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/control-catalog.json +200 -0
  9. package/defaults/security-policy.json +2 -2
  10. package/dist/cli/index.js +82 -5
  11. package/dist/cli/install.js +36 -6
  12. package/dist/cli/onboarding.js +6 -0
  13. package/dist/gate/baseline.js +82 -7
  14. package/dist/gate/catalog.js +10 -2
  15. package/dist/gate/checks/ai.js +757 -39
  16. package/dist/gate/checks/auth-deep.js +935 -0
  17. package/dist/gate/checks/business-logic.js +751 -0
  18. package/dist/gate/checks/ci-pipeline.js +399 -4
  19. package/dist/gate/checks/crypto.js +423 -2
  20. package/dist/gate/checks/dependencies.js +571 -15
  21. package/dist/gate/checks/graphql.js +201 -19
  22. package/dist/gate/checks/infra.js +246 -1
  23. package/dist/gate/checks/injection-deep.js +848 -0
  24. package/dist/gate/checks/k8s.js +114 -1
  25. package/dist/gate/checks/mobile-android.js +917 -3
  26. package/dist/gate/checks/mobile-ios.js +797 -5
  27. package/dist/gate/checks/required-artifacts.js +194 -0
  28. package/dist/gate/checks/runtime.js +178 -0
  29. package/dist/gate/checks/secrets.js +244 -13
  30. package/dist/gate/checks/supply-chain-deep.js +787 -0
  31. package/dist/gate/checks/web-nextjs.js +572 -48
  32. package/dist/gate/diff.js +17 -5
  33. package/dist/gate/evidence.js +8 -1
  34. package/dist/gate/exceptions.js +131 -9
  35. package/dist/gate/policy.js +282 -129
  36. package/dist/mcp/audit-chain.js +122 -28
  37. package/dist/mcp/auth.js +169 -0
  38. package/dist/mcp/learning.js +129 -4
  39. package/dist/mcp/model-router.js +158 -21
  40. package/dist/mcp/orchestration.js +186 -51
  41. package/dist/mcp/server.js +608 -94
  42. package/dist/repo/fs.js +24 -1
  43. package/dist/repo/search.js +31 -6
  44. package/dist/review/store.js +52 -1
  45. package/package.json +7 -7
  46. package/prompts/SECURITY_PROMPT.md +73 -0
  47. package/skills/_TEMPLATE/SKILL.md +99 -0
  48. package/skills/advanced-dos-tester/SKILL.md +109 -0
  49. package/skills/agentic-loop-exploiter/SKILL.md +368 -0
  50. package/skills/ai-llm-redteam/SKILL.md +104 -0
  51. package/skills/ai-model-supply-chain-agent/SKILL.md +103 -0
  52. package/skills/algorithm-implementation-reviewer/SKILL.md +98 -0
  53. package/skills/android-penetration-tester/SKILL.md +455 -46
  54. package/skills/anti-replay-tester/SKILL.md +106 -0
  55. package/skills/appsec-code-auditor/SKILL.md +120 -0
  56. package/skills/artifact-integrity-analyst/SKILL.md +441 -0
  57. package/skills/attack-navigator/SKILL.md +467 -8
  58. package/skills/auth-session-hacker/SKILL.md +128 -0
  59. package/skills/aws-penetration-tester/SKILL.md +456 -0
  60. package/skills/azure-penetration-tester/SKILL.md +490 -3
  61. package/skills/binary-auth-validator/SKILL.md +111 -0
  62. package/skills/bot-detection-specialist/SKILL.md +109 -0
  63. package/skills/business-logic-attacker/SKILL.md +231 -0
  64. package/skills/capec-code-mapper/SKILL.md +84 -0
  65. package/skills/cert-pin-rotation-specialist/SKILL.md +112 -0
  66. package/skills/cicd-pipeline-hijacker/SKILL.md +405 -0
  67. package/skills/ciso-orchestrator/SKILL.md +454 -43
  68. package/skills/cloud-infra-specialist/SKILL.md +118 -0
  69. package/skills/compliance-gap-analyst/SKILL.md +422 -0
  70. package/skills/compliance-grc/SKILL.md +85 -0
  71. package/skills/compliance-lifecycle-tracker/SKILL.md +84 -0
  72. package/skills/credential-stuffing-specialist/SKILL.md +102 -0
  73. package/skills/crypto-pki-specialist/SKILL.md +87 -0
  74. package/skills/csa-ccm-mapper/SKILL.md +84 -0
  75. package/skills/csf2-governance-mapper/SKILL.md +84 -0
  76. package/skills/deep-link-fuzzer/SKILL.md +109 -0
  77. package/skills/dependency-confusion-attacker/SKILL.md +415 -0
  78. package/skills/device-integrity-aggregator/SKILL.md +108 -0
  79. package/skills/dos-resilience-tester/SKILL.md +97 -0
  80. package/skills/dread-scorer/SKILL.md +84 -0
  81. package/skills/egress-policy-enforcer/SKILL.md +99 -0
  82. package/skills/evidence-collector/SKILL.md +98 -0
  83. package/skills/file-upload-attacker/SKILL.md +109 -0
  84. package/skills/gcp-penetration-tester/SKILL.md +459 -2
  85. package/skills/git-history-secret-scanner/SKILL.md +106 -0
  86. package/skills/iam-privesc-graph-builder/SKILL.md +152 -0
  87. package/skills/incident-responder/SKILL.md +111 -0
  88. package/skills/injection-specialist/SKILL.md +131 -0
  89. package/skills/ios-security-auditor/SKILL.md +282 -0
  90. package/skills/json-ambiguity-tester/SKILL.md +0 -0
  91. package/skills/k8s-container-escaper/SKILL.md +384 -0
  92. package/skills/key-management-lifecycle-analyst/SKILL.md +98 -0
  93. package/skills/kill-switch-engineer/SKILL.md +102 -0
  94. package/skills/linddun-privacy-analyst/SKILL.md +102 -0
  95. package/skills/logic-race-fuzzer/SKILL.md +443 -0
  96. package/skills/mobile-api-network-attacker/SKILL.md +421 -0
  97. package/skills/mobile-binary-hardener/SKILL.md +102 -0
  98. package/skills/mobile-security-specialist/SKILL.md +85 -0
  99. package/skills/mobile-webview-auditor/SKILL.md +96 -0
  100. package/skills/model-extraction-attacker/SKILL.md +219 -0
  101. package/skills/multipart-abuse-tester/SKILL.md +84 -0
  102. package/skills/oauth-pkce-specialist/SKILL.md +104 -0
  103. package/skills/parser-exhaustion-tester/SKILL.md +142 -0
  104. package/skills/pentest-infra/SKILL.md +141 -0
  105. package/skills/pentest-social/SKILL.md +201 -0
  106. package/skills/pentest-team/SKILL.md +134 -0
  107. package/skills/pentest-web-api/SKILL.md +151 -0
  108. package/skills/privacy-flow-analyst/SKILL.md +234 -0
  109. package/skills/prompt-injection-specialist/SKILL.md +394 -0
  110. package/skills/quantum-migration-planner/SKILL.md +96 -0
  111. package/skills/rag-poisoning-specialist/SKILL.md +358 -0
  112. package/skills/registry-mirror-enforcer/SKILL.md +84 -0
  113. package/skills/rotation-validation-agent/SKILL.md +112 -0
  114. package/skills/samm-assessor/SKILL.md +85 -0
  115. package/skills/secrets-mask-bypass-tester/SKILL.md +100 -0
  116. package/skills/senior-security-engineer/SKILL.md +370 -2
  117. package/skills/serialization-memory-attacker/SKILL.md +332 -0
  118. package/skills/session-timeout-tester/SKILL.md +161 -0
  119. package/skills/slsa-level3-enforcer/SKILL.md +112 -0
  120. package/skills/slsa-provenance-enforcer/SKILL.md +102 -0
  121. package/skills/ssrf-detection-validator/SKILL.md +108 -0
  122. package/skills/step-up-auth-enforcer/SKILL.md +84 -0
  123. package/skills/stride-pasta-analyst/SKILL.md +420 -0
  124. package/skills/supply-chain-devsecops/SKILL.md +98 -0
  125. package/skills/threat-infrastructure-analyst/SKILL.md +84 -0
  126. package/skills/threat-modeler/SKILL.md +85 -0
  127. package/skills/tls-certificate-auditor/SKILL.md +573 -18
  128. package/skills/token-reuse-detector/SKILL.md +95 -0
  129. package/skills/trike-risk-modeler/SKILL.md +84 -0
  130. package/skills/unicode-homograph-tester/SKILL.md +84 -0
  131. package/skills/waf-rule-lifecycle-agent/SKILL.md +97 -0
  132. package/skills/webhook-security-tester/SKILL.md +102 -0
  133. package/skills/zero-trust-architect/SKILL.md +109 -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,27 @@ 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
+ const depsToCheck = [...new Set([...prodDeps, ...devDepsUsedInScripts])].slice(0, 20);
246
+ const totalAllDeps = prodDeps.length + Object.keys(pkg.devDependencies ?? {}).length;
247
+ for (const dep of depsToCheck) {
108
248
  const score = await fetchScorecardScore(dep);
109
249
  if (score !== null && score < 5.0) {
110
250
  findings.push({
@@ -119,6 +259,20 @@ async function checkNpmProvenance() {
119
259
  });
120
260
  }
121
261
  }
262
+ // Report partial coverage when total deps exceed the cap
263
+ if (totalAllDeps > 20) {
264
+ findings.push({
265
+ id: "SUPPLY_CHAIN_SCORECARD_PARTIAL",
266
+ title: `OpenSSF Scorecard checked ${depsToCheck.length} of ${totalAllDeps} total dependencies — coverage is partial`,
267
+ severity: "LOW",
268
+ evidence: [`Checked: ${depsToCheck.length} deps. Unchecked: ${totalAllDeps - depsToCheck.length} deps.`],
269
+ requiredActions: [
270
+ `${totalAllDeps - depsToCheck.length} dependencies were not checked against OpenSSF Scorecard due to the 20-dep cap.`,
271
+ "Run `npx ossf-scorecard` or review https://scorecard.dev for full coverage.",
272
+ "Consider using socket.dev or Snyk for continuous supply chain monitoring across all dependencies."
273
+ ]
274
+ });
275
+ }
122
276
  }
123
277
  catch {
124
278
  // package.json unreadable or API unavailable — skip
@@ -174,8 +328,18 @@ export async function checkDependencies(_) {
174
328
  findings.push(...transitive);
175
329
  const typosquat = await checkTyposquatting();
176
330
  findings.push(...typosquat);
331
+ const depConfusion = await checkDependencyConfusion();
332
+ findings.push(...depConfusion);
177
333
  const threatIntel = await checkCveExploitation();
178
334
  findings.push(...threatIntel);
335
+ const goSum = await checkGoSumMissing();
336
+ findings.push(...goSum);
337
+ const cargoLock = await checkCargoLockMissing();
338
+ findings.push(...cargoLock);
339
+ const lockfileSync = await checkLockfileSync();
340
+ findings.push(...lockfileSync);
341
+ const maintainerRisk = await checkMaintainerRisk();
342
+ findings.push(...maintainerRisk);
179
343
  return findings;
180
344
  }
181
345
  function extractCveIds(audit) {
@@ -265,35 +429,163 @@ function hasLifecycleScript(pkg) {
265
429
  function scanLockfilePackages(packages) {
266
430
  const scriptPkgs = [];
267
431
  const missingIntegrityPkgs = [];
432
+ const maliciousScriptPkgs = [];
268
433
  for (const [name, pkg] of Object.entries(packages)) {
269
434
  if (!name)
270
435
  continue; // skip root entry
271
436
  const pkgName = name.replace(/^node_modules\//, "");
272
- if (hasLifecycleScript(pkg))
437
+ if (hasLifecycleScript(pkg)) {
273
438
  scriptPkgs.push(pkgName);
439
+ // Check each lifecycle script VALUE for malicious patterns
440
+ for (const scriptKey of LIFECYCLE_SCRIPTS) {
441
+ const scriptVal = pkg.scripts?.[scriptKey];
442
+ if (scriptVal && isScriptMalicious(scriptVal)) {
443
+ maliciousScriptPkgs.push(`${pkgName} [${scriptKey}]: ${scriptVal.slice(0, 120)}`);
444
+ }
445
+ }
446
+ }
274
447
  if (pkg.version && !pkg.integrity)
275
448
  missingIntegrityPkgs.push(pkgName);
276
449
  }
277
- return { scriptPkgs, missingIntegrityPkgs };
450
+ return { scriptPkgs, missingIntegrityPkgs, maliciousScriptPkgs };
451
+ }
452
+ /**
453
+ * Scan yarn.lock content for lifecycle script values using a regex-based approach
454
+ * (yarn.lock is a custom format, not JSON/YAML, so we use heuristic line scanning).
455
+ * We look for `postinstall`, `preinstall`, or `install` key lines followed by a value
456
+ * in the same stanza, and check the value for malicious patterns.
457
+ */
458
+ function scanYarnLockForMaliciousScripts(content) {
459
+ const maliciousScriptPkgs = [];
460
+ const scriptPkgs = [];
461
+ const lines = content.split("\n");
462
+ let currentPkg = "";
463
+ for (const line of lines) {
464
+ // New stanza starts with a non-space character that ends with a colon (package header)
465
+ const pkgHeader = /^"?([^"#\s][^:]*)(?:@[^:]+)?":?\s*$/.exec(line);
466
+ if (pkgHeader) {
467
+ currentPkg = pkgHeader[1].trim();
468
+ continue;
469
+ }
470
+ // Lifecycle script lines inside a stanza look like: postinstall "cmd"
471
+ const scriptLine = /^\s+(postinstall|preinstall|install)\s+"([^"]+)"/.exec(line);
472
+ if (scriptLine && currentPkg) {
473
+ const scriptKey = scriptLine[1];
474
+ const scriptVal = scriptLine[2];
475
+ scriptPkgs.push(`${currentPkg} [${scriptKey}]`);
476
+ if (isScriptMalicious(scriptVal)) {
477
+ maliciousScriptPkgs.push(`${currentPkg} [${scriptKey}]: ${scriptVal.slice(0, 120)}`);
478
+ }
479
+ }
480
+ }
481
+ return { maliciousScriptPkgs, scriptPkgs };
482
+ }
483
+ /**
484
+ * Scan pnpm-lock.yaml content for lifecycle script values.
485
+ * pnpm-lock.yaml stores scripts in `requiresBuild: true` stanzas; the actual
486
+ * script content is in node_modules (not the lockfile itself). We flag any
487
+ * package that sets `requiresBuild: true` as having a lifecycle script,
488
+ * and also scan for inline `scripts:` blocks using a heuristic regex.
489
+ */
490
+ function scanPnpmLockForMaliciousScripts(content) {
491
+ const maliciousScriptPkgs = [];
492
+ const scriptPkgs = [];
493
+ const lines = content.split("\n");
494
+ let currentPkg = "";
495
+ for (const line of lines) {
496
+ // pnpm-lock.yaml package stanza: starts with exactly 2-space indent + "/" or name
497
+ const pkgHeader = /^ {2}\/?([^\s:][^:]+):$/.exec(line);
498
+ if (pkgHeader) {
499
+ currentPkg = pkgHeader[1].trim();
500
+ continue;
501
+ }
502
+ // requiresBuild: true means the package has a lifecycle script
503
+ if (/^\s+requiresBuild:\s*true/.test(line) && currentPkg) {
504
+ scriptPkgs.push(currentPkg);
505
+ }
506
+ // Inline script value (rare in pnpm-lock but possible in older format)
507
+ const scriptLine = /^\s+(?:postinstall|preinstall|install):\s+"?([^"]+)"?/.exec(line);
508
+ if (scriptLine && currentPkg) {
509
+ const scriptVal = scriptLine[1];
510
+ if (isScriptMalicious(scriptVal)) {
511
+ maliciousScriptPkgs.push(`${currentPkg}: ${scriptVal.slice(0, 120)}`);
512
+ }
513
+ }
514
+ }
515
+ return { maliciousScriptPkgs, scriptPkgs };
278
516
  }
279
517
  async function checkTransitiveDependencies() {
280
518
  const findings = [];
281
519
  try {
282
- let lockRaw;
520
+ // Try package-lock.json first (npm), then yarn.lock, then pnpm-lock.yaml.
521
+ // Each lockfile type has different structure; we use format-specific parsers.
522
+ let scriptPkgs = [];
523
+ let missingIntegrityPkgs = [];
524
+ let maliciousScriptPkgs = [];
525
+ let lockfileFound = false;
526
+ // ── npm package-lock.json ──────────────────────────────────────────────
283
527
  try {
284
- lockRaw = await readFile("package-lock.json", "utf8");
528
+ const lockRaw = await readFile("package-lock.json", "utf8");
529
+ let lock;
530
+ try {
531
+ lock = JSON.parse(lockRaw);
532
+ const result = scanLockfilePackages(lock.packages ?? {});
533
+ scriptPkgs = result.scriptPkgs;
534
+ missingIntegrityPkgs = result.missingIntegrityPkgs;
535
+ maliciousScriptPkgs = result.maliciousScriptPkgs;
536
+ lockfileFound = true;
537
+ }
538
+ catch {
539
+ // JSON parse failure — skip
540
+ }
285
541
  }
286
542
  catch {
287
- return [];
543
+ // package-lock.json not present — try alternatives
288
544
  }
289
- let lock;
290
- try {
291
- lock = JSON.parse(lockRaw);
545
+ // ── yarn.lock ──────────────────────────────────────────────────────────
546
+ if (!lockfileFound) {
547
+ try {
548
+ const yarnRaw = await readFile("yarn.lock", "utf8");
549
+ const result = scanYarnLockForMaliciousScripts(yarnRaw);
550
+ scriptPkgs = result.scriptPkgs;
551
+ maliciousScriptPkgs = result.maliciousScriptPkgs;
552
+ lockfileFound = true;
553
+ // yarn.lock does not encode integrity per entry the same way; skip integrity check
554
+ }
555
+ catch {
556
+ // yarn.lock not present
557
+ }
292
558
  }
293
- catch {
294
- return [];
559
+ // ── pnpm-lock.yaml ─────────────────────────────────────────────────────
560
+ if (!lockfileFound) {
561
+ try {
562
+ const pnpmRaw = await readFile("pnpm-lock.yaml", "utf8");
563
+ const result = scanPnpmLockForMaliciousScripts(pnpmRaw);
564
+ scriptPkgs = result.scriptPkgs;
565
+ maliciousScriptPkgs = result.maliciousScriptPkgs;
566
+ lockfileFound = true;
567
+ }
568
+ catch {
569
+ // pnpm-lock.yaml not present
570
+ }
571
+ }
572
+ if (!lockfileFound) {
573
+ return findings;
574
+ }
575
+ // Report malicious script patterns first — CRITICAL severity
576
+ if (maliciousScriptPkgs.length > 0) {
577
+ findings.push({
578
+ id: "DEPENDENCY_MALICIOUS_SCRIPT",
579
+ title: `${maliciousScriptPkgs.length} transitive dependency lifecycle script(s) contain malicious execution patterns`,
580
+ severity: "CRITICAL",
581
+ evidence: maliciousScriptPkgs.slice(0, 10),
582
+ requiredActions: [
583
+ "A transitive dependency has a lifecycle script matching known malicious patterns (download-and-execute, inline eval, base64 decode).",
584
+ "CWE-494 / ATT&CK T1195.002 — malicious postinstall scripts run automatically on every npm install.",
585
+ "Remove or replace these dependencies immediately. Treat the development environment as potentially compromised and rotate all secrets."
586
+ ]
587
+ });
295
588
  }
296
- const { scriptPkgs, missingIntegrityPkgs } = scanLockfilePackages(lock.packages ?? {});
297
589
  if (scriptPkgs.length > 0) {
298
590
  findings.push({
299
591
  id: "DEP_LIFECYCLE_SCRIPTS",
@@ -398,7 +690,31 @@ const KNOWN_TYPOSQUATS = {
398
690
  "debugg": "debug",
399
691
  "debu": "debug",
400
692
  "async-": "async",
401
- "asyncs": "async"
693
+ "asyncs": "async",
694
+ // Extended ecosystem packages
695
+ "prism": "prisma",
696
+ "prismaa": "prisma",
697
+ "nextjs": "next",
698
+ "nextt": "next",
699
+ "nuxt3": "nuxt",
700
+ "vue3": "vue",
701
+ "sveltejs": "svelte",
702
+ "mongoosejs": "mongoose",
703
+ "sequelizejs": "sequelize",
704
+ "passportjs": "passport",
705
+ "jsonwebtoken-": "jsonwebtoken",
706
+ "bcryptjs-": "bcrypt",
707
+ "multerjs": "multer",
708
+ "axiosjs": "axios",
709
+ "socketio": "socket.io",
710
+ "redisjs": "redis",
711
+ "mysql-2": "mysql2",
712
+ "pgg": "pg",
713
+ "typeormjs": "typeorm",
714
+ "expressjs": "express",
715
+ "fastifyjs": "fastify",
716
+ "helmetjs": "helmet",
717
+ "corsjs": "cors"
402
718
  };
403
719
  // Suspicious version patterns used in dependency confusion / version injection attacks
404
720
  const SUSPICIOUS_VERSION_RE = /^\^?999\.|^0\.0\.[01]$/;
@@ -462,3 +778,243 @@ async function checkTyposquatting() {
462
778
  }
463
779
  return findings;
464
780
  }
781
+ // ─── Go module integrity ────────────────────────────────────────────────────
782
+ async function checkGoSumMissing() {
783
+ const findings = [];
784
+ try {
785
+ const goModFiles = await fg(["**/go.mod"], {
786
+ dot: true,
787
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
788
+ });
789
+ const missing = [];
790
+ for (const goModPath of goModFiles) {
791
+ const dir = goModPath.replace(/\/go\.mod$/, "") || ".";
792
+ const goSumPath = dir === "." ? "go.sum" : `${dir}/go.sum`;
793
+ try {
794
+ const content = await readFileSafe(goSumPath);
795
+ if (!content) {
796
+ missing.push(goModPath);
797
+ }
798
+ }
799
+ catch {
800
+ missing.push(goModPath);
801
+ }
802
+ }
803
+ if (missing.length > 0) {
804
+ findings.push({
805
+ id: "GO_SUM_MISSING",
806
+ title: `${missing.length} go.mod file(s) present without a corresponding go.sum — Go module integrity unverified`,
807
+ severity: "HIGH",
808
+ evidence: missing.slice(0, 10),
809
+ requiredActions: [
810
+ "go.mod present without go.sum — Go module integrity unverified, compromised proxy can serve any content (ATT&CK T1195.001)",
811
+ "Run `go mod tidy` to generate go.sum, then commit it alongside go.mod.",
812
+ "Without go.sum, the Go toolchain cannot verify cryptographic hashes of downloaded modules."
813
+ ]
814
+ });
815
+ }
816
+ }
817
+ catch (err) {
818
+ console.warn("[checkGoSumMissing] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
819
+ }
820
+ return findings;
821
+ }
822
+ // ─── Cargo lock integrity ───────────────────────────────────────────────────
823
+ function isBinaryCrate(tomlContent) {
824
+ const hasBinSection = /^\[\[bin\]\]/m.test(tomlContent);
825
+ const hasLibSection = /^\[lib\]/m.test(tomlContent);
826
+ const hasPackageSection = /^\[package\]/m.test(tomlContent);
827
+ return hasBinSection || (hasPackageSection && !hasLibSection);
828
+ }
829
+ async function cargoLockMissingForToml(cargoTomlPath) {
830
+ let tomlContent = "";
831
+ try {
832
+ tomlContent = await readFileSafe(cargoTomlPath);
833
+ }
834
+ catch {
835
+ return false;
836
+ }
837
+ if (!tomlContent || !isBinaryCrate(tomlContent))
838
+ return false;
839
+ const dir = cargoTomlPath.replace(/\/Cargo\.toml$/, "") || ".";
840
+ const cargoLockPath = dir === "." ? "Cargo.lock" : `${dir}/Cargo.lock`;
841
+ try {
842
+ const lockContent = await readFileSafe(cargoLockPath);
843
+ return !lockContent;
844
+ }
845
+ catch {
846
+ return true;
847
+ }
848
+ }
849
+ async function checkCargoLockMissing() {
850
+ const findings = [];
851
+ try {
852
+ const cargoTomlFiles = await fg(["**/Cargo.toml"], {
853
+ dot: true,
854
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
855
+ });
856
+ const results = await Promise.all(cargoTomlFiles.map(async (p) => ({ path: p, missing: await cargoLockMissingForToml(p) })));
857
+ const missing = results.filter((r) => r.missing).map((r) => r.path);
858
+ if (missing.length > 0) {
859
+ findings.push({
860
+ id: "CARGO_LOCK_MISSING",
861
+ title: `${missing.length} Cargo.toml binary crate(s) present without Cargo.lock — Rust dependency resolution unverified`,
862
+ severity: "MEDIUM",
863
+ evidence: missing.slice(0, 10),
864
+ requiredActions: [
865
+ "Cargo.toml without Cargo.lock — Rust binary crate dependency resolution unverified (ATT&CK T1195.001)",
866
+ "Run `cargo generate-lockfile` to create Cargo.lock and commit it to version control.",
867
+ "Cargo.lock ensures reproducible builds and prevents silent dependency upgrades."
868
+ ]
869
+ });
870
+ }
871
+ }
872
+ catch (err) {
873
+ console.warn("[checkCargoLockMissing] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
874
+ }
875
+ return findings;
876
+ }
877
+ // ─── Lockfile sync check ────────────────────────────────────────────────────
878
+ function parsePkgDeps(content) {
879
+ try {
880
+ const pkg = JSON.parse(content);
881
+ return { ...pkg.dependencies, ...pkg.devDependencies };
882
+ }
883
+ catch {
884
+ return null;
885
+ }
886
+ }
887
+ function parseLockPackages(content) {
888
+ try {
889
+ const lock = JSON.parse(content);
890
+ return lock.packages ?? {};
891
+ }
892
+ catch {
893
+ return null;
894
+ }
895
+ }
896
+ function findOutOfSyncDeps(allDeps, lockPackages) {
897
+ const outOfSync = [];
898
+ for (const depName of Object.keys(allDeps)) {
899
+ // package-lock.json v2/v3 stores entries as "node_modules/<name>"
900
+ const key = `node_modules/${depName}`;
901
+ if (!(key in lockPackages) && !(depName in lockPackages)) {
902
+ outOfSync.push(depName);
903
+ }
904
+ }
905
+ return outOfSync;
906
+ }
907
+ async function checkLockfileSyncForPkg(pkgPath) {
908
+ const dir = pkgPath.replace(/\/package\.json$/, "") || ".";
909
+ const lockPath = dir === "." ? "package-lock.json" : `${dir}/package-lock.json`;
910
+ let pkgContent = "";
911
+ try {
912
+ pkgContent = await readFileSafe(pkgPath);
913
+ }
914
+ catch {
915
+ return [];
916
+ }
917
+ if (!pkgContent)
918
+ return [];
919
+ const allDeps = parsePkgDeps(pkgContent);
920
+ if (!allDeps || Object.keys(allDeps).length === 0)
921
+ return [];
922
+ let lockContent = "";
923
+ try {
924
+ lockContent = await readFileSafe(lockPath);
925
+ }
926
+ catch {
927
+ return [];
928
+ }
929
+ if (!lockContent)
930
+ return [];
931
+ const lockPackages = parseLockPackages(lockContent);
932
+ if (!lockPackages)
933
+ return [];
934
+ return findOutOfSyncDeps(allDeps, lockPackages);
935
+ }
936
+ async function checkLockfileSync() {
937
+ const findings = [];
938
+ try {
939
+ const pkgFiles = await fg(["**/package.json"], {
940
+ dot: true,
941
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
942
+ });
943
+ for (const pkgPath of pkgFiles) {
944
+ const outOfSync = await checkLockfileSyncForPkg(pkgPath);
945
+ if (outOfSync.length > 0) {
946
+ findings.push({
947
+ id: "LOCKFILE_OUT_OF_SYNC",
948
+ title: `${pkgPath}: ${outOfSync.length} dependency(ies) in package.json not present in package-lock.json`,
949
+ severity: "HIGH",
950
+ evidence: outOfSync.slice(0, 15),
951
+ requiredActions: [
952
+ "package.json has dependencies not present in package-lock.json — lockfile out of sync (ATT&CK T1195.001)",
953
+ "Run `npm install` to regenerate package-lock.json and commit the updated lockfile.",
954
+ "Use `npm ci` in CI/CD pipelines — it fails if package-lock.json is out of sync with package.json."
955
+ ]
956
+ });
957
+ }
958
+ }
959
+ }
960
+ catch (err) {
961
+ console.warn("[checkLockfileSync] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
962
+ }
963
+ return findings;
964
+ }
965
+ // ─── Known supply-chain incident packages ──────────────────────────────────
966
+ const KNOWN_INCIDENT_PACKAGES = new Set([
967
+ "node-ipc",
968
+ "event-stream",
969
+ "ua-parser-js",
970
+ "faker",
971
+ "colors",
972
+ "left-pad"
973
+ ]);
974
+ function flaggedIncidentDeps(allDeps) {
975
+ return Object.keys(allDeps).filter((name) => KNOWN_INCIDENT_PACKAGES.has(name));
976
+ }
977
+ async function maintainerRiskForPkg(pkgPath) {
978
+ let pkgContent = "";
979
+ try {
980
+ pkgContent = await readFileSafe(pkgPath);
981
+ }
982
+ catch {
983
+ return [];
984
+ }
985
+ if (!pkgContent)
986
+ return [];
987
+ const allDeps = parsePkgDeps(pkgContent);
988
+ if (!allDeps)
989
+ return [];
990
+ return flaggedIncidentDeps(allDeps);
991
+ }
992
+ async function checkMaintainerRisk() {
993
+ const findings = [];
994
+ try {
995
+ const pkgFiles = await fg(["**/package.json"], {
996
+ dot: true,
997
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
998
+ });
999
+ for (const pkgPath of pkgFiles) {
1000
+ const flagged = await maintainerRiskForPkg(pkgPath);
1001
+ if (flagged.length > 0) {
1002
+ findings.push({
1003
+ id: "DEP_MAINTAINER_RISK",
1004
+ title: `${pkgPath}: ${flagged.length} dependency(ies) with known supply-chain incident history detected`,
1005
+ severity: "MEDIUM",
1006
+ evidence: flagged.slice(0, 10),
1007
+ requiredActions: [
1008
+ "Dependency with known supply-chain incident history detected — review and pin to safe version (ATT&CK T1195.001)",
1009
+ "Audit each flagged package: verify the current maintainer, review recent publish history on npmjs.com, and pin to a specific safe version.",
1010
+ "Consider replacing abandoned or historically-compromised packages with actively-maintained alternatives."
1011
+ ]
1012
+ });
1013
+ }
1014
+ }
1015
+ }
1016
+ catch (err) {
1017
+ console.warn("[checkMaintainerRisk] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
1018
+ }
1019
+ return findings;
1020
+ }