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.
Files changed (81) hide show
  1. package/README.md +963 -193
  2. package/defaults/agent-run-schema.json +98 -0
  3. package/defaults/checklists/ai.json +25 -0
  4. package/defaults/checklists/api.json +27 -0
  5. package/defaults/checklists/infra.json +27 -0
  6. package/defaults/checklists/mobile.json +25 -0
  7. package/defaults/checklists/payments.json +25 -0
  8. package/defaults/checklists/web.json +30 -0
  9. package/defaults/control-catalog.json +392 -0
  10. package/defaults/evidence-map.json +194 -0
  11. package/defaults/security-policy.json +41 -2
  12. package/dist/cli/index.js +13 -8
  13. package/dist/cli/install.js +80 -2
  14. package/dist/cli/onboarding.js +590 -0
  15. package/dist/cli/update.js +83 -15
  16. package/dist/gate/baseline.js +115 -0
  17. package/dist/gate/checks/ai-redteam.js +398 -0
  18. package/dist/gate/checks/api.js +93 -0
  19. package/dist/gate/checks/crypto.js +153 -0
  20. package/dist/gate/checks/database.js +144 -0
  21. package/dist/gate/checks/dependencies.js +126 -0
  22. package/dist/gate/checks/dlp.js +153 -0
  23. package/dist/gate/checks/graphql.js +122 -0
  24. package/dist/gate/checks/infra.js +126 -12
  25. package/dist/gate/checks/k8s.js +190 -0
  26. package/dist/gate/checks/playbook.js +160 -0
  27. package/dist/gate/checks/runtime.js +316 -0
  28. package/dist/gate/checks/sbom.js +199 -0
  29. package/dist/gate/checks/scanners.js +379 -8
  30. package/dist/gate/checks/secrets.js +85 -20
  31. package/dist/gate/exceptions.js +6 -1
  32. package/dist/gate/policy.js +85 -19
  33. package/dist/gate/threat-intel.js +157 -0
  34. package/dist/mcp/orchestration.js +586 -0
  35. package/dist/mcp/server.js +568 -16
  36. package/dist/repo/search.js +11 -1
  37. package/dist/review/store.js +133 -0
  38. package/dist/types/agent-run.js +8 -0
  39. package/package.json +5 -5
  40. package/prompts/SECURITY_PROMPT.md +415 -1
  41. package/skills/agentic-loop-exploiter/SKILL.md +69 -0
  42. package/skills/ai-llm-redteam/SKILL.md +118 -0
  43. package/skills/algorithm-implementation-reviewer/SKILL.md +85 -0
  44. package/skills/android-penetration-tester/SKILL.md +83 -0
  45. package/skills/appsec-code-auditor/SKILL.md +86 -0
  46. package/skills/artifact-integrity-analyst/SKILL.md +68 -0
  47. package/skills/attack-navigator/SKILL.md +64 -0
  48. package/skills/auth-session-hacker/SKILL.md +87 -0
  49. package/skills/aws-penetration-tester/SKILL.md +60 -0
  50. package/skills/azure-penetration-tester/SKILL.md +64 -0
  51. package/skills/business-logic-attacker/SKILL.md +76 -0
  52. package/skills/cicd-pipeline-hijacker/SKILL.md +81 -0
  53. package/skills/ciso-orchestrator/SKILL.md +165 -0
  54. package/skills/cloud-infra-specialist/SKILL.md +85 -0
  55. package/skills/compliance-gap-analyst/SKILL.md +77 -0
  56. package/skills/compliance-grc/SKILL.md +148 -0
  57. package/skills/crypto-pki-specialist/SKILL.md +136 -0
  58. package/skills/dependency-confusion-attacker/SKILL.md +78 -0
  59. package/skills/evidence-collector/SKILL.md +86 -0
  60. package/skills/gcp-penetration-tester/SKILL.md +63 -0
  61. package/skills/injection-specialist/SKILL.md +62 -0
  62. package/skills/ios-security-auditor/SKILL.md +77 -0
  63. package/skills/k8s-container-escaper/SKILL.md +74 -0
  64. package/skills/key-management-lifecycle-analyst/SKILL.md +92 -0
  65. package/skills/logic-race-fuzzer/SKILL.md +67 -0
  66. package/skills/mobile-api-network-attacker/SKILL.md +81 -0
  67. package/skills/mobile-security-specialist/SKILL.md +124 -0
  68. package/skills/model-extraction-attacker/SKILL.md +68 -0
  69. package/skills/pentest-infra/SKILL.md +69 -0
  70. package/skills/pentest-social/SKILL.md +72 -0
  71. package/skills/pentest-team/SKILL.md +126 -0
  72. package/skills/pentest-web-api/SKILL.md +71 -0
  73. package/skills/privacy-flow-analyst/SKILL.md +70 -0
  74. package/skills/prompt-injection-specialist/SKILL.md +76 -0
  75. package/skills/rag-poisoning-specialist/SKILL.md +71 -0
  76. package/skills/senior-security-engineer/SKILL.md +75 -13
  77. package/skills/serialization-memory-attacker/SKILL.md +78 -0
  78. package/skills/stride-pasta-analyst/SKILL.md +72 -0
  79. package/skills/supply-chain-devsecops/SKILL.md +82 -0
  80. package/skills/threat-modeler/SKILL.md +116 -0
  81. 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
- const secretManagerRefs = await searchRepo({
5
- query: "secretmanager|Secret Manager|google_secret_manager",
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 (secretManagerRefs.length === 0) {
79
+ if (iamWildcards.length > 0) {
10
80
  findings.push({
11
- id: "SECRET_MANAGER_NOT_DETECTED",
12
- title: "GCP Secret Manager usage not detected in infra/app config",
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
- "Store secrets only in GCP Secret Manager.",
16
- "Configure workload identity / service accounts to access secrets, never plaintext env in repo."
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: String.raw `0\.0\.0\.0/0|::/0|public\s*=\s*true|allowAll|allUsers`,
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: "Potential public exposure patterns detected in IaC/config",
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}:${m.preview}`),
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
- "Remove or justify public ingress. Enforce Zero Trust. No implicit trust for any request or service call.",
33
- "Use private services, IAM-based auth, and least-privileged firewall rules."
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
+ }