Skip to content

feat: full rule sweep for the four lagging apps + restore per-attack card info - #168

Merged
entlein merged 4 commits into
mainfrom
feat/rule-coverage-cards
Jul 30, 2026
Merged

entlein merged 4 commits into
mainfrom
feat/rule-coverage-cards

Conversation

@entlein

@entlein entlein commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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: Rxxxx per 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:

app verified before after tune
webapp 6 14 score 0
postgres 5 13 score 0
postgres-vuln 7 14 re-tune in flight
mariadb 3 13 score 0

Generated by scripts/emit-rule-sweep.py and dogfooded on the live containers. The primitives are not portable — 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

Both found by dogfooding, both silently uncredited before:

  • 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, exec-net-recon had no successIndicators, 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 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.

The three new cp-argocd-*.yaml SBoBs are comment-free per the no-comments-in-SBoB rule. cp-argocd-server.yaml still carries its comments — left alone since it was edited directly on main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm

…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
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Argo CD container profiles

Layer / File(s) Summary
Argo CD profile manifests
example/argocd/sbobs/cp-argocd-*.yaml
Adds ContainerProfile resources for three Argo CD controllers with process, file, policy, label, ingress, and egress declarations.

Rule-family coverage and tooling

Layer / File(s) Summary
Rule-sweep generation
scripts/emit-rule-sweep.py, pkg
Adds parameterized YAML probe generation, CLI rule validation, Perl-dependent variants, and updates the pkg reference.
Rule-family attack probes
example/*-attacks.yaml
Adds detection probes and success indicators for Kubernetes API access, sensitive files, egress, drifted binaries, volume mounts, fileless execution, DNS anomalies, and link creation.
MariaDB execution selection
scripts/fast-tune.sh, scripts/local-ci.sh
Adds MariaDB tuning configuration and selects the mariadb-client profile for attack execution.
Coverage configuration and rendering
kubescape/rule-coverage.yaml, scripts/render-rule-coverage-gif.py
Adds metrics and probe metadata and includes the first asserting attack in rule-coverage animation captions.

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
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change set: broad rule-sweep coverage plus restored per-attack card info.
Description check ✅ Passed The description is clearly about the same sweep, coverage, and card-info changes reflected in the files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rule-coverage-cards

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between f00bd10 and ae478f1.

⛔ Files ignored due to path filters (6)
  • example/argocd/argocd-killchain.gif is excluded by !**/*.gif
  • example/redis-client/redis-killchain.gif is excluded by !**/*.gif
  • example/rule-coverage/mariadb-killchain.gif is excluded by !**/*.gif
  • example/rule-coverage/postgres-killchain.gif is excluded by !**/*.gif
  • example/rule-coverage/postgres-vuln-killchain.gif is excluded by !**/*.gif
  • example/rule-coverage/webapp-killchain.gif is excluded by !**/*.gif
📒 Files selected for processing (13)
  • example/argocd/sbobs/cp-argocd-application-controller.yaml
  • example/argocd/sbobs/cp-argocd-applicationset-controller.yaml
  • example/argocd/sbobs/cp-argocd-repo-server.yaml
  • example/mariadb-attacks.yaml
  • example/postgres-attacks.yaml
  • example/postgres-vuln-attacks.yaml
  • example/webapp-attacks.yaml
  • kubescape/rule-coverage.yaml
  • pkg
  • scripts/emit-rule-sweep.py
  • scripts/fast-tune.sh
  • scripts/local-ci.sh
  • scripts/render-rule-coverage-gif.py

Comment on lines +53 to +72
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}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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 -200

Repository: 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:


🌐 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:


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 by app.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 to k8s-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.

Comment on lines +386 to +391
- 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 }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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
fi

Repository: 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
fi

Repository: 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.

Comment on lines +392 to +397
- 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 }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
- 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.

Comment on lines +404 to +408
- name: exec-etc-shadow
type: cmdinject
exec: { command: ["cat", "/etc/shadow"] }
expectedDetections:
- { ruleID: R0010, ruleName: Unexpected Sensitive File Access, containerName: client, command: cat }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 -50

Repository: 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)
PY

Repository: 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.yaml

Repository: 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.

Comment on lines +429 to +434
- 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 }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment on lines +30 to +43
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} }}
'''

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 emitting dns_attempted / miner_dns_attempted.
  • scripts/emit-rule-sweep.py#L115-L128: verify nc exists 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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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

Comment on lines +145 to +146
want = [r.strip() for r in args.rules.split(",") if r.strip()]
unknown = [r for r in want if r not in b]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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

Comment on lines 350 to +353
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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
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

entlein and others added 3 commits July 30, 2026 13:51
…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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Require an actual symlink completion marker.

  • example/postgres-attacks.yaml#L488-L493: emit a marker only after ln -sf succeeds 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 win

Make 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 both sa_token_read_done and sa_token_absent.
  • example/postgres-vuln-attacks.yaml#L230-L236: make the head command 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 win

Remove success indicators from the documented unwritable volume probes.

  • example/postgres-attacks.yaml#L474-L481: remove mount_exec_done or 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 win

Make the memfd probe architecture-aware.

  • example/postgres-attacks.yaml#L482-L487: replace syscall(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 win

Resolve the Kubernetes API endpoint instead of hardcoding the service IP.

  • example/postgres-attacks.yaml#L451-L456: replace 10.43.0.1 with KUBERNETES_SERVICE_HOST or 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 win

Rename 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 win

Keep hardlink targets on the source filesystem.

  • example/postgres-attacks.yaml#L494-L499: replace /dev/shm/hl_probe with 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

📥 Commits

Reviewing files that changed from the base of the PR and between ae478f1 and b364704.

⛔ Files ignored due to path filters (2)
  • example/rule-coverage/postgres-killchain.gif is excluded by !**/*.gif
  • example/rule-coverage/postgres-vuln-killchain.gif is excluded by !**/*.gif
📒 Files selected for processing (4)
  • example/postgres-attacks.yaml
  • example/postgres-vuln-attacks.yaml
  • kubescape/rule-coverage.yaml
  • pkg

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant