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.
- package/README.md +341 -1018
- package/defaults/checklists/ai.json +20 -1
- package/defaults/checklists/api.json +35 -1
- package/defaults/checklists/infra.json +34 -1
- package/defaults/checklists/mobile.json +23 -1
- package/defaults/checklists/payments.json +15 -1
- package/defaults/checklists/web.json +11 -1
- package/defaults/cloud-controls/aws.json +10712 -0
- package/defaults/cloud-controls/azure.json +7201 -0
- package/defaults/cloud-controls/gcp.json +4061 -0
- package/defaults/control-catalog.json +24 -0
- package/defaults/security-policy.json +2 -2
- package/dist/ci/pr-gate.js +22 -5
- package/dist/cli/index.js +73 -2
- package/dist/cli/install.js +4 -55
- package/dist/cli/onboarding.js +18 -10
- package/dist/gate/baseline.js +82 -7
- package/dist/gate/catalog.js +10 -2
- package/dist/gate/checks/agentic-instructions.js +515 -0
- package/dist/gate/checks/ai-governance.js +132 -0
- package/dist/gate/checks/ai.js +757 -39
- package/dist/gate/checks/auth-deep.js +920 -216
- package/dist/gate/checks/business-logic.js +751 -0
- package/dist/gate/checks/ci-pipeline.js +399 -4
- package/dist/gate/checks/cloud-controls.js +69 -0
- package/dist/gate/checks/crypto.js +423 -2
- package/dist/gate/checks/data-platform.js +954 -0
- package/dist/gate/checks/dependencies.js +582 -15
- package/dist/gate/checks/docker-deep.js +1236 -0
- package/dist/gate/checks/gitops.js +724 -0
- package/dist/gate/checks/graphql.js +201 -19
- package/dist/gate/checks/iac.js +1230 -0
- package/dist/gate/checks/infra.js +246 -1
- package/dist/gate/checks/injection-deep.js +827 -184
- package/dist/gate/checks/k8s.js +955 -2
- package/dist/gate/checks/mobile-android.js +917 -3
- package/dist/gate/checks/mobile-ios.js +797 -5
- package/dist/gate/checks/required-artifacts.js +194 -0
- package/dist/gate/checks/runtime.js +178 -0
- package/dist/gate/checks/secrets.js +256 -13
- package/dist/gate/checks/supply-chain-deep.js +787 -0
- package/dist/gate/checks/web-nextjs.js +572 -48
- package/dist/gate/cloud-controls/apply.js +115 -0
- package/dist/gate/cloud-controls/bicep.js +36 -0
- package/dist/gate/cloud-controls/cfn.js +125 -0
- package/dist/gate/cloud-controls/detect.js +104 -0
- package/dist/gate/cloud-controls/hcl.js +140 -0
- package/dist/gate/cloud-controls/types.js +87 -0
- package/dist/gate/diff.js +17 -5
- package/dist/gate/evidence.js +8 -1
- package/dist/gate/exceptions.js +202 -9
- package/dist/gate/findings.js +15 -2
- package/dist/gate/policy.js +316 -130
- package/dist/gate/threat-intel.js +6 -0
- package/dist/mcp/audit-chain.js +131 -28
- package/dist/mcp/auth.js +169 -0
- package/dist/mcp/learning.js +129 -4
- package/dist/mcp/model-router.js +161 -24
- package/dist/mcp/orchestration.js +377 -89
- package/dist/mcp/server.js +460 -69
- package/dist/mcp/tool-audit.js +193 -0
- package/dist/repo/fs.js +37 -1
- package/dist/repo/search.js +31 -6
- package/dist/review/store.js +56 -3
- package/dist/tests/run.js +124 -1
- package/package.json +9 -9
- package/skills/_TEMPLATE/SKILL.md +99 -0
- package/skills/advanced-dos-tester/SKILL.md +118 -0
- package/skills/agentic-instruction-auditor/SKILL.md +111 -0
- package/skills/agentic-loop-exploiter/SKILL.md +377 -0
- package/skills/ai-llm-redteam/SKILL.md +113 -0
- package/skills/ai-model-supply-chain-agent/SKILL.md +112 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +107 -0
- package/skills/android-penetration-tester/SKILL.md +464 -46
- package/skills/anti-replay-tester/SKILL.md +115 -0
- package/skills/appsec-code-auditor/SKILL.md +94 -0
- package/skills/artifact-integrity-analyst/SKILL.md +450 -0
- package/skills/attack-navigator/SKILL.md +476 -8
- package/skills/auth-session-hacker/SKILL.md +111 -0
- package/skills/aws-penetration-tester/SKILL.md +510 -0
- package/skills/azure-penetration-tester/SKILL.md +542 -3
- package/skills/binary-auth-validator/SKILL.md +120 -0
- package/skills/bot-detection-specialist/SKILL.md +118 -0
- package/skills/business-logic-attacker/SKILL.md +240 -0
- package/skills/capec-code-mapper/SKILL.md +93 -0
- package/skills/cert-pin-rotation-specialist/SKILL.md +121 -0
- package/skills/cicd-pipeline-hijacker/SKILL.md +414 -0
- package/skills/ciso-orchestrator/SKILL.md +465 -43
- package/skills/cloud-infra-specialist/SKILL.md +127 -0
- package/skills/compliance-gap-analyst/SKILL.md +431 -0
- package/skills/compliance-grc/SKILL.md +94 -0
- package/skills/compliance-lifecycle-tracker/SKILL.md +93 -0
- package/skills/container-hardening-auditor/SKILL.md +125 -0
- package/skills/credential-stuffing-specialist/SKILL.md +111 -0
- package/skills/crypto-pki-specialist/SKILL.md +96 -0
- package/skills/csa-ccm-mapper/SKILL.md +93 -0
- package/skills/csf2-governance-mapper/SKILL.md +93 -0
- package/skills/data-platform-auditor/SKILL.md +125 -0
- package/skills/deep-link-fuzzer/SKILL.md +118 -0
- package/skills/dependency-confusion-attacker/SKILL.md +424 -0
- package/skills/device-integrity-aggregator/SKILL.md +117 -0
- package/skills/dos-resilience-tester/SKILL.md +106 -0
- package/skills/dread-scorer/SKILL.md +93 -0
- package/skills/egress-policy-enforcer/SKILL.md +108 -0
- package/skills/evidence-collector/SKILL.md +107 -0
- package/skills/file-upload-attacker/SKILL.md +118 -0
- package/skills/gcp-penetration-tester/SKILL.md +510 -2
- package/skills/git-history-secret-scanner/SKILL.md +115 -0
- package/skills/gitops-delivery-auditor/SKILL.md +120 -0
- package/skills/iac-security-auditor/SKILL.md +125 -0
- package/skills/iam-privesc-graph-builder/SKILL.md +161 -0
- package/skills/incident-responder/SKILL.md +120 -0
- package/skills/injection-specialist/SKILL.md +111 -0
- package/skills/ios-security-auditor/SKILL.md +291 -0
- package/skills/json-ambiguity-tester/SKILL.md +145 -0
- package/skills/k8s-container-escaper/SKILL.md +406 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +107 -0
- package/skills/kill-switch-engineer/SKILL.md +111 -0
- package/skills/linddun-privacy-analyst/SKILL.md +111 -0
- package/skills/logic-race-fuzzer/SKILL.md +452 -0
- package/skills/mobile-api-network-attacker/SKILL.md +430 -0
- package/skills/mobile-binary-hardener/SKILL.md +111 -0
- package/skills/mobile-security-specialist/SKILL.md +94 -0
- package/skills/mobile-webview-auditor/SKILL.md +105 -0
- package/skills/model-extraction-attacker/SKILL.md +228 -0
- package/skills/multipart-abuse-tester/SKILL.md +93 -0
- package/skills/oauth-pkce-specialist/SKILL.md +113 -0
- package/skills/parser-exhaustion-tester/SKILL.md +151 -0
- package/skills/pentest-infra/SKILL.md +107 -0
- package/skills/pentest-social/SKILL.md +210 -0
- package/skills/pentest-team/SKILL.md +96 -0
- package/skills/pentest-web-api/SKILL.md +107 -0
- package/skills/privacy-flow-analyst/SKILL.md +243 -0
- package/skills/prompt-injection-specialist/SKILL.md +403 -0
- package/skills/quantum-migration-planner/SKILL.md +105 -0
- package/skills/rag-poisoning-specialist/SKILL.md +367 -0
- package/skills/registry-mirror-enforcer/SKILL.md +93 -0
- package/skills/rotation-validation-agent/SKILL.md +121 -0
- package/skills/samm-assessor/SKILL.md +94 -0
- package/skills/secrets-mask-bypass-tester/SKILL.md +109 -0
- package/skills/senior-security-engineer/SKILL.md +178 -0
- package/skills/serialization-memory-attacker/SKILL.md +341 -0
- package/skills/session-timeout-tester/SKILL.md +170 -0
- package/skills/slsa-level3-enforcer/SKILL.md +121 -0
- package/skills/slsa-provenance-enforcer/SKILL.md +111 -0
- package/skills/ssrf-detection-validator/SKILL.md +117 -0
- package/skills/step-up-auth-enforcer/SKILL.md +93 -0
- package/skills/stride-pasta-analyst/SKILL.md +429 -0
- package/skills/supply-chain-devsecops/SKILL.md +107 -0
- package/skills/threat-infrastructure-analyst/SKILL.md +93 -0
- package/skills/threat-modeler/SKILL.md +94 -0
- package/skills/tls-certificate-auditor/SKILL.md +582 -18
- package/skills/token-reuse-detector/SKILL.md +104 -0
- package/skills/trike-risk-modeler/SKILL.md +93 -0
- package/skills/unicode-homograph-tester/SKILL.md +93 -0
- package/skills/waf-rule-lifecycle-agent/SKILL.md +106 -0
- package/skills/webhook-security-tester/SKILL.md +111 -0
- 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
|
|
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 ?? {})
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
554
|
+
// package-lock.json not present — try alternatives
|
|
288
555
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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
|
+
}
|