security-mcp 1.1.3 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -185
- 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/control-catalog.json +200 -0
- package/defaults/security-policy.json +2 -2
- package/dist/cli/index.js +82 -5
- package/dist/cli/install.js +36 -6
- package/dist/cli/onboarding.js +6 -0
- package/dist/gate/baseline.js +82 -7
- package/dist/gate/catalog.js +10 -2
- package/dist/gate/checks/ai.js +757 -39
- package/dist/gate/checks/auth-deep.js +935 -0
- package/dist/gate/checks/business-logic.js +751 -0
- package/dist/gate/checks/ci-pipeline.js +399 -4
- package/dist/gate/checks/crypto.js +423 -2
- package/dist/gate/checks/dependencies.js +571 -15
- package/dist/gate/checks/graphql.js +201 -19
- package/dist/gate/checks/infra.js +246 -1
- package/dist/gate/checks/injection-deep.js +848 -0
- package/dist/gate/checks/k8s.js +114 -1
- 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 +244 -13
- package/dist/gate/checks/supply-chain-deep.js +787 -0
- package/dist/gate/checks/web-nextjs.js +572 -48
- package/dist/gate/diff.js +17 -5
- package/dist/gate/evidence.js +8 -1
- package/dist/gate/exceptions.js +131 -9
- package/dist/gate/policy.js +282 -129
- package/dist/mcp/audit-chain.js +122 -28
- package/dist/mcp/auth.js +169 -0
- package/dist/mcp/learning.js +129 -4
- package/dist/mcp/model-router.js +158 -21
- package/dist/mcp/orchestration.js +186 -51
- package/dist/mcp/server.js +608 -94
- package/dist/repo/fs.js +24 -1
- package/dist/repo/search.js +31 -6
- package/dist/review/store.js +52 -1
- package/package.json +7 -7
- package/prompts/SECURITY_PROMPT.md +73 -0
- package/skills/_TEMPLATE/SKILL.md +99 -0
- package/skills/advanced-dos-tester/SKILL.md +109 -0
- package/skills/agentic-loop-exploiter/SKILL.md +368 -0
- package/skills/ai-llm-redteam/SKILL.md +104 -0
- package/skills/ai-model-supply-chain-agent/SKILL.md +103 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +98 -0
- package/skills/android-penetration-tester/SKILL.md +455 -46
- package/skills/anti-replay-tester/SKILL.md +106 -0
- package/skills/appsec-code-auditor/SKILL.md +120 -0
- package/skills/artifact-integrity-analyst/SKILL.md +441 -0
- package/skills/attack-navigator/SKILL.md +467 -8
- package/skills/auth-session-hacker/SKILL.md +128 -0
- package/skills/aws-penetration-tester/SKILL.md +456 -0
- package/skills/azure-penetration-tester/SKILL.md +490 -3
- package/skills/binary-auth-validator/SKILL.md +111 -0
- package/skills/bot-detection-specialist/SKILL.md +109 -0
- package/skills/business-logic-attacker/SKILL.md +231 -0
- package/skills/capec-code-mapper/SKILL.md +84 -0
- package/skills/cert-pin-rotation-specialist/SKILL.md +112 -0
- package/skills/cicd-pipeline-hijacker/SKILL.md +405 -0
- package/skills/ciso-orchestrator/SKILL.md +454 -43
- package/skills/cloud-infra-specialist/SKILL.md +118 -0
- package/skills/compliance-gap-analyst/SKILL.md +422 -0
- package/skills/compliance-grc/SKILL.md +85 -0
- package/skills/compliance-lifecycle-tracker/SKILL.md +84 -0
- package/skills/credential-stuffing-specialist/SKILL.md +102 -0
- package/skills/crypto-pki-specialist/SKILL.md +87 -0
- package/skills/csa-ccm-mapper/SKILL.md +84 -0
- package/skills/csf2-governance-mapper/SKILL.md +84 -0
- package/skills/deep-link-fuzzer/SKILL.md +109 -0
- package/skills/dependency-confusion-attacker/SKILL.md +415 -0
- package/skills/device-integrity-aggregator/SKILL.md +108 -0
- package/skills/dos-resilience-tester/SKILL.md +97 -0
- package/skills/dread-scorer/SKILL.md +84 -0
- package/skills/egress-policy-enforcer/SKILL.md +99 -0
- package/skills/evidence-collector/SKILL.md +98 -0
- package/skills/file-upload-attacker/SKILL.md +109 -0
- package/skills/gcp-penetration-tester/SKILL.md +459 -2
- package/skills/git-history-secret-scanner/SKILL.md +106 -0
- package/skills/iam-privesc-graph-builder/SKILL.md +152 -0
- package/skills/incident-responder/SKILL.md +111 -0
- package/skills/injection-specialist/SKILL.md +131 -0
- package/skills/ios-security-auditor/SKILL.md +282 -0
- package/skills/json-ambiguity-tester/SKILL.md +0 -0
- package/skills/k8s-container-escaper/SKILL.md +384 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +98 -0
- package/skills/kill-switch-engineer/SKILL.md +102 -0
- package/skills/linddun-privacy-analyst/SKILL.md +102 -0
- package/skills/logic-race-fuzzer/SKILL.md +443 -0
- package/skills/mobile-api-network-attacker/SKILL.md +421 -0
- package/skills/mobile-binary-hardener/SKILL.md +102 -0
- package/skills/mobile-security-specialist/SKILL.md +85 -0
- package/skills/mobile-webview-auditor/SKILL.md +96 -0
- package/skills/model-extraction-attacker/SKILL.md +219 -0
- package/skills/multipart-abuse-tester/SKILL.md +84 -0
- package/skills/oauth-pkce-specialist/SKILL.md +104 -0
- package/skills/parser-exhaustion-tester/SKILL.md +142 -0
- package/skills/pentest-infra/SKILL.md +141 -0
- package/skills/pentest-social/SKILL.md +201 -0
- package/skills/pentest-team/SKILL.md +134 -0
- package/skills/pentest-web-api/SKILL.md +151 -0
- package/skills/privacy-flow-analyst/SKILL.md +234 -0
- package/skills/prompt-injection-specialist/SKILL.md +394 -0
- package/skills/quantum-migration-planner/SKILL.md +96 -0
- package/skills/rag-poisoning-specialist/SKILL.md +358 -0
- package/skills/registry-mirror-enforcer/SKILL.md +84 -0
- package/skills/rotation-validation-agent/SKILL.md +112 -0
- package/skills/samm-assessor/SKILL.md +85 -0
- package/skills/secrets-mask-bypass-tester/SKILL.md +100 -0
- package/skills/senior-security-engineer/SKILL.md +370 -2
- package/skills/serialization-memory-attacker/SKILL.md +332 -0
- package/skills/session-timeout-tester/SKILL.md +161 -0
- package/skills/slsa-level3-enforcer/SKILL.md +112 -0
- package/skills/slsa-provenance-enforcer/SKILL.md +102 -0
- package/skills/ssrf-detection-validator/SKILL.md +108 -0
- package/skills/step-up-auth-enforcer/SKILL.md +84 -0
- package/skills/stride-pasta-analyst/SKILL.md +420 -0
- package/skills/supply-chain-devsecops/SKILL.md +98 -0
- package/skills/threat-infrastructure-analyst/SKILL.md +84 -0
- package/skills/threat-modeler/SKILL.md +85 -0
- package/skills/tls-certificate-auditor/SKILL.md +573 -18
- package/skills/token-reuse-detector/SKILL.md +95 -0
- package/skills/trike-risk-modeler/SKILL.md +84 -0
- package/skills/unicode-homograph-tester/SKILL.md +84 -0
- package/skills/waf-rule-lifecycle-agent/SKILL.md +97 -0
- package/skills/webhook-security-tester/SKILL.md +102 -0
- package/skills/zero-trust-architect/SKILL.md +109 -0
|
@@ -1,7 +1,192 @@
|
|
|
1
1
|
import fg from "fast-glob";
|
|
2
2
|
import picomatch from "picomatch";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/** Return true if at least one file matching any of the given glob patterns
|
|
7
|
+
* exists on disk. */
|
|
8
|
+
async function anyExists(patterns) {
|
|
9
|
+
const hits = await fg(patterns, { dot: true });
|
|
10
|
+
return hits.length > 0;
|
|
11
|
+
}
|
|
12
|
+
/** Return true if any of the changedFiles matches at least one picomatch
|
|
13
|
+
* pattern from `patterns`. */
|
|
14
|
+
function anyChanged(changedFiles, patterns) {
|
|
15
|
+
const matchers = patterns.map((p) => picomatch(p, { dot: true }));
|
|
16
|
+
return changedFiles.some((f) => matchers.some((m) => m(f)));
|
|
17
|
+
}
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Static check helpers — each returns 0 or 1 Finding(s)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
async function checkThreatModel() {
|
|
22
|
+
const found = await anyExists([
|
|
23
|
+
".mcp/threat-model.json",
|
|
24
|
+
".mcp/threat-model.md",
|
|
25
|
+
"docs/threat-model.json",
|
|
26
|
+
"docs/threat-model.md",
|
|
27
|
+
"security/threat-model.json",
|
|
28
|
+
"security/threat-model.md",
|
|
29
|
+
"**/threat-model.json",
|
|
30
|
+
"**/threat-model.md"
|
|
31
|
+
]);
|
|
32
|
+
if (found)
|
|
33
|
+
return [];
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
id: "ARTIFACTS_NO_THREAT_MODEL",
|
|
37
|
+
title: "No threat model file found in .mcp/, docs/, or security/",
|
|
38
|
+
severity: "HIGH",
|
|
39
|
+
evidence: [
|
|
40
|
+
"Searched for: threat-model.json, threat-model.md under .mcp/, docs/, security/",
|
|
41
|
+
"No match found."
|
|
42
|
+
],
|
|
43
|
+
requiredActions: [
|
|
44
|
+
"Create a threat model document (threat-model.json or threat-model.md) in .mcp/, docs/, or security/.",
|
|
45
|
+
"Include STRIDE analysis, OWASP Top-10 mapping, MITRE ATT&CK mapping, trust boundaries, and data flow diagrams.",
|
|
46
|
+
"Reference the threat model from your PR description and link it to changed components."
|
|
47
|
+
],
|
|
48
|
+
sla: "7d"
|
|
49
|
+
}
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
async function checkSbom() {
|
|
53
|
+
const found = await anyExists([
|
|
54
|
+
"**/*.cdx.json",
|
|
55
|
+
"**/*.spdx",
|
|
56
|
+
"**/*.spdx.json",
|
|
57
|
+
"**/sbom.json",
|
|
58
|
+
"**/sbom.xml",
|
|
59
|
+
"**/bom.json",
|
|
60
|
+
"**/bom.xml"
|
|
61
|
+
]);
|
|
62
|
+
if (found)
|
|
63
|
+
return [];
|
|
64
|
+
return [
|
|
65
|
+
{
|
|
66
|
+
id: "ARTIFACTS_NO_SBOM",
|
|
67
|
+
title: "No SBOM (Software Bill of Materials) found in repository",
|
|
68
|
+
severity: "HIGH",
|
|
69
|
+
evidence: [
|
|
70
|
+
"Searched for CycloneDX (.cdx.json, bom.json, bom.xml) and SPDX (.spdx, .spdx.json) files.",
|
|
71
|
+
"No match found."
|
|
72
|
+
],
|
|
73
|
+
requiredActions: [
|
|
74
|
+
"Generate an SBOM in CycloneDX or SPDX format and commit it to the repository.",
|
|
75
|
+
"For Node.js: `npx @cyclonedx/cyclonedx-npm --output-file sbom.json`.",
|
|
76
|
+
"For Python: `cyclonedx-bom -o sbom.json`.",
|
|
77
|
+
"Automate SBOM generation in CI so it stays current with every dependency change.",
|
|
78
|
+
"Ensure the SBOM is included in any artifact upload to your registry (SLSA Level 2+)."
|
|
79
|
+
],
|
|
80
|
+
sla: "7d"
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
async function checkPentestSignoff(changedFiles) {
|
|
85
|
+
const triggerPatterns = ["**/*payment*", "**/*auth*", "**/*checkout*", "**/*stripe*"];
|
|
86
|
+
if (!anyChanged(changedFiles, triggerPatterns))
|
|
87
|
+
return [];
|
|
88
|
+
const found = await anyExists([
|
|
89
|
+
".mcp/pentest-report*",
|
|
90
|
+
"security/pentest-report*",
|
|
91
|
+
".mcp/*pentest*",
|
|
92
|
+
"security/*pentest*"
|
|
93
|
+
]);
|
|
94
|
+
if (found)
|
|
95
|
+
return [];
|
|
96
|
+
const triggeredFiles = changedFiles.filter((f) => anyChanged([f], triggerPatterns));
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
id: "ARTIFACTS_NO_PENTEST_SIGNOFF",
|
|
100
|
+
title: "Payment/auth files changed but no pentest report or sign-off found",
|
|
101
|
+
severity: "MEDIUM",
|
|
102
|
+
evidence: [
|
|
103
|
+
`Changed files triggering this check: ${triggeredFiles.slice(0, 10).join(", ")}`,
|
|
104
|
+
"Searched .mcp/ and security/ for pentest-report* — no match found."
|
|
105
|
+
],
|
|
106
|
+
requiredActions: [
|
|
107
|
+
"Obtain a pentest sign-off for payment and authentication flows before shipping.",
|
|
108
|
+
"Place the report as pentest-report-<date>.md (or .pdf) in .mcp/ or security/.",
|
|
109
|
+
"The report must cover OWASP Top-10 auth/session flaws, insecure direct object references, and PCI DSS requirements 6.3-6.5.",
|
|
110
|
+
"If a full pentest is not yet complete, document interim risk acceptance with CISO sign-off."
|
|
111
|
+
],
|
|
112
|
+
sla: "30d"
|
|
113
|
+
}
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
async function checkRedteamResults(changedFiles) {
|
|
117
|
+
const triggerPatterns = ["**/*llm*", "**/*openai*", "**/*anthropic*", "**/*langchain*", "**/*rag*"];
|
|
118
|
+
if (!anyChanged(changedFiles, triggerPatterns))
|
|
119
|
+
return [];
|
|
120
|
+
const found = await anyExists([
|
|
121
|
+
".mcp/agent-runs/ai-findings*",
|
|
122
|
+
".mcp/agent-runs/redteam*",
|
|
123
|
+
"security/ai-findings*",
|
|
124
|
+
"security/redteam*",
|
|
125
|
+
".mcp/ai-findings*",
|
|
126
|
+
".mcp/redteam*"
|
|
127
|
+
]);
|
|
128
|
+
if (found)
|
|
129
|
+
return [];
|
|
130
|
+
const triggeredFiles = changedFiles.filter((f) => anyChanged([f], triggerPatterns));
|
|
131
|
+
return [
|
|
132
|
+
{
|
|
133
|
+
id: "ARTIFACTS_NO_REDTEAM_RESULTS",
|
|
134
|
+
title: "AI/LLM files changed but no AI red team results found",
|
|
135
|
+
severity: "MEDIUM",
|
|
136
|
+
evidence: [
|
|
137
|
+
`Changed files triggering this check: ${triggeredFiles.slice(0, 10).join(", ")}`,
|
|
138
|
+
"Searched .mcp/agent-runs/ and security/ for ai-findings* or redteam* — no match found."
|
|
139
|
+
],
|
|
140
|
+
requiredActions: [
|
|
141
|
+
"Run an AI red team exercise covering prompt injection, indirect prompt injection, jailbreak attempts, and data exfiltration via LLM outputs.",
|
|
142
|
+
"Document results in .mcp/agent-runs/redteam-<date>.md or security/ai-findings-<date>.md.",
|
|
143
|
+
"Address any HIGH/CRITICAL findings before merging LLM-touching changes.",
|
|
144
|
+
"Reference OWASP LLM Top 10 (LLM01–LLM10) and MITRE ATLAS tactics."
|
|
145
|
+
],
|
|
146
|
+
sla: "30d"
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
async function checkComplianceGap(changedFiles) {
|
|
151
|
+
const triggerPatterns = ["**/*hipaa*", "**/*pci*", "**/*gdpr*", "**/*compliance*", "**/*policy*"];
|
|
152
|
+
if (!anyChanged(changedFiles, triggerPatterns))
|
|
153
|
+
return [];
|
|
154
|
+
const found = await anyExists([
|
|
155
|
+
".mcp/compliance-gap*",
|
|
156
|
+
".mcp/compliance-findings*",
|
|
157
|
+
"security/compliance-gap*",
|
|
158
|
+
"security/compliance-findings*",
|
|
159
|
+
"docs/compliance-gap*",
|
|
160
|
+
"docs/compliance-findings*"
|
|
161
|
+
]);
|
|
162
|
+
if (found)
|
|
163
|
+
return [];
|
|
164
|
+
const triggeredFiles = changedFiles.filter((f) => anyChanged([f], triggerPatterns));
|
|
165
|
+
return [
|
|
166
|
+
{
|
|
167
|
+
id: "ARTIFACTS_COMPLIANCE_GAP",
|
|
168
|
+
title: "Compliance-related files changed but no compliance gap analysis found",
|
|
169
|
+
severity: "MEDIUM",
|
|
170
|
+
evidence: [
|
|
171
|
+
`Changed files triggering this check: ${triggeredFiles.slice(0, 10).join(", ")}`,
|
|
172
|
+
"Searched .mcp/, security/, and docs/ for compliance-gap* or compliance-findings* — no match found."
|
|
173
|
+
],
|
|
174
|
+
requiredActions: [
|
|
175
|
+
"Produce a compliance gap analysis document before merging compliance/policy changes.",
|
|
176
|
+
"Place it as compliance-gap-<date>.md in .mcp/, security/, or docs/.",
|
|
177
|
+
"The gap analysis must map each changed control to its framework requirement (HIPAA §164, PCI DSS 4.0, GDPR Art. 32, etc.).",
|
|
178
|
+
"Document any residual risk and obtain sign-off from the compliance owner."
|
|
179
|
+
],
|
|
180
|
+
sla: "30d"
|
|
181
|
+
}
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Main export
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
3
187
|
export async function checkRequiredArtifacts(opts) {
|
|
4
188
|
const findings = [];
|
|
189
|
+
// 1. Policy-driven artifacts check (existing behaviour — do not change)
|
|
5
190
|
for (const req of opts.policy.artifacts_required ?? []) {
|
|
6
191
|
const matchers = req.on_changes.map((pattern) => picomatch(pattern, { dot: true }));
|
|
7
192
|
const touched = opts.changedFiles.some((file) => matchers.some((match) => match(file)));
|
|
@@ -21,5 +206,14 @@ export async function checkRequiredArtifacts(opts) {
|
|
|
21
206
|
});
|
|
22
207
|
}
|
|
23
208
|
}
|
|
209
|
+
// 2–6. Static checks (parallel — order of results is deterministic via spread)
|
|
210
|
+
const [threatModel, sbom, pentest, redteam, compliance] = await Promise.all([
|
|
211
|
+
checkThreatModel(),
|
|
212
|
+
checkSbom(),
|
|
213
|
+
checkPentestSignoff(opts.changedFiles),
|
|
214
|
+
checkRedteamResults(opts.changedFiles),
|
|
215
|
+
checkComplianceGap(opts.changedFiles)
|
|
216
|
+
]);
|
|
217
|
+
findings.push(...threatModel, ...sbom, ...pentest, ...redteam, ...compliance);
|
|
24
218
|
return findings;
|
|
25
219
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runtime evidence verification.
|
|
3
3
|
* Checks HTTP security headers and TLS configuration against a live target.
|
|
4
|
+
* Also contains static Dockerfile security analysis.
|
|
4
5
|
*/
|
|
5
6
|
import * as dns from "node:dns/promises";
|
|
6
7
|
import * as net from "node:net";
|
|
7
8
|
import * as https from "node:https";
|
|
8
9
|
import * as tls from "node:tls";
|
|
10
|
+
import fg from "fast-glob";
|
|
11
|
+
import { readFileSafe } from "../../repo/fs.js";
|
|
9
12
|
// CWE-918: SSRF guard — block private/link-local/metadata IP ranges
|
|
10
13
|
const PRIVATE_CIDR_PATTERNS = [
|
|
11
14
|
/^127\./, // loopback
|
|
@@ -328,3 +331,178 @@ export async function runRuntimeChecks(opts) {
|
|
|
328
331
|
}
|
|
329
332
|
return findings;
|
|
330
333
|
}
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
// Static Dockerfile analysis
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
const DOCKERFILE_GLOBS = ["**/Dockerfile", "**/Dockerfile.*", "**/*.dockerfile"];
|
|
338
|
+
const COMPOSE_GLOBS = ["**/docker-compose*.yml", "**/docker-compose*.yaml"];
|
|
339
|
+
const IGNORE = ["**/node_modules/**", "**/dist/**", "**/.git/**"];
|
|
340
|
+
async function loadDockerfiles() {
|
|
341
|
+
const paths = await fg(DOCKERFILE_GLOBS, { ignore: IGNORE });
|
|
342
|
+
const results = [];
|
|
343
|
+
for (const file of paths) {
|
|
344
|
+
try {
|
|
345
|
+
const content = await readFileSafe(file);
|
|
346
|
+
results.push({ file, content });
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
// skip unreadable files
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return results;
|
|
353
|
+
}
|
|
354
|
+
async function loadComposeFiles() {
|
|
355
|
+
const paths = await fg(COMPOSE_GLOBS, { ignore: IGNORE });
|
|
356
|
+
const results = [];
|
|
357
|
+
for (const file of paths) {
|
|
358
|
+
try {
|
|
359
|
+
const content = await readFileSafe(file);
|
|
360
|
+
results.push({ file, content });
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
// skip unreadable files
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return results;
|
|
367
|
+
}
|
|
368
|
+
async function checkDockerfileNoUser() {
|
|
369
|
+
const dockerfiles = await loadDockerfiles();
|
|
370
|
+
const offending = dockerfiles
|
|
371
|
+
.filter(({ content }) => {
|
|
372
|
+
if (!/^FROM\s/m.test(content))
|
|
373
|
+
return false;
|
|
374
|
+
// For multi-stage builds the USER directive must appear in the final stage
|
|
375
|
+
// (after the last FROM). A USER only in an earlier build stage still leaves
|
|
376
|
+
// the runtime stage running as root.
|
|
377
|
+
const lines = content.split("\n");
|
|
378
|
+
let lastFromIdx = -1;
|
|
379
|
+
lines.forEach((line, idx) => { if (/^FROM\s/i.test(line))
|
|
380
|
+
lastFromIdx = idx; });
|
|
381
|
+
return !lines.slice(lastFromIdx).some((l) => /^USER\s/i.test(l));
|
|
382
|
+
})
|
|
383
|
+
.map(({ file }) => file)
|
|
384
|
+
.slice(0, 10);
|
|
385
|
+
if (offending.length === 0)
|
|
386
|
+
return [];
|
|
387
|
+
return [{
|
|
388
|
+
id: "DOCKER_NO_USER_DIRECTIVE",
|
|
389
|
+
title: "Dockerfile has no USER directive — container runs all processes as root (CWE-250)",
|
|
390
|
+
severity: "HIGH",
|
|
391
|
+
files: offending,
|
|
392
|
+
requiredActions: [
|
|
393
|
+
"Add a USER directive to each Dockerfile to run the process as a non-root user.",
|
|
394
|
+
"Create a dedicated low-privilege user (e.g. RUN adduser --disabled-password appuser) and switch to it before CMD/ENTRYPOINT."
|
|
395
|
+
]
|
|
396
|
+
}];
|
|
397
|
+
}
|
|
398
|
+
async function checkDockerfileAddUrl() {
|
|
399
|
+
const dockerfiles = await loadDockerfiles();
|
|
400
|
+
const offending = dockerfiles
|
|
401
|
+
.filter(({ content }) => /^ADD\s+https?:\/\//m.test(content))
|
|
402
|
+
.map(({ file }) => file)
|
|
403
|
+
.slice(0, 10);
|
|
404
|
+
if (offending.length === 0)
|
|
405
|
+
return [];
|
|
406
|
+
return [{
|
|
407
|
+
id: "DOCKER_ADD_REMOTE_URL",
|
|
408
|
+
title: "Dockerfile ADD with remote URL — no integrity check, CDN compromise or DNS hijack injects malicious content",
|
|
409
|
+
severity: "HIGH",
|
|
410
|
+
files: offending,
|
|
411
|
+
requiredActions: [
|
|
412
|
+
"Replace ADD <url> with RUN curl --fail -sSL <url> | sha256sum -c <expected> to verify integrity.",
|
|
413
|
+
"Prefer COPY over ADD for local files; use a multi-stage build to fetch and verify remote artifacts."
|
|
414
|
+
]
|
|
415
|
+
}];
|
|
416
|
+
}
|
|
417
|
+
async function checkDockerfileSecretsInEnv() {
|
|
418
|
+
const dockerfiles = await loadDockerfiles();
|
|
419
|
+
const offending = dockerfiles
|
|
420
|
+
.filter(({ content }) =>
|
|
421
|
+
// Match assignment form (ENV KEY=val), legacy space form (ENV KEY val), and
|
|
422
|
+
// secret as a non-first variable on one line (ENV PORT=3000 DB_PASSWORD=x).
|
|
423
|
+
// Negative lookbehind ensures keyword is not mid-word (e.g. MONKEY won't match KEY).
|
|
424
|
+
/^ENV\s+.*(?<![A-Z\d])(?:PASSWORD|SECRET|TOKEN|CREDENTIAL|PRIVATE_KEY|API_KEY|KEY)(?:\s*=|\s+\S)/im.test(content))
|
|
425
|
+
.map(({ file }) => file)
|
|
426
|
+
.slice(0, 10);
|
|
427
|
+
if (offending.length === 0)
|
|
428
|
+
return [];
|
|
429
|
+
return [{
|
|
430
|
+
id: "DOCKER_SECRETS_IN_ENV",
|
|
431
|
+
title: "Dockerfile ENV instruction contains secret-named variable — credentials baked into image layer, visible in docker inspect",
|
|
432
|
+
severity: "CRITICAL",
|
|
433
|
+
files: offending,
|
|
434
|
+
requiredActions: [
|
|
435
|
+
"Remove secret values from ENV instructions; inject secrets at runtime via Docker secrets, environment variables passed at container start, or a secrets manager.",
|
|
436
|
+
"Audit existing image layers with 'docker history --no-trunc' to confirm no secret values are stored."
|
|
437
|
+
]
|
|
438
|
+
}];
|
|
439
|
+
}
|
|
440
|
+
async function checkDockerPrivilegedFlag() {
|
|
441
|
+
const allGlobs = [
|
|
442
|
+
...DOCKERFILE_GLOBS,
|
|
443
|
+
"**/docker-compose*.yml",
|
|
444
|
+
"**/docker-compose*.yaml",
|
|
445
|
+
"**/*.docker-compose.yml"
|
|
446
|
+
];
|
|
447
|
+
const paths = await fg(allGlobs, { ignore: IGNORE });
|
|
448
|
+
const offending = [];
|
|
449
|
+
for (const file of paths) {
|
|
450
|
+
try {
|
|
451
|
+
const content = await readFileSafe(file);
|
|
452
|
+
if (/privileged:\s*true|--privileged/.test(content)) {
|
|
453
|
+
offending.push(file);
|
|
454
|
+
if (offending.length >= 10)
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
// skip unreadable files
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (offending.length === 0)
|
|
463
|
+
return [];
|
|
464
|
+
return [{
|
|
465
|
+
id: "DOCKER_PRIVILEGED_FLAG",
|
|
466
|
+
title: "Container started with --privileged or privileged:true — all Linux capabilities granted, complete isolation disabled",
|
|
467
|
+
severity: "CRITICAL",
|
|
468
|
+
files: offending,
|
|
469
|
+
requiredActions: [
|
|
470
|
+
"Remove privileged: true and --privileged from all container configurations.",
|
|
471
|
+
"Grant only the specific Linux capabilities required using the cap_add directive (e.g. NET_ADMIN, SYS_PTRACE)."
|
|
472
|
+
]
|
|
473
|
+
}];
|
|
474
|
+
}
|
|
475
|
+
async function checkDockerSocketMountCompose() {
|
|
476
|
+
const composeFiles = await loadComposeFiles();
|
|
477
|
+
const offending = composeFiles
|
|
478
|
+
.filter(({ content }) => /\/var\/run\/docker\.sock/.test(content))
|
|
479
|
+
.map(({ file }) => file)
|
|
480
|
+
.slice(0, 10);
|
|
481
|
+
if (offending.length === 0)
|
|
482
|
+
return [];
|
|
483
|
+
return [{
|
|
484
|
+
id: "DOCKER_SOCKET_MOUNT",
|
|
485
|
+
title: "Docker socket mounted into container in docker-compose — full Docker daemon control enables host root escape",
|
|
486
|
+
severity: "CRITICAL",
|
|
487
|
+
files: offending,
|
|
488
|
+
requiredActions: [
|
|
489
|
+
"Remove /var/run/docker.sock volume mounts from all docker-compose services.",
|
|
490
|
+
"If Docker-in-Docker is required, use rootless Docker or a dedicated DinD sidecar with a restricted socket proxy (e.g. Tecnativa/docker-socket-proxy)."
|
|
491
|
+
]
|
|
492
|
+
}];
|
|
493
|
+
}
|
|
494
|
+
export async function runDockerChecks(_opts) {
|
|
495
|
+
const settled = await Promise.allSettled([
|
|
496
|
+
checkDockerfileNoUser(),
|
|
497
|
+
checkDockerfileAddUrl(),
|
|
498
|
+
checkDockerfileSecretsInEnv(),
|
|
499
|
+
checkDockerPrivilegedFlag(),
|
|
500
|
+
checkDockerSocketMountCompose()
|
|
501
|
+
]);
|
|
502
|
+
const findings = [];
|
|
503
|
+
for (const r of settled) {
|
|
504
|
+
if (r.status === "fulfilled")
|
|
505
|
+
findings.push(...r.value);
|
|
506
|
+
}
|
|
507
|
+
return findings;
|
|
508
|
+
}
|