feat(argocd): per-container contrast legs for all 7 subcomponents - #167
Conversation
…nal tests) First Argo CD subcomponent leg, redis-aligned per the add-new-app-for-bobctl- tuning skill. VALIDATED live: R0001 fires on exec attacks (cat/id/perl/ln) against the hand-authored SBoB; the sole benign file-access (projected SA token) is covered so R0002/R0006 don't FP on the legitimate k8s-client read. Key finding: Argo's Go control-plane components NEVER fork-exec, so a *learned* profile has execs=[] and gives no R0001 contrast. The SBoB must DECLARE the exec baseline (tini -> argocd-server -> argocd) — every other exec is then anomalous. This means control-plane legs are demo-verification (apply SBoB -> attack -> verify), not learn->tune like redis. Files: - example/argocd-vulnerable.yaml full-stack vulnerable overlay (all 7 subcomponents) - example/argocd-server-attacks.yaml exec kill-chain (R0001) + HTTP-logic CVE probes - example/argocd-server-functional-tests.yaml Argo's REAL swagger API read-path (token-substituted) - example/argocd/sbobs/cp-argocd-server.yaml demo SBoB with declared exec baseline - example/argocd-repo-server-attacks.yaml render path-traversal/symlink/exec-RCE leg (attacks) - Makefile deploy-argocd full upstream install + overlay + wait Deploy order (critical): node-agent FIRST, then the app — node-agent hooks a container at its start. pkg bump wires the suites into the static sweeps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWQXE6qkAbWkuv8rcUSTVq
Argo CD is one contrast leg PER container (skill §8), so this adds an
attack suite and functional tests for each of the seven: server,
repo-server, application-controller, applicationset-controller,
notifications-controller, dex and redis.
Coverage. The union across the legs is 13 rules — the same set redis
asserts: R0001 R0005 R0006 R0007 R0008 R0010 R0011 R1000 R1004 R1005
R1008 R1010 R1012. argocd-server alone lands 12 live and tunes to
score=0 with 16/16 expected detections matched, 0 benign FPs, and a
contrast gate of 25 Separable / 4 Ambiguous / 0 Blind.
Rules are placed on the container where they actually discriminate,
which is the whole point of splitting by container:
- R0006/R0007 live on argocd-redis. It is the only subcomponent with
the SA token mounted that never reads it; on every controller the
token read IS the baseline, so the rules are structurally suppressed,
and repo-server has no token mounted at all.
- R0009 is a documented probe everywhere: bpf(2) returns EPERM in an
unprivileged container, so no program ever loads.
- R1001 is never emitted by this node-agent build (verified zero
cluster-wide); the drifted exec trips R1000, which is asserted.
Infrastructure:
- kubescape/values.yaml: point node-agent's fanotify hook at the runc
this host actually uses (k3s --data-dir /mnt/dev-data/k3s) and mount
that partition, without which node-agent sees NO container started
after its own boot and nothing is ever learned.
- Makefile/local-ci.sh: node-agent is never restarted; wait for it.
Streaming is forced at render time by a Helm post-renderer instead of
a patch-then-bounce.
- fast-tune.sh: fix --kubescape-namespace -> --ks-namespace (this was
broken for every app, not just argocd) and add the seven argocd legs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
|
Warning Review limit reached
Next review available in: 25 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (6)
📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds a pinned Argo CD deployment with a vulnerable overlay, attack and functional suites for Argo CD components, an ChangesArgo CD and Kubescape integration
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Makefile
participant Helm
participant PostRenderer
participant NodeAgent
Makefile->>Helm: Render Kubescape chart
Helm->>PostRenderer: Process rendered manifests
PostRenderer->>Helm: Set networkStreamingEnabled true
Helm->>NodeAgent: Apply updated DaemonSet
Makefile->>NodeAgent: Wait for readiness and verify streaming
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Makefile`:
- Around line 273-283: Deduplicate the repeated post-renderer and validation
sequence across the kubescape, alertmanager, and kubescape-vendor targets by
introducing a shared .PHONY target such as post-kubescape-checks that runs
wait-node-agent and verify-streaming. Make all three targets depend on or invoke
this shared check while preserving the existing KS_POST_RENDERER usage and
validation order.
- Around line 101-109: Remove the error-swallowing `-` prefixes from every
`kubectl rollout status` command in the Argo CD subcomponent wait block so
`deploy-argocd` fails when any rollout fails. Preserve execution of all status
checks so each subcomponent is still reported before the target exits nonzero.
In `@scripts/fast-tune.sh`:
- Around line 59-66: Update the PROFILE_MATCH assignment in the argocd-server
case to match the hand-authored profile name argocd-server rather than the
hash-based learned-profile regex. Keep the surrounding case configuration
unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b9679b03-d1dc-46f0-9852-cabc698b3f5f
📒 Files selected for processing (21)
Makefileexample/argocd-application-controller-attacks.yamlexample/argocd-application-controller-functional-tests.yamlexample/argocd-applicationset-controller-attacks.yamlexample/argocd-applicationset-controller-functional-tests.yamlexample/argocd-dex-server-attacks.yamlexample/argocd-notifications-controller-attacks.yamlexample/argocd-notifications-controller-functional-tests.yamlexample/argocd-redis-attacks.yamlexample/argocd-redis-functional-tests.yamlexample/argocd-repo-server-attacks.yamlexample/argocd-repo-server-functional-tests.yamlexample/argocd-server-attacks.yamlexample/argocd-server-functional-tests.yamlexample/argocd-vulnerable.yamlexample/argocd/sbobs/cp-argocd-server.yamlkubescape/force-network-streaming.shkubescape/values.yamlpkgscripts/fast-tune.shscripts/local-ci.sh
👮 Files not reviewed due to content moderation or server errors (4)
- example/argocd-repo-server-attacks.yaml
- example/argocd-repo-server-functional-tests.yaml
- example/argocd-server-attacks.yaml
- example/argocd-server-functional-tests.yaml
| .PHONY: kubescape | ||
| kubescape: | ||
| kubescape: | ||
| helm repo add kubescape https://kubescape.github.io/helm-charts/ | ||
| helm repo update | ||
| helm upgrade --install kubescape kubescape/kubescape-operator --version $(KUBESCAPE_CHART_VER) -n honey --create-namespace --values kubescape/values.yaml | ||
| helm upgrade --install kubescape kubescape/kubescape-operator --version $(KUBESCAPE_CHART_VER) -n honey --create-namespace --values kubescape/values.yaml --post-renderer $(KS_POST_RENDERER) | ||
| @echo "Ensuring CRDs are up-to-date (helm upgrade skips CRDs)..." | ||
| -helm show crds kubescape/kubescape-operator --version $(KUBESCAPE_CHART_VER) | kubectl apply --server-side --force-conflicts -f - 2>/dev/null || true | ||
| -kubectl apply -f kubescape/default-rules.yaml | ||
| sleep 5 | ||
| -kubectl rollout status -n honey deploy/kubevuln --timeout=120s | ||
| $(MAKE) enable-streaming | ||
|
|
||
| # enable-streaming forces networkStreamingEnabled=true in the node-agent | ||
| # configmap and rolls the DaemonSet. The chart gates the flag behind | ||
| # cloud-submit (rendered = submit AND enable; submit = non-empty .Values.server) | ||
| # so on an on-prem stack with no server it renders FALSE regardless of | ||
| # capabilities.networkEventsStreaming. This target is idempotent and MUST be | ||
| # re-run after EVERY helm upgrade — kubescape/alertmanager/kubescape-vendor all | ||
| # call it — so the setting never drifts. See docs/portability-spec.md D7a. | ||
| .PHONY: enable-streaming | ||
| enable-streaming: | ||
| @echo "Forcing node-agent networkStreamingEnabled=true (chart renders FALSE without cloud-submit)..." | ||
| @PATCH=$$(kubectl -n honey get configmap node-agent -o jsonpath='{.data.config\.json}' | python3 -c 'import json,sys; cfg=json.load(sys.stdin); cfg["networkStreamingEnabled"]=True; print(json.dumps({"data":{"config.json":json.dumps(cfg)}}))'); \ | ||
| kubectl -n honey patch configmap node-agent --type merge -p "$$PATCH" | ||
| -kubectl rollout restart -n honey ds node-agent | ||
| -kubectl rollout status -n honey ds node-agent --timeout=180s | ||
| $(MAKE) wait-node-agent | ||
| $(MAKE) verify-streaming |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Duplicated wait-node-agent + verify-streaming sequence across three targets.
kubescape, alertmanager, and kubescape-vendor each repeat the identical --post-renderer $(KS_POST_RENDERER) + $(MAKE) wait-node-agent + $(MAKE) verify-streaming trio. Consider a shared .PHONY prerequisite (e.g. post-kubescape-checks: wait-node-agent verify-streaming) that all three depend on, to avoid the copies drifting.
Also applies to: 313-326, 337-345
🧰 Tools
🪛 checkmake (0.3.2)
[warning] 274-274: Target body for "kubescape" exceeds allowed length of 5 lines (9).
(maxbodylength)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Makefile` around lines 273 - 283, Deduplicate the repeated post-renderer and
validation sequence across the kubescape, alertmanager, and kubescape-vendor
targets by introducing a shared .PHONY target such as post-kubescape-checks that
runs wait-node-agent and verify-streaming. Make all three targets depend on or
invoke this shared check while preserving the existing KS_POST_RENDERER usage
and validation order.
| argocd-server) | ||
| NS=argocd | ||
| SUITE=example/argocd-server-attacks.yaml | ||
| FUNCTESTS=example/argocd-server-functional-tests.yaml | ||
| PROFILE_MATCH=argocd-server-.*-argocd-server | ||
| SERVICE_SCHEME=https | ||
| NEEDS_ARGOCD_TOKEN=true | ||
| ;; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n 'PROFILE_MATCH' pkg scripts -A3 -B3Repository: k8sstormcenter/bob
Length of output: 8163
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== fast-tune script outline =="
ast-grep outline scripts/fast-tune.sh --view expanded || true
echo
echo "== scripts/fast-tune.sh relevant sections =="
sed -n '1,190p' scripts/fast-tune.sh
echo
echo "== cp-argocd-server.yaml =="
if git ls-files | grep -qx 'cp-argocd-server.yaml'; then
sed -n '1,220p' cp-argocd-server.yaml
else
fd -a 'cp-argocd-server\.yaml' . -x sh -c 'echo "--- $1"; sed -n "1,220p" "$1"' sh {}
fi
echo
echo "== argocd-server profile references =="
rg -n 'argocd-server|user-defined-profile|containerprofiles|ContainerProfile|PROFILE_MATCH|learned-profile' . -S | head -200Repository: k8sstormcenter/bob
Length of output: 33954
🌐 Web query:
kubescape node-agent user-defined-profile containerprofile name binding
💡 Result:
In Kubescape's architecture, user-defined profiles are bound to workloads (specifically pods) using metadata labels [1][2]. This binding mechanism allows the node-agent to associate specific ApplicationProfile and NetworkNeighborhood resources—collectively representing a Software Bill of Behavior (SBOB)—with the containers running in a pod [3][2][4]. To bind a user-defined profile, you apply the relevant CRD (ApplicationProfile or NetworkNeighborhood) to the cluster and then add the corresponding labels to the pod template in your workload definition [1][2]. The required labels are: kubescape.io/user-defined-profile: kubescape.io/user-defined-network: The node-agent monitors these labels during pod creation [1][4]. When a pod starts, the agent identifies these labels, fetches the referenced user-defined profiles, and applies them as an overlay to the workload's behavior profile [5][6]. Key points regarding this binding: 1. Lifecycle: Binding is currently "late-binding" at the pod level, meaning the node-agent picks up the labels and binds the profile only at pod startup [1][4]. If you change the profile or the label, you must restart the pod for the changes to take effect [1][4]. 2. Evolution to ContainerProfile: While the system has historically relied on separate ApplicationProfile and NetworkNeighborhood CRDs, Kubescape is in the process of consolidating these into a unified ContainerProfile CRD [3][2]. Functionality remains consistent during this migration, though internal implementation details in the node-agent have been updated to support consolidated, server-side merged profiles alongside the legacy client-side user-defined overlays [5][6]. 3. Verification: User-defined profiles can be cryptographically signed [1][6]. The node-agent performs signature verification at bind time to prevent tampering, and signed overlays are subject to tamper detection during runtime [1][6]. If a signed profile is modified without a valid signature update, the node-agent will detect the discrepancy [1][6]. While ContainerProfile is the newer standard for representing workload behavior, the user-defined binding mechanism primarily remains label-driven as of the current architecture [5][6][2].
Citations:
- 1: https://kubescape.io/docs/operator/bill-of-behavior/signing-and-tamper/
- 2: https://kubescape.io/docs/operator/bill-of-behavior/quickstart/
- 3: https://kubescape.io/docs/operator/bill-of-behavior/
- 4: https://github.com/k8sstormcenter/bob/blob/main/_artifacts/log4j-sbobs/README.md
- 5: Remove client-side ug- profile merge once storage server-side merge (storage#319) is deployed kubescape/node-agent#830
- 6: kubescape/node-agent@e643967
🌐 Web query:
site:github.com/kubescape/node-agent "kubescape.io/user-defined-profile" ContainerProfile
💡 Result:
In the context of the Kubescape node-agent, a ContainerProfile is a core object used to maintain the learned runtime behavior of a container [1]. While there is no single entity named "user-defined ContainerProfile" in the public documentation, the system supports user-defined overlays and custom profiles through a mechanism often referred to as user-managed or user-defined profiles [2][3][4]. Key technical aspects include: 1. User-Managed Overlays: The node-agent allows for the application of user-defined overlays, specifically targeting Application Profiles (AP) and Network Neighborhoods (NN) [2][4]. These are often identified by labels (e.g., via the UserDefinedProfileMetadataKey) and are projected into the container's runtime profile cache [2][3]. 2. Merging and Projection: User-defined overrides are merged with the base, automatically learned ContainerProfile [2]. Recent architecture changes have moved much of this merging logic to the server side (Kubescape storage) to ensure consistency, though some client-side overlay projections remain active [2]. 3. Integrity and Tamper Detection: User-managed profiles can be cryptographically signed (using tools like Cosign) [4]. The node-agent includes mechanisms to verify these signatures upon loading them into the cache. If a signed profile is found to be tampered with, the system is designed to emit an R1016 "Signed profile tampered" alert [3][4]. 4. Implementation: These profiles are managed via the NodeAgent's object cache system, which utilizes a projection mechanism to combine learned base profiles with user-provided specifications [5]. Users typically interact with these through Kubernetes custom resources or specific metadata labels attached to their pods [2][3]. For further configuration and details, please refer to the official Kubescape node-agent documentation and the project repository on GitHub [1][6][7].
Citations:
- 1: https://www.github.com/kubescape/node-agent
- 2: Remove client-side ug- profile merge once storage server-side merge (storage#319) is deployed kubescape/node-agent#830
- 3: profile-compaction: CollapseConfig CRD + projection overlay + user-ma… kubescape/node-agent#808
- 4: profile-compaction: CollapseConfig CRD + projection overlay + signing/tamper detection kubescape/node-agent#847
- 5: implement Rule-Aware Profile Projection kubescape/node-agent#799
- 6: https://github.com/kubescape/node-agent/blob/main/README.md
- 7: https://github.com/kubescape/node-agent
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Locate argocd manifests =="
git ls-files | rg '(^|/)argocd' | head -100
echo
echo "== argocd workloads and user-defined profile labels =="
rg -n 'kind: (Deployment|Pod|ServiceAccount|ConfigMap|Role|ClusterRole|RoleBinding)|metadata:|name: argocd-server|kubescape\.io/user-defined-profile|replicaset-hash|replicaset|chart' example/argocd example/argocd-vulnerable.yaml kubescape .github -S | head -200
echo
echo "== Search profile name patterns in argocd manifests =="
rg -n 'metadata:\n( name:| namespace:)|name: argocd-server|argocd-server|kubescape\.io/user-defined-profile' example/argocd example/argocd-vulnerable.yaml kubescape -S
echo
echo "== Read argocd setup/manifest files =="
for f in $(git ls-files | rg 'example/argocd|example/argocd-vulnerable.yaml'); do
echo "--- $f"
sed -n '1,220p' "$f"
doneRepository: k8sstormcenter/bob
Length of output: 13734
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== example/argocd/argocd.manifest.yaml =="
sed -n '1,120p' example/argocd/argocd.manifest.yaml
echo
echo "== example/argocd/argocd.yaml =="
sed -n '1,80p' example/argocd/argocd.yaml
echo
echo "== example/argocd/sbobs/cp-argocd-server.yaml first 30 lines =="
sed -n '1,30p' example/argocd/sbobs/cp-argocd-server.yaml
echo
echo "== Find exact user-defined-profile and replicaset references =="
rg -n -U 'kubescape\.io/user-defined-profile|profile_match:\s*replicaset-argocd-server|replicaset-argocd-server' example/argocd example/argocd-vulnerable.yaml kubescape -S || true
echo
echo "== Parse argocd manifests yaml-ish for ContainerProfile names/labels =="
python3 - <<'PY'
from pathlib import Path
for p in Path("example/argocd").rglob("*.yaml"):
text = p.read_text()
for i,line in enumerate(text.splitlines(),1):
if any(s in line for s in ("profile_match", "user-defined-profile", "replicaset-argocd-server", "name: argocd-server")):
print(f"{p}:{i}:{line}")
PYRepository: k8sstormcenter/bob
Length of output: 6442
🌐 Web query:
kubescape argocd-server replicaset ContainerProfile profile_match argocd-server-.*-argocd-server
💡 Result:
The term profile_match is not a standard configuration field in Kubescape’s official core API but is typically used in user-defined scripts (such as custom automation or tuning scripts) to identify specific application profiles for container security analysis [1]. In the context of Kubescape and Kubernetes, ContainerProfile objects are automatically generated resources that capture the runtime behavior of workloads [2][3]. When scanning or tuning security policies for a workload like argocd-server, users often need to map a specific, learned ContainerProfile to their deployment [1]. Key technical insights regarding your query: 1. ContainerProfile Lookup: Kubescape maps ContainerProfile objects to workloads using a slug-based naming convention [2]. A profile_match string, such as argocd-server-.*-argocd-server, acts as a regular expression or pattern to select the correct ApplicationProfile or ContainerProfile from the cluster for analysis or tuning [1]. 2. ArgoCD Context: The argocd-server deployment is a standard Kubernetes workload [4]. If you are seeing profile_match errors or configurations, it indicates that a script is attempting to find a previously learned security profile for the ArgoCD server pod, likely to compare its current runtime behavior against a known-good baseline or to auto-generate security policies [1]. 3. Troubleshooting: If you are encountering issues where a profile is not being matched: - Ensure the Kubescape operator is correctly installed and configured to monitor the namespace where ArgoCD is running (typically argocd) [5]. - Verify that the ContainerProfile has actually been generated for the target workload by checking the applicationprofiles.spdx.softwarecomposition.kubescape.io custom resources in the relevant namespace [1]. - The regex argocd-server-.*-argocd-server suggests the script expects a naming structure that includes the deployment and potentially a replicaset or unique pod identifier suffix [1]. If the naming convention in your cluster differs (e.g., due to specific Helm chart configurations), the regex may fail to match [1][6]. For authoritative guidance, please refer to the official Kubescape documentation regarding Runtime Security and the ContainerProfile resource, as manual script tuning often relies on internal naming conventions that may change between versions [3].
Citations:
- 1: https://github.com/k8sstormcenter/bob/blob/main/scripts/fast-tune.sh
- 2: fix: look up ContainerProfile by with-container slug, not workload slug kubescape/operator#396
- 3: feat: add container profile tools and resources to MCP server kubescape/kubescape#2479
- 4: https://1337skills.com/es/cheatsheets/argocd/
- 5: https://github.com/kubescape/helm-charts/blob/main/ArgoCDApplication.yaml
- 6: https://github.com/argoproj/argo-helm/blob/main/charts/argo-cd/templates/argocd-server/deployment.yaml
Set PROFILE_MATCH to the hand-authored argocd-server profile name
cp-argocd-server.yaml is named argocd-server and bound by kubescape.io/user-defined-profile: argocd-server, so the hash-based learned-profile regex is misleading for this leg. Use a stable match for the hand-authored profile, e.g. argocd-server, unless this tooling intentionally matches an autosync generated profile.
🧰 Tools
🪛 ast-grep (0.45.0)
[warning] 64-64: A credential-bearing variable (e.g. PASSWORD, PASSWD, SECRET, TOKEN, API_KEY) is assigned a hardcoded string literal. Secrets committed to a script are exposed in source control, process listings, and shell history, and cannot be rotated without a code change. Read the value from a secrets manager or an injected environment variable at runtime instead (e.g. PASSWORD="${DB_PASSWORD:?must be set}"), and never commit the literal.
Context: NEEDS_ARGOCD_TOKEN=true
Note: [CWE-798] Use of Hard-coded Credentials.
(hardcoded-password-assignment-bash)
🪛 Shellcheck (0.11.0)
[warning] 63-63: Brace expansions and globs are literal in assignments. Quote it or use an array.
(SC2125)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/fast-tune.sh` around lines 59 - 66, Update the PROFILE_MATCH
assignment in the argocd-server case to match the hand-authored profile name
argocd-server rather than the hash-based learned-profile regex. Keep the
surrounding case configuration unchanged.
…path The previous commit put global.overrideRuntimePath and a hostPath mount of /mnt/dev-data straight into kubescape/values.yaml. That path only exists on the dev laptop (k3s --data-dir /mnt/dev-data/k3s), and the volume declares type: Directory, so on a CI runner node-agent could not schedule at all and "Install kubescape" failed for every app leg. Detect the runc actually in use from the live containerd shim instead. On a stock layout — CI runners, ordinary k3s, plain docker — KS_RUNC_FLAGS is EMPTY and nothing is overridden, so behaviour is byte-identical to before. Only a non-standard data-dir sets RUNTIME_PATH, and only then is the filesystem holding it hostPath-mounted (node-agent's own `host` volume is a non-recursive bind of "/", so it does not carry a separate partition). `make show-runc` prints what was detected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
…er SBoB Adds example/argocd/argocd-killchain.gif, rendered by the same design-1 kill-chain heatmap that produces example/redis-client/redis-killchain.gif. Also corrects the opens wildcards. Per storage's dynamicpathdetector, "*" matches ZERO-OR-MORE whole path segments (glob-style **) and is special only as a complete segment; "⋯" matches exactly one. So "/proc/⋯/*" forced "at least two segments below /proc" rather than the intended "anything under /proc" — the ⋯ adds nothing a trailing * does not already cover. Replaced with /proc/* and /sys/*. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
Every leg was dogfooded against the live cluster and every remaining miss
was chased to its cause rather than asserted away.
Real bugs fixed:
- argocd-repo-server named its container "repo-server" in three legacy
attacks; it is "argocd-repo-server". Those three had looked like a
ledger attribution bug for hours — the new diagnostic in the inner repo
identified it on first run. Leg goes 4 misses -> 0.
- R1004 needs the payload on a REAL volume mount. /tmp is not a mount in
the controllers and an exec from /dev/shm scores as R1000 here, so
application-controller now execs from /home/argocd.
- dex has no reachable benign surface (Argo CD only runs it with SSO
configured, nothing dials it, it does not listen on localhost), so its
leg is attack-only and FUNCTESTS is empty.
Honest boundaries, each verified by hand and commented in place:
- argocd-redis R0001: the learned baseline is /bin/busybox and every
coreutil IS busybox, so the exec path is already allowed and R0001
cannot fire. The read is still caught by R0010.
- argocd-redis R1004: a successful exec from the /data mount fires R0001
only (checked with a valid applet name so the exec really succeeds).
- argocd-notifications-controller R1004: its only writable mount is
/dev/shm, which scores as R1000 on this build.
Coverage unchanged at 13 rules across the legs — the same set redis
asserts. Contrast gates report 0 Blind on every leg.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
example/argocd-repo-server-attacks.yaml (3)
51-56: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winReplace the empty success indicator.
responseContains: ""matches every response, including a failedlncommand, so this leg cannot prove that the symlink was created. Emit a marker after successful creation and clean up the probe path.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@example/argocd-repo-server-attacks.yaml` around lines 51 - 56, Update the cve-2022-31036-symlink-sensitive command so it emits a unique success marker only after the symlink is created successfully, and remove the probe path afterward. Replace the empty responseContains value with that marker to ensure the success indicator verifies the complete command sequence.
83-93: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winMake the no-token boundary satisfiable.
The file states that no ServiceAccount token is mounted, but this command only emits
token_readwhen the read succeeds. In the declared environment it produces no success marker, so the suite cannot validate the intended no-detection boundary. Add a fallback marker such astoken_absent, or explicitly mark this leg unreachable.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@example/argocd-repo-server-attacks.yaml` around lines 83 - 93, Update the repo-server-sa-token-read command so it emits a success marker when the ServiceAccount token is absent, such as token_absent, while retaining token_read for successful reads. Add the corresponding success indicator so the no-token boundary is satisfiable.
32-46: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftThese CVE-labelled legs do not exercise the claimed render/API paths.
head /etc/passwd,ln -sf /etc/shadow ...,whoami, and the credentialechoare direct command-injection probes. They do not drive repo-server path traversal, Git symlink rendering, config-management-plugin execution, or the repo-credential API error path. Either invoke the actual vulnerable render/API flows with fixtures, or rename these as generic post-exploitation behavior probes and document that CVE coverage is not being tested.Also applies to: 48-81, 95-104
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@example/argocd-repo-server-attacks.yaml` around lines 32 - 46, The CVE-labelled attack legs use direct command probes rather than exercising the claimed repo-server render or API vulnerabilities. Update the scenarios around cve-2022-24348-render-path-traversal and the referenced legs to invoke the actual vulnerable render/API flows with appropriate fixtures; alternatively, rename them as generic post-exploitation probes and document that they do not provide CVE coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@example/argocd-notifications-controller-attacks.yaml`:
- Around line 77-84: Update the exec-from-volume-mount command in the cmdinject
scenario to copy, execute, and remove the payload under /dev/shm/mnt_payload
instead of /home/argocd/mnt_payload. Change expectedDetections to assert the
documented R1000 event, while keeping R1004 unasserted and preserving the
existing success indicator.
In `@example/argocd-redis-attacks.yaml`:
- Around line 71-78: The exec-from-volume-mount test currently documents an
expected R0001 detection but declares none. Update expectedDetections for
exec-from-volume-mount to assert the observed R0001 event, including the command
attribution required by the matcher; alternatively remove the successful
execution, while preserving the documented boundary behavior.
In `@Makefile`:
- Line 306: Update the Kubescape install command in the Makefile target to
invoke the configured $(HELM) variable instead of the hardcoded helm binary,
preserving all existing arguments and flags.
---
Outside diff comments:
In `@example/argocd-repo-server-attacks.yaml`:
- Around line 51-56: Update the cve-2022-31036-symlink-sensitive command so it
emits a unique success marker only after the symlink is created successfully,
and remove the probe path afterward. Replace the empty responseContains value
with that marker to ensure the success indicator verifies the complete command
sequence.
- Around line 83-93: Update the repo-server-sa-token-read command so it emits a
success marker when the ServiceAccount token is absent, such as token_absent,
while retaining token_read for successful reads. Add the corresponding success
indicator so the no-token boundary is satisfiable.
- Around line 32-46: The CVE-labelled attack legs use direct command probes
rather than exercising the claimed repo-server render or API vulnerabilities.
Update the scenarios around cve-2022-24348-render-path-traversal and the
referenced legs to invoke the actual vulnerable render/API flows with
appropriate fixtures; alternatively, rename them as generic post-exploitation
probes and document that they do not provide CVE coverage.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 5bdfb721-3be7-4287-aff6-fbc1e24a0ec0
⛔ Files ignored due to path filters (1)
example/argocd/argocd-killchain.gifis excluded by!**/*.gif
📒 Files selected for processing (8)
Makefileexample/argocd-application-controller-attacks.yamlexample/argocd-notifications-controller-attacks.yamlexample/argocd-redis-attacks.yamlexample/argocd-repo-server-attacks.yamlexample/argocd/sbobs/cp-argocd-server.yamlpkgscripts/fast-tune.sh
| - name: exec-from-volume-mount | ||
| type: cmdinject | ||
| exec: { command: ["sh", "-c", "cp /bin/echo /home/argocd/mnt_payload && chmod +x /home/argocd/mnt_payload && /home/argocd/mnt_payload shm_exec_done; rm -f /home/argocd/mnt_payload"] } | ||
| successIndicators: [{ responseContains: "shm_exec_done" }] | ||
| # This container's only writable mount is /dev/shm (everything else is a | ||
| # read-only TLS projection), and an exec from /dev/shm scores as R1000 here, | ||
| # not R1004. No reachable path to R1004 — honest boundary. | ||
| expectedDetections: [] |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Align the command with the documented writable mount.
Line 79 writes to /home/argocd, while Lines 81-83 state that only /dev/shm is writable. With the declared filesystem boundary, the copy cannot reach shm_exec_done, so this leg does not exercise the intended mount boundary. Use /dev/shm/mnt_payload and update expectedDetections to include the documented R1000 event while leaving R1004 unasserted.
Proposed path correction
- exec: { command: ["sh", "-c", "cp /bin/echo /home/argocd/mnt_payload && chmod +x /home/argocd/mnt_payload && /home/argocd/mnt_payload shm_exec_done; rm -f /home/argocd/mnt_payload"] }
+ exec: { command: ["sh", "-c", "cp /bin/echo /dev/shm/mnt_payload && chmod +x /dev/shm/mnt_payload && /dev/shm/mnt_payload shm_exec_done; rm -f /dev/shm/mnt_payload"] }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@example/argocd-notifications-controller-attacks.yaml` around lines 77 - 84,
Update the exec-from-volume-mount command in the cmdinject scenario to copy,
execute, and remove the payload under /dev/shm/mnt_payload instead of
/home/argocd/mnt_payload. Change expectedDetections to assert the documented
R1000 event, while keeping R1004 unasserted and preserving the existing success
indicator.
| - name: exec-from-volume-mount | ||
| type: cmdinject | ||
| exec: { command: ["sh", "-c", "cp /bin/busybox /data/mnt_payload && chmod +x /data/mnt_payload && /data/mnt_payload true && echo shm_exec_done; rm -f /data/mnt_payload"] } | ||
| successIndicators: [{ responseContains: "shm_exec_done" }] | ||
| # Verified unreachable on this container: a successful exec of a binary copied | ||
| # onto the /data volume mount fires R0001 only, never R1004 (checked with a | ||
| # valid busybox applet name so the exec genuinely succeeds). Honest boundary. | ||
| expectedDetections: [] |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not discard the documented R0001 detection.
The comment states that successful execution from /data fires R0001, but expectedDetections: [] declares no detection. This boundary leg will therefore fail or classify the observed process alert as unexpected. Assert the observed R0001 event, including command attribution if required by the matcher, or remove the successful execution.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@example/argocd-redis-attacks.yaml` around lines 71 - 78, The
exec-from-volume-mount test currently documents an expected R0001 detection but
declares none. Update expectedDetections for exec-from-volume-mount to assert
the observed R0001 event, including the command attribution required by the
matcher; alternatively remove the successful execution, while preserving the
documented boundary behavior.
| helm repo add kubescape https://kubescape.github.io/helm-charts/ | ||
| helm repo update | ||
| helm upgrade --install kubescape kubescape/kubescape-operator --version $(KUBESCAPE_CHART_VER) -n honey --create-namespace --values kubescape/values.yaml | ||
| helm upgrade --install kubescape kubescape/kubescape-operator --version $(KUBESCAPE_CHART_VER) -n honey --create-namespace --values kubescape/values.yaml $(KS_RUNC_FLAGS) --post-renderer $(KS_POST_RENDERER) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Honor $(HELM) for the Kubescape install.
This primary install path bypasses the configured Helm binary/wrapper, unlike the related targets.
Proposed fix
- helm upgrade --install kubescape kubescape/kubescape-operator --version $(KUBESCAPE_CHART_VER) -n honey --create-namespace --values kubescape/values.yaml $(KS_RUNC_FLAGS) --post-renderer $(KS_POST_RENDERER)
+ $(HELM) upgrade --install kubescape kubescape/kubescape-operator --version $(KUBESCAPE_CHART_VER) -n honey --create-namespace --values kubescape/values.yaml $(KS_RUNC_FLAGS) --post-renderer $(KS_POST_RENDERER)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| helm upgrade --install kubescape kubescape/kubescape-operator --version $(KUBESCAPE_CHART_VER) -n honey --create-namespace --values kubescape/values.yaml $(KS_RUNC_FLAGS) --post-renderer $(KS_POST_RENDERER) | |
| $(HELM) upgrade --install kubescape kubescape/kubescape-operator --version $(KUBESCAPE_CHART_VER) -n honey --create-namespace --values kubescape/values.yaml $(KS_RUNC_FLAGS) --post-renderer $(KS_POST_RENDERER) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Makefile` at line 306, Update the Kubescape install command in the Makefile
target to invoke the configured $(HELM) variable instead of the hardcoded helm
binary, preserving all existing arguments and flags.
…/tmp staging The GIF. A single Argo CD subcomponent tunes in two or three iterations — its profile is already minimal and the over-broad guard blocks the degenerate collapse — so a per-iteration GIF is two or three frames. The story worth showing for Argo CD is that the SAME contrast method lands on all seven containers of one product, so each frame is a COMPONENT: its tuned profile size, its kill-chain coverage, its score. All seven read PERFECT, 84 expected detections in total. scripts/merge-leg-metrics.py builds the combined metrics from the per-leg runs. Renderer fix (scripts/render-metrics-gif.py, the maintained one): savefig(bbox_inches="tight") trims each frame to its own content, so a frame whose kill-chain panel carries different labels came out a different pixel size. A GIF has one canvas, so mismatched frames jitter and the widest frame's edges bleed over the narrower ones. Frames are now padded onto a common canvas. Reproduced and verified on the 7-frame Argo CD metrics. Suite fix: /tmp is READ-ONLY in argocd-application-controller, argocd-notifications-controller and argocd-redis, so the drift attack could never stage its binary — `cp` failed outright, and the leg was only "passing" because a different attack emitted an alert that satisfied its comm-unpinned expectation. All three now stage in /dev/shm, which is writable in all seven containers. Each still tunes to 0, now for the right reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
The redis kill-chain GIF was a one-off with no generator in the repo, so it
could not be reproduced or made for any other app. This adds the generator and
a config, and renders a card for every SBoB.
scripts/render-rule-coverage-gif.py forces every rule in the cluster ruleset
into exactly one state per app:
green verified asserted by an attack AND observed firing
amber probe exercised but not assertable here — kernel-gated, EPERM,
baseline-suppressed, not emitted by this node-agent build
grey excluded deliberately out of scope for this app
red gap in the ruleset, nothing in the suite covers it
That four-way split is the point. A rule that CANNOT fire is not the same as a
rule nobody tried to cover, and a blank tile hides the difference. The red
tiles are the backlog, stated rather than omitted.
With `metrics_glob` pointing at tuner output, only rules the tune actually SAW
firing earn "verified" — an asserted-but-unseen rule drops to gap, so the card
cannot overstate coverage. Probes and exclusions are declared in
kubescape/rule-coverage.yaml with their reason, because a probe attack carries
`expectedDetections: []` and names no rule to derive from.
make rule-coverage-gifs # every app
make rule-coverage-gifs APP=argocd # one app
redis regenerates to its committed path and reproduces the original: 14 rules
fire, 8 kernel/gated probes, same amber and grey sets. The two differences are
the current ruleset carrying R1016/R1031 (the original was made against an
older 29-rule set with R2001), and R1000 now showing as a gap because nothing
asserts it and there is no evidence it fires here.
argocd-killchain.gif switches to this format — 14 verified, 2 probe, 6
excluded, 8 gap across the seven legs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
Argo CD is one contrast leg per container (skill §8), so this adds an attack suite and functional tests for each of the seven:
server,repo-server,application-controller,applicationset-controller,notifications-controller,dex,redis.Depends on entlein/bob#58 (three tuner fixes without which none of this runs).
Coverage
Union across the legs is 13 rules — the same set redis asserts:
R0001 R0005 R0006 R0007 R0008 R0010 R0011 R1000 R1004 R1005 R1008 R1010 R1012argocd-serveralone lands 12 live and tunes to score=0, 16/16 expected detections matched, 0 benign FPs, contrast gate25 Separable / 4 Ambiguous / 0 Blindwith--type web-app --expect reads-host-files,reads-secrets.Rules placed where they actually discriminate
This is the point of splitting by container rather than writing one suite:
argocd-redis— the only subcomponent with the SA token mounted that never reads it. On every controller the token read is the baseline, so the rules are structurally suppressed;repo-serverhas no token mounted at all.bpf(2)returns EPERM in an unprivileged container, so no program ever loads.Each of those carries
expectedDetections: []plus the reason, per "honest boundary, never fake a detection".Infrastructure
kubescape/values.yaml— point node-agent's fanotify hook at the runc this host actually uses (k3s runs with--data-dir /mnt/dev-data/k3s) and mount that partition. Without it node-agent sees no container started after its own boot, so nothing is ever learned — it just looks healthy. This is what the oldrollout restart ds/node-agentwas hiding.Makefile/local-ci.sh— node-agent is never restarted, only waited on. Network streaming is forced at render time by a Helm post-renderer instead of patch-then-bounce.fast-tune.sh— fix--kubescape-namespace→--ks-namespace(this was broken for every app, not just argocd), add the seven argocd legs, and mint the Argo session token through the K8s API proxy.Status
argocd-serveris green.argocd-repo-servercurrently scores 4 on a ledger attribution bug tracked in entlein/bob#58 — the alerts fire and are present in alertmanager, they are just not credited. The remaining five legs are generated and validated (TestAttackSuite_AssertionRealismpasses) but not yet individually tuned.🤖 Generated with Claude Code
https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm