security-mcp 1.0.5 → 1.1.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 +963 -193
- package/defaults/agent-run-schema.json +98 -0
- package/defaults/checklists/ai.json +25 -0
- package/defaults/checklists/api.json +27 -0
- package/defaults/checklists/infra.json +27 -0
- package/defaults/checklists/mobile.json +25 -0
- package/defaults/checklists/payments.json +25 -0
- package/defaults/checklists/web.json +30 -0
- package/defaults/control-catalog.json +392 -0
- package/defaults/evidence-map.json +194 -0
- package/defaults/security-policy.json +41 -2
- package/dist/cli/index.js +13 -8
- package/dist/cli/install.js +80 -2
- package/dist/cli/onboarding.js +590 -0
- package/dist/cli/update.js +83 -15
- package/dist/gate/baseline.js +115 -0
- package/dist/gate/checks/ai-redteam.js +398 -0
- package/dist/gate/checks/api.js +93 -0
- package/dist/gate/checks/crypto.js +153 -0
- package/dist/gate/checks/database.js +144 -0
- package/dist/gate/checks/dependencies.js +126 -0
- package/dist/gate/checks/dlp.js +153 -0
- package/dist/gate/checks/graphql.js +122 -0
- package/dist/gate/checks/infra.js +126 -12
- package/dist/gate/checks/k8s.js +190 -0
- package/dist/gate/checks/playbook.js +160 -0
- package/dist/gate/checks/runtime.js +316 -0
- package/dist/gate/checks/sbom.js +199 -0
- package/dist/gate/checks/scanners.js +379 -8
- package/dist/gate/checks/secrets.js +85 -20
- package/dist/gate/exceptions.js +6 -1
- package/dist/gate/policy.js +85 -19
- package/dist/gate/threat-intel.js +157 -0
- package/dist/mcp/orchestration.js +586 -0
- package/dist/mcp/server.js +568 -16
- package/dist/repo/search.js +11 -1
- package/dist/review/store.js +133 -0
- package/dist/types/agent-run.js +8 -0
- package/package.json +5 -5
- package/prompts/SECURITY_PROMPT.md +415 -1
- package/skills/agentic-loop-exploiter/SKILL.md +69 -0
- package/skills/ai-llm-redteam/SKILL.md +118 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +85 -0
- package/skills/android-penetration-tester/SKILL.md +83 -0
- package/skills/appsec-code-auditor/SKILL.md +86 -0
- package/skills/artifact-integrity-analyst/SKILL.md +68 -0
- package/skills/attack-navigator/SKILL.md +64 -0
- package/skills/auth-session-hacker/SKILL.md +87 -0
- package/skills/aws-penetration-tester/SKILL.md +60 -0
- package/skills/azure-penetration-tester/SKILL.md +64 -0
- package/skills/business-logic-attacker/SKILL.md +76 -0
- package/skills/cicd-pipeline-hijacker/SKILL.md +81 -0
- package/skills/ciso-orchestrator/SKILL.md +165 -0
- package/skills/cloud-infra-specialist/SKILL.md +85 -0
- package/skills/compliance-gap-analyst/SKILL.md +77 -0
- package/skills/compliance-grc/SKILL.md +148 -0
- package/skills/crypto-pki-specialist/SKILL.md +136 -0
- package/skills/dependency-confusion-attacker/SKILL.md +78 -0
- package/skills/evidence-collector/SKILL.md +86 -0
- package/skills/gcp-penetration-tester/SKILL.md +63 -0
- package/skills/injection-specialist/SKILL.md +62 -0
- package/skills/ios-security-auditor/SKILL.md +77 -0
- package/skills/k8s-container-escaper/SKILL.md +74 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +92 -0
- package/skills/logic-race-fuzzer/SKILL.md +67 -0
- package/skills/mobile-api-network-attacker/SKILL.md +81 -0
- package/skills/mobile-security-specialist/SKILL.md +124 -0
- package/skills/model-extraction-attacker/SKILL.md +68 -0
- package/skills/pentest-infra/SKILL.md +69 -0
- package/skills/pentest-social/SKILL.md +72 -0
- package/skills/pentest-team/SKILL.md +126 -0
- package/skills/pentest-web-api/SKILL.md +71 -0
- package/skills/privacy-flow-analyst/SKILL.md +70 -0
- package/skills/prompt-injection-specialist/SKILL.md +76 -0
- package/skills/rag-poisoning-specialist/SKILL.md +71 -0
- package/skills/senior-security-engineer/SKILL.md +75 -13
- package/skills/serialization-memory-attacker/SKILL.md +78 -0
- package/skills/stride-pasta-analyst/SKILL.md +72 -0
- package/skills/supply-chain-devsecops/SKILL.md +82 -0
- package/skills/threat-modeler/SKILL.md +116 -0
- package/skills/tls-certificate-auditor/SKILL.md +76 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { searchRepo } from "../../repo/search.js";
|
|
2
|
+
export async function checkDlp(_opts) {
|
|
3
|
+
const findings = [];
|
|
4
|
+
try {
|
|
5
|
+
// 1. SSN in logs
|
|
6
|
+
const ssnHits = await searchRepo({
|
|
7
|
+
query: String.raw `(?:console\.log|logger\.\w+|log\.\w+)\s*\([^)]*\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b`,
|
|
8
|
+
isRegex: true,
|
|
9
|
+
maxMatches: 200
|
|
10
|
+
});
|
|
11
|
+
if (ssnHits.length > 0) {
|
|
12
|
+
findings.push({
|
|
13
|
+
id: "DLP_SSN_IN_LOGS",
|
|
14
|
+
title: "Social Security Number pattern detected in log statement",
|
|
15
|
+
severity: "CRITICAL",
|
|
16
|
+
evidence: ssnHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
17
|
+
files: [...new Set(ssnHits.slice(0, 10).map((m) => m.file))],
|
|
18
|
+
requiredActions: [
|
|
19
|
+
"Remove SSN values from log statements immediately.",
|
|
20
|
+
"HIPAA requires protection of SSNs as Protected Health Information (PHI).",
|
|
21
|
+
"Use tokenization or masking before logging any government ID."
|
|
22
|
+
]
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
// 2. Credit card in logs (PAN)
|
|
26
|
+
const panHits = await searchRepo({
|
|
27
|
+
query: String.raw `(?:console\.log|logger\.\w+)\s*\([^)]*\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13})\b`,
|
|
28
|
+
isRegex: true,
|
|
29
|
+
maxMatches: 200
|
|
30
|
+
});
|
|
31
|
+
if (panHits.length > 0) {
|
|
32
|
+
findings.push({
|
|
33
|
+
id: "DLP_PAN_IN_LOGS",
|
|
34
|
+
title: "Credit card PAN pattern detected in log statement — PCI DSS violation",
|
|
35
|
+
severity: "CRITICAL",
|
|
36
|
+
evidence: panHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
37
|
+
files: [...new Set(panHits.slice(0, 10).map((m) => m.file))],
|
|
38
|
+
requiredActions: [
|
|
39
|
+
"Remove all PAN values from log statements immediately.",
|
|
40
|
+
"PCI DSS Requirement 3: Never log full card numbers.",
|
|
41
|
+
"Use masked PANs (show only last 4 digits) if logging is required."
|
|
42
|
+
]
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// 3. Full request body logged
|
|
46
|
+
const reqBodyLogHits = await searchRepo({
|
|
47
|
+
query: String.raw `(?:console\.log|logger\.\w+)\s*\(\s*(?:req\.body|request\.body|ctx\.body|\{\.\.\.req)`,
|
|
48
|
+
isRegex: true,
|
|
49
|
+
maxMatches: 200
|
|
50
|
+
});
|
|
51
|
+
if (reqBodyLogHits.length > 0) {
|
|
52
|
+
findings.push({
|
|
53
|
+
id: "DLP_REQUEST_BODY_LOGGED",
|
|
54
|
+
title: "Full request body logged — may expose PII/credentials",
|
|
55
|
+
severity: "HIGH",
|
|
56
|
+
evidence: reqBodyLogHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
57
|
+
files: [...new Set(reqBodyLogHits.slice(0, 10).map((m) => m.file))],
|
|
58
|
+
requiredActions: [
|
|
59
|
+
"Never log full request bodies — use field allowlists to log only non-sensitive fields.",
|
|
60
|
+
"GDPR Article 5: data minimization applies to logs. HIPAA prohibits logging PHI."
|
|
61
|
+
]
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// 4. User object logged
|
|
65
|
+
const userLogHits = await searchRepo({
|
|
66
|
+
query: String.raw `(?:console\.log|logger\.\w+)\s*\(\s*(?:user|currentUser|req\.user|session\.user)\s*[,)]`,
|
|
67
|
+
isRegex: true,
|
|
68
|
+
maxMatches: 200
|
|
69
|
+
});
|
|
70
|
+
if (userLogHits.length > 0) {
|
|
71
|
+
findings.push({
|
|
72
|
+
id: "DLP_USER_OBJECT_LOGGED",
|
|
73
|
+
title: "User object logged — may expose PII, hashed passwords, or tokens",
|
|
74
|
+
severity: "HIGH",
|
|
75
|
+
evidence: userLogHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
76
|
+
files: [...new Set(userLogHits.slice(0, 10).map((m) => m.file))],
|
|
77
|
+
requiredActions: [
|
|
78
|
+
"Log only specific non-sensitive user fields (e.g. userId, role).",
|
|
79
|
+
"Never log the full user object — it likely contains PII and auth data (GDPR, HIPAA)."
|
|
80
|
+
]
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// 5. Email in logs
|
|
84
|
+
const emailLogHits = await searchRepo({
|
|
85
|
+
query: String.raw `(?:console\.log|logger\.\w+)\s*\([^)]*[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}`,
|
|
86
|
+
isRegex: true,
|
|
87
|
+
maxMatches: 200
|
|
88
|
+
});
|
|
89
|
+
if (emailLogHits.length > 0) {
|
|
90
|
+
findings.push({
|
|
91
|
+
id: "DLP_EMAIL_IN_LOGS",
|
|
92
|
+
title: "Email address detected in log statement",
|
|
93
|
+
severity: "MEDIUM",
|
|
94
|
+
evidence: emailLogHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
95
|
+
files: [...new Set(emailLogHits.slice(0, 10).map((m) => m.file))],
|
|
96
|
+
requiredActions: [
|
|
97
|
+
"Mask or hash email addresses before logging (GDPR Article 5 — data minimization).",
|
|
98
|
+
"Use a user ID or anonymized identifier in logs instead of the email."
|
|
99
|
+
]
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// 6. Stack traces in API responses
|
|
103
|
+
const stackTraceHits = await searchRepo({
|
|
104
|
+
query: String.raw `(?:res\.json|res\.send|response\.json)\s*\(\s*\{[^}]*(?:stack|stackTrace|error\.stack)`,
|
|
105
|
+
isRegex: true,
|
|
106
|
+
maxMatches: 200
|
|
107
|
+
});
|
|
108
|
+
if (stackTraceHits.length > 0) {
|
|
109
|
+
findings.push({
|
|
110
|
+
id: "DLP_STACK_TRACE_IN_RESPONSE",
|
|
111
|
+
title: "Stack trace exposed in API response — CWE-209 information leakage",
|
|
112
|
+
severity: "HIGH",
|
|
113
|
+
evidence: stackTraceHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
114
|
+
files: [...new Set(stackTraceHits.slice(0, 10).map((m) => m.file))],
|
|
115
|
+
requiredActions: [
|
|
116
|
+
"Never expose stack traces in API responses (CWE-209).",
|
|
117
|
+
"Log errors internally with a correlation ID; return only a safe error message to clients."
|
|
118
|
+
]
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// 7. Server version disclosure
|
|
122
|
+
const poweredByHits = await searchRepo({
|
|
123
|
+
query: String.raw `X-Powered-By|Server:\s*(?:Express|nginx|Apache)|app\.set\s*\(\s*['"]x-powered-by['"]`,
|
|
124
|
+
isRegex: true,
|
|
125
|
+
maxMatches: 200
|
|
126
|
+
});
|
|
127
|
+
if (poweredByHits.length > 0) {
|
|
128
|
+
// Check if x-powered-by is disabled nearby
|
|
129
|
+
const disableHits = await searchRepo({
|
|
130
|
+
query: String.raw `app\.disable\s*\(\s*['"]x-powered-by['"]`,
|
|
131
|
+
isRegex: true,
|
|
132
|
+
maxMatches: 200
|
|
133
|
+
});
|
|
134
|
+
if (disableHits.length === 0) {
|
|
135
|
+
findings.push({
|
|
136
|
+
id: "DLP_SERVER_HEADER_DISCLOSURE",
|
|
137
|
+
title: "Server technology disclosed via X-Powered-By or Server response header",
|
|
138
|
+
severity: "MEDIUM",
|
|
139
|
+
evidence: poweredByHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
140
|
+
files: [...new Set(poweredByHits.slice(0, 10).map((m) => m.file))],
|
|
141
|
+
requiredActions: [
|
|
142
|
+
"Call app.disable('x-powered-by') in Express.",
|
|
143
|
+
"Remove or obscure Server headers — version disclosure aids attacker reconnaissance."
|
|
144
|
+
]
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
console.warn("[checkDlp] Internal error:", err instanceof Error ? err.message : String(err));
|
|
151
|
+
}
|
|
152
|
+
return findings;
|
|
153
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { searchRepo } from "../../repo/search.js";
|
|
2
|
+
import fg from "fast-glob";
|
|
3
|
+
import { readFileSafe } from "../../repo/fs.js";
|
|
4
|
+
export async function checkGraphQL(_opts) {
|
|
5
|
+
const findings = [];
|
|
6
|
+
try {
|
|
7
|
+
// 1. Detect if GraphQL is in use
|
|
8
|
+
const graphqlHits = await searchRepo({
|
|
9
|
+
query: "graphql|typeDefs|makeExecutableSchema|gql`|@graphql|graphene|strawberry",
|
|
10
|
+
isRegex: true,
|
|
11
|
+
maxMatches: 200
|
|
12
|
+
});
|
|
13
|
+
if (graphqlHits.length === 0) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
// 2. Introspection enabled in prod
|
|
17
|
+
const introspectionHits = await searchRepo({
|
|
18
|
+
query: String.raw `introspection.*true|disableIntrospection.*false|GraphQLSchema.*introspection`,
|
|
19
|
+
isRegex: true,
|
|
20
|
+
maxMatches: 200
|
|
21
|
+
});
|
|
22
|
+
if (introspectionHits.length > 0) {
|
|
23
|
+
findings.push({
|
|
24
|
+
id: "GRAPHQL_INTROSPECTION_ENABLED",
|
|
25
|
+
title: "GraphQL introspection is enabled — exposes full schema to attackers",
|
|
26
|
+
severity: "HIGH",
|
|
27
|
+
evidence: introspectionHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
28
|
+
files: [...new Set(introspectionHits.slice(0, 10).map((m) => m.file))],
|
|
29
|
+
requiredActions: [
|
|
30
|
+
"Disable introspection in non-dev environments.",
|
|
31
|
+
"Use persisted queries instead of ad-hoc introspection in production."
|
|
32
|
+
]
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// 3. No query depth/complexity limiting
|
|
36
|
+
const depthLimitHits = await searchRepo({
|
|
37
|
+
query: String.raw `depthLimit|complexityLimit|queryComplexity|createComplexityRule|maxDepth`,
|
|
38
|
+
isRegex: true,
|
|
39
|
+
maxMatches: 200
|
|
40
|
+
});
|
|
41
|
+
if (depthLimitHits.length === 0) {
|
|
42
|
+
findings.push({
|
|
43
|
+
id: "GRAPHQL_NO_DEPTH_LIMIT",
|
|
44
|
+
title: "No GraphQL query depth or complexity limiting detected",
|
|
45
|
+
severity: "HIGH",
|
|
46
|
+
requiredActions: [
|
|
47
|
+
"Add graphql-depth-limit or graphql-query-complexity library.",
|
|
48
|
+
"Set max depth ≤ 10 to prevent deeply nested query DoS attacks."
|
|
49
|
+
]
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// 4. No query batching limits
|
|
53
|
+
const batchingHits = await searchRepo({
|
|
54
|
+
query: String.raw `queryBatching|batchRequests|allowBatchedQueries`,
|
|
55
|
+
isRegex: true,
|
|
56
|
+
maxMatches: 200
|
|
57
|
+
});
|
|
58
|
+
if (batchingHits.length === 0) {
|
|
59
|
+
findings.push({
|
|
60
|
+
id: "GRAPHQL_NO_BATCH_LIMIT",
|
|
61
|
+
title: "No GraphQL query batching limits detected",
|
|
62
|
+
severity: "MEDIUM",
|
|
63
|
+
requiredActions: [
|
|
64
|
+
"Configure batching limits to prevent batch-based DoS attacks.",
|
|
65
|
+
"Limit the number of operations per batch request."
|
|
66
|
+
]
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// 5. Schema files found but no auth directives
|
|
70
|
+
const schemaFiles = await fg(["**/*.graphql", "**/*.gql"], {
|
|
71
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"]
|
|
72
|
+
});
|
|
73
|
+
if (schemaFiles.length > 0) {
|
|
74
|
+
let hasAuthDirectives = false;
|
|
75
|
+
for (const file of schemaFiles) {
|
|
76
|
+
try {
|
|
77
|
+
const content = await readFileSafe(file);
|
|
78
|
+
if (/@auth|@authenticated|@hasRole|@requiresAuth|directive.*auth/i.test(content)) {
|
|
79
|
+
hasAuthDirectives = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// skip unreadable files
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!hasAuthDirectives) {
|
|
88
|
+
findings.push({
|
|
89
|
+
id: "GRAPHQL_NO_FIELD_AUTH",
|
|
90
|
+
title: "GraphQL schema files found but no auth directives detected",
|
|
91
|
+
severity: "HIGH",
|
|
92
|
+
files: schemaFiles.slice(0, 10),
|
|
93
|
+
requiredActions: [
|
|
94
|
+
"Add @auth, @authenticated, or @hasRole directives to protect sensitive fields.",
|
|
95
|
+
"Use a GraphQL auth plugin (e.g. graphql-shield) for field-level authorization."
|
|
96
|
+
]
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// 6. N+1 query protection
|
|
101
|
+
const dataloaderHits = await searchRepo({
|
|
102
|
+
query: String.raw `DataLoader|dataloader|BatchLoader`,
|
|
103
|
+
isRegex: true,
|
|
104
|
+
maxMatches: 200
|
|
105
|
+
});
|
|
106
|
+
if (dataloaderHits.length === 0) {
|
|
107
|
+
findings.push({
|
|
108
|
+
id: "GRAPHQL_NO_DATALOADER",
|
|
109
|
+
title: "No DataLoader detected — GraphQL resolvers may be vulnerable to N+1 query attacks",
|
|
110
|
+
severity: "MEDIUM",
|
|
111
|
+
requiredActions: [
|
|
112
|
+
"Add DataLoader (or equivalent batch loader) to batch and cache resolver requests.",
|
|
113
|
+
"Prevent N+1 database queries which can be exploited as a DoS vector."
|
|
114
|
+
]
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
console.warn("[checkGraphQL] Internal error:", err instanceof Error ? err.message : String(err));
|
|
120
|
+
}
|
|
121
|
+
return findings;
|
|
122
|
+
}
|
|
@@ -1,36 +1,150 @@
|
|
|
1
1
|
import { searchRepo } from "../../repo/search.js";
|
|
2
|
+
// Split into two patterns to stay under the 256-char ReDoS guard in searchRepo.
|
|
3
|
+
// AWS + GCP secret managers
|
|
4
|
+
const SECRET_MANAGER_PATTERN_A = [
|
|
5
|
+
"secretsmanager", // AWS Secrets Manager
|
|
6
|
+
"ssm:GetParameter|GetSecretValue", // AWS SSM Parameter Store
|
|
7
|
+
String.raw `secretmanager\.googleapis`, // GCP Secret Manager REST/gRPC
|
|
8
|
+
"google_secret_manager", // GCP Terraform resource
|
|
9
|
+
"SecretManagerServiceClient", // GCP Secret Manager client lib
|
|
10
|
+
].join("|");
|
|
11
|
+
// Azure + HashiCorp Vault + Doppler + 1Password
|
|
12
|
+
const SECRET_MANAGER_PATTERN_B = [
|
|
13
|
+
"@azure/keyvault", // Azure Key Vault SDK (JS/TS)
|
|
14
|
+
String.raw `azure\.keyvault`, // Azure Key Vault (Python)
|
|
15
|
+
"KeyVaultSecret|SecretClient", // Azure Key Vault client classes
|
|
16
|
+
String.raw `vault\.read|vault\.write`, // HashiCorp Vault API calls
|
|
17
|
+
"hvault:|vault_generic_secret", // HashiCorp Vault Terraform
|
|
18
|
+
"doppler run|DOPPLER_TOKEN", // Doppler
|
|
19
|
+
"op run|op read|onepassword", // 1Password Secrets Automation
|
|
20
|
+
].join("|");
|
|
21
|
+
// IAM wildcard patterns — any cloud provider
|
|
22
|
+
const IAM_WILDCARD_PATTERN = String.raw `"Action"\s*:\s*"\*"|` + // AWS IAM wildcard action
|
|
23
|
+
String.raw `"Resource"\s*:\s*"\*"|` + // AWS IAM wildcard resource
|
|
24
|
+
String.raw `roles/owner|roles/editor|` + // GCP over-privileged built-in roles
|
|
25
|
+
String.raw `allUsers|allAuthenticatedUsers|` + // GCP public IAM
|
|
26
|
+
String.raw `"role"\s*:\s*"roles/owner"|` + // GCP Terraform owner binding
|
|
27
|
+
String.raw `contributor|Owner\b.*roleDefinitionId`; // Azure Contributor/Owner
|
|
28
|
+
// Public network exposure — Terraform, K8s, CloudFormation, ARM, CDK
|
|
29
|
+
const PUBLIC_INGRESS_PATTERN = String.raw `0\.0\.0\.0/0|::/0|` +
|
|
30
|
+
String.raw `public\s*=\s*true|` +
|
|
31
|
+
String.raw `PubliclyAccessible\s*:\s*true|` + // AWS RDS
|
|
32
|
+
String.raw `allow_stopping_for_update.*true|` +
|
|
33
|
+
String.raw `internet-facing|` + // AWS ALB scheme
|
|
34
|
+
String.raw `"Scheme"\s*:\s*"internet-facing"|` +
|
|
35
|
+
String.raw `block_public_acls\s*=\s*false|` + // AWS S3 block public access disabled
|
|
36
|
+
String.raw `restrict_public_buckets\s*=\s*false`;
|
|
37
|
+
// Logging / audit disabled
|
|
38
|
+
const LOGGING_DISABLED_PATTERN = String.raw `enable_logging\s*=\s*false|` +
|
|
39
|
+
String.raw `log_config\s*\{\s*\}|` + // GCP empty log config
|
|
40
|
+
String.raw `"CloudWatchLogs"\s*:\s*\{\s*\}|` + // AWS empty CloudWatch config
|
|
41
|
+
String.raw `disable_api_termination\s*=\s*true|` +
|
|
42
|
+
String.raw `deletion_protection\s*=\s*false`;
|
|
43
|
+
// Encryption disabled
|
|
44
|
+
const ENCRYPTION_DISABLED_PATTERN = String.raw `encrypted\s*=\s*false|` + // AWS EBS, RDS
|
|
45
|
+
String.raw `enable_encryption\s*=\s*false|` +
|
|
46
|
+
String.raw `kms_key_id\s*=\s*""|` +
|
|
47
|
+
String.raw `storage_encrypted\s*=\s*false|` +
|
|
48
|
+
String.raw `"EnableEncryption"\s*:\s*false`;
|
|
2
49
|
export async function checkInfra(_) {
|
|
3
50
|
const findings = [];
|
|
4
|
-
|
|
5
|
-
|
|
51
|
+
// 1. Secret manager usage — cloud-agnostic check (split across two searches
|
|
52
|
+
// to stay under the 256-char ReDoS guard in searchRepo)
|
|
53
|
+
const [smRefsA, smRefsB] = await Promise.all([
|
|
54
|
+
searchRepo({ query: SECRET_MANAGER_PATTERN_A, isRegex: true, maxMatches: 5 }),
|
|
55
|
+
searchRepo({ query: SECRET_MANAGER_PATTERN_B, isRegex: true, maxMatches: 5 })
|
|
56
|
+
]);
|
|
57
|
+
const secretManagerRefs = [...smRefsA, ...smRefsB];
|
|
58
|
+
if (secretManagerRefs.length === 0) {
|
|
59
|
+
findings.push({
|
|
60
|
+
id: "SECRET_MANAGER_NOT_DETECTED",
|
|
61
|
+
title: "No secret manager usage detected — secrets may be hardcoded or in env files",
|
|
62
|
+
severity: "HIGH",
|
|
63
|
+
requiredActions: [
|
|
64
|
+
"Integrate a cloud secret manager appropriate for your platform:",
|
|
65
|
+
" • AWS: AWS Secrets Manager or SSM Parameter Store (SecureString)",
|
|
66
|
+
" • GCP: Secret Manager with Workload Identity",
|
|
67
|
+
" • Azure: Azure Key Vault with Managed Identity",
|
|
68
|
+
" • Multi-cloud / self-hosted: HashiCorp Vault, Doppler, or 1Password Secrets Automation",
|
|
69
|
+
"Never store secrets in environment files committed to the repo, CI log output, or container images."
|
|
70
|
+
]
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// 2. IAM wildcards / over-privileged roles
|
|
74
|
+
const iamWildcards = await searchRepo({
|
|
75
|
+
query: IAM_WILDCARD_PATTERN,
|
|
6
76
|
isRegex: true,
|
|
7
77
|
maxMatches: 200
|
|
8
78
|
});
|
|
9
|
-
if (
|
|
79
|
+
if (iamWildcards.length > 0) {
|
|
10
80
|
findings.push({
|
|
11
|
-
id: "
|
|
12
|
-
title: "
|
|
81
|
+
id: "IAM_OVERPRIVILEGED",
|
|
82
|
+
title: "Overprivileged IAM role or wildcard permission detected",
|
|
13
83
|
severity: "HIGH",
|
|
84
|
+
evidence: iamWildcards.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
|
|
14
85
|
requiredActions: [
|
|
15
|
-
"
|
|
16
|
-
"
|
|
86
|
+
"Apply least-privilege to every IAM role — enumerate only the specific actions and resources required.",
|
|
87
|
+
"Replace wildcard actions ('*') with explicit action lists.",
|
|
88
|
+
"Replace Owner/Contributor/Editor bindings with purpose-scoped custom roles.",
|
|
89
|
+
"Run IAM Access Analyzer (AWS) or Policy Analyzer (GCP) to detect unused permissions."
|
|
17
90
|
]
|
|
18
91
|
});
|
|
19
92
|
}
|
|
93
|
+
// 3. Public network exposure
|
|
20
94
|
const publicIngress = await searchRepo({
|
|
21
|
-
query:
|
|
95
|
+
query: PUBLIC_INGRESS_PATTERN,
|
|
22
96
|
isRegex: true,
|
|
23
97
|
maxMatches: 200
|
|
24
98
|
});
|
|
25
99
|
if (publicIngress.length > 0) {
|
|
26
100
|
findings.push({
|
|
27
101
|
id: "PUBLIC_EXPOSURE_RISK",
|
|
28
|
-
title: "
|
|
102
|
+
title: "Public network exposure detected in IaC or cloud config",
|
|
29
103
|
severity: "HIGH",
|
|
30
|
-
evidence: publicIngress.slice(0, 20).map((m) => `${m.file}:${m.line}
|
|
104
|
+
evidence: publicIngress.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
|
|
105
|
+
requiredActions: [
|
|
106
|
+
"Restrict ingress to known CIDR ranges or private VPC subnets only.",
|
|
107
|
+
"Place public load balancers in a DMZ; never expose internal services directly.",
|
|
108
|
+
"Enable S3 Block Public Access at the account level.",
|
|
109
|
+
"Use Zero Trust network access (BeyondCorp / Zscaler / Cloudflare Access) instead of IP allowlisting."
|
|
110
|
+
]
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// 4. Encryption disabled
|
|
114
|
+
const encryptionDisabled = await searchRepo({
|
|
115
|
+
query: ENCRYPTION_DISABLED_PATTERN,
|
|
116
|
+
isRegex: true,
|
|
117
|
+
maxMatches: 200
|
|
118
|
+
});
|
|
119
|
+
if (encryptionDisabled.length > 0) {
|
|
120
|
+
findings.push({
|
|
121
|
+
id: "ENCRYPTION_DISABLED",
|
|
122
|
+
title: "Encryption at rest explicitly disabled in IaC config",
|
|
123
|
+
severity: "HIGH",
|
|
124
|
+
evidence: encryptionDisabled.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
|
|
125
|
+
requiredActions: [
|
|
126
|
+
"Enable encryption at rest on all storage resources (RDS, EBS, S3, GCS, Azure Blob, etc.).",
|
|
127
|
+
"Use customer-managed keys (CMK/CMEK) for regulated data (PCI, HIPAA, SOC 2).",
|
|
128
|
+
"Never set encrypted=false or storage_encrypted=false in Terraform."
|
|
129
|
+
]
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// 5. Audit logging disabled
|
|
133
|
+
const loggingDisabled = await searchRepo({
|
|
134
|
+
query: LOGGING_DISABLED_PATTERN,
|
|
135
|
+
isRegex: true,
|
|
136
|
+
maxMatches: 200
|
|
137
|
+
});
|
|
138
|
+
if (loggingDisabled.length > 0) {
|
|
139
|
+
findings.push({
|
|
140
|
+
id: "AUDIT_LOGGING_DISABLED",
|
|
141
|
+
title: "Audit logging or deletion protection explicitly disabled in IaC config",
|
|
142
|
+
severity: "MEDIUM",
|
|
143
|
+
evidence: loggingDisabled.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
|
|
31
144
|
requiredActions: [
|
|
32
|
-
"
|
|
33
|
-
"
|
|
145
|
+
"Enable audit logging on all cloud resources and ship logs to a centralised, tamper-evident store.",
|
|
146
|
+
"Enable deletion protection on databases and stateful resources.",
|
|
147
|
+
"Retain audit logs for at least 1 year (SOC 2 / PCI DSS requirement)."
|
|
34
148
|
]
|
|
35
149
|
});
|
|
36
150
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fg from "fast-glob";
|
|
2
|
+
import { readFileSafe } from "../../repo/fs.js";
|
|
3
|
+
export async function checkKubernetes(_opts) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
try {
|
|
6
|
+
// 1. Glob YAML files and filter to K8s manifests
|
|
7
|
+
const yamlFiles = await fg(["**/*.yaml", "**/*.yml"], {
|
|
8
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
|
|
9
|
+
});
|
|
10
|
+
const k8sFiles = [];
|
|
11
|
+
const k8sContents = new Map();
|
|
12
|
+
for (const file of yamlFiles) {
|
|
13
|
+
try {
|
|
14
|
+
const content = await readFileSafe(file);
|
|
15
|
+
if (/kind\s*:/.test(content)) {
|
|
16
|
+
k8sFiles.push(file);
|
|
17
|
+
k8sContents.set(file, content);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// skip unreadable files
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (k8sFiles.length === 0) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
// Helper to collect files matching a pattern
|
|
28
|
+
function filesMatching(pattern) {
|
|
29
|
+
return k8sFiles.filter((f) => pattern.test(k8sContents.get(f) ?? "")).slice(0, 10);
|
|
30
|
+
}
|
|
31
|
+
// 3. Privileged containers
|
|
32
|
+
const privilegedFiles = filesMatching(/privileged:\s*true/);
|
|
33
|
+
if (privilegedFiles.length > 0) {
|
|
34
|
+
findings.push({
|
|
35
|
+
id: "K8S_PRIVILEGED_CONTAINER",
|
|
36
|
+
title: "Kubernetes manifests with privileged containers detected",
|
|
37
|
+
severity: "CRITICAL",
|
|
38
|
+
files: privilegedFiles,
|
|
39
|
+
requiredActions: [
|
|
40
|
+
"Remove privileged: true from all container securityContexts.",
|
|
41
|
+
"Use specific capability grants (e.g. NET_ADMIN) instead of full privileged mode."
|
|
42
|
+
]
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// 4. allowPrivilegeEscalation
|
|
46
|
+
const escFiles = filesMatching(/allowPrivilegeEscalation:\s*true/);
|
|
47
|
+
if (escFiles.length > 0) {
|
|
48
|
+
findings.push({
|
|
49
|
+
id: "K8S_PRIVILEGE_ESCALATION",
|
|
50
|
+
title: "Kubernetes manifests allow privilege escalation",
|
|
51
|
+
severity: "HIGH",
|
|
52
|
+
files: escFiles,
|
|
53
|
+
requiredActions: [
|
|
54
|
+
"Set allowPrivilegeEscalation: false in all container securityContexts.",
|
|
55
|
+
"This prevents child processes from gaining more privileges than their parent."
|
|
56
|
+
]
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// 5. Host namespaces
|
|
60
|
+
const hostNsFiles = filesMatching(/hostPID:\s*true|hostNetwork:\s*true|hostIPC:\s*true/);
|
|
61
|
+
if (hostNsFiles.length > 0) {
|
|
62
|
+
findings.push({
|
|
63
|
+
id: "K8S_HOST_NAMESPACE",
|
|
64
|
+
title: "Kubernetes manifests use host namespaces (hostPID/hostNetwork/hostIPC)",
|
|
65
|
+
severity: "HIGH",
|
|
66
|
+
files: hostNsFiles,
|
|
67
|
+
requiredActions: [
|
|
68
|
+
"Remove hostPID, hostNetwork, and hostIPC settings from pod specs.",
|
|
69
|
+
"Host namespace sharing breaks container isolation and exposes the host."
|
|
70
|
+
]
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// 6. Missing securityContext
|
|
74
|
+
const missingSecCtxFiles = k8sFiles.filter((f) => {
|
|
75
|
+
const c = k8sContents.get(f) ?? "";
|
|
76
|
+
return /containers:/.test(c) && !/securityContext:/.test(c);
|
|
77
|
+
}).slice(0, 10);
|
|
78
|
+
if (missingSecCtxFiles.length > 0) {
|
|
79
|
+
findings.push({
|
|
80
|
+
id: "K8S_NO_SECURITY_CONTEXT",
|
|
81
|
+
title: "Kubernetes manifests with containers but no securityContext",
|
|
82
|
+
severity: "MEDIUM",
|
|
83
|
+
files: missingSecCtxFiles,
|
|
84
|
+
requiredActions: [
|
|
85
|
+
"Add securityContext to all containers with runAsNonRoot: true, readOnlyRootFilesystem: true.",
|
|
86
|
+
"Set allowPrivilegeEscalation: false and drop all capabilities."
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// 7. Secrets in ConfigMap
|
|
91
|
+
const configMapFiles = k8sFiles.filter((f) => {
|
|
92
|
+
const c = k8sContents.get(f) ?? "";
|
|
93
|
+
return /kind:\s*ConfigMap/.test(c) && /password|secret|key|token/i.test(c);
|
|
94
|
+
}).slice(0, 10);
|
|
95
|
+
if (configMapFiles.length > 0) {
|
|
96
|
+
findings.push({
|
|
97
|
+
id: "K8S_SECRET_IN_CONFIGMAP",
|
|
98
|
+
title: "Sensitive data (password/secret/key/token) found in Kubernetes ConfigMap",
|
|
99
|
+
severity: "CRITICAL",
|
|
100
|
+
files: configMapFiles,
|
|
101
|
+
requiredActions: [
|
|
102
|
+
"Move secrets to Kubernetes Secrets objects or a secrets manager (Vault, AWS SM).",
|
|
103
|
+
"Never store plaintext credentials in ConfigMaps — they are not encrypted at rest by default."
|
|
104
|
+
]
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// 8. ClusterAdmin binding
|
|
108
|
+
const clusterAdminFiles = k8sFiles.filter((f) => {
|
|
109
|
+
const c = k8sContents.get(f) ?? "";
|
|
110
|
+
return /kind:\s*ClusterRoleBinding/.test(c) && /cluster-admin/.test(c);
|
|
111
|
+
}).slice(0, 10);
|
|
112
|
+
if (clusterAdminFiles.length > 0) {
|
|
113
|
+
findings.push({
|
|
114
|
+
id: "K8S_CLUSTER_ADMIN_BINDING",
|
|
115
|
+
title: "ClusterRoleBinding to cluster-admin detected",
|
|
116
|
+
severity: "CRITICAL",
|
|
117
|
+
files: clusterAdminFiles,
|
|
118
|
+
requiredActions: [
|
|
119
|
+
"Remove cluster-admin bindings and apply least-privilege RBAC roles.",
|
|
120
|
+
"Create scoped Roles/ClusterRoles with only the permissions actually needed."
|
|
121
|
+
]
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// 9. No resource limits
|
|
125
|
+
const noLimitsFiles = k8sFiles.filter((f) => {
|
|
126
|
+
const c = k8sContents.get(f) ?? "";
|
|
127
|
+
return /containers:/.test(c) && !/limits:/.test(c);
|
|
128
|
+
}).slice(0, 10);
|
|
129
|
+
if (noLimitsFiles.length > 0) {
|
|
130
|
+
findings.push({
|
|
131
|
+
id: "K8S_NO_RESOURCE_LIMITS",
|
|
132
|
+
title: "Kubernetes containers without resource limits detected",
|
|
133
|
+
severity: "MEDIUM",
|
|
134
|
+
files: noLimitsFiles,
|
|
135
|
+
requiredActions: [
|
|
136
|
+
"Add resources.limits (cpu, memory) to all containers.",
|
|
137
|
+
"Missing limits allow a single container to starve the entire node (DoS)."
|
|
138
|
+
]
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
// 10. Default namespace
|
|
142
|
+
const defaultNsFiles = k8sFiles.filter((f) => {
|
|
143
|
+
const c = k8sContents.get(f) ?? "";
|
|
144
|
+
return /namespace:\s*default/.test(c) || (!/namespace:/.test(c) && /kind:\s*(?:Deployment|Service|Pod|StatefulSet)/.test(c));
|
|
145
|
+
}).slice(0, 10);
|
|
146
|
+
if (defaultNsFiles.length > 0) {
|
|
147
|
+
findings.push({
|
|
148
|
+
id: "K8S_DEFAULT_NAMESPACE",
|
|
149
|
+
title: "Kubernetes manifests use default namespace or have no namespace set",
|
|
150
|
+
severity: "LOW",
|
|
151
|
+
files: defaultNsFiles,
|
|
152
|
+
requiredActions: [
|
|
153
|
+
"Create dedicated namespaces for each application/team.",
|
|
154
|
+
"Apply RBAC and NetworkPolicies scoped to those namespaces."
|
|
155
|
+
]
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// 11. Latest image tag
|
|
159
|
+
const latestTagFiles = filesMatching(/:latest\b/);
|
|
160
|
+
if (latestTagFiles.length > 0) {
|
|
161
|
+
findings.push({
|
|
162
|
+
id: "K8S_LATEST_IMAGE_TAG",
|
|
163
|
+
title: "Kubernetes manifests use ':latest' image tag",
|
|
164
|
+
severity: "HIGH",
|
|
165
|
+
files: latestTagFiles,
|
|
166
|
+
requiredActions: [
|
|
167
|
+
"Pin container images to an immutable digest (e.g. image@sha256:...).",
|
|
168
|
+
"Never use :latest in production — it leads to unpredictable deployments."
|
|
169
|
+
]
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// 12. No network policy
|
|
173
|
+
const networkPolicyFiles = await fg(["**/NetworkPolicy*.yaml", "**/*network-policy*.yaml", "**/NetworkPolicy*.yml", "**/*network-policy*.yml"], { ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"] });
|
|
174
|
+
if (networkPolicyFiles.length === 0) {
|
|
175
|
+
findings.push({
|
|
176
|
+
id: "K8S_NO_NETWORK_POLICY",
|
|
177
|
+
title: "No Kubernetes NetworkPolicy found — all pod-to-pod traffic is allowed",
|
|
178
|
+
severity: "HIGH",
|
|
179
|
+
requiredActions: [
|
|
180
|
+
"Create NetworkPolicy resources to restrict ingress and egress traffic.",
|
|
181
|
+
"Default-deny all traffic and only allow explicitly required paths."
|
|
182
|
+
]
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
console.warn("[checkKubernetes] Internal error:", err instanceof Error ? err.message : String(err));
|
|
188
|
+
}
|
|
189
|
+
return findings;
|
|
190
|
+
}
|