security-mcp 1.3.1 → 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.
Files changed (131) hide show
  1. package/README.md +356 -885
  2. package/defaults/cloud-controls/aws.json +10712 -0
  3. package/defaults/cloud-controls/azure.json +7201 -0
  4. package/defaults/cloud-controls/gcp.json +4061 -0
  5. package/defaults/control-catalog.json +24 -0
  6. package/dist/ci/pr-gate.js +22 -5
  7. package/dist/cli/index.js +73 -2
  8. package/dist/cli/install.js +4 -55
  9. package/dist/cli/onboarding.js +18 -10
  10. package/dist/gate/checks/agentic-instructions.js +515 -0
  11. package/dist/gate/checks/ai-governance.js +132 -0
  12. package/dist/gate/checks/ai.js +1 -1
  13. package/dist/gate/checks/cloud-controls.js +69 -0
  14. package/dist/gate/checks/crypto.js +1 -1
  15. package/dist/gate/checks/data-platform.js +954 -0
  16. package/dist/gate/checks/dependencies.js +14 -3
  17. package/dist/gate/checks/docker-deep.js +1236 -0
  18. package/dist/gate/checks/gitops.js +724 -0
  19. package/dist/gate/checks/iac.js +1230 -0
  20. package/dist/gate/checks/k8s.js +841 -1
  21. package/dist/gate/checks/secrets.js +49 -37
  22. package/dist/gate/cloud-controls/apply.js +115 -0
  23. package/dist/gate/cloud-controls/bicep.js +36 -0
  24. package/dist/gate/cloud-controls/cfn.js +125 -0
  25. package/dist/gate/cloud-controls/detect.js +104 -0
  26. package/dist/gate/cloud-controls/hcl.js +140 -0
  27. package/dist/gate/cloud-controls/types.js +87 -0
  28. package/dist/gate/exceptions.js +78 -7
  29. package/dist/gate/findings.js +15 -2
  30. package/dist/gate/policy.js +40 -3
  31. package/dist/gate/threat-intel.js +6 -0
  32. package/dist/mcp/audit-chain.js +9 -0
  33. package/dist/mcp/model-router.js +3 -3
  34. package/dist/mcp/orchestration.js +194 -41
  35. package/dist/mcp/server.js +124 -17
  36. package/dist/mcp/tool-audit.js +193 -0
  37. package/dist/repo/fs.js +14 -1
  38. package/dist/review/store.js +4 -2
  39. package/dist/tests/run.js +124 -1
  40. package/package.json +3 -3
  41. package/skills/advanced-dos-tester/SKILL.md +9 -0
  42. package/skills/agentic-instruction-auditor/SKILL.md +111 -0
  43. package/skills/agentic-loop-exploiter/SKILL.md +9 -0
  44. package/skills/ai-llm-redteam/SKILL.md +9 -0
  45. package/skills/ai-model-supply-chain-agent/SKILL.md +9 -0
  46. package/skills/algorithm-implementation-reviewer/SKILL.md +9 -0
  47. package/skills/android-penetration-tester/SKILL.md +9 -0
  48. package/skills/anti-replay-tester/SKILL.md +9 -0
  49. package/skills/appsec-code-auditor/SKILL.md +9 -0
  50. package/skills/artifact-integrity-analyst/SKILL.md +9 -0
  51. package/skills/attack-navigator/SKILL.md +9 -0
  52. package/skills/auth-session-hacker/SKILL.md +9 -0
  53. package/skills/aws-penetration-tester/SKILL.md +54 -0
  54. package/skills/azure-penetration-tester/SKILL.md +52 -0
  55. package/skills/binary-auth-validator/SKILL.md +9 -0
  56. package/skills/bot-detection-specialist/SKILL.md +9 -0
  57. package/skills/business-logic-attacker/SKILL.md +9 -0
  58. package/skills/capec-code-mapper/SKILL.md +9 -0
  59. package/skills/cert-pin-rotation-specialist/SKILL.md +9 -0
  60. package/skills/cicd-pipeline-hijacker/SKILL.md +9 -0
  61. package/skills/ciso-orchestrator/SKILL.md +11 -0
  62. package/skills/cloud-infra-specialist/SKILL.md +9 -0
  63. package/skills/compliance-gap-analyst/SKILL.md +9 -0
  64. package/skills/compliance-grc/SKILL.md +9 -0
  65. package/skills/compliance-lifecycle-tracker/SKILL.md +9 -0
  66. package/skills/container-hardening-auditor/SKILL.md +125 -0
  67. package/skills/credential-stuffing-specialist/SKILL.md +9 -0
  68. package/skills/crypto-pki-specialist/SKILL.md +9 -0
  69. package/skills/csa-ccm-mapper/SKILL.md +9 -0
  70. package/skills/csf2-governance-mapper/SKILL.md +9 -0
  71. package/skills/data-platform-auditor/SKILL.md +125 -0
  72. package/skills/deep-link-fuzzer/SKILL.md +9 -0
  73. package/skills/dependency-confusion-attacker/SKILL.md +9 -0
  74. package/skills/device-integrity-aggregator/SKILL.md +9 -0
  75. package/skills/dos-resilience-tester/SKILL.md +9 -0
  76. package/skills/dread-scorer/SKILL.md +9 -0
  77. package/skills/egress-policy-enforcer/SKILL.md +9 -0
  78. package/skills/evidence-collector/SKILL.md +9 -0
  79. package/skills/file-upload-attacker/SKILL.md +9 -0
  80. package/skills/gcp-penetration-tester/SKILL.md +51 -0
  81. package/skills/git-history-secret-scanner/SKILL.md +9 -0
  82. package/skills/gitops-delivery-auditor/SKILL.md +120 -0
  83. package/skills/iac-security-auditor/SKILL.md +125 -0
  84. package/skills/iam-privesc-graph-builder/SKILL.md +9 -0
  85. package/skills/incident-responder/SKILL.md +9 -0
  86. package/skills/injection-specialist/SKILL.md +9 -0
  87. package/skills/ios-security-auditor/SKILL.md +9 -0
  88. package/skills/json-ambiguity-tester/SKILL.md +0 -0
  89. package/skills/k8s-container-escaper/SKILL.md +22 -0
  90. package/skills/key-management-lifecycle-analyst/SKILL.md +9 -0
  91. package/skills/kill-switch-engineer/SKILL.md +9 -0
  92. package/skills/linddun-privacy-analyst/SKILL.md +9 -0
  93. package/skills/logic-race-fuzzer/SKILL.md +9 -0
  94. package/skills/mobile-api-network-attacker/SKILL.md +9 -0
  95. package/skills/mobile-binary-hardener/SKILL.md +9 -0
  96. package/skills/mobile-security-specialist/SKILL.md +9 -0
  97. package/skills/mobile-webview-auditor/SKILL.md +9 -0
  98. package/skills/model-extraction-attacker/SKILL.md +9 -0
  99. package/skills/multipart-abuse-tester/SKILL.md +9 -0
  100. package/skills/oauth-pkce-specialist/SKILL.md +9 -0
  101. package/skills/parser-exhaustion-tester/SKILL.md +9 -0
  102. package/skills/pentest-infra/SKILL.md +9 -0
  103. package/skills/pentest-social/SKILL.md +9 -0
  104. package/skills/pentest-team/SKILL.md +9 -0
  105. package/skills/pentest-web-api/SKILL.md +9 -0
  106. package/skills/privacy-flow-analyst/SKILL.md +9 -0
  107. package/skills/prompt-injection-specialist/SKILL.md +9 -0
  108. package/skills/quantum-migration-planner/SKILL.md +9 -0
  109. package/skills/rag-poisoning-specialist/SKILL.md +9 -0
  110. package/skills/registry-mirror-enforcer/SKILL.md +9 -0
  111. package/skills/rotation-validation-agent/SKILL.md +9 -0
  112. package/skills/samm-assessor/SKILL.md +9 -0
  113. package/skills/secrets-mask-bypass-tester/SKILL.md +9 -0
  114. package/skills/senior-security-engineer/SKILL.md +11 -0
  115. package/skills/serialization-memory-attacker/SKILL.md +9 -0
  116. package/skills/session-timeout-tester/SKILL.md +9 -0
  117. package/skills/slsa-level3-enforcer/SKILL.md +9 -0
  118. package/skills/slsa-provenance-enforcer/SKILL.md +9 -0
  119. package/skills/ssrf-detection-validator/SKILL.md +9 -0
  120. package/skills/step-up-auth-enforcer/SKILL.md +9 -0
  121. package/skills/stride-pasta-analyst/SKILL.md +9 -0
  122. package/skills/supply-chain-devsecops/SKILL.md +9 -0
  123. package/skills/threat-infrastructure-analyst/SKILL.md +9 -0
  124. package/skills/threat-modeler/SKILL.md +9 -0
  125. package/skills/tls-certificate-auditor/SKILL.md +9 -0
  126. package/skills/token-reuse-detector/SKILL.md +9 -0
  127. package/skills/trike-risk-modeler/SKILL.md +9 -0
  128. package/skills/unicode-homograph-tester/SKILL.md +9 -0
  129. package/skills/waf-rule-lifecycle-agent/SKILL.md +9 -0
  130. package/skills/webhook-security-tester/SKILL.md +9 -0
  131. package/skills/zero-trust-architect/SKILL.md +9 -0
@@ -329,6 +329,839 @@ async function checkNetworkAndAdmission(ctx) {
329
329
  }
330
330
  return findings;
331
331
  }
332
+ /**
333
+ * RBAC privilege-escalation depth — wildcard verbs/resources/apiGroups, dangerous
334
+ * verbs, workload-create token theft paths, and binding to system superuser groups.
335
+ */
336
+ function checkRbacEscalationDepth(ctx) {
337
+ const findings = [];
338
+ const isRbac = (c) => /kind:\s*(?:Cluster)?Role\b/.test(c);
339
+ const wildcardFiles = ctx.files
340
+ .filter((f) => {
341
+ const c = ctx.contents.get(f) ?? "";
342
+ return (isRbac(c) &&
343
+ (/verbs:\s*\[?\s*["']?\*/.test(c) ||
344
+ /resources:\s*\[?\s*["']?\*/.test(c) ||
345
+ /apiGroups:\s*\[?\s*["']?\*/.test(c)));
346
+ })
347
+ .slice(0, 10);
348
+ if (wildcardFiles.length > 0) {
349
+ findings.push({
350
+ id: "K8S_RBAC_WILDCARD",
351
+ title: "RBAC Role/ClusterRole grants wildcard verbs, resources, or apiGroups",
352
+ severity: "CRITICAL",
353
+ files: wildcardFiles,
354
+ requiredActions: [
355
+ "Wildcard ('*') verbs/resources/apiGroups grant effective cluster-admin — any holder can read all Secrets and create workloads to harvest other ServiceAccount tokens.",
356
+ "Replace '*' with the explicit minimal verbs (e.g. [\"get\",\"list\"]) and named resources/apiGroups each subject actually requires."
357
+ ]
358
+ });
359
+ }
360
+ const escalateVerbFiles = ctx.files
361
+ .filter((f) => {
362
+ const c = ctx.contents.get(f) ?? "";
363
+ return isRbac(c) && /["']?(?:escalate|bind|impersonate)["']?/.test(c) && /verbs:/.test(c);
364
+ })
365
+ .slice(0, 10);
366
+ if (escalateVerbFiles.length > 0) {
367
+ findings.push({
368
+ id: "K8S_RBAC_ESCALATE_VERB",
369
+ title: "RBAC grants escalate / bind / impersonate verbs",
370
+ severity: "CRITICAL",
371
+ files: escalateVerbFiles,
372
+ requiredActions: [
373
+ "The escalate verb lets a subject grant itself permissions it does not have; bind lets it create bindings to any role; impersonate lets it act as any user/group/SA — each is a direct path to cluster-admin.",
374
+ "Remove escalate, bind, and impersonate verbs from all Roles/ClusterRoles unless the subject is a trusted controller, and scope impersonate to specific named users."
375
+ ]
376
+ });
377
+ }
378
+ const execFiles = matching(ctx, /pods\/(?:exec|attach|portforward)/);
379
+ if (execFiles.length > 0) {
380
+ findings.push({
381
+ id: "K8S_RBAC_PODS_EXEC",
382
+ title: "RBAC grants pods/exec, pods/attach, or pods/portforward",
383
+ severity: "HIGH",
384
+ files: execFiles,
385
+ requiredActions: [
386
+ "pods/exec, pods/attach, and pods/portforward give an interactive shell into running pods — an attacker can read mounted Secrets and SA tokens and pivot laterally.",
387
+ "Remove these subresources from general-purpose roles; restrict to a tightly scoped break-glass role gated by audit logging and MFA."
388
+ ]
389
+ });
390
+ }
391
+ const workloadCreateFiles = ctx.files
392
+ .filter((f) => {
393
+ const c = ctx.contents.get(f) ?? "";
394
+ return (isRbac(c) &&
395
+ /verbs:[\s\S]{0,80}(?:create|patch)/.test(c) &&
396
+ /resources:[\s\S]{0,80}(?:pods|deployments|daemonsets|statefulsets|replicasets|jobs|cronjobs)/.test(c));
397
+ })
398
+ .slice(0, 10);
399
+ if (workloadCreateFiles.length > 0) {
400
+ findings.push({
401
+ id: "K8S_RBAC_WORKLOAD_CREATE",
402
+ title: "RBAC allows create/patch on workload resources (token theft path)",
403
+ severity: "HIGH",
404
+ files: workloadCreateFiles,
405
+ requiredActions: [
406
+ "create/patch on pods/deployments/daemonsets lets an attacker schedule a pod mounting any ServiceAccount token (including privileged ones) and exfiltrate it — a well-known privilege-escalation primitive.",
407
+ "Restrict workload create/patch to CI/controller identities only and enforce a Gatekeeper/Kyverno policy blocking pods that mount privileged SA tokens."
408
+ ]
409
+ });
410
+ }
411
+ const clusterSecretFiles = ctx.files
412
+ .filter((f) => {
413
+ const c = ctx.contents.get(f) ?? "";
414
+ return /kind:\s*ClusterRole\b/.test(c) && /resources:[\s\S]{0,60}secrets/.test(c) && /verbs:[\s\S]{0,60}(?:get|list|watch)/.test(c);
415
+ })
416
+ .slice(0, 10);
417
+ if (clusterSecretFiles.length > 0) {
418
+ findings.push({
419
+ id: "K8S_RBAC_CLUSTER_SECRETS",
420
+ title: "ClusterRole grants get/list/watch on Secrets at cluster scope",
421
+ severity: "CRITICAL",
422
+ files: clusterSecretFiles,
423
+ requiredActions: [
424
+ "Cluster-scoped get/list/watch on Secrets exposes every Secret in every namespace, including SA tokens, registry creds, and TLS keys.",
425
+ "Replace the ClusterRole with namespaced Roles, or restrict the ClusterRole to specific named Secrets via resourceNames."
426
+ ]
427
+ });
428
+ }
429
+ const tokenCreateFiles = matching(ctx, /serviceaccounts\/token/);
430
+ if (tokenCreateFiles.length > 0) {
431
+ findings.push({
432
+ id: "K8S_RBAC_SA_TOKEN_CREATE",
433
+ title: "RBAC grants create on serviceaccounts/token (TokenRequest abuse)",
434
+ severity: "HIGH",
435
+ files: tokenCreateFiles,
436
+ requiredActions: [
437
+ "create on serviceaccounts/token lets a subject mint short-lived tokens for any ServiceAccount it can name — effectively impersonating those SAs.",
438
+ "Remove serviceaccounts/token create permission, or scope it via resourceNames to the single SA the controller legitimately needs."
439
+ ]
440
+ });
441
+ }
442
+ const nodesProxyFiles = matching(ctx, /nodes\/proxy/);
443
+ if (nodesProxyFiles.length > 0) {
444
+ findings.push({
445
+ id: "K8S_RBAC_NODES_PROXY",
446
+ title: "RBAC grants nodes/proxy (kubelet API access)",
447
+ severity: "HIGH",
448
+ files: nodesProxyFiles,
449
+ requiredActions: [
450
+ "nodes/proxy exposes the kubelet API, allowing exec into any pod on the node and bypassing pod-level RBAC.",
451
+ "Remove nodes/proxy from all roles; use the API server's pods/exec with audit logging for any legitimate debugging need."
452
+ ]
453
+ });
454
+ }
455
+ const csrApproveFiles = ctx.files
456
+ .filter((f) => {
457
+ const c = ctx.contents.get(f) ?? "";
458
+ return /certificatesigningrequests/.test(c) && /approve|signers/.test(c);
459
+ })
460
+ .slice(0, 10);
461
+ if (csrApproveFiles.length > 0) {
462
+ findings.push({
463
+ id: "K8S_RBAC_CSR_APPROVE",
464
+ title: "RBAC grants approve on certificatesigningrequests",
465
+ severity: "CRITICAL",
466
+ files: csrApproveFiles,
467
+ requiredActions: [
468
+ "Approving CSRs lets an attacker issue client certs for arbitrary identities (e.g. CN=system:masters) and authenticate as cluster-admin, bypassing RBAC entirely.",
469
+ "Remove CSR approve/sign permissions from all subjects except the controller-manager; audit all approved CSRs."
470
+ ]
471
+ });
472
+ }
473
+ const superuserBindFiles = matching(ctx, /system:masters|system:anonymous|system:unauthenticated/);
474
+ if (superuserBindFiles.length > 0) {
475
+ findings.push({
476
+ id: "K8S_RBAC_SUPERUSER_SUBJECT",
477
+ title: "RBAC binding references system:masters / system:anonymous / system:unauthenticated",
478
+ severity: "CRITICAL",
479
+ files: superuserBindFiles,
480
+ requiredActions: [
481
+ "system:masters bypasses all RBAC (hardcoded superuser); binding roles to system:anonymous or system:unauthenticated grants unauthenticated callers access.",
482
+ "Remove all bindings to these subjects. Never add users to system:masters — use scoped admin roles instead."
483
+ ]
484
+ });
485
+ }
486
+ return findings;
487
+ }
488
+ /**
489
+ * Pod / workload container-escape depth — securityContext gaps, dangerous added
490
+ * capabilities, host user namespace, unconfined profiles, token automounting.
491
+ */
492
+ function checkPodEscapeDepth(ctx) {
493
+ const findings = [];
494
+ const hasContainers = (c) => /containers:/.test(c);
495
+ const apeMissingFiles = ctx.files
496
+ .filter((f) => {
497
+ const c = ctx.contents.get(f) ?? "";
498
+ return hasContainers(c) && !/allowPrivilegeEscalation:\s*false/.test(c);
499
+ })
500
+ .slice(0, 10);
501
+ if (apeMissingFiles.length > 0) {
502
+ findings.push({
503
+ id: "K8S_ALLOW_PRIV_ESC_NOT_FALSE",
504
+ title: "Container does not set allowPrivilegeEscalation: false",
505
+ severity: "HIGH",
506
+ files: apeMissingFiles,
507
+ requiredActions: [
508
+ "Without allowPrivilegeEscalation: false, a setuid binary or file capabilities can let a process gain more privileges than its parent — a building block for escape.",
509
+ "Explicitly set securityContext.allowPrivilegeEscalation: false on every container."
510
+ ]
511
+ });
512
+ }
513
+ const procMountFiles = matching(ctx, /procMount:\s*Unmasked/);
514
+ if (procMountFiles.length > 0) {
515
+ findings.push({
516
+ id: "K8S_PROCMOUNT_UNMASKED",
517
+ title: "Container uses procMount: Unmasked",
518
+ severity: "HIGH",
519
+ files: procMountFiles,
520
+ requiredActions: [
521
+ "procMount: Unmasked exposes the full host /proc (including /proc/sysrq-trigger and kcore), enabling host inspection and several escape techniques.",
522
+ "Remove procMount: Unmasked and use the Default masked /proc."
523
+ ]
524
+ });
525
+ }
526
+ const shareProcFiles = matching(ctx, /shareProcessNamespace:\s*true/);
527
+ if (shareProcFiles.length > 0) {
528
+ findings.push({
529
+ id: "K8S_SHARE_PROCESS_NAMESPACE",
530
+ title: "Pod sets shareProcessNamespace: true",
531
+ severity: "MEDIUM",
532
+ files: shareProcFiles,
533
+ requiredActions: [
534
+ "shareProcessNamespace lets every container in the pod see and signal other containers' processes and read their /proc memory — a sidecar can steal secrets from the main container.",
535
+ "Remove shareProcessNamespace: true unless required, and never combine it with untrusted sidecars."
536
+ ]
537
+ });
538
+ }
539
+ const hostUsersFiles = matching(ctx, /hostUsers:\s*true/);
540
+ if (hostUsersFiles.length > 0) {
541
+ findings.push({
542
+ id: "K8S_HOST_USERS_TRUE",
543
+ title: "Pod sets hostUsers: true (no user namespace isolation)",
544
+ severity: "HIGH",
545
+ files: hostUsersFiles,
546
+ requiredActions: [
547
+ "hostUsers: true disables the user namespace, so container UID 0 maps directly to host root — a container escape yields immediate host root.",
548
+ "Set hostUsers: false to enable user-namespace remapping (UserNamespacesSupport)."
549
+ ]
550
+ });
551
+ }
552
+ const seccompUnconfinedFiles = matching(ctx, /seccompProfile:[\s\S]{0,40}Unconfined|type:\s*Unconfined/);
553
+ if (seccompUnconfinedFiles.length > 0) {
554
+ findings.push({
555
+ id: "K8S_SECCOMP_UNCONFINED",
556
+ title: "seccompProfile set to Unconfined",
557
+ severity: "HIGH",
558
+ files: seccompUnconfinedFiles,
559
+ requiredActions: [
560
+ "seccompProfile: Unconfined removes syscall filtering, exposing the full kernel attack surface (keyctl, unshare, etc.) used in container-escape exploits.",
561
+ "Set seccompProfile.type: RuntimeDefault (or a tighter Localhost profile) on every pod/container."
562
+ ]
563
+ });
564
+ }
565
+ const apparmorUnconfinedFiles = matching(ctx, /appArmorProfile:[\s\S]{0,40}Unconfined|apparmor[^\n]*unconfined/);
566
+ if (apparmorUnconfinedFiles.length > 0) {
567
+ findings.push({
568
+ id: "K8S_APPARMOR_UNCONFINED",
569
+ title: "AppArmor profile set to unconfined",
570
+ severity: "HIGH",
571
+ files: apparmorUnconfinedFiles,
572
+ requiredActions: [
573
+ "An unconfined AppArmor profile removes mandatory access control on file and capability use inside the container.",
574
+ "Set appArmorProfile.type: RuntimeDefault or a Localhost profile, and remove any container.apparmor.security.beta.kubernetes.io/*: unconfined annotation."
575
+ ]
576
+ });
577
+ }
578
+ const dangerousCapsFiles = matching(ctx, /SYS_ADMIN|SYS_PTRACE|SYS_MODULE|DAC_READ_SEARCH|\bBPF\b|\bNET_RAW\b/);
579
+ if (dangerousCapsFiles.length > 0) {
580
+ findings.push({
581
+ id: "K8S_DANGEROUS_CAPABILITY_ADDED",
582
+ title: "Dangerous Linux capability added (SYS_ADMIN/SYS_PTRACE/SYS_MODULE/DAC_READ_SEARCH/BPF/NET_RAW)",
583
+ severity: "CRITICAL",
584
+ files: dangerousCapsFiles,
585
+ requiredActions: [
586
+ "SYS_ADMIN ≈ root; SYS_PTRACE allows debugging host processes; SYS_MODULE loads kernel modules; DAC_READ_SEARCH bypasses file permissions (CVE-2014-9357 style); BPF and NET_RAW enable kernel/network attacks.",
587
+ "Remove these from capabilities.add. Drop ALL capabilities and re-add only the minimal non-dangerous ones the workload needs."
588
+ ]
589
+ });
590
+ }
591
+ const automountFiles = matching(ctx, /automountServiceAccountToken:\s*true/);
592
+ if (automountFiles.length > 0) {
593
+ findings.push({
594
+ id: "K8S_SA_TOKEN_AUTOMOUNT_TRUE",
595
+ title: "automountServiceAccountToken: true explicitly set",
596
+ severity: "MEDIUM",
597
+ files: automountFiles,
598
+ requiredActions: [
599
+ "Auto-mounting the SA token into every pod hands an attacker who compromises the container a credential to call the Kubernetes API.",
600
+ "Set automountServiceAccountToken: false at the pod and ServiceAccount level unless the workload genuinely calls the API; then mount a scoped projected token."
601
+ ]
602
+ });
603
+ }
604
+ const ephemeralFiles = matching(ctx, /ephemeralContainers:/);
605
+ if (ephemeralFiles.length > 0) {
606
+ findings.push({
607
+ id: "K8S_EPHEMERAL_CONTAINERS",
608
+ title: "ephemeralContainers declared in manifest",
609
+ severity: "MEDIUM",
610
+ files: ephemeralFiles,
611
+ requiredActions: [
612
+ "Ephemeral/debug containers can attach to a running pod's namespaces and read its process memory and mounted secrets, bypassing the original container's securityContext.",
613
+ "Remove ephemeralContainers from committed manifests; gate kubectl debug behind RBAC and audit logging."
614
+ ]
615
+ });
616
+ }
617
+ const hostAliasesFiles = matching(ctx, /hostAliases:/);
618
+ if (hostAliasesFiles.length > 0) {
619
+ findings.push({
620
+ id: "K8S_HOST_ALIASES_SPOOF",
621
+ title: "hostAliases entries injected into /etc/hosts",
622
+ severity: "LOW",
623
+ files: hostAliasesFiles,
624
+ requiredActions: [
625
+ "hostAliases override /etc/hosts and can spoof internal service names to redirect traffic to an attacker-controlled IP.",
626
+ "Remove hostAliases and rely on cluster DNS; if static mapping is required, validate the IPs against an allowlist."
627
+ ]
628
+ });
629
+ }
630
+ const sysctlFiles = matching(ctx, /sysctls:|securityContext\.sysctls/);
631
+ if (sysctlFiles.length > 0) {
632
+ findings.push({
633
+ id: "K8S_UNSAFE_SYSCTLS",
634
+ title: "Pod sets sysctls (potentially unsafe kernel tunables)",
635
+ severity: "MEDIUM",
636
+ files: sysctlFiles,
637
+ requiredActions: [
638
+ "Unsafe sysctls (e.g. kernel.* , net.* namespaced tunables) can weaken host kernel protections or be used to disrupt the node.",
639
+ "Remove sysctls unless required; allow only specific safe sysctls via the kubelet --allowed-unsafe-sysctls allowlist."
640
+ ]
641
+ });
642
+ }
643
+ const saPathMountFiles = matching(ctx, /\/var\/run\/secrets\/kubernetes\.io|\/var\/run\/secrets\/serviceaccount/);
644
+ if (saPathMountFiles.length > 0) {
645
+ findings.push({
646
+ id: "K8S_SA_PATH_MOUNT",
647
+ title: "ServiceAccount token path explicitly mounted as a volume",
648
+ severity: "HIGH",
649
+ files: saPathMountFiles,
650
+ requiredActions: [
651
+ "Mounting /var/run/secrets/kubernetes.io into an untrusted container hands it the API credential even when automount is disabled.",
652
+ "Remove explicit hostPath/volume mounts of the SA token path; let the kubelet project a scoped token only where needed."
653
+ ]
654
+ });
655
+ }
656
+ const hostPortFiles = matching(ctx, /hostPort:\s*\d+/);
657
+ if (hostPortFiles.length > 0) {
658
+ findings.push({
659
+ id: "K8S_HOST_PORT_BINDING",
660
+ title: "Container binds a hostPort",
661
+ severity: "MEDIUM",
662
+ files: hostPortFiles,
663
+ requiredActions: [
664
+ "hostPort binds the container's port directly on the node's network interface, bypassing Services/NetworkPolicies and exposing it on the node IP.",
665
+ "Remove hostPort and expose the workload through a Service/Ingress instead."
666
+ ]
667
+ });
668
+ }
669
+ const projectedTokenFiles = ctx.files
670
+ .filter((f) => {
671
+ const c = ctx.contents.get(f) ?? "";
672
+ return /serviceAccountToken:/.test(c) && !/audience:/.test(c);
673
+ })
674
+ .slice(0, 10);
675
+ if (projectedTokenFiles.length > 0) {
676
+ findings.push({
677
+ id: "K8S_PROJECTED_TOKEN_NO_AUDIENCE",
678
+ title: "Projected ServiceAccount token without an audience / expirationSeconds",
679
+ severity: "MEDIUM",
680
+ files: projectedTokenFiles,
681
+ requiredActions: [
682
+ "A projected SA token with no audience is valid against the API server and can be replayed broadly; no expirationSeconds means it is long-lived.",
683
+ "Set a specific audience and a short expirationSeconds (e.g. 3600) on every projected serviceAccountToken volume."
684
+ ]
685
+ });
686
+ }
687
+ return findings;
688
+ }
689
+ /**
690
+ * Supply-chain & image integrity — digest pinning, pull policy cache poisoning,
691
+ * inline pull secrets, runAsNonRoot, PodDisruptionBudget.
692
+ */
693
+ function checkSupplyChainIntegrity(ctx) {
694
+ const findings = [];
695
+ // image: ... with a tag but no @sha256 digest
696
+ const noDigestFiles = ctx.files
697
+ .filter((f) => {
698
+ const c = ctx.contents.get(f) ?? "";
699
+ return /image:\s*["']?[\w./-]+:[\w.-]+/.test(c) && !/@sha256:/.test(c);
700
+ })
701
+ .slice(0, 10);
702
+ if (noDigestFiles.length > 0) {
703
+ findings.push({
704
+ id: "K8S_IMAGE_NO_DIGEST_PIN",
705
+ title: "Container image referenced by tag without a sha256 digest pin",
706
+ severity: "MEDIUM",
707
+ files: noDigestFiles,
708
+ requiredActions: [
709
+ "A mutable tag can be repointed to a malicious image after review (supply-chain attack); only a @sha256 digest is immutable.",
710
+ "Pin every image to image@sha256:<digest> and verify signatures with Cosign / sigstore policy-controller."
711
+ ]
712
+ });
713
+ }
714
+ const pullPolicyFiles = matching(ctx, /imagePullPolicy:\s*(?:Never|IfNotPresent)/);
715
+ if (pullPolicyFiles.length > 0) {
716
+ findings.push({
717
+ id: "K8S_IMAGE_PULL_POLICY_CACHE",
718
+ title: "imagePullPolicy Never/IfNotPresent enables node image-cache poisoning",
719
+ severity: "LOW",
720
+ files: pullPolicyFiles,
721
+ requiredActions: [
722
+ "With Never/IfNotPresent a pod can run a same-tagged image already cached on the node by another tenant, bypassing registry auth and admission scanning.",
723
+ "Use imagePullPolicy: Always together with digest-pinned images so the node fetches and verifies the intended image."
724
+ ]
725
+ });
726
+ }
727
+ const inlineDockercfgFiles = matching(ctx, /\.dockerconfigjson|dockercfg/);
728
+ if (inlineDockercfgFiles.length > 0) {
729
+ findings.push({
730
+ id: "K8S_INLINE_DOCKERCONFIG",
731
+ title: "Inline dockerconfigjson / dockercfg registry credentials in manifest",
732
+ severity: "HIGH",
733
+ files: inlineDockercfgFiles,
734
+ requiredActions: [
735
+ "Embedding .dockerconfigjson in a committed manifest leaks registry credentials that can be base64-decoded from git history.",
736
+ "Store registry creds in a sealed/external Secret (e.g. External Secrets Operator) and reference via imagePullSecrets, never inline in version control."
737
+ ]
738
+ });
739
+ }
740
+ const noRunAsNonRootFiles = ctx.files
741
+ .filter((f) => {
742
+ const c = ctx.contents.get(f) ?? "";
743
+ return /containers:/.test(c) && !/runAsNonRoot:\s*true/.test(c);
744
+ })
745
+ .slice(0, 10);
746
+ if (noRunAsNonRootFiles.length > 0) {
747
+ findings.push({
748
+ id: "K8S_MISSING_RUN_AS_NONROOT",
749
+ title: "Container does not set runAsNonRoot: true",
750
+ severity: "MEDIUM",
751
+ files: noRunAsNonRootFiles,
752
+ requiredActions: [
753
+ "Without runAsNonRoot: true the kubelet will happily start an image whose default user is root, maximizing escape blast radius.",
754
+ "Set securityContext.runAsNonRoot: true and a non-zero runAsUser on every container."
755
+ ]
756
+ });
757
+ }
758
+ const hasPdb = ctx.files.some((f) => /kind:\s*PodDisruptionBudget/.test(ctx.contents.get(f) ?? ""));
759
+ const hasWorkload = ctx.files.some((f) => /kind:\s*(?:Deployment|StatefulSet|ReplicaSet)/.test(ctx.contents.get(f) ?? ""));
760
+ if (hasWorkload && !hasPdb) {
761
+ findings.push({
762
+ id: "K8S_NO_POD_DISRUPTION_BUDGET",
763
+ title: "Workloads present but no PodDisruptionBudget defined",
764
+ severity: "LOW",
765
+ requiredActions: [
766
+ "Without a PodDisruptionBudget, a node drain or voluntary disruption can take all replicas down at once (availability/DoS risk).",
767
+ "Add a PodDisruptionBudget with minAvailable (or maxUnavailable) for each critical workload."
768
+ ]
769
+ });
770
+ }
771
+ return findings;
772
+ }
773
+ /**
774
+ * Network & exposure depth — LoadBalancer source ranges, Ingress TLS, externalIPs,
775
+ * hostNetwork+privileged combo, allow-all egress NetworkPolicy, dnsPolicy.
776
+ */
777
+ function checkNetworkExposureDepth(ctx) {
778
+ const findings = [];
779
+ const lbOpenFiles = ctx.files
780
+ .filter((f) => {
781
+ const c = ctx.contents.get(f) ?? "";
782
+ return /type:\s*LoadBalancer/.test(c) && (!/loadBalancerSourceRanges:/.test(c) || /0\.0\.0\.0\/0/.test(c));
783
+ })
784
+ .slice(0, 10);
785
+ if (lbOpenFiles.length > 0) {
786
+ findings.push({
787
+ id: "K8S_LB_OPEN_SOURCE_RANGES",
788
+ title: "LoadBalancer Service with no loadBalancerSourceRanges or 0.0.0.0/0",
789
+ severity: "HIGH",
790
+ files: lbOpenFiles,
791
+ requiredActions: [
792
+ "A LoadBalancer without source-range restriction is reachable from the entire internet, bypassing any WAF.",
793
+ "Set loadBalancerSourceRanges to the specific trusted CIDRs, or front the service with an Ingress/API gateway and a WAF."
794
+ ]
795
+ });
796
+ }
797
+ const ingressNoTlsFiles = ctx.files
798
+ .filter((f) => {
799
+ const c = ctx.contents.get(f) ?? "";
800
+ return /kind:\s*Ingress/.test(c) && !/tls:/.test(c);
801
+ })
802
+ .slice(0, 10);
803
+ if (ingressNoTlsFiles.length > 0) {
804
+ findings.push({
805
+ id: "K8S_INGRESS_NO_TLS",
806
+ title: "Ingress without a tls: block",
807
+ severity: "HIGH",
808
+ files: ingressNoTlsFiles,
809
+ requiredActions: [
810
+ "An Ingress with no tls block serves traffic over plaintext HTTP, exposing credentials and sessions to interception.",
811
+ "Add a tls: section with a valid certificate (cert-manager) and enforce HTTPS redirects."
812
+ ]
813
+ });
814
+ }
815
+ const externalIpFiles = matching(ctx, /externalIPs:/);
816
+ if (externalIpFiles.length > 0) {
817
+ findings.push({
818
+ id: "K8S_SERVICE_EXTERNAL_IPS",
819
+ title: "Service sets externalIPs",
820
+ severity: "MEDIUM",
821
+ files: externalIpFiles,
822
+ requiredActions: [
823
+ "externalIPs route arbitrary node-destined traffic to the service and have historically enabled traffic-hijack / MITM between tenants.",
824
+ "Remove externalIPs; expose services via LoadBalancer or Ingress with explicit source restrictions."
825
+ ]
826
+ });
827
+ }
828
+ const hostNetPrivFiles = ctx.files
829
+ .filter((f) => {
830
+ const c = ctx.contents.get(f) ?? "";
831
+ return /hostNetwork:\s*true/.test(c) && /privileged:\s*true/.test(c);
832
+ })
833
+ .slice(0, 10);
834
+ if (hostNetPrivFiles.length > 0) {
835
+ findings.push({
836
+ id: "K8S_HOSTNETWORK_PRIVILEGED_COMBO",
837
+ title: "Pod combines hostNetwork: true with privileged: true",
838
+ severity: "CRITICAL",
839
+ files: hostNetPrivFiles,
840
+ requiredActions: [
841
+ "hostNetwork + privileged gives the container the node's network stack and full device access — it can sniff all node traffic and trivially escape to host root.",
842
+ "Remove both settings; if host networking is unavoidable, drop privileged and all unnecessary capabilities."
843
+ ]
844
+ });
845
+ }
846
+ const allowAllEgressFiles = ctx.files
847
+ .filter((f) => {
848
+ const c = ctx.contents.get(f) ?? "";
849
+ return /kind:\s*NetworkPolicy/.test(c) && /podSelector:\s*\{\s*\}/.test(c) && /egress:/.test(c);
850
+ })
851
+ .slice(0, 10);
852
+ if (allowAllEgressFiles.length > 0) {
853
+ findings.push({
854
+ id: "K8S_NETPOL_ALLOW_ALL_EGRESS",
855
+ title: "NetworkPolicy with empty podSelector allows all egress",
856
+ severity: "MEDIUM",
857
+ files: allowAllEgressFiles,
858
+ requiredActions: [
859
+ "An empty podSelector ({}) selecting all pods combined with an open egress rule lets any compromised pod exfiltrate data anywhere on the internet.",
860
+ "Scope the podSelector and restrict egress to the specific destinations (CIDRs/namespaces/ports) each workload needs."
861
+ ]
862
+ });
863
+ }
864
+ const dnsDefaultFiles = matching(ctx, /dnsPolicy:\s*Default/);
865
+ if (dnsDefaultFiles.length > 0) {
866
+ findings.push({
867
+ id: "K8S_DNS_POLICY_DEFAULT",
868
+ title: "Pod uses dnsPolicy: Default (node resolver)",
869
+ severity: "LOW",
870
+ files: dnsDefaultFiles,
871
+ requiredActions: [
872
+ "dnsPolicy: Default inherits the node's resolver, bypassing cluster DNS policy and any DNS-based egress controls.",
873
+ "Use dnsPolicy: ClusterFirst so pods resolve through CoreDNS and are subject to cluster DNS controls."
874
+ ]
875
+ });
876
+ }
877
+ return findings;
878
+ }
879
+ /**
880
+ * Secrets & ServiceAccount config — plaintext stringData creds, literal secret env,
881
+ * default token automounting on ServiceAccounts.
882
+ */
883
+ function checkSecretsConfig(ctx) {
884
+ const findings = [];
885
+ const stringDataCredFiles = ctx.files
886
+ .filter((f) => {
887
+ const c = ctx.contents.get(f) ?? "";
888
+ return /kind:\s*Secret/.test(c) && /stringData:/.test(c) && /password|token|apikey|api_key|secret/i.test(c);
889
+ })
890
+ .slice(0, 10);
891
+ if (stringDataCredFiles.length > 0) {
892
+ findings.push({
893
+ id: "K8S_SECRET_PLAINTEXT_STRINGDATA",
894
+ title: "Secret manifest contains plaintext credentials in stringData",
895
+ severity: "HIGH",
896
+ files: stringDataCredFiles,
897
+ requiredActions: [
898
+ "stringData stores the credential as plaintext in the committed manifest and git history — anyone with repo read access gets the secret.",
899
+ "Remove plaintext secrets from manifests; use Sealed Secrets, External Secrets Operator, or SOPS-encrypted values referencing a KMS."
900
+ ]
901
+ });
902
+ }
903
+ const literalEnvSecretFiles = ctx.files
904
+ .filter((f) => {
905
+ const c = ctx.contents.get(f) ?? "";
906
+ return /env:/.test(c) && /value:\s*["']?[^\n]*(?:password|secret|token|apikey)/i.test(c) && !/valueFrom:/.test(c);
907
+ })
908
+ .slice(0, 10);
909
+ if (literalEnvSecretFiles.length > 0) {
910
+ findings.push({
911
+ id: "K8S_ENV_LITERAL_SECRET",
912
+ title: "Literal secret value in container env (no valueFrom)",
913
+ severity: "HIGH",
914
+ files: literalEnvSecretFiles,
915
+ requiredActions: [
916
+ "A literal env value: containing a password/token is baked into the pod spec and is visible to anyone with get pod / describe access.",
917
+ "Use valueFrom.secretKeyRef to reference a Secret, and prefer mounting secrets as files over env vars."
918
+ ]
919
+ });
920
+ }
921
+ const saAutomountFiles = ctx.files
922
+ .filter((f) => {
923
+ const c = ctx.contents.get(f) ?? "";
924
+ return /kind:\s*ServiceAccount/.test(c) && !/automountServiceAccountToken:\s*false/.test(c);
925
+ })
926
+ .slice(0, 10);
927
+ if (saAutomountFiles.length > 0) {
928
+ findings.push({
929
+ id: "K8S_SA_DEFAULT_AUTOMOUNT",
930
+ title: "ServiceAccount does not disable automountServiceAccountToken",
931
+ severity: "MEDIUM",
932
+ files: saAutomountFiles,
933
+ requiredActions: [
934
+ "ServiceAccounts default to automounting their token into every pod, handing an attacker an API credential on container compromise.",
935
+ "Set automountServiceAccountToken: false on the ServiceAccount and opt in per-pod only where API access is required."
936
+ ]
937
+ });
938
+ }
939
+ return findings;
940
+ }
941
+ /**
942
+ * Admission, API server, kubelet & etcd policy — PSA enforce level, deprecated PSP,
943
+ * policy-engine presence, dangerous apiserver/kubelet flags, etcd TLS.
944
+ */
945
+ function checkAdmissionAndComponents(ctx) {
946
+ const findings = [];
947
+ const psaNotRestrictedFiles = ctx.files
948
+ .filter((f) => {
949
+ const c = ctx.contents.get(f) ?? "";
950
+ return /pod-security\.kubernetes\.io\/enforce:\s*(?:baseline|privileged)/.test(c);
951
+ })
952
+ .slice(0, 10);
953
+ if (psaNotRestrictedFiles.length > 0) {
954
+ findings.push({
955
+ id: "K8S_PSA_NOT_RESTRICTED",
956
+ title: "PodSecurityAdmission enforce level is baseline/privileged, not restricted",
957
+ severity: "MEDIUM",
958
+ files: psaNotRestrictedFiles,
959
+ requiredActions: [
960
+ "enforce: baseline still permits hostPath, running as root, and added capabilities; privileged permits everything.",
961
+ "Set pod-security.kubernetes.io/enforce: restricted on application namespaces and warn/audit at restricted too."
962
+ ]
963
+ });
964
+ }
965
+ const pspFiles = matching(ctx, /kind:\s*PodSecurityPolicy|policy\/v1beta1.*PodSecurityPolicy/);
966
+ if (pspFiles.length > 0) {
967
+ findings.push({
968
+ id: "K8S_DEPRECATED_PSP",
969
+ title: "Deprecated PodSecurityPolicy still referenced",
970
+ severity: "MEDIUM",
971
+ files: pspFiles,
972
+ requiredActions: [
973
+ "PodSecurityPolicy was removed in Kubernetes 1.25 — it is silently non-enforcing on modern clusters, leaving pods unconstrained.",
974
+ "Migrate to PodSecurityAdmission (restricted) and/or a policy engine (Kyverno / Gatekeeper)."
975
+ ]
976
+ });
977
+ }
978
+ // Heuristic LOW: no policy engine present anywhere in the manifests
979
+ const hasPolicyEngine = ctx.files.some((f) => {
980
+ const c = ctx.contents.get(f) ?? "";
981
+ return /gatekeeper|ConstraintTemplate|kyverno|kind:\s*ClusterPolicy|kind:\s*Policy\b/i.test(c);
982
+ });
983
+ if (!hasPolicyEngine) {
984
+ findings.push({
985
+ id: "K8S_NO_POLICY_ENGINE",
986
+ title: "No admission policy engine (OPA Gatekeeper / Kyverno) detected",
987
+ severity: "LOW",
988
+ requiredActions: [
989
+ "Without a policy engine, security invariants (no privileged, image-signature required, etc.) are not enforced at admission time.",
990
+ "Deploy Kyverno or OPA Gatekeeper and codify your pod-security and supply-chain policies as enforced constraints."
991
+ ]
992
+ });
993
+ }
994
+ const apiserverFlagFiles = matching(ctx, /--authorization-mode=AlwaysAllow|--insecure-port=[1-9]|--insecure-bind-address/);
995
+ if (apiserverFlagFiles.length > 0) {
996
+ findings.push({
997
+ id: "K8S_APISERVER_INSECURE_FLAGS",
998
+ title: "kube-apiserver started with AlwaysAllow / insecure-port flags",
999
+ severity: "CRITICAL",
1000
+ files: apiserverFlagFiles,
1001
+ requiredActions: [
1002
+ "--authorization-mode=AlwaysAllow disables RBAC; --insecure-port / --insecure-bind-address expose an unauthenticated API endpoint.",
1003
+ "Set --authorization-mode=Node,RBAC, remove all insecure-port flags, and require TLS client auth on the API server."
1004
+ ]
1005
+ });
1006
+ }
1007
+ const kubeletFlagFiles = matching(ctx, /--read-only-port=(?!0)\d|readOnlyPort:\s*(?!0)\d|authorization-mode.*AlwaysAllow|authorization:[\s\S]{0,40}AlwaysAllow/);
1008
+ if (kubeletFlagFiles.length > 0) {
1009
+ findings.push({
1010
+ id: "K8S_KUBELET_INSECURE_CONFIG",
1011
+ title: "Kubelet read-only port enabled or authorization mode AlwaysAllow",
1012
+ severity: "HIGH",
1013
+ files: kubeletFlagFiles,
1014
+ requiredActions: [
1015
+ "The kubelet read-only port (10255) exposes pod and node data unauthenticated; authorization AlwaysAllow lets any caller hit the kubelet API.",
1016
+ "Set readOnlyPort: 0, authentication.anonymous.enabled: false, and authorization.mode: Webhook in the kubelet config."
1017
+ ]
1018
+ });
1019
+ }
1020
+ const kubeletAnonFiles = ctx.files
1021
+ .filter((f) => {
1022
+ const c = ctx.contents.get(f) ?? "";
1023
+ return /kind:\s*KubeletConfiguration/.test(c) && /anonymous:[\s\S]{0,40}enabled:\s*true|--anonymous-auth=true/.test(c);
1024
+ })
1025
+ .slice(0, 10);
1026
+ if (kubeletAnonFiles.length > 0) {
1027
+ findings.push({
1028
+ id: "K8S_KUBELET_ANON_AUTH",
1029
+ title: "Kubelet anonymous authentication enabled",
1030
+ severity: "CRITICAL",
1031
+ files: kubeletAnonFiles,
1032
+ requiredActions: [
1033
+ "Anonymous kubelet auth lets unauthenticated callers exec into pods and read node secrets on port 10250.",
1034
+ "Set authentication.anonymous.enabled: false and require X509/Webhook auth on the kubelet."
1035
+ ]
1036
+ });
1037
+ }
1038
+ const etcdNoTlsFiles = ctx.files
1039
+ .filter((f) => {
1040
+ const c = ctx.contents.get(f) ?? "";
1041
+ return /etcd/i.test(c) && (/--client-cert-auth=false/.test(c) || (/--listen-client-urls=http:/.test(c) && !/https:/.test(c)));
1042
+ })
1043
+ .slice(0, 10);
1044
+ if (etcdNoTlsFiles.length > 0) {
1045
+ findings.push({
1046
+ id: "K8S_ETCD_NO_TLS",
1047
+ title: "etcd configured without client-cert TLS",
1048
+ severity: "CRITICAL",
1049
+ files: etcdNoTlsFiles,
1050
+ requiredActions: [
1051
+ "etcd holds every Secret in the cluster in plaintext; a plaintext/unauthenticated etcd endpoint is total cluster compromise.",
1052
+ "Set --client-cert-auth=true, serve only https client URLs, and enable peer TLS (--peer-client-cert-auth=true)."
1053
+ ]
1054
+ });
1055
+ }
1056
+ return findings;
1057
+ }
1058
+ /**
1059
+ * CRD / operator & miscellaneous — default-SA bindings, broad system:authenticated
1060
+ * group grants, privileged Helm hooks, critical priorityClass, missing runtimeClass,
1061
+ * Windows hostProcess.
1062
+ */
1063
+ function checkCrdOperatorMisc(ctx) {
1064
+ const findings = [];
1065
+ const defaultSaBindingFiles = ctx.files
1066
+ .filter((f) => {
1067
+ const c = ctx.contents.get(f) ?? "";
1068
+ return /kind:\s*ClusterRoleBinding/.test(c) && /kind:\s*ServiceAccount[\s\S]{0,60}name:\s*default/.test(c);
1069
+ })
1070
+ .slice(0, 10);
1071
+ if (defaultSaBindingFiles.length > 0) {
1072
+ findings.push({
1073
+ id: "K8S_CRB_DEFAULT_SA",
1074
+ title: "ClusterRoleBinding grants a role to a 'default' ServiceAccount",
1075
+ severity: "HIGH",
1076
+ files: defaultSaBindingFiles,
1077
+ requiredActions: [
1078
+ "Binding cluster permissions to the default SA gives every pod in that namespace (which uses default unless overridden) those permissions.",
1079
+ "Bind to a dedicated, named ServiceAccount with least privilege and set the pod's serviceAccountName explicitly."
1080
+ ]
1081
+ });
1082
+ }
1083
+ const systemAuthGroupFiles = ctx.files
1084
+ .filter((f) => {
1085
+ const c = ctx.contents.get(f) ?? "";
1086
+ return /kind:\s*Group[\s\S]{0,40}system:authenticated|name:\s*system:authenticated/.test(c);
1087
+ })
1088
+ .slice(0, 10);
1089
+ if (systemAuthGroupFiles.length > 0) {
1090
+ findings.push({
1091
+ id: "K8S_BIND_SYSTEM_AUTHENTICATED",
1092
+ title: "RBAC binding to the system:authenticated group",
1093
+ severity: "HIGH",
1094
+ files: systemAuthGroupFiles,
1095
+ requiredActions: [
1096
+ "system:authenticated includes every authenticated identity in the cluster — binding any non-trivial role to it is effectively cluster-wide access.",
1097
+ "Replace the system:authenticated subject with specific users/groups/ServiceAccounts that require the role."
1098
+ ]
1099
+ });
1100
+ }
1101
+ const helmHookPrivFiles = ctx.files
1102
+ .filter((f) => {
1103
+ const c = ctx.contents.get(f) ?? "";
1104
+ return /helm\.sh\/hook/.test(c) && /privileged:\s*true/.test(c);
1105
+ })
1106
+ .slice(0, 10);
1107
+ if (helmHookPrivFiles.length > 0) {
1108
+ findings.push({
1109
+ id: "K8S_HELM_HOOK_PRIVILEGED",
1110
+ title: "Helm hook job runs as a privileged container",
1111
+ severity: "HIGH",
1112
+ files: helmHookPrivFiles,
1113
+ requiredActions: [
1114
+ "A privileged Helm pre/post-install hook runs with full host access during every release — a compromised chart gains node root.",
1115
+ "Drop privileged from hook jobs, run them as non-root with a minimal securityContext, and review third-party chart hooks."
1116
+ ]
1117
+ });
1118
+ }
1119
+ const criticalPriorityFiles = matching(ctx, /priorityClassName:\s*system-(?:cluster|node)-critical/);
1120
+ if (criticalPriorityFiles.length > 0) {
1121
+ findings.push({
1122
+ id: "K8S_SYSTEM_CRITICAL_PRIORITY",
1123
+ title: "Untrusted workload uses system-cluster-critical / system-node-critical priorityClass",
1124
+ severity: "MEDIUM",
1125
+ files: criticalPriorityFiles,
1126
+ requiredActions: [
1127
+ "system-*-critical priority lets the pod preempt and evict legitimate workloads, enabling a DoS or guaranteeing scheduling for a malicious pod.",
1128
+ "Reserve system-*-critical for genuine control-plane components; use a normal/custom PriorityClass for application workloads."
1129
+ ]
1130
+ });
1131
+ }
1132
+ const noRuntimeClassFiles = ctx.files
1133
+ .filter((f) => {
1134
+ const c = ctx.contents.get(f) ?? "";
1135
+ return /kind:\s*(?:Pod|Deployment)/.test(c) && /containers:/.test(c) && !/runtimeClassName:/.test(c);
1136
+ })
1137
+ .slice(0, 10);
1138
+ if (noRuntimeClassFiles.length > 0) {
1139
+ findings.push({
1140
+ id: "K8S_NO_RUNTIME_CLASS",
1141
+ title: "Workload does not set a hardened runtimeClassName (gVisor/Kata)",
1142
+ severity: "LOW",
1143
+ files: noRuntimeClassFiles,
1144
+ requiredActions: [
1145
+ "Without a sandboxed runtimeClass (gVisor/Kata), a kernel exploit in the container reaches the shared host kernel directly.",
1146
+ "For untrusted or multi-tenant workloads set runtimeClassName to a gVisor (runsc) or Kata Containers runtime."
1147
+ ]
1148
+ });
1149
+ }
1150
+ const hostProcessFiles = matching(ctx, /hostProcess:\s*true/);
1151
+ if (hostProcessFiles.length > 0) {
1152
+ findings.push({
1153
+ id: "K8S_WINDOWS_HOSTPROCESS",
1154
+ title: "Windows hostProcess container (hostProcess: true)",
1155
+ severity: "CRITICAL",
1156
+ files: hostProcessFiles,
1157
+ requiredActions: [
1158
+ "A Windows hostProcess container runs directly on the host with the node's privileges — equivalent to privileged on Linux, full node compromise on escape.",
1159
+ "Remove hostProcess: true; run the workload as a normal Windows container, or isolate host-management pods on dedicated, restricted nodes."
1160
+ ]
1161
+ });
1162
+ }
1163
+ return findings;
1164
+ }
332
1165
  export async function checkKubernetes(_opts) {
333
1166
  try {
334
1167
  const ctx = await loadK8sManifests();
@@ -341,7 +1174,14 @@ export async function checkKubernetes(_opts) {
341
1174
  ...checkDockerSocketMount(ctx),
342
1175
  ...checkTillerHelm(ctx),
343
1176
  ...checkMtlsPolicy(ctx),
344
- ...networkFindings
1177
+ ...networkFindings,
1178
+ ...checkRbacEscalationDepth(ctx),
1179
+ ...checkPodEscapeDepth(ctx),
1180
+ ...checkSupplyChainIntegrity(ctx),
1181
+ ...checkNetworkExposureDepth(ctx),
1182
+ ...checkSecretsConfig(ctx),
1183
+ ...checkAdmissionAndComponents(ctx),
1184
+ ...checkCrdOperatorMisc(ctx)
345
1185
  ];
346
1186
  }
347
1187
  catch (err) {