feat: full rule sweep for the four lagging apps + restore per-attack card info - #168
Conversation
…card info
Card regression, first. The reference redis GIF names, on every frame, the
ATTACK that lit the tile and the rule it fires ("cve-2022-24735-lua-poisoning /
fires: R0001"). The generator collected only rule ids and replaced that footer
with a static suite label, throwing the provenance away. It now records which
attack asserts each rule and shows attack name + type + `fires: Rxxxx` per
frame, as the original did.
Suites. webapp, postgres, postgres-vuln and mariadb asserted 5/4/6/2 rules
against redis's 13. Each now runs the same rule-family sweep, generated by
scripts/emit-rule-sweep.py and dogfooded on the live containers:
webapp 6 -> 14 verified tune score 0
postgres 5 -> 13 tune score 0
postgres-vuln 7 -> 14
mariadb 3 -> 13 tune score 0
The primitives are NOT portable and had to be probed per image — which is why
the emitter takes them as arguments rather than guessing:
* /tmp is READ-ONLY in several images; /dev/shm is writable in all of them
* R1004 only fires from certain volume mounts. Verified unreachable on
mariadb (/var/lib/mysql), postgres and postgres-vuln (/var/lib/postgresql/
data not writable from the client) — a successful exec there fires
R0001+R1000 and never R1004. Recorded as a probe with the evidence.
* webapp has /bin/bash AND /bin/dash in its learned baseline, so `sh -c` is
never anomalous; every assertion pins the child binary instead.
Two pre-existing dead assertions found by dogfooding:
* webapp `cmdinject-reverse-shell` asserted R0001 on bash, which is in the
baseline — and bash's /dev/tcp is a builtin, so no process spawns at all.
* postgres-vuln `exec-sa-token`, `exec-etc-passwd` and `exec-net-recon` had
NO successIndicators, so the runner never marked them successful and the
ledger refused to credit their detections. They had been dead the whole
time. Anchored each on its real output.
Also: "replicaset-mariadb" substring-matched BOTH the server profile and
replicaset-mariadb-client-<hash>, so which profile got tuned depended on API
listing order. Pinned to the client, which is what every assertion targets.
fast-tune.sh had no mariadb case at all despite advertising one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
📝 WalkthroughWalkthroughThe change adds three Argo CD Kubescape container profiles, expands rule-family attack probes across four application suites, adds a probe generator, updates MariaDB profile selection, and enriches rule-coverage metrics and GIF rendering with attack provenance. ChangesArgo CD container profiles
Rule-family coverage and tooling
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant emit-rule-sweep.py
participant AttackSuite
participant Kubescape
participant render-rule-coverage-gif.py
emit-rule-sweep.py->>AttackSuite: Generate rule-family probes
AttackSuite->>Kubescape: Execute probes
Kubescape-->>render-rule-coverage-gif.py: Return verified detections
render-rule-coverage-gif.py->>render-rule-coverage-gif.py: Associate rules with asserting attacks
render-rule-coverage-gif.py-->>render-rule-coverage-gif.py: Render attack-specific captions
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 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: 26
🤖 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/sbobs/cp-argocd-application-controller.yaml`:
- Around line 53-72: Add an internal TCP/8081 egress entry targeting pods
labeled app.kubernetes.io/name: argocd-repo-server in the egress rules of
cp-argocd-application-controller.yaml (lines 53-72) and
cp-argocd-applicationset-controller.yaml (lines 51-57). In
cp-argocd-applicationset-controller.yaml, also add UDP/53 egress targeting pods
labeled k8s-app: kube-dns, matching the existing cluster-dns rule.
In `@example/mariadb-attacks.yaml`:
- Around line 429-434: Update the fileless-memfd-exec Perl command to avoid
hardcoding the x86_64-only syscall number 319; select the correct memfd_create
syscall for the runtime architecture, including arm64, while preserving the
existing fileless execution and memfd_exec_done success behavior.
- Around line 392-397: Update the k8s-api-unexpected-call probe command in the
k8s-api-unexpected-call attack to resolve the Kubernetes API endpoint from the
injected KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT environment
variables instead of hardcoding 10.43.0.1 and port 443. Preserve the existing
connection and k8s_api_probe_done success indicator so R0007 remains validated
across cluster configurations.
- Around line 386-391: Update the sa-token-read probe’s successIndicators to
accept the sa_token_absent response as an additional successful variant, while
preserving the existing sa_token_read_done indicator and expected detection for
the attempted cat command.
- Around line 441-446: Update the hardlink-shadow cmdinject probe to create its
temporary link under /tmp instead of /dev/shm, matching the same-filesystem
target used by the webapp equivalent. Keep the existing success indicator,
cleanup command, and expected detection metadata unchanged.
- Around line 404-408: Rename the later attack entry currently named
exec-etc-shadow to a unique name such as exec-etc-shadow-r0010, while leaving
its command and expectedDetections unchanged.
In `@example/postgres-attacks.yaml`:
- Around line 447-452: Update the k8s-api-unexpected-call attack command to
resolve the Kubernetes API host from the KUBERNETES_SERVICE_HOST environment
variable instead of hardcoding 10.43.0.1, while preserving the existing probe
behavior and success indicator.
- Around line 478-483: Update the fileless-memfd-exec command’s memfd_create
invocation to avoid the hardcoded x86_64 syscall number 319, using the portable
syscall mechanism or architecture-aware value established by the corresponding
MariaDB attack example. Preserve the existing payload behavior and
successIndicators.
- Around line 453-457: Rename the newly added attack entry identified by name
exec-etc-shadow in the postgres attack definitions to a unique name, preserving
its cmdinject command and R0010 expected detection unchanged.
- Around line 441-446: The sa-token-read probe currently reports failure when
the service-account token mount is absent. Update the sa-token-read
successIndicators to accept both token-read and token-absent outcomes, or remove
this duplicate probe while preserving the existing
exec-sa-token-attempt/exec-sa-token-mounted handling.
- Around line 470-477: Update the exec-from-volume-mount probe while preserving
its documented unreachable-boundary behavior: avoid requiring the
mount_exec_done success indicator when expectedDetections is empty, or change
the command to use a path writable by the PostgreSQL client. Keep the
expectedDetections: [] declaration and adjacent boundary documentation
consistent with the selected behavior.
- Around line 491-495: Update the cmdinject hardlink probe in the postgres
attack configuration so the source file is first staged under /dev/shm, then
link that staged file to hl_probe within the same filesystem. Preserve the
hardlink_done success indicator, cleanup behavior, and R1012 detection
expectations.
In `@example/postgres-vuln-attacks.yaml`:
- Around line 859-864: Update the k8s-api-unexpected-call attack’s command to
avoid hardcoding the Kubernetes API address 10.43.0.1; resolve or obtain the
cluster API endpoint dynamically while preserving the existing probe behavior
and success indicator.
- Around line 884-889: Update the fileless-memfd-exec command in the fileless
attack definition to avoid hardcoding the x86_64-specific syscall number 319;
use the repository’s established portable memfd_create mechanism or
architecture-aware syscall lookup while preserving the existing execution and
success-indicator behavior.
- Around line 865-869: Rename the later exec-etc-shadow attack entry for the cat
/etc/shadow command to a unique attack name, preserving the earlier
exec-etc-shadow definition and the existing R0010 expected detection.
- Around line 876-883: Remove the impossible mount_exec_done success indicator
from the exec-from-volume-mount probe in postgres-vuln-attacks.yaml, and apply
the same correction to the duplicate probe in postgres-attacks.yaml. Keep
expectedDetections empty and ensure the probe cannot report success for the
documented non-writable volume path.
- Around line 896-901: Update the hardlink-shadow probe’s command to create the
link target on the same filesystem as /etc/shadow, preserving the hardlink_done
success indicator and existing R1012 detection expectation; do not use /dev/shm
as the target location.
- Around line 853-858: Update the sa-token-read probe’s successIndicators so it
succeeds when the service-account token mount is absent, while preserving
detection of the token access rule. Prefer an empty indicator for this
CI-unreliable command, or add sa_token_absent with the corresponding reason and
expectedDetections entry.
In `@example/webapp-attacks.yaml`:
- Around line 596-601: Update the k8s-api-unexpected-call attack’s Perl command
to resolve the Kubernetes API host from the KUBERNETES_SERVICE_HOST environment
variable instead of hardcoding 10.43.0.1, while preserving the existing port,
probe output, and detection metadata.
- Around line 620-625: Replace the hardcoded syscall number in the
fileless-memfd-exec test command with the portable architecture-aware mechanism
used by the other suites, while preserving the existing memfd execution flow and
success indicator.
In `@pkg`:
- Line 1: Make the updated pkg submodule commit
c02adb840ebcfc5349b7440a72cda4325897a74e available through its configured remote
git@github.com:entlein/bob.git by pushing it or exposing it via a reachable
tag/branch, then verify a fresh submodule checkout can retrieve that commit
before merging.
In `@scripts/emit-rule-sweep.py`:
- Around line 145-146: Validate the parsed rule selection immediately after
constructing want in the rule-sweep argument flow, and fail when want is empty,
including inputs such as --rules=,. Preserve the existing unknown-rule
validation and normal processing for non-empty selections.
- Line 136: Update the argument handling around the mount option and R1004 rule
selection so an R1004 assertion cannot use the known-invalid /dev/shm default:
require --mount when R1004 is selected, or validate and reject /dev/shm for that
rule while preserving existing behavior for other rules.
- Around line 30-43: Update scripts/emit-rule-sweep.py lines 30-43 so the R0005
and R1008 probes verify getent is available before emitting dns_attempted or
miner_dns_attempted, while preserving markers after genuine DNS attempts. Also
update scripts/emit-rule-sweep.py lines 115-128 to verify nc exists before
emitting its completion marker, retaining the marker after an actual connection
attempt.
In `@scripts/render-rule-coverage-gif.py`:
- Line 143: Reduce the statement count in draw() by extracting the
footer/caption rendering logic, including the lit_seq handling, into a focused
helper; keep draw() responsible for orchestration and preserve the existing
output.
- Around line 350-353: Replace the append-based loop constructing frames with a
list comprehension over range(n_verified + 1), preserving the existing draw
arguments and ordering.
🪄 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: 36ae6ff3-c4f6-4339-9e66-27acc79c78a6
⛔ Files ignored due to path filters (6)
example/argocd/argocd-killchain.gifis excluded by!**/*.gifexample/redis-client/redis-killchain.gifis excluded by!**/*.gifexample/rule-coverage/mariadb-killchain.gifis excluded by!**/*.gifexample/rule-coverage/postgres-killchain.gifis excluded by!**/*.gifexample/rule-coverage/postgres-vuln-killchain.gifis excluded by!**/*.gifexample/rule-coverage/webapp-killchain.gifis excluded by!**/*.gif
📒 Files selected for processing (13)
example/argocd/sbobs/cp-argocd-application-controller.yamlexample/argocd/sbobs/cp-argocd-applicationset-controller.yamlexample/argocd/sbobs/cp-argocd-repo-server.yamlexample/mariadb-attacks.yamlexample/postgres-attacks.yamlexample/postgres-vuln-attacks.yamlexample/webapp-attacks.yamlkubescape/rule-coverage.yamlpkgscripts/emit-rule-sweep.pyscripts/fast-tune.shscripts/local-ci.shscripts/render-rule-coverage-gif.py
| egress: | ||
| - identifier: kube-apiserver | ||
| type: internal | ||
| ipAddress: 10.43.0.1 | ||
| ports: | ||
| - {name: TCP-443, port: 443, protocol: TCP} | ||
| - identifier: argocd-redis | ||
| type: internal | ||
| podSelector: | ||
| matchLabels: | ||
| app.kubernetes.io/name: argocd-redis | ||
| ports: | ||
| - {name: TCP-6379, port: 6379, protocol: TCP} | ||
| - identifier: cluster-dns | ||
| type: internal | ||
| podSelector: | ||
| matchLabels: | ||
| k8s-app: kube-dns | ||
| ports: | ||
| - {name: UDP-53, port: 53, protocol: UDP} |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate files =="
git ls-files 'example/argocd/sbobs/*argocd*controller*.yaml' | sort
echo
echo "== application-controller relevant section =="
cat -n example/argocd/sbobs/cp-argocd-application-controller.yaml | sed -n '1,120p'
echo
echo "== applicationset-controller relevant section =="
cat -n example/argocd/sbobs/cp-argocd-applicationset-controller.yaml | sed -n '1,120p'
echo
echo "== repo-server profile references =="
rg -n "argocd-repo-server|repo-server|8081|DNS|dns|kubectl|server-url|applicationset|application-controller|sbo|sbobs|egress" example -S || true
echo
echo "== argocd repo image/service defaults in repo (if any) =="
rg -n "argocd-repo-server|argo-cd" -S --glob '*.yaml' --glob '*.yml' --glob '*.json' . | head -200Repository: k8sstormcenter/bob
Length of output: 50374
🌐 Web query:
Argo CD application-controller connects to repo-server default host argocd-repo-server:8081 repo-server gRPC port 8081
💡 Result:
The Argo CD application-controller communicates with the argocd-repo-server via a gRPC API [1][2][3]. The default address for this connection is argocd-repo-server:8081 [4][5], where 8081 is the standard gRPC port used by the repository server [5][6][7]. Key details regarding this connection include: Communication Protocol: The application-controller and other components (such as argocd-server, argocd-applicationset-controller, and argocd-notifications-controller) use gRPC to request repository operations and manifest generation from the repo-server [2][3]. Default Settings: Address: argocd-repo-server:8081 [4][5] Port: 8081 [5][6][7] TLS: By default, the communication uses TLS, but since the repo-server generates a non-persistent, self-signed certificate on startup that cannot be verified by outside consumers, these components typically use a non-validating connection [1][2][8]. Configuration Options: The address can be overridden using the --repo-server flag [9][10]. TLS validation can be enforced by enabling the --repo-server-strict-tls parameter, which requires setting up a persistent, verifiable TLS certificate [1][2][8]. Alternatively, TLS can be disabled entirely for the connection using the --repo-server-plaintext flag [1][2][8]. The repo-server service is defined in the Argo CD manifests to expose this gRPC endpoint on port 8081 [6][7], alongside a metrics port on 8084 [6][7].
Citations:
- 1: argoproj/argo-cd@4edc1a9
- 2: https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/tls.md
- 3: https://www.youngju.dev/blog/argocd/argocd_architecture_internals.en
- 4: https://github.com/argoproj/argo-cd/blob/3eb51047/docs/operator-manual/argocd-cmd-params-cm.yaml
- 5: https://github.com/argoproj/argo-cd/blob/3eb51047/common/common.go
- 6: https://github.com/argoproj/argo-cd/blob/master/manifests/base/repo-server/argocd-repo-server-service.yaml
- 7: https://github.com/argoproj/argo-cd/blob/43ea991a/manifests/base/repo-server/argocd-repo-server-deployment.yaml
- 8: https://argo-cd.readthedocs.io/en/release-2.5/operator-manual/tls/
- 9: https://github.com/argoproj/argo-cd/blob/master/cmd/argocd-application-controller/commands/argocd_application_controller.go
- 10: https://github.com/argoproj/argo-cd/blob/3eb51047/cmd/argocd-application-controller/commands/argocd_application_controller.go
🌐 Web query:
Argo CD applicationset-controller default repo server address argocd-repo-server:8081
💡 Result:
The default repository server address for the Argo CD ApplicationSet controller is argocd-repo-server:8081 [1][2][3]. This address is defined by the constant DefaultRepoServerAddr within the Argo CD codebase [2] and serves as the default value for the --argocd-repo-server command-line flag used by the applicationset-controller [4][1][5]. Configuration Details: - Flag: --argocd-repo-server [1][3] - Environment Variable: ARGOCD_APPLICATIONSET_CONTROLLER_REPO_SERVER [4][5] - Default Value: argocd-repo-server:8081 [1][3] The ApplicationSet controller uses this address to communicate with the Argo CD repository server, which is an internal service responsible for maintaining a local cache of Git repositories and generating Kubernetes manifests [6]. In standard Kubernetes installations of Argo CD, the service argocd-repo-server is typically exposed on port 8081 for gRPC communication [2][7]. If you need to override this, you can provide a different address via the --argocd-repo-server flag or the corresponding environment variable [4][5].
Citations:
- 1: https://argo-cd.readthedocs.io/en/stable/operator-manual/server-commands/argocd-applicationset-controller/
- 2: https://github.com/argoproj/argo-cd/blob/43ea991a/common/common.go
- 3: https://github.com/argoproj/argo-cd/blob/bb33bb0d/docs/operator-manual/server-commands/argocd-applicationset-controller.md
- 4: https://github.com/argoproj/argo-cd/blob/master/cmd/argocd-applicationset-controller/commands/applicationset_controller.go
- 5: https://github.com/argoproj/argo-cd/blob/v2.14.21/cmd/argocd-applicationset-controller/commands/applicationset_controller.go
- 6: https://argo-cd.readthedocs.io/en/stable/operator-manual/server-commands/argocd-repo-server/
- 7: https://github.com/argoproj/applicationset/blob/ed59975a/docs/Development.md
Allow controller access to argocd-repo-server.
Apply the repo-server egress to both controllers so they can reach the Argo CD repo-server gRPC endpoint at argocd-repo-server:8081. The ApplicationSet profile also needs DNS egress because it resolves that in-cluster service name.
example/argocd/sbobs/cp-argocd-application-controller.yaml#L53-L72: add internal TCP/8081 egress selected byapp.kubernetes.io/name: argocd-repo-server.example/argocd/sbobs/cp-argocd-applicationset-controller.yaml#L51-L57: add the same repo-server route and UDP/53 egress tok8s-app: kube-dns.
📍 Affects 2 files
example/argocd/sbobs/cp-argocd-application-controller.yaml#L53-L72(this comment)example/argocd/sbobs/cp-argocd-applicationset-controller.yaml#L51-L57
🤖 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/sbobs/cp-argocd-application-controller.yaml` around lines 53 -
72, Add an internal TCP/8081 egress entry targeting pods labeled
app.kubernetes.io/name: argocd-repo-server in the egress rules of
cp-argocd-application-controller.yaml (lines 53-72) and
cp-argocd-applicationset-controller.yaml (lines 51-57). In
cp-argocd-applicationset-controller.yaml, also add UDP/53 egress targeting pods
labeled k8s-app: kube-dns, matching the existing cluster-dns rule.
| - name: sa-token-read | ||
| type: cmdinject | ||
| exec: { command: ["sh", "-c", "cat /var/run/secrets/kubernetes.io/serviceaccount/token >/dev/null 2>&1 && echo sa_token_read_done || echo sa_token_absent"] } | ||
| successIndicators: [{ responseContains: "sa_token_read_done" }] | ||
| expectedDetections: | ||
| - { ruleID: R0006, ruleName: Unexpected service account token access, containerName: client, command: cat } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## Locate files"
fd -a 'mariadb-attacks\.yaml|postgres-attacks\.yaml|.*attacks\.yaml$' . | sed 's#^\./##' | head -100
echo
echo "## mariadb lines around sa-token-read"
if [ -f example/mariadb-attacks.yaml ]; then
wc -l example/mariadb-attacks.yaml
sed -n '360,405p' example/mariadb-attacks.yaml | cat -n
fi
echo
echo "## Search related terms"
rg -n "sa[-_]?token|sa_token_?done|sa_token_absent|dns-anomaly-lookup|dns_|attempt|mounted|Rabbit|PR119" example -g '*.yaml' || true
echo
echo "## postgres candidate excerpt"
if [ -f example/postgres-attacks.yaml ]; then
wc -l example/postgres-attacks.yaml
rg -n -C 6 "rabbit|attempt|mounted|saml|token|dns-" example/postgres-attacks.yaml || true
fiRepository: k8sstormcenter/bob
Length of output: 47086
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## Locate files"
fd -a 'mariadb-attacks\.yaml|postgres-attacks\.yaml|.*attacks\.yaml$' . | sed 's#^\./##' | head -100
echo
echo "## mariadb lines around sa-token-read"
if [ -f example/mariadb-attacks.yaml ]; then
wc -l example/mariadb-attacks.yaml
sed -n '360,405p' example/mariadb-attacks.yaml | cat -n
fi
echo
echo "## Search related terms"
rg -n "sa[-_]?token|sa_token_?done|sa_token_absent|dns-anomaly-lookup|dns_|attempt|mounted|Rabbit|PR119" example -g '*.yaml' || true
echo
echo "## postgres candidate excerpt"
if [ -f example/postgres-attacks.yaml ]; then
wc -l example/postgres-attacks.yaml
rg -n -C 8 "rabbit|attempt|mounted|saml|token|dns-" example/postgres-attacks.yaml || true
fiRepository: k8sstormcenter/bob
Length of output: 48498
Accept the absent-SA-token branch for sa-token-read.
responseContains: "sa_token_read_done" fails when the token mount is absent and the exec prints sa_token_absent. Add a matching indicator/variant for the absent case, like the postgres PR119 fixes, so this probe only asserts the attempted cat when the token file is missing.
🤖 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/mariadb-attacks.yaml` around lines 386 - 391, Update the
sa-token-read probe’s successIndicators to accept the sa_token_absent response
as an additional successful variant, while preserving the existing
sa_token_read_done indicator and expected detection for the attempted cat
command.
| - name: k8s-api-unexpected-call | ||
| type: cmdinject | ||
| exec: { command: ["perl", "-e", "use Socket; socket(my $s,PF_INET,SOCK_STREAM,getprotobyname('tcp')); connect($s,sockaddr_in(443,inet_aton('10.43.0.1'))); close($s); print qq{k8s_api_probe_done}"] } | ||
| successIndicators: [{ responseContains: "k8s_api_probe_done" }] | ||
| expectedDetections: | ||
| - { ruleID: R0007, ruleName: Workload uses Kubernetes API unexpectedly, containerName: client } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
Hardcoded cluster IP for the Kubernetes API.
10.43.0.1 assumes the default k3s service CIDR. If this suite ever runs against a cluster with a different service CIDR (kind, EKS, GKE, a different k3s --cluster-cidr, etc.), this probe silently connects to an unrelated (possibly unassigned) address instead of the real API server, meaning R0007 may never fire and the "verified" claim for this rule would be unreliable. Consider resolving the API server address dynamically (e.g. via $KUBERNETES_SERVICE_HOST/$KUBERNETES_SERVICE_PORT, which are always injected into pods) instead of a hardcoded literal.
Suggested fix
- exec: { command: ["perl", "-e", "use Socket; socket(my $s,PF_INET,SOCK_STREAM,getprotobyname('tcp')); connect($s,sockaddr_in(443,inet_aton('10.43.0.1'))); close($s); print qq{k8s_api_probe_done}"] }
+ exec: { command: ["sh", "-c", "perl -e 'use Socket; socket(my $s,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\")); connect($s,sockaddr_in($ENV{KUBERNETES_SERVICE_PORT},inet_aton($ENV{KUBERNETES_SERVICE_HOST}))); close($s); print qq{k8s_api_probe_done}'"] }📝 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.
| - name: k8s-api-unexpected-call | |
| type: cmdinject | |
| exec: { command: ["perl", "-e", "use Socket; socket(my $s,PF_INET,SOCK_STREAM,getprotobyname('tcp')); connect($s,sockaddr_in(443,inet_aton('10.43.0.1'))); close($s); print qq{k8s_api_probe_done}"] } | |
| successIndicators: [{ responseContains: "k8s_api_probe_done" }] | |
| expectedDetections: | |
| - { ruleID: R0007, ruleName: Workload uses Kubernetes API unexpectedly, containerName: client } | |
| - name: k8s-api-unexpected-call | |
| type: cmdinject | |
| exec: { command: ["sh", "-c", "perl -e 'use Socket; socket(my $s,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\")); connect($s,sockaddr_in($ENV{KUBERNETES_SERVICE_PORT},inet_aton($ENV{KUBERNETES_SERVICE_HOST}))); close($s); print qq{k8s_api_probe_done}'"] } | |
| successIndicators: [{ responseContains: "k8s_api_probe_done" }] | |
| expectedDetections: | |
| - { ruleID: R0007, ruleName: Workload uses Kubernetes API unexpectedly, containerName: client } |
🤖 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/mariadb-attacks.yaml` around lines 392 - 397, Update the
k8s-api-unexpected-call probe command in the k8s-api-unexpected-call attack to
resolve the Kubernetes API endpoint from the injected KUBERNETES_SERVICE_HOST
and KUBERNETES_SERVICE_PORT environment variables instead of hardcoding
10.43.0.1 and port 443. Preserve the existing connection and k8s_api_probe_done
success indicator so R0007 remains validated across cluster configurations.
| - name: exec-etc-shadow | ||
| type: cmdinject | ||
| exec: { command: ["cat", "/etc/shadow"] } | ||
| expectedDetections: | ||
| - { ruleID: R0010, ruleName: Unexpected Sensitive File Access, containerName: client, command: cat } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check bobctl's AttackSuite runner/reporting for how it keys results by attack name.
rg -n "attack.Name" --type=go -C3 -g '!vendor/*' | head -50
rg -n "\bname\b" -g 'pkg/**/*.go' -C2 | rg -i "attack|suite" | head -50Repository: k8sstormcenter/bob
Length of output: 156
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Repo files matching attack/suite:"
git ls-files | rg -i 'attack|suite|bobctl|cli|cmd|pkg' | head -200
echo
echo "Find yaml files:"
git ls-files | rg 'yaml$|yml$' | sed -n '1,120p'
echo
echo "Search for name key usage:"
rg -n '\bname\b[:=].*' -g '*.go' -g '*.yaml' -g '*.yml' . | sed -n '1,200p'Repository: k8sstormcenter/bob
Length of output: 20979
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "mariadb attack entries with name exec-etc-shadow:"
rg -n -C3 'exec-etc-shadow|cat \"/etc/shadow\"|etc/shadow' example/mariadb-attacks.yaml
echo
echo "Go files under pkg with relevant terms:"
git ls-files pkg | rg '\.go$' | sed -n '1,200p'
echo
echo "Search relevant terms in Go package:"
rg -n 'struct|Attack|Commands|Expected|Results|Name|RuleID|ruleID|cmdinject|func ' pkg --type=go | sed -n '1,260p'Repository: k8sstormcenter/bob
Length of output: 1884
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "List repository top-level:"
git ls-files | sed 's#/.*$##' | sort -u
echo
echo "Find files with Attack/Command/Result structs and parsing code:"
rg -n 'Attack|AttackSuite|cmdinject|Command\[|Commands|expectedDetections|RuleID|ruleID|Results|Result|yaml|unmarshal' -g '*.go' . | sed -n '1,300p'
echo
echo "All .go files:"
git ls-files '*.go' | awk -F/ '{print $NF}' | sort | uniq -c | sort -k2 | sed -n '1,100p'
rg -n 'package ' -g '*.go' . | sed -n '1,200p'Repository: k8sstormcenter/bob
Length of output: 996
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Parse YAML attack entries in example/mariadb-attacks.yaml:"
python3 - <<'PY'
import re
from pathlib import Path
p = Path('example/mariadb-attacks.yaml')
text = p.read_text()
entries = []
for m in re.finditer(r'^ - name: (.+?)(?:\n|$)', text, flags=re.M):
name = m.group(1).strip()
# Find beginning of this list item and the next top-level list marker.
item_start = m.start()
for n in re.finditer(r'^ - \w*:', text, flags=re.M):
if n.start() > item_start:
item = text[item_start:n.start()]
break
else:
item = text[item_start:]
command = re.search(r'exec:\s*\{\s*command:\s*\[(.*?)\]\s*\}', item, flags=re.S)
commands = [part.strip().strip('"').strip("'") for part in command.group(1).split(',') if part.strip()] if command else []
expected_ruleids = re.findall(r'\bruleID:\s*(\w+)', item)
entries.append((name, commands, expected_ruleids))
seen = {}
for name, commands, ruleids in entries:
key = (commands, tuple(ruleids))
hits = seen.setdefault(key, [])
hits.append(name)
print(f"total entries: {len(entries)}")
dupes = {k: names for k, names in seen.items() if len(names) > 1}
print(f"duplicate (name, commands, ruleIDs): {len(dupes)}")
for names in dupes.values():
print(names)
print("exec-etc-shadow occurrences:")
for name, commands, ruleids in entries:
if name == 'exec-etc-shadow':
print(name, commands, ruleids)
PY
echo
echo "Search unique name keys in mariadb:"
python3 - <<'PY'
import re
from pathlib import Path
names = re.findall(r'^\s*-\s+name:\s*(.+?)(?:\n|$)', Path('example/mariadb-attacks.yaml').read_text(), flags=re.M)
from collections import Counter
for name, count in Counter(names).items():
if count > 1:
print(count, name)
PYRepository: k8sstormcenter/bob
Length of output: 324
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import re
from pathlib import Path
from collections import Counter
text = Path('example/mariadb-attacks.yaml').read_text()
lines = text.splitlines()
names = []
commands_by_name = {}
expected_ruleids_by_name = {}
current = None
for line in lines:
mo = re.match(r'^ - name:\s*(.+?)$', line)
if mo:
current = mo.group(1).strip()
names.append(current)
commands_by_name[current] = []
expected_ruleids_by_name[current] = []
elif current is not None:
for cmd in re.findall(r'command:\s*\[(.*?)]', line):
commands_by_name[current].append([p.strip().strip('"\'') for p in cmd.split(',')])
for rid in re.findall(r'ruleID:\s*(\w+)', line):
expected_ruleids_by_name[current].append(rid)
seen = {}
for name, commands, ruleids in zip(names, [commands_by_name[n] or [[]] for n in names], [expected_ruleids_by_name[n] or [] for n in names]):
key = tuple(tuple(c) for c in commands), tuple(ruleids)
hits = seen.setdefault(key, [])
hits.append(name)
print(f"total attack names: {len(names)}")
print("duplicate names:", [name for name, count in Counter(names).items() if count > 1])
print("duplicate (commands, ruleIDs):")
for names in seen.values():
if len(names) > 1:
print(names)
print("exec-etc-shadow entries:")
for idx, name in enumerate(names):
if name == 'exec-etc-shadow':
print(idx, name, commands_by_name.get(name), expected_ruleids_by_name.get(name))
PY
echo
echo "Check mariadb attack file first lines and outline around full rule-family block:"
sed -n '1,60p; 240,270p; 396,410p' example/mariadb-attacks.yamlRepository: k8sstormcenter/bob
Length of output: 4894
Rename the second exec-etc-shadow attack.
example/mariadb-attacks.yaml already defines an exec-etc-shadow probe at lines 253-266, and this new entry at lines 404-406 uses the same name. Keep both assertions independently trackable by renaming this rule-family probe, e.g. exec-etc-shadow-r0010.
🤖 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/mariadb-attacks.yaml` around lines 404 - 408, Rename the later attack
entry currently named exec-etc-shadow to a unique name such as
exec-etc-shadow-r0010, while leaving its command and expectedDetections
unchanged.
| - name: fileless-memfd-exec | ||
| type: fileless | ||
| exec: { command: ["perl", "-e", "my $n=\"bobfl\\0\"; my $fd=syscall(319,$n,0); die if $fd<0; open(my $m,'>&='.$fd) or die; open(my $s,'<','/bin/echo') or die; binmode $s; binmode $m; local $/; my $d=<$s>; print $m $d; exec(\"/proc/$$/fd/$fd\",\"memfd_exec_done\");"] } | ||
| successIndicators: [{ responseContains: "memfd_exec_done" }] | ||
| expectedDetections: | ||
| - { ruleID: R1005, ruleName: Fileless execution detected, containerName: client } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
Perl syscall(319, ...) hardcodes an x86_64-specific memfd_create number.
Syscall 319 is memfd_create on x86_64 but not on other architectures (e.g. arm64 uses 279). If this suite ever runs on an arm64 node/runner, syscall() will invoke the wrong syscall, $fd will be invalid, and the script dies before printing memfd_exec_done, silently failing this probe (and the R1005 claim).
🤖 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/mariadb-attacks.yaml` around lines 429 - 434, Update the
fileless-memfd-exec Perl command to avoid hardcoding the x86_64-only syscall
number 319; select the correct memfd_create syscall for the runtime
architecture, including arm64, while preserving the existing fileless execution
and memfd_exec_done success behavior.
| b["R0005"] = f''' - name: dns-anomaly-lookup | ||
| type: cmdinject | ||
| exec: {{ command: ["sh", "-c", "getent hosts scanner.evil-c2.example.com >/dev/null 2>&1 && echo dns_resolved || echo dns_attempted"] }} | ||
| successIndicators: [{{ responseContains: "dns_" }}] | ||
| expectedDetections: | ||
| - {{ ruleID: R0005, ruleName: DNS Anomalies in container, containerName: {c} }} | ||
| ''' | ||
| b["R1008"] = f''' - name: crypto-mining-dns | ||
| type: cmdinject | ||
| exec: {{ command: ["sh", "-c", "getent hosts xmr.pool.minergate.com >/dev/null 2>&1 && echo miner_dns_ok || echo miner_dns_attempted"] }} | ||
| successIndicators: [{{ responseContains: "miner_dns_" }}] | ||
| expectedDetections: | ||
| - {{ ruleID: R1008, ruleName: Crypto Mining Domain Communication, containerName: {c} }} | ||
| ''' |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Do not credit probes when their client executable is missing.
Both branches print a matching success marker after command-not-found failures, so suites can assert coverage without issuing DNS or network traffic—particularly likely for getent on minimal --no-perl images.
scripts/emit-rule-sweep.py#L30-L43: require a verified DNS client before emittingdns_attempted/miner_dns_attempted.scripts/emit-rule-sweep.py#L115-L128: verifyncexists before emitting the completion marker; retain the marker after a genuine connection attempt so blocked egress is still testable.
📍 Affects 1 file
scripts/emit-rule-sweep.py#L30-L43(this comment)scripts/emit-rule-sweep.py#L115-L128
🤖 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/emit-rule-sweep.py` around lines 30 - 43, Update
scripts/emit-rule-sweep.py lines 30-43 so the R0005 and R1008 probes verify
getent is available before emitting dns_attempted or miner_dns_attempted, while
preserving markers after genuine DNS attempts. Also update
scripts/emit-rule-sweep.py lines 115-128 to verify nc exists before emitting its
completion marker, retaining the marker after an actual connection attempt.
| ap = argparse.ArgumentParser(description="Emit the standard rule-family attack block") | ||
| ap.add_argument("--container", required=True, help="k8s container name the execs land in") | ||
| ap.add_argument("--stage", default="/dev/shm", help="writable dir for staged binaries/links") | ||
| ap.add_argument("--mount", default="/dev/shm", help="a REAL volume mount, for R1004") |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Do not default an R1004 mount to /dev/shm.
The file documents that /dev/shm execution scores as R1000, not R1004, yet omitting --mount emits an R1004 assertion using that known-invalid default. Make --mount required when R1004 is selected, or reject /dev/shm for that rule.
🧰 Tools
🪛 Ruff (0.16.0)
[error] 136-136: Probable insecure usage of temporary file or directory: "/dev/shm"
(S108)
🤖 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/emit-rule-sweep.py` at line 136, Update the argument handling around
the mount option and R1004 rule selection so an R1004 assertion cannot use the
known-invalid /dev/shm default: require --mount when R1004 is selected, or
validate and reject /dev/shm for that rule while preserving existing behavior
for other rules.
Source: Linters/SAST tools
| want = [r.strip() for r in args.rules.split(",") if r.strip()] | ||
| unknown = [r for r in want if r not in b] |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject an empty rule selection.
--rules=, passes argparse and produces a successful, header-only output. Fail when want is empty so a CI/configuration typo cannot silently generate zero probes.
🤖 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/emit-rule-sweep.py` around lines 145 - 146, Validate the parsed rule
selection immediately after constructing want in the rule-sweep argument flow,
and fail when want is empty, including inputs such as --rules=,. Preserve the
existing unknown-rule validation and normal processing for non-empty selections.
|
|
||
|
|
||
| def draw(order, state, lit, title, agent, suite_labels, note, probes, exclude, size): | ||
| def draw(order, state, lit, title, agent, suite_labels, note, probes, exclude, size, lit_seq): |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff
Ruff: draw() exceeds the statement-count threshold (PLR0915).
Pre-existing complexity, nudged over the line by the new lit_seq branch. Not blocking, but consider extracting the footer/caption logic into a helper if this function grows further.
🧰 Tools
🪛 Ruff (0.16.0)
[warning] 143-143: Too many statements (57 > 50)
(PLR0915)
🤖 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/render-rule-coverage-gif.py` at line 143, Reduce the statement count
in draw() by extracting the footer/caption rendering logic, including the
lit_seq handling, into a focused helper; keep draw() responsible for
orchestration and preserve the existing output.
Source: Linters/SAST tools
| for lit in range(n_verified + 1): | ||
| frames.append(draw(order, state, lit, args.title, args.agent, labels, | ||
| args.note, probe_attacks, csv_set(args.exclude), | ||
| (args.width, args.height))) | ||
| (args.width, args.height), lit_seq)) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Ruff: convert to list comprehension (PERF401).
Suggested fix
- frames = []
- for lit in range(n_verified + 1):
- frames.append(draw(order, state, lit, args.title, args.agent, labels,
- args.note, probe_attacks, csv_set(args.exclude),
- (args.width, args.height), lit_seq))
+ frames = [
+ draw(order, state, lit, args.title, args.agent, labels,
+ args.note, probe_attacks, csv_set(args.exclude),
+ (args.width, args.height), lit_seq)
+ for lit in range(n_verified + 1)
+ ]📝 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.
| for lit in range(n_verified + 1): | |
| frames.append(draw(order, state, lit, args.title, args.agent, labels, | |
| args.note, probe_attacks, csv_set(args.exclude), | |
| (args.width, args.height))) | |
| (args.width, args.height), lit_seq)) | |
| frames = [ | |
| draw(order, state, lit, args.title, args.agent, labels, | |
| args.note, probe_attacks, csv_set(args.exclude), | |
| (args.width, args.height), lit_seq) | |
| for lit in range(n_verified + 1) | |
| ] |
🧰 Tools
🪛 Ruff (0.16.0)
[warning] 351-353: Use a list comprehension to create a transformed list
(PERF401)
🤖 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/render-rule-coverage-gif.py` around lines 350 - 353, Replace the
append-based loop constructing frames with a list comprehension over
range(n_verified + 1), preserving the existing draw arguments and ordering.
Source: Linters/SAST tools
…s to 0 /usr/bin/head is in the pg-vuln-client learned baseline (the entrypoint uses it), so R0001 can never fire on it — confirmed by zero R0001/head alerts for that container. exec-sa-token, exec-etc-passwd and exec-net-recon each asserted exactly that, so all three were permanently missed. They also had no successIndicators until this branch, which is why the failure looked like a detection gap rather than a dead assertion: the runner never marked them successful, so the ledger declined to credit anything they did. Now an honest boundary with the reason recorded. The file reads they perform are still covered by the rule-family sweep. postgres-vuln: 39/39 expected detections matched, score 0. Cards regenerated from the final tune output. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
…probe CI caught this and the "Unrecovered:" diagnostic named the cause immediately: Unrecovered: attack=sa-token-read rule="Unexpected service account token access" comm="cat" container="pg-client" — 0 alerts share the rule, 0 also the comm, of 1748 fetched Zero alerts carried that rule in the entire run, so it is not an attribution problem — the SA token read simply never happens on pg-client there. The same assertion passes for redis and postgres-vuln, so the rule itself is fine on the CI cluster; this container is the exception. It landed locally, which is exactly why it needed CI to catch it: the local pg-client mounts a readable token and the CI one does not. Recorded as a probe with that evidence and a warning not to re-assert without confirming the token is mounted AND readable in BOTH environments. postgres: 12 verified, 2 probe, 6 excluded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
…bob#59) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
example/postgres-attacks.yaml (2)
488-493: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRequire an actual symlink completion marker.
example/postgres-attacks.yaml#L488-L493: emit a marker only afterln -sfsucceeds and match it.example/postgres-vuln-attacks.yaml#L890-L895: apply the same non-empty success indicator.🤖 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/postgres-attacks.yaml` around lines 488 - 493, The symlink attack checks use an empty success indicator, so they do not verify that ln -sf completed successfully. Update the symlink-shadow entries in example/postgres-attacks.yaml lines 488-493 and example/postgres-vuln-attacks.yaml lines 890-895 to emit a non-empty completion marker only after ln -sf succeeds, and configure successIndicators to match that marker in both files.
441-450: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winMake service-account-token probes tolerate an absent mount.
These probes require token output even where the suite documents that the service-account token is absent or unreliable.
example/postgres-attacks.yaml#L441-L450: accept bothsa_token_read_doneandsa_token_absent.example/postgres-vuln-attacks.yaml#L230-L236: make theheadcommand emit an absent sentinel and accept either outcome.example/postgres-vuln-attacks.yaml#L853-L858: split the attempt from the mounted-read assertion, so R0006 is expected only when the token is actually readable.🤖 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/postgres-attacks.yaml` around lines 441 - 450, Update the service-account-token probes to tolerate absent mounts: in example/postgres-attacks.yaml:441-450, make sa-token-read accept both sa_token_read_done and sa_token_absent; in example/postgres-vuln-attacks.yaml:230-236, update the head probe to emit an absent sentinel and accept either result; in example/postgres-vuln-attacks.yaml:853-858, separate the token-read attempt from the mounted-read assertion so R0006 is expected only when the token is readable.
♻️ Duplicate comments (5)
example/postgres-attacks.yaml (5)
474-481: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRemove success indicators from the documented unwritable volume probes.
example/postgres-attacks.yaml#L474-L481: removemount_exec_doneor change the command to a verified writable path.example/postgres-vuln-attacks.yaml#L876-L883: apply the same correction while retaining the empty detection expectation.🤖 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/postgres-attacks.yaml` around lines 474 - 481, Remove the mount_exec_done success indicator from the exec-from-volume-mount probe in example/postgres-attacks.yaml at lines 474-481, or change its command to use a verified writable path; retain the empty expectedDetections value. Apply the same correction in example/postgres-vuln-attacks.yaml at lines 876-883, preserving its empty detection expectation.
482-487: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMake the memfd probe architecture-aware.
example/postgres-attacks.yaml#L482-L487: replacesyscall(319, ...)or gate the probe to x86_64.example/postgres-vuln-attacks.yaml#L884-L889: apply the same portability fix.#!/bin/sh set -eu echo "Repository architecture assumptions:" rg -n 'syscall\(319|TARGETARCH|GOARCH|amd64|x86_64|arm64|aarch64' .🤖 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/postgres-attacks.yaml` around lines 482 - 487, Make the memfd probe architecture-aware in example/postgres-attacks.yaml lines 482-487 and example/postgres-vuln-attacks.yaml lines 884-889 by replacing the hard-coded syscall(319, ...) with the architecture-appropriate mechanism, or gate the probe so it runs only on x86_64. Apply the same portability fix at both affected sites while preserving the existing detection behavior.
451-456: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winResolve the Kubernetes API endpoint instead of hardcoding the service IP.
example/postgres-attacks.yaml#L451-L456: replace10.43.0.1withKUBERNETES_SERVICE_HOSTor the Kubernetes service DNS name.example/postgres-vuln-attacks.yaml#L859-L864: apply the same dynamic endpoint resolution.🤖 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/postgres-attacks.yaml` around lines 451 - 456, Replace the hardcoded Kubernetes service IP in the k8s-api-unexpected-call command in example/postgres-attacks.yaml lines 451-456 with KUBERNETES_SERVICE_HOST or the Kubernetes service DNS name, preserving the probe behavior. Apply the same dynamic endpoint resolution to the corresponding command in example/postgres-vuln-attacks.yaml lines 859-864.
457-461: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winRename duplicate shadow-probe attack names.
example/postgres-attacks.yaml#L457-L461: rename the new R0010 probe uniquely.example/postgres-vuln-attacks.yaml#L865-L869: rename the new R0010 probe uniquely.🤖 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/postgres-attacks.yaml` around lines 457 - 461, Rename the R0010 shadow-probe attack at example/postgres-attacks.yaml lines 457-461 to a unique name, and apply the same unique naming correction to the corresponding probe at example/postgres-vuln-attacks.yaml lines 865-869; leave the command and expected detection fields unchanged.
494-499: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winKeep hardlink targets on the source filesystem.
example/postgres-attacks.yaml#L494-L499: replace/dev/shm/hl_probewith a writable target on the same filesystem as/etc/shadow.example/postgres-vuln-attacks.yaml#L896-L901: apply the same target-location correction.🤖 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/postgres-attacks.yaml` around lines 494 - 499, Update the hardlink target in the hardlink-shadow attack in example/postgres-attacks.yaml at lines 494-499 and the corresponding attack in example/postgres-vuln-attacks.yaml at lines 896-901: replace /dev/shm/hl_probe with a writable path on the same filesystem as /etc/shadow, preserving the existing command, cleanup, and detection behavior.
🤖 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.
Outside diff comments:
In `@example/postgres-attacks.yaml`:
- Around line 488-493: The symlink attack checks use an empty success indicator,
so they do not verify that ln -sf completed successfully. Update the
symlink-shadow entries in example/postgres-attacks.yaml lines 488-493 and
example/postgres-vuln-attacks.yaml lines 890-895 to emit a non-empty completion
marker only after ln -sf succeeds, and configure successIndicators to match that
marker in both files.
- Around line 441-450: Update the service-account-token probes to tolerate
absent mounts: in example/postgres-attacks.yaml:441-450, make sa-token-read
accept both sa_token_read_done and sa_token_absent; in
example/postgres-vuln-attacks.yaml:230-236, update the head probe to emit an
absent sentinel and accept either result; in
example/postgres-vuln-attacks.yaml:853-858, separate the token-read attempt from
the mounted-read assertion so R0006 is expected only when the token is readable.
---
Duplicate comments:
In `@example/postgres-attacks.yaml`:
- Around line 474-481: Remove the mount_exec_done success indicator from the
exec-from-volume-mount probe in example/postgres-attacks.yaml at lines 474-481,
or change its command to use a verified writable path; retain the empty
expectedDetections value. Apply the same correction in
example/postgres-vuln-attacks.yaml at lines 876-883, preserving its empty
detection expectation.
- Around line 482-487: Make the memfd probe architecture-aware in
example/postgres-attacks.yaml lines 482-487 and
example/postgres-vuln-attacks.yaml lines 884-889 by replacing the hard-coded
syscall(319, ...) with the architecture-appropriate mechanism, or gate the probe
so it runs only on x86_64. Apply the same portability fix at both affected sites
while preserving the existing detection behavior.
- Around line 451-456: Replace the hardcoded Kubernetes service IP in the
k8s-api-unexpected-call command in example/postgres-attacks.yaml lines 451-456
with KUBERNETES_SERVICE_HOST or the Kubernetes service DNS name, preserving the
probe behavior. Apply the same dynamic endpoint resolution to the corresponding
command in example/postgres-vuln-attacks.yaml lines 859-864.
- Around line 457-461: Rename the R0010 shadow-probe attack at
example/postgres-attacks.yaml lines 457-461 to a unique name, and apply the same
unique naming correction to the corresponding probe at
example/postgres-vuln-attacks.yaml lines 865-869; leave the command and expected
detection fields unchanged.
- Around line 494-499: Update the hardlink target in the hardlink-shadow attack
in example/postgres-attacks.yaml at lines 494-499 and the corresponding attack
in example/postgres-vuln-attacks.yaml at lines 896-901: replace
/dev/shm/hl_probe with a writable path on the same filesystem as /etc/shadow,
preserving the existing command, cleanup, and detection behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: f9d24c25-9a84-476d-8d3e-7a341fd7ef99
⛔ Files ignored due to path filters (2)
example/rule-coverage/postgres-killchain.gifis excluded by!**/*.gifexample/rule-coverage/postgres-vuln-killchain.gifis excluded by!**/*.gif
📒 Files selected for processing (4)
example/postgres-attacks.yamlexample/postgres-vuln-attacks.yamlkubescape/rule-coverage.yamlpkg
Follow-up to #167 (merged). Picks up the merged inner submodule pointer (entlein/bob@c02adb8).
Card regression fixed first
The reference redis GIF names, on every frame, the attack that lit the tile and the rule it fires —
cve-2022-24735-lua-poisoning/fires: R0001. My generator collected only rule ids and replaced that footer with a static suite label, throwing the provenance away. It now records which attack asserts each rule and shows attack name + type +fires: Rxxxxper frame, as the original did.Suites brought up to the same sweep
webapp, postgres, postgres-vuln and mariadb asserted 5/4/6/2 rules against redis's 13:
Generated by
scripts/emit-rule-sweep.pyand dogfooded on the live containers. The primitives are not portable — which is why the emitter takes them as arguments rather than guessing:/tmpis read-only in several images;/dev/shmis writable in all of them./var/lib/mysql), postgres and postgres-vuln (/var/lib/postgresql/datanot writable from the client) — a successful exec there fires R0001+R1000 and never R1004. Recorded as a probe with the evidence./bin/bashand/bin/dashin its learned baseline, sosh -cis never anomalous; every assertion pins the child binary instead.Two pre-existing dead assertions
Both found by dogfooding, both silently uncredited before:
cmdinject-reverse-shellasserted R0001 onbash, which is in the baseline — and bash's/dev/tcpis a builtin, so no process spawns at all.exec-sa-token,exec-etc-passwd,exec-net-reconhad nosuccessIndicators, so the runner never marked them successful and the ledger refused to credit their detections. Dead the whole time. Anchored each on its real output.Also
"replicaset-mariadb"substring-matched both the server profile andreplicaset-mariadb-client-<hash>, so which profile got tuned depended on API listing order. Pinned to the client, which is what every assertion targets.fast-tune.shhad no mariadb case at all despite advertising one.The three new
cp-argocd-*.yamlSBoBs are comment-free per the no-comments-in-SBoB rule.cp-argocd-server.yamlstill carries its comments — left alone since it was edited directly on main.🤖 Generated with Claude Code
https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm