security-mcp 1.1.4 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +341 -1018
  2. package/defaults/checklists/ai.json +20 -1
  3. package/defaults/checklists/api.json +35 -1
  4. package/defaults/checklists/infra.json +34 -1
  5. package/defaults/checklists/mobile.json +23 -1
  6. package/defaults/checklists/payments.json +15 -1
  7. package/defaults/checklists/web.json +11 -1
  8. package/defaults/cloud-controls/aws.json +10712 -0
  9. package/defaults/cloud-controls/azure.json +7201 -0
  10. package/defaults/cloud-controls/gcp.json +4061 -0
  11. package/defaults/control-catalog.json +24 -0
  12. package/defaults/security-policy.json +2 -2
  13. package/dist/ci/pr-gate.js +22 -5
  14. package/dist/cli/index.js +73 -2
  15. package/dist/cli/install.js +4 -55
  16. package/dist/cli/onboarding.js +18 -10
  17. package/dist/gate/baseline.js +82 -7
  18. package/dist/gate/catalog.js +10 -2
  19. package/dist/gate/checks/agentic-instructions.js +515 -0
  20. package/dist/gate/checks/ai-governance.js +132 -0
  21. package/dist/gate/checks/ai.js +757 -39
  22. package/dist/gate/checks/auth-deep.js +920 -216
  23. package/dist/gate/checks/business-logic.js +751 -0
  24. package/dist/gate/checks/ci-pipeline.js +399 -4
  25. package/dist/gate/checks/cloud-controls.js +69 -0
  26. package/dist/gate/checks/crypto.js +423 -2
  27. package/dist/gate/checks/data-platform.js +954 -0
  28. package/dist/gate/checks/dependencies.js +582 -15
  29. package/dist/gate/checks/docker-deep.js +1236 -0
  30. package/dist/gate/checks/gitops.js +724 -0
  31. package/dist/gate/checks/graphql.js +201 -19
  32. package/dist/gate/checks/iac.js +1230 -0
  33. package/dist/gate/checks/infra.js +246 -1
  34. package/dist/gate/checks/injection-deep.js +827 -184
  35. package/dist/gate/checks/k8s.js +955 -2
  36. package/dist/gate/checks/mobile-android.js +917 -3
  37. package/dist/gate/checks/mobile-ios.js +797 -5
  38. package/dist/gate/checks/required-artifacts.js +194 -0
  39. package/dist/gate/checks/runtime.js +178 -0
  40. package/dist/gate/checks/secrets.js +256 -13
  41. package/dist/gate/checks/supply-chain-deep.js +787 -0
  42. package/dist/gate/checks/web-nextjs.js +572 -48
  43. package/dist/gate/cloud-controls/apply.js +115 -0
  44. package/dist/gate/cloud-controls/bicep.js +36 -0
  45. package/dist/gate/cloud-controls/cfn.js +125 -0
  46. package/dist/gate/cloud-controls/detect.js +104 -0
  47. package/dist/gate/cloud-controls/hcl.js +140 -0
  48. package/dist/gate/cloud-controls/types.js +87 -0
  49. package/dist/gate/diff.js +17 -5
  50. package/dist/gate/evidence.js +8 -1
  51. package/dist/gate/exceptions.js +202 -9
  52. package/dist/gate/findings.js +15 -2
  53. package/dist/gate/policy.js +316 -130
  54. package/dist/gate/threat-intel.js +6 -0
  55. package/dist/mcp/audit-chain.js +131 -28
  56. package/dist/mcp/auth.js +169 -0
  57. package/dist/mcp/learning.js +129 -4
  58. package/dist/mcp/model-router.js +161 -24
  59. package/dist/mcp/orchestration.js +377 -89
  60. package/dist/mcp/server.js +460 -69
  61. package/dist/mcp/tool-audit.js +193 -0
  62. package/dist/repo/fs.js +37 -1
  63. package/dist/repo/search.js +31 -6
  64. package/dist/review/store.js +56 -3
  65. package/dist/tests/run.js +124 -1
  66. package/package.json +9 -9
  67. package/skills/_TEMPLATE/SKILL.md +99 -0
  68. package/skills/advanced-dos-tester/SKILL.md +118 -0
  69. package/skills/agentic-instruction-auditor/SKILL.md +111 -0
  70. package/skills/agentic-loop-exploiter/SKILL.md +377 -0
  71. package/skills/ai-llm-redteam/SKILL.md +113 -0
  72. package/skills/ai-model-supply-chain-agent/SKILL.md +112 -0
  73. package/skills/algorithm-implementation-reviewer/SKILL.md +107 -0
  74. package/skills/android-penetration-tester/SKILL.md +464 -46
  75. package/skills/anti-replay-tester/SKILL.md +115 -0
  76. package/skills/appsec-code-auditor/SKILL.md +94 -0
  77. package/skills/artifact-integrity-analyst/SKILL.md +450 -0
  78. package/skills/attack-navigator/SKILL.md +476 -8
  79. package/skills/auth-session-hacker/SKILL.md +111 -0
  80. package/skills/aws-penetration-tester/SKILL.md +510 -0
  81. package/skills/azure-penetration-tester/SKILL.md +542 -3
  82. package/skills/binary-auth-validator/SKILL.md +120 -0
  83. package/skills/bot-detection-specialist/SKILL.md +118 -0
  84. package/skills/business-logic-attacker/SKILL.md +240 -0
  85. package/skills/capec-code-mapper/SKILL.md +93 -0
  86. package/skills/cert-pin-rotation-specialist/SKILL.md +121 -0
  87. package/skills/cicd-pipeline-hijacker/SKILL.md +414 -0
  88. package/skills/ciso-orchestrator/SKILL.md +465 -43
  89. package/skills/cloud-infra-specialist/SKILL.md +127 -0
  90. package/skills/compliance-gap-analyst/SKILL.md +431 -0
  91. package/skills/compliance-grc/SKILL.md +94 -0
  92. package/skills/compliance-lifecycle-tracker/SKILL.md +93 -0
  93. package/skills/container-hardening-auditor/SKILL.md +125 -0
  94. package/skills/credential-stuffing-specialist/SKILL.md +111 -0
  95. package/skills/crypto-pki-specialist/SKILL.md +96 -0
  96. package/skills/csa-ccm-mapper/SKILL.md +93 -0
  97. package/skills/csf2-governance-mapper/SKILL.md +93 -0
  98. package/skills/data-platform-auditor/SKILL.md +125 -0
  99. package/skills/deep-link-fuzzer/SKILL.md +118 -0
  100. package/skills/dependency-confusion-attacker/SKILL.md +424 -0
  101. package/skills/device-integrity-aggregator/SKILL.md +117 -0
  102. package/skills/dos-resilience-tester/SKILL.md +106 -0
  103. package/skills/dread-scorer/SKILL.md +93 -0
  104. package/skills/egress-policy-enforcer/SKILL.md +108 -0
  105. package/skills/evidence-collector/SKILL.md +107 -0
  106. package/skills/file-upload-attacker/SKILL.md +118 -0
  107. package/skills/gcp-penetration-tester/SKILL.md +510 -2
  108. package/skills/git-history-secret-scanner/SKILL.md +115 -0
  109. package/skills/gitops-delivery-auditor/SKILL.md +120 -0
  110. package/skills/iac-security-auditor/SKILL.md +125 -0
  111. package/skills/iam-privesc-graph-builder/SKILL.md +161 -0
  112. package/skills/incident-responder/SKILL.md +120 -0
  113. package/skills/injection-specialist/SKILL.md +111 -0
  114. package/skills/ios-security-auditor/SKILL.md +291 -0
  115. package/skills/json-ambiguity-tester/SKILL.md +145 -0
  116. package/skills/k8s-container-escaper/SKILL.md +406 -0
  117. package/skills/key-management-lifecycle-analyst/SKILL.md +107 -0
  118. package/skills/kill-switch-engineer/SKILL.md +111 -0
  119. package/skills/linddun-privacy-analyst/SKILL.md +111 -0
  120. package/skills/logic-race-fuzzer/SKILL.md +452 -0
  121. package/skills/mobile-api-network-attacker/SKILL.md +430 -0
  122. package/skills/mobile-binary-hardener/SKILL.md +111 -0
  123. package/skills/mobile-security-specialist/SKILL.md +94 -0
  124. package/skills/mobile-webview-auditor/SKILL.md +105 -0
  125. package/skills/model-extraction-attacker/SKILL.md +228 -0
  126. package/skills/multipart-abuse-tester/SKILL.md +93 -0
  127. package/skills/oauth-pkce-specialist/SKILL.md +113 -0
  128. package/skills/parser-exhaustion-tester/SKILL.md +151 -0
  129. package/skills/pentest-infra/SKILL.md +107 -0
  130. package/skills/pentest-social/SKILL.md +210 -0
  131. package/skills/pentest-team/SKILL.md +96 -0
  132. package/skills/pentest-web-api/SKILL.md +107 -0
  133. package/skills/privacy-flow-analyst/SKILL.md +243 -0
  134. package/skills/prompt-injection-specialist/SKILL.md +403 -0
  135. package/skills/quantum-migration-planner/SKILL.md +105 -0
  136. package/skills/rag-poisoning-specialist/SKILL.md +367 -0
  137. package/skills/registry-mirror-enforcer/SKILL.md +93 -0
  138. package/skills/rotation-validation-agent/SKILL.md +121 -0
  139. package/skills/samm-assessor/SKILL.md +94 -0
  140. package/skills/secrets-mask-bypass-tester/SKILL.md +109 -0
  141. package/skills/senior-security-engineer/SKILL.md +178 -0
  142. package/skills/serialization-memory-attacker/SKILL.md +341 -0
  143. package/skills/session-timeout-tester/SKILL.md +170 -0
  144. package/skills/slsa-level3-enforcer/SKILL.md +121 -0
  145. package/skills/slsa-provenance-enforcer/SKILL.md +111 -0
  146. package/skills/ssrf-detection-validator/SKILL.md +117 -0
  147. package/skills/step-up-auth-enforcer/SKILL.md +93 -0
  148. package/skills/stride-pasta-analyst/SKILL.md +429 -0
  149. package/skills/supply-chain-devsecops/SKILL.md +107 -0
  150. package/skills/threat-infrastructure-analyst/SKILL.md +93 -0
  151. package/skills/threat-modeler/SKILL.md +94 -0
  152. package/skills/tls-certificate-auditor/SKILL.md +582 -18
  153. package/skills/token-reuse-detector/SKILL.md +104 -0
  154. package/skills/trike-risk-modeler/SKILL.md +93 -0
  155. package/skills/unicode-homograph-tester/SKILL.md +93 -0
  156. package/skills/waf-rule-lifecycle-agent/SKILL.md +106 -0
  157. package/skills/webhook-security-tester/SKILL.md +111 -0
  158. package/skills/zero-trust-architect/SKILL.md +118 -0
@@ -0,0 +1,724 @@
1
+ import { searchRepo } from "../../repo/search.js";
2
+ const MAX = 200;
3
+ function evidence(matches) {
4
+ return matches.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`);
5
+ }
6
+ export async function checkGitOps(_opts) {
7
+ const findings = [];
8
+ const [argoAutomatedSync, // automated sync stanza
9
+ argoSelfHeal, // selfHeal: true
10
+ argoPrune, // prune: true
11
+ argoTargetHead, // targetRevision: HEAD / branch
12
+ argoProjectDefault, // project: default
13
+ appProjectKind, // kind: AppProject present
14
+ appProjectWildcardSource, // sourceRepos: ['*']
15
+ appProjectWildcardDest, // destinations namespace/server '*'
16
+ appProjectWildcardClusterRes, // clusterResourceWhitelist '*' '*'
17
+ plaintextSecret, // kind: Secret + stringData/data
18
+ secretMgmtOperator, // sealed-secrets / sops / external-secrets present
19
+ argoPlugin, // config-management-plugin / plugin exec
20
+ helmDangerousFlags, // --include-crds / arbitrary value files
21
+ syncValidateFalse, // syncOptions Validate=false / ServerSideApply
22
+ argoRbacAdmin, // role:admin broad grant / g, *, role:admin
23
+ argoServerInsecure, // server.insecure / disable.auth / anonymous
24
+ argoResourceIgnoreHealth, // resource.customizations ignore health / --insecure repo-server
25
+ ] = await Promise.all([
26
+ searchRepo({ query: String.raw `automated\s*:|syncPolicy\s*:`, isRegex: true, maxMatches: MAX }),
27
+ searchRepo({ query: String.raw `selfHeal\s*:\s*true`, isRegex: true, maxMatches: MAX }),
28
+ searchRepo({ query: String.raw `prune\s*:\s*true`, isRegex: true, maxMatches: MAX }),
29
+ searchRepo({ query: String.raw `targetRevision\s*:\s*['"]?(?:HEAD|main|master|develop|latest)['"]?`, isRegex: true, maxMatches: MAX }),
30
+ searchRepo({ query: String.raw `project\s*:\s*['"]?default['"]?`, isRegex: true, maxMatches: MAX }),
31
+ searchRepo({ query: String.raw `kind\s*:\s*AppProject`, isRegex: true, maxMatches: MAX }),
32
+ searchRepo({ query: String.raw `sourceRepos\s*:\s*\[?\s*['"]?\*['"]?`, isRegex: true, maxMatches: MAX }),
33
+ searchRepo({ query: String.raw `(?:namespace|server)\s*:\s*['"]?\*['"]?`, isRegex: true, maxMatches: MAX }),
34
+ searchRepo({ query: String.raw `(?:group|kind)\s*:\s*['"]?\*['"]?`, isRegex: true, maxMatches: MAX }),
35
+ searchRepo({ query: String.raw `kind\s*:\s*Secret`, isRegex: true, maxMatches: MAX }),
36
+ searchRepo({ query: String.raw `SealedSecret|kind\s*:\s*ExternalSecret|sops\s*:|encryptedRegex|kind\s*:\s*SecretStore`, isRegex: true, maxMatches: MAX }),
37
+ searchRepo({ query: String.raw `configManagementPlugin|config-management-plugin|kind\s*:\s*ConfigManagementPlugin|plugin\s*:\s*name`, isRegex: true, maxMatches: MAX }),
38
+ searchRepo({ query: String.raw `--include-crds|skipCrds\s*:\s*false|valueFiles\s*:|fileParameters\s*:`, isRegex: true, maxMatches: MAX }),
39
+ searchRepo({ query: String.raw `Validate=false|ServerSideApply=true|SkipDryRunOnMissingResource=true|RespectIgnoreDifferences=true`, isRegex: true, maxMatches: MAX }),
40
+ searchRepo({ query: String.raw `role\s*:\s*admin|g,\s*\*,\s*role:admin|p,\s*role:admin|,\s*role:admin`, isRegex: true, maxMatches: MAX }),
41
+ searchRepo({ query: String.raw `server\.insecure\s*:\s*['"]?true|disable\.auth\s*:\s*['"]?true|users\.anonymous\.enabled\s*:\s*['"]?true|--insecure`, isRegex: true, maxMatches: MAX }),
42
+ searchRepo({ query: String.raw `resource\.customizations|ignoreDifferences\s*:|health\.lua|--disable-tls`, isRegex: true, maxMatches: MAX }),
43
+ ]);
44
+ const [fluxGitRepo, // kind: GitRepository / OCIRepository
45
+ fluxVerify, // verify: (cosign/signature)
46
+ fluxInsecure, // insecure: true
47
+ fluxKustomization, // kind: Kustomization (flux)
48
+ fluxHelmRelease, // kind: HelmRelease
49
+ fluxDecryption, // decryption: (SOPS)
50
+ fluxImagePolicy, // ImagePolicy / ImageUpdateAutomation
51
+ fluxImageRangeTag, // semver range / latest in imagePolicy
52
+ helmRepoHttp, // HelmRepository url: http://
53
+ ] = await Promise.all([
54
+ searchRepo({ query: String.raw `kind\s*:\s*(?:GitRepository|OCIRepository)`, isRegex: true, maxMatches: MAX }),
55
+ searchRepo({ query: String.raw `verify\s*:`, isRegex: true, maxMatches: MAX }),
56
+ searchRepo({ query: String.raw `insecure\s*:\s*true`, isRegex: true, maxMatches: MAX }),
57
+ searchRepo({ query: String.raw `kind\s*:\s*Kustomization`, isRegex: true, maxMatches: MAX }),
58
+ searchRepo({ query: String.raw `kind\s*:\s*HelmRelease`, isRegex: true, maxMatches: MAX }),
59
+ searchRepo({ query: String.raw `decryption\s*:`, isRegex: true, maxMatches: MAX }),
60
+ searchRepo({ query: String.raw `kind\s*:\s*(?:ImagePolicy|ImageUpdateAutomation)`, isRegex: true, maxMatches: MAX }),
61
+ searchRepo({ query: String.raw `semver\s*:|range\s*:\s*['"]?[\^~>]|tag\s*:\s*['"]?latest`, isRegex: true, maxMatches: MAX }),
62
+ searchRepo({ query: String.raw `kind\s*:\s*HelmRepository|url\s*:\s*http://`, isRegex: true, maxMatches: MAX }),
63
+ ]);
64
+ // ---- ArgoCD ----
65
+ // 1. Auto-deploy of a mutable upstream = RCE-on-cluster.
66
+ if (argoAutomatedSync.length > 0 &&
67
+ argoSelfHeal.length > 0 &&
68
+ argoPrune.length > 0 &&
69
+ argoTargetHead.length > 0) {
70
+ findings.push({
71
+ id: "ARGOCD_AUTOSYNC_MUTABLE_SOURCE",
72
+ title: "ArgoCD Application auto-syncs (selfHeal+prune) from a mutable/unpinned source (targetRevision: HEAD/branch) — anyone who pushes to the tracked ref gets RCE on the cluster",
73
+ severity: "CRITICAL",
74
+ evidence: evidence([...argoTargetHead, ...argoSelfHeal]),
75
+ requiredActions: [
76
+ "Pin targetRevision to an immutable Git tag or commit SHA, not HEAD or a branch name.",
77
+ "Require signed commits/tags and enable ArgoCD GnuPG/cosign source verification (project signatureKeys).",
78
+ "Gate auto-sync behind a protected branch with mandatory PR review and CODEOWNERS on manifests.",
79
+ "Consider disabling automated.selfHeal in production so a human approves drift reconciliation."
80
+ ]
81
+ });
82
+ }
83
+ // 2. Application bound to the default AppProject (no restrictions).
84
+ if (argoProjectDefault.length > 0) {
85
+ findings.push({
86
+ id: "ARGOCD_DEFAULT_PROJECT",
87
+ title: "ArgoCD Application uses the 'default' AppProject — no source/destination/cluster-resource restrictions, unrestricted blast radius",
88
+ severity: "HIGH",
89
+ evidence: evidence(argoProjectDefault),
90
+ requiredActions: [
91
+ "Create a dedicated AppProject per team/app and set spec.project to it; never use 'default'.",
92
+ "Restrict the AppProject sourceRepos, destinations, and clusterResourceWhitelist to explicit allowlists.",
93
+ "Lock down the built-in default AppProject so it cannot deploy anywhere."
94
+ ]
95
+ });
96
+ }
97
+ // 3. AppProject with wildcard sources/destinations/cluster resources.
98
+ if (appProjectKind.length > 0 &&
99
+ (appProjectWildcardSource.length > 0 ||
100
+ appProjectWildcardDest.length > 0 ||
101
+ appProjectWildcardClusterRes.length > 0)) {
102
+ findings.push({
103
+ id: "ARGOCD_APPPROJECT_WILDCARD",
104
+ title: "ArgoCD AppProject grants wildcards in sourceRepos / destinations / clusterResourceWhitelist — any repo can deploy any cluster-scoped resource to any namespace",
105
+ severity: "CRITICAL",
106
+ evidence: evidence([
107
+ ...appProjectWildcardSource,
108
+ ...appProjectWildcardDest,
109
+ ...appProjectWildcardClusterRes
110
+ ]),
111
+ requiredActions: [
112
+ "Restrict AppProject sourceRepos/destinations/clusterResourceWhitelist to explicit allowlists.",
113
+ "Remove '*'/'*' from clusterResourceWhitelist; whitelist only the specific cluster-scoped groups/kinds needed.",
114
+ "Pin destinations to explicit namespace + server (no '*'), and pin sourceRepos to exact repo URLs.",
115
+ "Set a clusterResourceBlacklist for high-risk kinds (ClusterRoleBinding, ValidatingWebhookConfiguration)."
116
+ ]
117
+ });
118
+ }
119
+ // 4. Plaintext Secret committed without a secret-management operator.
120
+ if (plaintextSecret.length > 0 && secretMgmtOperator.length === 0) {
121
+ findings.push({
122
+ id: "GITOPS_PLAINTEXT_SECRET",
123
+ title: "Plaintext kind: Secret committed to a GitOps repo with no Sealed Secrets / SOPS / External Secrets in use — credentials exposed to anyone with repo read access",
124
+ severity: "CRITICAL",
125
+ evidence: evidence(plaintextSecret),
126
+ requiredActions: [
127
+ "Move Secrets to Sealed Secrets, SOPS, or External Secrets Operator — never commit kind: Secret with stringData/data.",
128
+ "Rotate every credential that was ever committed in plaintext; Git history is forever.",
129
+ "Add a pre-commit/CI guard that blocks raw kind: Secret manifests from being committed."
130
+ ]
131
+ });
132
+ }
133
+ // 5. ArgoCD config-management-plugin / Helm privilege escalation.
134
+ if (argoPlugin.length > 0 || helmDangerousFlags.length > 0) {
135
+ findings.push({
136
+ id: "ARGOCD_PLUGIN_EXEC",
137
+ title: "ArgoCD config-management-plugin / Helm with --include-crds or arbitrary value files — manifest generation runs attacker-controllable code in the repo-server",
138
+ severity: "HIGH",
139
+ evidence: evidence([...argoPlugin, ...helmDangerousFlags]),
140
+ requiredActions: [
141
+ "Run config-management plugins as a sidecar with a read-only, non-root securityContext and no cluster credentials.",
142
+ "Avoid --include-crds and arbitrary valueFiles/fileParameters sourced from the application repo; pin them in a trusted repo.",
143
+ "Disable plugins entirely if not required; never let plugins shell out to repo-controlled scripts."
144
+ ]
145
+ });
146
+ }
147
+ // 6. syncOptions disabling validation / forcing server-side apply.
148
+ if (syncValidateFalse.length > 0) {
149
+ findings.push({
150
+ id: "ARGOCD_SYNC_VALIDATE_DISABLED",
151
+ title: "ArgoCD syncOptions disable schema validation (Validate=false) or force ServerSideApply — malformed/malicious manifests applied without admission checks",
152
+ severity: "MEDIUM",
153
+ evidence: evidence(syncValidateFalse),
154
+ requiredActions: [
155
+ "Remove Validate=false from syncOptions so kubectl/server schema validation runs on every apply.",
156
+ "Only use ServerSideApply with field-manager conflict handling reviewed; do not use it to bypass validating webhooks.",
157
+ "Keep admission controllers (OPA Gatekeeper / Kyverno) in the apply path for all GitOps-managed namespaces."
158
+ ]
159
+ });
160
+ }
161
+ // 7. ArgoCD RBAC granting broad admin.
162
+ if (argoRbacAdmin.length > 0) {
163
+ findings.push({
164
+ id: "ARGOCD_RBAC_ADMIN_BROAD",
165
+ title: "ArgoCD RBAC grants role:admin to a broad group (or g, *, role:admin) — broad principals gain full control of all Applications and clusters",
166
+ severity: "HIGH",
167
+ evidence: evidence(argoRbacAdmin),
168
+ requiredActions: [
169
+ "Replace role:admin grants with project-scoped custom roles in policy.csv (p, proj:team:role, applications, ...).",
170
+ "Never grant 'g, *, role:admin'; bind admin only to a named, minimal SSO group.",
171
+ "Set policy.default to role:'' (deny) and enumerate every allowed action explicitly."
172
+ ]
173
+ });
174
+ }
175
+ // 8. ArgoCD server exposed insecurely / auth disabled.
176
+ if (argoServerInsecure.length > 0) {
177
+ findings.push({
178
+ id: "ARGOCD_SERVER_INSECURE",
179
+ title: "ArgoCD server runs with insecure/anonymous access (server.insecure, disable.auth, users.anonymous.enabled, or --insecure) — unauthenticated control of the cluster delivery plane",
180
+ severity: "CRITICAL",
181
+ evidence: evidence(argoServerInsecure),
182
+ requiredActions: [
183
+ "Set server.insecure: false and terminate TLS at the ArgoCD server or ingress.",
184
+ "Never set users.anonymous.enabled: true or disable.auth: true; require SSO/OIDC for every login.",
185
+ "Remove --insecure flags from argocd-server and argocd-repo-server deployments."
186
+ ]
187
+ });
188
+ }
189
+ // 9. Resource health/diff ignored or insecure repo-server flags.
190
+ if (argoResourceIgnoreHealth.length > 0) {
191
+ findings.push({
192
+ id: "ARGOCD_HEALTH_IGNORED",
193
+ title: "ArgoCD resource.customizations / ignoreDifferences suppress health and drift detection (or --disable-tls on repo-server) — malicious drift goes unreported",
194
+ severity: "MEDIUM",
195
+ evidence: evidence(argoResourceIgnoreHealth),
196
+ requiredActions: [
197
+ "Scope ignoreDifferences narrowly to specific jsonPointers; never blanket-ignore whole resource health.",
198
+ "Do not use health.lua overrides that always report Healthy; keep real health assessment.",
199
+ "Remove --disable-tls and enforce TLS between repo-server, application-controller, and Git/Helm sources."
200
+ ]
201
+ });
202
+ }
203
+ // ---- Flux ----
204
+ // 10. Git/OCI source with no signature verification or insecure transport.
205
+ if (fluxGitRepo.length > 0 && (fluxVerify.length === 0 || fluxInsecure.length > 0)) {
206
+ findings.push({
207
+ id: "FLUX_SOURCE_UNVERIFIED",
208
+ title: "Flux GitRepository/OCIRepository has no verify: (no cosign/PGP signature check) or sets insecure: true — Flux pulls and applies unauthenticated, tamperable source",
209
+ severity: "HIGH",
210
+ evidence: evidence([...fluxGitRepo, ...fluxInsecure]),
211
+ requiredActions: [
212
+ "Add a spec.verify block (provider: cosign or pgp) to every GitRepository/OCIRepository so Flux rejects unsigned revisions.",
213
+ "Remove insecure: true; require TLS for all Git/OCI/Helm source fetches.",
214
+ "Pin the source to an immutable tag/digest and sign artifacts in CI with cosign keyless (OIDC)."
215
+ ]
216
+ });
217
+ }
218
+ // 11. Kustomization/HelmRelease auto-prune from a source with no decryption (secrets) configured.
219
+ if ((fluxKustomization.length > 0 || fluxHelmRelease.length > 0) &&
220
+ argoPrune.length > 0 &&
221
+ fluxDecryption.length === 0) {
222
+ findings.push({
223
+ id: "FLUX_AUTOPRUNE_NO_DECRYPTION",
224
+ title: "Flux Kustomization/HelmRelease auto-prunes on an interval but has no decryption: (SOPS) block — secrets are unmanaged and reconciliation auto-applies upstream changes",
225
+ severity: "HIGH",
226
+ evidence: evidence([...fluxKustomization, ...fluxHelmRelease]),
227
+ requiredActions: [
228
+ "Add a spec.decryption block (provider: sops) so Secrets are decrypted in-cluster, never stored in plaintext.",
229
+ "Pin the Kustomization/HelmRelease sourceRef to a verified, immutable revision before enabling prune: true.",
230
+ "Increase the reconcile interval / require manual approval for production so unreviewed upstream changes do not auto-apply."
231
+ ]
232
+ });
233
+ }
234
+ // 12. ImagePolicy / ImageUpdateAutomation auto-pulling floating tags.
235
+ if (fluxImagePolicy.length > 0 && fluxImageRangeTag.length > 0) {
236
+ findings.push({
237
+ id: "FLUX_IMAGE_AUTOUPDATE_FLOATING_TAG",
238
+ title: "Flux ImagePolicy/ImageUpdateAutomation auto-deploys a semver range or :latest tag — a poisoned upstream image is auto-pulled and rolled out (supply-chain auto-pull)",
239
+ severity: "HIGH",
240
+ evidence: evidence([...fluxImagePolicy, ...fluxImageRangeTag]),
241
+ requiredActions: [
242
+ "Pin images to an immutable digest (image@sha256:...) instead of a semver range or :latest.",
243
+ "Require cosign signature verification (Kyverno/policy-controller) before any auto-updated image is admitted.",
244
+ "Gate ImageUpdateAutomation commits behind a protected branch + PR review rather than direct push to the deploy branch."
245
+ ]
246
+ });
247
+ }
248
+ // 13. HelmRepository over plaintext HTTP.
249
+ if (helmRepoHttp.length > 0) {
250
+ findings.push({
251
+ id: "FLUX_HELM_REPO_HTTP",
252
+ title: "Flux HelmRepository / chart source uses plaintext HTTP (url: http://) — chart payloads are MITM-tamperable in transit",
253
+ severity: "HIGH",
254
+ evidence: evidence(helmRepoHttp),
255
+ requiredActions: [
256
+ "Use https:// (or oci://) for all HelmRepository/chart URLs; never http://.",
257
+ "Provide a certSecretRef / CA bundle for private registries instead of disabling TLS.",
258
+ "Enable chart provenance verification (Helm --verify / cosign) so tampered charts are rejected."
259
+ ]
260
+ });
261
+ }
262
+ // ---- Round 2: deeper ArgoCD / Flux / Helm supply-chain checks ----
263
+ const [argoAppSetGenerator, // ApplicationSet SCM/PR/Git generator
264
+ argoAppSetGoTemplate, // goTemplate + unsanitized params
265
+ argoIgnoreDiffSensitive, // ignoreDifferences on RBAC/Secret kinds
266
+ argoRolloutAnalysis, // AnalysisTemplate running jobs
267
+ argoCompareIgnoreStatus, // resource.compareoptions ignoreResourceStatusField: all
268
+ argoAccountApiKey, // accounts.* apiKey capability
269
+ argoDexInlineSecret, // dex.config clientSecret inline
270
+ argoNotifWebhook, // notifications-cm webhook / template injection
271
+ argoRepoInlineCreds, // repositories/repo-creds inline password/sshPrivateKey
272
+ argoKustomizeLoadRestrictor, // --load-restrictor LoadRestrictionsNone
273
+ argoHelmPostRenderer, // helm --post-renderer exec
274
+ argoExecExtensions, // exec / extensions enabled
275
+ argoResourceTrackingLabel, // resourceTrackingMethod: label
276
+ ] = await Promise.all([
277
+ searchRepo({ query: String.raw `kind\s*:\s*ApplicationSet|generators\s*:|scmProvider\s*:|pullRequest\s*:`, isRegex: true, maxMatches: MAX }),
278
+ searchRepo({ query: String.raw `goTemplate\s*:\s*true|goTemplateOptions\s*:`, isRegex: true, maxMatches: MAX }),
279
+ searchRepo({ query: String.raw `kind\s*:\s*(?:Secret|Role|RoleBinding|ClusterRole|ClusterRoleBinding|ServiceAccount)`, isRegex: true, maxMatches: MAX }),
280
+ searchRepo({ query: String.raw `kind\s*:\s*AnalysisTemplate|provider\s*:\s*job|kind\s*:\s*ClusterAnalysisTemplate`, isRegex: true, maxMatches: MAX }),
281
+ searchRepo({ query: String.raw `ignoreResourceStatusField\s*:\s*all|resource\.compareoptions`, isRegex: true, maxMatches: MAX }),
282
+ searchRepo({ query: String.raw `accounts\..+\s*:\s*apiKey|accounts\..+\s*:\s*['"]?apiKey,\s*login|capabilities\s*:\s*\[?\s*apiKey`, isRegex: true, maxMatches: MAX }),
283
+ searchRepo({ query: String.raw `dex\.config|clientSecret\s*:\s*['"]?[A-Za-z0-9._-]{6,}`, isRegex: true, maxMatches: MAX }),
284
+ searchRepo({ query: String.raw `notifications-cm|service\.webhook|trigger\.on-|template\.app-`, isRegex: true, maxMatches: MAX }),
285
+ searchRepo({ query: String.raw `sshPrivateKey\s*:|password\s*:\s*['"]?\S|kind\s*:\s*repo-creds|argocd\.argoproj\.io/secret-type\s*:\s*repo`, isRegex: true, maxMatches: MAX }),
286
+ searchRepo({ query: String.raw `--load-restrictor[=\s]+LoadRestrictionsNone|Load_RestrictionsNone|buildOptions\s*:`, isRegex: true, maxMatches: MAX }),
287
+ searchRepo({ query: String.raw `--post-renderer|postRenderer\s*:`, isRegex: true, maxMatches: MAX }),
288
+ searchRepo({ query: String.raw `exec\.enabled\s*:\s*['"]?true|extension\.config|kind\s*:\s*ArgoCDExtension`, isRegex: true, maxMatches: MAX }),
289
+ searchRepo({ query: String.raw `application\.resourceTrackingMethod\s*:\s*['"]?label|resourceTrackingMethod\s*:\s*['"]?label`, isRegex: true, maxMatches: MAX }),
290
+ ]);
291
+ const [fluxReceiverWeakToken, // Receiver/webhook weak/no token
292
+ fluxBucketInsecure, // Bucket source http / public
293
+ fluxPostBuildSubstitute, // postBuild.substituteFrom unverified
294
+ fluxHelmInlineSecret, // HelmRelease values inline secrets
295
+ fluxPathTraversal, // Kustomization path ../
296
+ fluxSaImpersonation, // serviceAccountName impersonation / cluster-admin
297
+ fluxOciFloatingNoVerify, // OCIRepository floating tag (verify checked earlier)
298
+ fluxDependsOn, // dependsOn present (absence => ordering bypass)
299
+ fluxEnableHelm, // KustomizeConfig --enable-helm
300
+ fluxImageGitPushBranch, // image automation push branch
301
+ ] = await Promise.all([
302
+ searchRepo({ query: String.raw `kind\s*:\s*Receiver|secretRef\s*:\s*$|type\s*:\s*generic-hmac|type\s*:\s*github`, isRegex: true, maxMatches: MAX }),
303
+ searchRepo({ query: String.raw `kind\s*:\s*Bucket`, isRegex: true, maxMatches: MAX }),
304
+ searchRepo({ query: String.raw `substituteFrom\s*:|postBuild\s*:`, isRegex: true, maxMatches: MAX }),
305
+ searchRepo({ query: String.raw `values\s*:\s*$|password\s*:|apiKey\s*:|token\s*:\s*['"]?[A-Za-z0-9]`, isRegex: true, maxMatches: MAX }),
306
+ searchRepo({ query: String.raw `path\s*:\s*['"]?\.\./|path\s*:\s*['"]?\.\.`, isRegex: true, maxMatches: MAX }),
307
+ searchRepo({ query: String.raw `serviceAccountName\s*:\s*['"]?(?:default|cluster-admin|kustomize-controller|flux)`, isRegex: true, maxMatches: MAX }),
308
+ searchRepo({ query: String.raw `tag\s*:\s*['"]?(?:latest|main|stable|edge)|semverFilter\s*:`, isRegex: true, maxMatches: MAX }),
309
+ searchRepo({ query: String.raw `dependsOn\s*:`, isRegex: true, maxMatches: MAX }),
310
+ searchRepo({ query: String.raw `--enable-helm|enableHelm\s*:\s*true|helmGlobals\s*:`, isRegex: true, maxMatches: MAX }),
311
+ searchRepo({ query: String.raw `push\s*:\s*$|branch\s*:\s*['"]?(?:main|master|production|release)`, isRegex: true, maxMatches: MAX }),
312
+ ]);
313
+ const [helmDepHttp, // chart dependencies from http repo
314
+ helmChartLock, // Chart.lock present (absence => no digest pin)
315
+ helmChartYaml, // Chart.yaml present (scope for lock check)
316
+ helmUnpinnedVersion, // unpinned chart version range
317
+ helmFilesGetSecret, // .Files.Get on secrets
318
+ helmSetPrivileged, // --set privileged securityContext
319
+ ] = await Promise.all([
320
+ searchRepo({ query: String.raw `repository\s*:\s*['"]?http://|repository\s*:\s*['"]?@`, isRegex: true, maxMatches: MAX }),
321
+ searchRepo({ query: String.raw `digest\s*:\s*sha256:`, isRegex: true, maxMatches: MAX }),
322
+ searchRepo({ query: String.raw `^\s*apiVersion\s*:\s*v[12]\s*$|^\s*name\s*:.+|dependencies\s*:`, isRegex: true, maxMatches: MAX }),
323
+ searchRepo({ query: String.raw `version\s*:\s*['"]?[\^~><]|version\s*:\s*['"]?\*|version\s*:\s*['"]?x`, isRegex: true, maxMatches: MAX }),
324
+ searchRepo({ query: String.raw `\.Files\.Get\s+['"]?[^'"\s]*secret|\.Files\.Get\s+['"]?[^'"\s]*\.key|\.Files\.Get\s+['"]?[^'"\s]*password`, isRegex: true, maxMatches: MAX }),
325
+ searchRepo({ query: String.raw `--set\s+[^\s]*privileged=true|--set\s+[^\s]*runAsUser=0|--set\s+[^\s]*allowPrivilegeEscalation=true`, isRegex: true, maxMatches: MAX }),
326
+ ]);
327
+ // 14. ApplicationSet generator pulling from any org/repo.
328
+ if (argoAppSetGenerator.length > 0) {
329
+ findings.push({
330
+ id: "ARGOCD_APPLICATIONSET_GENERATOR_INJECTION",
331
+ title: "ArgoCD ApplicationSet uses an SCM/PR/Git generator that discovers repos/branches dynamically — an attacker who opens a PR or pushes a branch causes arbitrary Application creation (generator injection)",
332
+ severity: "CRITICAL",
333
+ evidence: evidence(argoAppSetGenerator),
334
+ requiredActions: [
335
+ "Scope SCM/PR generators to an explicit allowlist of repos and a trusted org; never match all repositories.",
336
+ "Set a requiresReview / label filter on pullRequest generators so untrusted PRs cannot spawn Applications.",
337
+ "Run applicationset-controller with the SCM provider token scoped read-only to the specific org.",
338
+ "Template the destination namespace/cluster from a trusted field — never directly from branch/PR-controlled values."
339
+ ]
340
+ });
341
+ }
342
+ // 15. ApplicationSet goTemplate with unsanitized params.
343
+ if (argoAppSetGoTemplate.length > 0) {
344
+ findings.push({
345
+ id: "ARGOCD_APPLICATIONSET_GOTEMPLATE_INJECTION",
346
+ title: "ArgoCD ApplicationSet enables goTemplate — generator parameters (branch names, PR titles, repo metadata) flow unsanitized into Application specs, enabling template/field injection",
347
+ severity: "HIGH",
348
+ evidence: evidence(argoAppSetGoTemplate),
349
+ requiredActions: [
350
+ "Treat all generator params as untrusted; never interpolate them into project, destination.namespace, or destination.server.",
351
+ "Pin project and destination to static values, not goTemplate expressions derived from SCM metadata.",
352
+ "Enable goTemplateOptions: [missingkey=error] so undefined/injected keys fail closed instead of rendering empty."
353
+ ]
354
+ });
355
+ }
356
+ // 16. ignoreDifferences hiding drift on RBAC/Secret resources.
357
+ if (argoIgnoreDiffSensitive.length > 0 && argoResourceIgnoreHealth.length > 0) {
358
+ findings.push({
359
+ id: "ARGOCD_IGNOREDIFF_SENSITIVE_DRIFT",
360
+ title: "ArgoCD ignoreDifferences is configured alongside Secret/RBAC resources — drift on Secrets, Roles, or RoleBindings can be silently ignored, hiding privilege escalation",
361
+ severity: "HIGH",
362
+ evidence: evidence(argoIgnoreDiffSensitive),
363
+ requiredActions: [
364
+ "Never apply ignoreDifferences to Secret, Role, RoleBinding, ClusterRole, ClusterRoleBinding, or ServiceAccount resources.",
365
+ "Scope ignoreDifferences to specific jsonPointers on non-security fields only (e.g. replica counts).",
366
+ "Alert on any OutOfSync RBAC/Secret resource so injected privilege grants are surfaced, not suppressed."
367
+ ]
368
+ });
369
+ }
370
+ // 17. Argo Rollouts AnalysisTemplate running arbitrary jobs.
371
+ if (argoRolloutAnalysis.length > 0) {
372
+ findings.push({
373
+ id: "ARGOCD_ROLLOUT_ANALYSIS_JOB",
374
+ title: "Argo Rollouts AnalysisTemplate runs a Job/metric provider during promotion — a repo-controlled analysis template executes arbitrary pods with the rollouts controller's RBAC",
375
+ severity: "HIGH",
376
+ evidence: evidence(argoRolloutAnalysis),
377
+ requiredActions: [
378
+ "Store AnalysisTemplates in a trusted, review-gated repo — never let the application repo define the job spec.",
379
+ "Run analysis Jobs under a dedicated, least-privilege ServiceAccount with no cluster-admin.",
380
+ "Pin job container images to digests and forbid privileged/hostPath in analysis job pod specs."
381
+ ]
382
+ });
383
+ }
384
+ // 18. compareoptions ignoreResourceStatusField: all.
385
+ if (argoCompareIgnoreStatus.length > 0) {
386
+ findings.push({
387
+ id: "ARGOCD_COMPARE_IGNORE_STATUS_ALL",
388
+ title: "ArgoCD resource.compareoptions sets ignoreResourceStatusField: all — status-field drift is globally ignored, weakening drift/health detection across every Application",
389
+ severity: "MEDIUM",
390
+ evidence: evidence(argoCompareIgnoreStatus),
391
+ requiredActions: [
392
+ "Set ignoreResourceStatusField to 'crd' (the safe default) rather than 'all'.",
393
+ "Do not globally suppress status comparison; handle noisy status fields per-resource with scoped ignoreDifferences.",
394
+ "Keep health assessment enabled so degraded/compromised workloads are reported."
395
+ ]
396
+ });
397
+ }
398
+ // 19. accounts.* with apiKey capability (long-lived tokens).
399
+ if (argoAccountApiKey.length > 0) {
400
+ findings.push({
401
+ id: "ARGOCD_ACCOUNT_APIKEY_CAPABILITY",
402
+ title: "ArgoCD argocd-cm grants an account the apiKey capability — enables long-lived, non-expiring bearer tokens that bypass SSO and survive offboarding",
403
+ severity: "HIGH",
404
+ evidence: evidence(argoAccountApiKey),
405
+ requiredActions: [
406
+ "Remove the apiKey capability from human accounts; rely on SSO/OIDC login only.",
407
+ "Where automation needs tokens, scope them via project-level roles and set short expiry; rotate frequently.",
408
+ "Audit existing API tokens (argocd account get) and revoke any unattended long-lived tokens."
409
+ ]
410
+ });
411
+ }
412
+ // 20. dex.config connector with inline clientSecret.
413
+ if (argoDexInlineSecret.length > 0) {
414
+ findings.push({
415
+ id: "ARGOCD_DEX_INLINE_CLIENT_SECRET",
416
+ title: "ArgoCD dex.config embeds an OIDC connector clientSecret inline in argocd-cm — the IdP client secret is committed to Git in plaintext",
417
+ severity: "HIGH",
418
+ evidence: evidence(argoDexInlineSecret),
419
+ requiredActions: [
420
+ "Reference the connector clientSecret via $<secret-name>:<key> indirection into argocd-secret, never inline.",
421
+ "Rotate any clientSecret that was committed inline; it is exposed to everyone with repo read access.",
422
+ "Restrict the OIDC client redirect URIs and allowed groups to least privilege."
423
+ ]
424
+ });
425
+ }
426
+ // 21. notifications-cm webhook to untrusted URL / template injection.
427
+ if (argoNotifWebhook.length > 0) {
428
+ findings.push({
429
+ id: "ARGOCD_NOTIFICATIONS_WEBHOOK_INJECTION",
430
+ title: "ArgoCD argocd-notifications-cm defines webhook services / templates — outbound webhooks to untrusted URLs and unsanitized template variables enable SSRF and notification template injection",
431
+ severity: "MEDIUM",
432
+ evidence: evidence(argoNotifWebhook),
433
+ requiredActions: [
434
+ "Pin webhook service URLs to known, internal endpoints; do not template the URL from Application-controlled fields.",
435
+ "Sanitize/escape app metadata used in notification templates to prevent injection into downstream systems.",
436
+ "Store webhook tokens/headers in argocd-notifications-secret, not inline in the ConfigMap."
437
+ ]
438
+ });
439
+ }
440
+ // 22. Repo creds with inline password / sshPrivateKey.
441
+ if (argoRepoInlineCreds.length > 0) {
442
+ findings.push({
443
+ id: "ARGOCD_REPO_INLINE_CREDENTIALS",
444
+ title: "ArgoCD repository / repo-creds Secret embeds an inline password or sshPrivateKey — Git credentials committed in plaintext grant write access to source repos",
445
+ severity: "CRITICAL",
446
+ evidence: evidence(argoRepoInlineCreds),
447
+ requiredActions: [
448
+ "Never commit password / sshPrivateKey inline; manage repo credentials via Sealed Secrets, SOPS, or External Secrets.",
449
+ "Rotate any Git credential / deploy key that was committed; Git history retains it forever.",
450
+ "Use short-lived, scoped tokens (GitHub App installation tokens) instead of long-lived passwords/keys."
451
+ ]
452
+ });
453
+ }
454
+ // 23. kustomize buildOptions --load-restrictor LoadRestrictionsNone (path traversal).
455
+ if (argoKustomizeLoadRestrictor.length > 0) {
456
+ findings.push({
457
+ id: "ARGOCD_KUSTOMIZE_LOAD_RESTRICTOR_NONE",
458
+ title: "Kustomize buildOptions disable the load restrictor (--load-restrictor LoadRestrictionsNone) — kustomizations can reference files outside their root, reading host/repo-server secrets via path traversal",
459
+ severity: "HIGH",
460
+ evidence: evidence(argoKustomizeLoadRestrictor),
461
+ requiredActions: [
462
+ "Remove --load-restrictor LoadRestrictionsNone; keep the default LoadRestrictionsRootOnly.",
463
+ "Vendor any genuinely external files into the kustomization root instead of disabling restrictions.",
464
+ "Run the repo-server with a read-only root filesystem and no host secret mounts."
465
+ ]
466
+ });
467
+ }
468
+ // 24. Helm --post-renderer exec.
469
+ if (argoHelmPostRenderer.length > 0) {
470
+ findings.push({
471
+ id: "ARGOCD_HELM_POST_RENDERER_EXEC",
472
+ title: "Helm source uses a --post-renderer — manifest rendering shells out to a repo-controlled binary/script inside the repo-server, an arbitrary-code-execution sink",
473
+ severity: "HIGH",
474
+ evidence: evidence(argoHelmPostRenderer),
475
+ requiredActions: [
476
+ "Avoid --post-renderer / postRenderer sourced from the application repo; render deterministically.",
477
+ "If post-rendering is required, run it in a locked-down CMP sidecar with no cluster credentials and a read-only FS.",
478
+ "Pin and review the post-renderer binary; never execute scripts fetched at sync time."
479
+ ]
480
+ });
481
+ }
482
+ // 25. exec / extensions enabled.
483
+ if (argoExecExtensions.length > 0) {
484
+ findings.push({
485
+ id: "ARGOCD_EXEC_EXTENSIONS_ENABLED",
486
+ title: "ArgoCD exec feature (exec.enabled: true) or UI extensions are enabled — operators can open shells into pods via the API, and extensions load remote JS into the console",
487
+ severity: "MEDIUM",
488
+ evidence: evidence(argoExecExtensions),
489
+ requiredActions: [
490
+ "Set exec.enabled: false unless interactive pod shells are strictly required; gate it behind a dedicated RBAC role.",
491
+ "Pin and review any UI extension sources; never load extension assets from untrusted URLs.",
492
+ "Audit exec usage via the ArgoCD audit log and alert on shell sessions in production namespaces."
493
+ ]
494
+ });
495
+ }
496
+ // 26. resourceTrackingMethod: label (tracking confusion).
497
+ if (argoResourceTrackingLabel.length > 0) {
498
+ findings.push({
499
+ id: "ARGOCD_RESOURCE_TRACKING_LABEL",
500
+ title: "ArgoCD resourceTrackingMethod is set to 'label' — the app.kubernetes.io/instance label is spoofable, letting a malicious manifest claim or hijack resources owned by another Application",
501
+ severity: "MEDIUM",
502
+ evidence: evidence(argoResourceTrackingLabel),
503
+ requiredActions: [
504
+ "Use resourceTrackingMethod: annotation (or annotation+label) instead of label — annotations carry the app name and group, resisting spoofing.",
505
+ "Avoid sharing the app.kubernetes.io/instance label across Applications.",
506
+ "Audit for resources whose tracking label does not match their owning Application."
507
+ ]
508
+ });
509
+ }
510
+ // ---- Flux depth ----
511
+ // 27. Receiver/webhook with weak or missing token.
512
+ if (fluxReceiverWeakToken.length > 0) {
513
+ findings.push({
514
+ id: "FLUX_RECEIVER_WEAK_TOKEN",
515
+ title: "Flux Receiver exposes a webhook — without a strong HMAC secretRef (or with a generic/empty token), anyone who can reach the receiver URL can force-reconcile and trigger deploys",
516
+ severity: "HIGH",
517
+ evidence: evidence(fluxReceiverWeakToken),
518
+ requiredActions: [
519
+ "Set a secretRef pointing to a high-entropy token and use type: generic-hmac (or a provider type that verifies signatures).",
520
+ "Restrict the receiver Ingress to the source provider's IP ranges; do not expose it broadly.",
521
+ "Rotate the receiver token periodically and store it via SOPS/Sealed Secrets, not inline."
522
+ ]
523
+ });
524
+ }
525
+ // 28. Bucket source insecure / public.
526
+ if (fluxBucketInsecure.length > 0 && (fluxInsecure.length > 0 || helmRepoHttp.length > 0)) {
527
+ findings.push({
528
+ id: "FLUX_BUCKET_INSECURE",
529
+ title: "Flux Bucket source is reachable over plaintext/insecure transport or a public endpoint — manifests fetched from object storage are tamperable and unauthenticated",
530
+ severity: "HIGH",
531
+ evidence: evidence(fluxBucketInsecure),
532
+ requiredActions: [
533
+ "Set the Bucket endpoint to an HTTPS/TLS endpoint and remove insecure: true.",
534
+ "Authenticate to the bucket via a secretRef (or workload identity); do not rely on public/anonymous read.",
535
+ "Pin and verify object integrity; restrict the bucket policy to the Flux controller identity only."
536
+ ]
537
+ });
538
+ }
539
+ // 29. postBuild.substituteFrom from unverified ConfigMap/Secret.
540
+ if (fluxPostBuildSubstitute.length > 0) {
541
+ findings.push({
542
+ id: "FLUX_POSTBUILD_SUBSTITUTE_INJECTION",
543
+ title: "Flux Kustomization uses postBuild.substituteFrom — variables pulled from a ConfigMap/Secret are substituted into rendered manifests, enabling var injection if that source is attacker-writable",
544
+ severity: "MEDIUM",
545
+ evidence: evidence(fluxPostBuildSubstitute),
546
+ requiredActions: [
547
+ "Source substituteFrom only from ConfigMaps/Secrets in a controller-only namespace with locked-down RBAC.",
548
+ "Never substitute into security-relevant fields (image, securityContext, serviceAccountName) from mutable sources.",
549
+ "Mark substituteFrom entries optional: false so a missing/tampered source fails closed."
550
+ ]
551
+ });
552
+ }
553
+ // 30. HelmRelease values with inline secrets.
554
+ if (fluxHelmRelease.length > 0 && fluxHelmInlineSecret.length > 0 && fluxDecryption.length === 0) {
555
+ findings.push({
556
+ id: "FLUX_HELMRELEASE_INLINE_SECRET",
557
+ title: "Flux HelmRelease embeds secret-like values (password/apiKey/token) inline under spec.values with no decryption configured — credentials are committed in plaintext",
558
+ severity: "HIGH",
559
+ evidence: evidence(fluxHelmInlineSecret),
560
+ requiredActions: [
561
+ "Move secret values out of spec.values; reference them via valuesFrom a SOPS-decrypted Secret.",
562
+ "Configure spec.decryption (provider: sops) so secrets are decrypted in-cluster, never stored plaintext.",
563
+ "Rotate any credential that was committed inline in a HelmRelease."
564
+ ]
565
+ });
566
+ }
567
+ // 31. Kustomization path traversal.
568
+ if (fluxPathTraversal.length > 0) {
569
+ findings.push({
570
+ id: "FLUX_KUSTOMIZATION_PATH_TRAVERSAL",
571
+ title: "Flux Kustomization spec.path uses '../' traversal — the reconciler can be pointed outside the intended directory to apply unintended/sibling manifests",
572
+ severity: "MEDIUM",
573
+ evidence: evidence(fluxPathTraversal),
574
+ requiredActions: [
575
+ "Set spec.path to a fixed subdirectory of the source; never use '../' to escape the configured root.",
576
+ "Use separate GitRepository sources scoped to each app rather than traversing across directories.",
577
+ "Review the source so the reconciled path cannot reach secrets or other teams' manifests."
578
+ ]
579
+ });
580
+ }
581
+ // 32. serviceAccountName impersonation / cluster-admin.
582
+ if ((fluxKustomization.length > 0 || fluxHelmRelease.length > 0) &&
583
+ fluxSaImpersonation.length > 0) {
584
+ findings.push({
585
+ id: "FLUX_SERVICEACCOUNT_IMPERSONATION",
586
+ title: "Flux Kustomization/HelmRelease sets a privileged serviceAccountName (default / cluster-admin / controller SA) — manifests are applied with broad RBAC, so a malicious manifest inherits cluster-admin",
587
+ severity: "HIGH",
588
+ evidence: evidence(fluxSaImpersonation),
589
+ requiredActions: [
590
+ "Set spec.serviceAccountName to a dedicated, least-privilege ServiceAccount scoped to the target namespace.",
591
+ "Never reconcile with the default SA or a cluster-admin-bound SA; bind only the exact Roles needed.",
592
+ "Enable Flux multi-tenancy lockdown (--default-service-account, --no-cross-namespace-refs)."
593
+ ]
594
+ });
595
+ }
596
+ // 33. OCIRepository floating tag with no cosign verify.
597
+ if (fluxGitRepo.length > 0 && fluxOciFloatingNoVerify.length > 0 && fluxVerify.length === 0) {
598
+ findings.push({
599
+ id: "FLUX_OCI_FLOATING_TAG_NO_VERIFY",
600
+ title: "Flux OCIRepository tracks a floating tag (latest/main/stable) with no verify.provider (cosign) — a re-pushed tag is auto-pulled and applied with no signature check",
601
+ severity: "HIGH",
602
+ evidence: evidence(fluxOciFloatingNoVerify),
603
+ requiredActions: [
604
+ "Pin OCIRepository ref to an immutable digest (ref.digest: sha256:...), not a floating tag.",
605
+ "Add spec.verify.provider: cosign with the publisher's identity/key so unsigned artifacts are rejected.",
606
+ "Sign artifacts in CI with cosign keyless (OIDC) and enforce verification before reconcile."
607
+ ]
608
+ });
609
+ }
610
+ // 34. dependsOn missing — apply ordering bypass (Kustomizations present but no dependsOn anywhere).
611
+ if (fluxKustomization.length > 1 && fluxDependsOn.length === 0) {
612
+ findings.push({
613
+ id: "FLUX_NO_DEPENDS_ON_ORDERING",
614
+ title: "Multiple Flux Kustomizations exist but none declare dependsOn — apply ordering is unenforced, so security-critical resources (NetworkPolicies, RBAC, PSA) may be applied after the workloads they protect",
615
+ severity: "MEDIUM",
616
+ requiredActions: [
617
+ "Declare spec.dependsOn so policy/RBAC/namespace Kustomizations reconcile before application workloads.",
618
+ "Gate workload reconciliation on the readiness of its security prerequisites (e.g. NetworkPolicy, Gatekeeper).",
619
+ "Use health checks (spec.healthChecks) so dependents wait for prerequisites to become Ready."
620
+ ]
621
+ });
622
+ }
623
+ // 35. KustomizeConfig --enable-helm (arbitrary chart hooks).
624
+ if (fluxEnableHelm.length > 0) {
625
+ findings.push({
626
+ id: "FLUX_KUSTOMIZE_ENABLE_HELM",
627
+ title: "Flux Kustomization enables the Helm chart inflator (--enable-helm / helmGlobals) — kustomize templates and renders arbitrary remote charts, running chart hooks/templating as a code-execution sink",
628
+ severity: "HIGH",
629
+ evidence: evidence(fluxEnableHelm),
630
+ requiredActions: [
631
+ "Prefer a HelmRelease (with verify + decryption) over kustomize --enable-helm for chart rendering.",
632
+ "If --enable-helm is required, pin chart name + version + repo and verify provenance before rendering.",
633
+ "Restrict helmGlobals.chartHome to a vendored, reviewed chart directory; do not pull charts at build time."
634
+ ]
635
+ });
636
+ }
637
+ // 36. Image automation pushing to a protected branch.
638
+ if (fluxImagePolicy.length > 0 && fluxImageGitPushBranch.length > 0) {
639
+ findings.push({
640
+ id: "FLUX_IMAGE_AUTOMATION_PUSH_PROTECTED_BRANCH",
641
+ title: "Flux ImageUpdateAutomation pushes commits directly to a protected/deploy branch (main/master/production/release) — automated image bumps bypass PR review and protected-branch controls",
642
+ severity: "MEDIUM",
643
+ evidence: evidence(fluxImageGitPushBranch),
644
+ requiredActions: [
645
+ "Configure git.push.branch to a dedicated automation branch and require a reviewed PR to merge into the deploy branch.",
646
+ "Require cosign signature verification on any image before it can be auto-promoted.",
647
+ "Restrict the automation deploy key to the automation branch only, not the protected branch."
648
+ ]
649
+ });
650
+ }
651
+ // ---- Helm / Kustomize supply-chain breadth ----
652
+ // 37. Chart dependencies from an http repo.
653
+ if (helmDepHttp.length > 0) {
654
+ findings.push({
655
+ id: "HELM_DEPENDENCY_HTTP_REPO",
656
+ title: "Helm Chart.yaml declares a dependency from a plaintext http:// (or alias '@') repository — subchart payloads are MITM-tamperable and unverified",
657
+ severity: "HIGH",
658
+ evidence: evidence(helmDepHttp),
659
+ requiredActions: [
660
+ "Use https:// or oci:// repositories for all chart dependencies; never http://.",
661
+ "Pin each dependency to an exact version and commit Chart.lock with digests.",
662
+ "Vendor critical subcharts into charts/ and verify provenance (helm verify / cosign)."
663
+ ]
664
+ });
665
+ }
666
+ // 38. Chart.lock digest missing (Chart with dependencies but no sha256 digest pin).
667
+ if (helmDepHttp.length > 0 || (helmChartYaml.length > 0 && helmUnpinnedVersion.length > 0)) {
668
+ if (helmChartLock.length === 0) {
669
+ findings.push({
670
+ id: "HELM_CHART_LOCK_DIGEST_MISSING",
671
+ title: "Helm chart declares dependencies but no Chart.lock with sha256 digests is present — dependency resolution is not reproducible and a mutated upstream chart can be silently pulled",
672
+ severity: "MEDIUM",
673
+ requiredActions: [
674
+ "Run `helm dependency update` and commit the generated Chart.lock with sha256 digests.",
675
+ "Pin every dependency to an exact version (no ranges) so the lock is stable.",
676
+ "Verify the lock digest in CI before packaging/deploying the chart."
677
+ ]
678
+ });
679
+ }
680
+ }
681
+ // 39. Unpinned chart version range.
682
+ if (helmUnpinnedVersion.length > 0) {
683
+ findings.push({
684
+ id: "HELM_UNPINNED_CHART_VERSION",
685
+ title: "Helm chart/dependency version uses a range or wildcard (^, ~, >, *, x) — a new upstream release is pulled automatically, enabling supply-chain auto-update of subcharts",
686
+ severity: "MEDIUM",
687
+ evidence: evidence(helmUnpinnedVersion),
688
+ requiredActions: [
689
+ "Pin chart and dependency versions to an exact semver (e.g. 1.4.2), never a range or wildcard.",
690
+ "Commit Chart.lock so the resolved versions/digests are reproducible.",
691
+ "Review and bump versions deliberately via PR rather than allowing range-based auto-resolution."
692
+ ]
693
+ });
694
+ }
695
+ // 40. Template using .Files.Get on secrets.
696
+ if (helmFilesGetSecret.length > 0) {
697
+ findings.push({
698
+ id: "HELM_FILES_GET_SECRET",
699
+ title: "Helm template uses .Files.Get to read a secret/key/password file into rendered output — secret material is baked into manifests and may land in ConfigMaps or chart packages",
700
+ severity: "HIGH",
701
+ evidence: evidence(helmFilesGetSecret),
702
+ requiredActions: [
703
+ "Do not read secret files via .Files.Get; provide secrets through valuesFrom a managed Secret at deploy time.",
704
+ "Ensure secret files are excluded via .helmignore so they are never packaged into the chart .tgz.",
705
+ "Use SOPS/Sealed Secrets/External Secrets for secret delivery instead of embedding files in the chart."
706
+ ]
707
+ });
708
+ }
709
+ // 41. --set injecting privileged securityContext.
710
+ if (helmSetPrivileged.length > 0) {
711
+ findings.push({
712
+ id: "HELM_SET_PRIVILEGED_OVERRIDE",
713
+ title: "Helm install/upgrade uses --set to inject a privileged securityContext (privileged=true / runAsUser=0 / allowPrivilegeEscalation=true) — overrides chart hardening at deploy time, granting container escape primitives",
714
+ severity: "HIGH",
715
+ evidence: evidence(helmSetPrivileged),
716
+ requiredActions: [
717
+ "Remove --set overrides that set privileged=true, runAsUser=0, or allowPrivilegeEscalation=true.",
718
+ "Enforce a restricted PodSecurity standard / Gatekeeper policy so privileged overrides are rejected at admission.",
719
+ "Keep securityContext hardening in version-controlled values files reviewed via PR, not ad-hoc --set flags."
720
+ ]
721
+ });
722
+ }
723
+ return findings;
724
+ }