Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified example/argocd/argocd-killchain.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 72 additions & 0 deletions example/argocd/sbobs/cp-argocd-application-controller.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
apiVersion: spdx.softwarecomposition.kubescape.io/v1beta1
kind: ContainerProfile
metadata:
name: argocd-application-controller
namespace: argocd
annotations:
kubescape.io/managed-by: User
spec:
architectures: [amd64]
execs:
- path: /usr/bin/tini
args: [/usr/bin/tini, "⋯⋯"]
- path: /usr/local/bin/argocd
args: [/usr/local/bin/argocd-application-controller, "⋯⋯"]
opens:
- {path: /etc/mime.types, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/apache/mime.types, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/apache2/mime.types, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/httpd/conf/mime.types, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/local/share/mime/globs2, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/share/mime/globs2, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/ld.so.cache, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/passwd, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/hosts, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/nsswitch.conf, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/resolv.conf, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/lib/x86_64-linux-gnu/libc.so.6, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/local/bin/argocd, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/share/zoneinfo/Etc/UTC, flags: [O_RDONLY]}
- {path: /home/argocd/.kube/config, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /run/secrets/kubernetes.io/serviceaccount/⋯/ca.crt, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /run/secrets/kubernetes.io/serviceaccount/⋯/namespace, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /run/secrets/kubernetes.io/serviceaccount/⋯/token, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /proc/⋯/net/core/somaxconn, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /proc/⋯/task/1/fd, flags: [O_RDONLY, O_DIRECTORY, O_CLOEXEC]}
- {path: /sys/kernel/mm/transparent_hugepage/hpage_pmd_size, flags: [O_RDONLY]}
capabilities: []
endpoints: []
rulePolicies:
R0002:
processAllowed: ["runc:[1:INIT]", "runc:[1:CHILD]", "runc:[2:INIT]", "runc:[3:INIT]"]
R0004:
processAllowed: ["runc:[1:INIT]", "runc:[1:CHILD]", "runc:[2:INIT]", "runc:[3:INIT]"]
R0006:
processAllowed: ["argocd-applicat"]
matchLabels:
app.kubernetes.io/name: argocd-application-controller
ingress:
- identifier: metrics-scrape
type: internal
ports:
- {name: TCP-8082, port: 8082, protocol: TCP}
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}
Comment on lines +53 to +72

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.

56 changes: 56 additions & 0 deletions example/argocd/sbobs/cp-argocd-applicationset-controller.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
apiVersion: spdx.softwarecomposition.kubescape.io/v1beta1
kind: ContainerProfile
metadata:
name: argocd-applicationset-controller
namespace: argocd
annotations:
kubescape.io/managed-by: User
spec:
architectures: [amd64]
execs:
- path: /usr/bin/tini
args: [/usr/bin/tini, "⋯⋯"]
- path: /usr/local/bin/argocd
args: [/usr/local/bin/argocd-applicationset-controller, "⋯⋯"]
opens:
- {path: /etc/mime.types, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/apache/mime.types, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/apache2/mime.types, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/httpd/conf/mime.types, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/local/share/mime/globs2, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/share/mime/globs2, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/ld.so.cache, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/passwd, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/lib/x86_64-linux-gnu/libc.so.6, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/local/bin/argocd, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/share/zoneinfo/Etc/UTC, flags: [O_RDONLY]}
- {path: /home/argocd/.kube/config, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /run/secrets/kubernetes.io/serviceaccount/⋯/ca.crt, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /run/secrets/kubernetes.io/serviceaccount/⋯/namespace, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /run/secrets/kubernetes.io/serviceaccount/⋯/token, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /proc/⋯/net/core/somaxconn, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /proc/⋯/task/1/fd, flags: [O_RDONLY, O_DIRECTORY, O_CLOEXEC]}
- {path: /sys/kernel/mm/transparent_hugepage/hpage_pmd_size, flags: [O_RDONLY]}
capabilities: []
endpoints: []
rulePolicies:
R0002:
processAllowed: ["runc:[1:INIT]", "runc:[1:CHILD]", "runc:[2:INIT]", "runc:[3:INIT]"]
R0004:
processAllowed: ["runc:[1:INIT]", "runc:[1:CHILD]", "runc:[2:INIT]", "runc:[3:INIT]"]
R0006:
processAllowed: ["argocd-applicat"]
matchLabels:
app.kubernetes.io/name: argocd-applicationset-controller
ingress:
- identifier: webhook-and-metrics
type: internal
ports:
- {name: TCP-7000, port: 7000, protocol: TCP}
- {name: TCP-8080, port: 8080, protocol: TCP}
egress:
- identifier: kube-apiserver
type: internal
ipAddress: 10.43.0.1
ports:
- {name: TCP-443, port: 443, protocol: TCP}
91 changes: 91 additions & 0 deletions example/argocd/sbobs/cp-argocd-repo-server.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
apiVersion: spdx.softwarecomposition.kubescape.io/v1beta1
kind: ContainerProfile
metadata:
name: argocd-repo-server
namespace: argocd
annotations:
kubescape.io/managed-by: User
spec:
architectures: [amd64]
execs:
- path: /usr/bin/tini
args: [/usr/bin/tini, "⋯⋯"]
- path: /usr/local/bin/argocd
args: [/usr/local/bin/argocd-repo-server, "⋯⋯"]
- path: /usr/bin/gpg
args: [/usr/bin/gpg, "⋯⋯"]
- path: /usr/bin/gpg-agent
args: [/usr/bin/gpg-agent, "⋯⋯"]
- path: /usr/bin/dash
args: [/usr/local/bin/gpg-wrapper.sh, "⋯⋯"]
opens:
- {path: /app/config/gpg/keys/⋯, flags: [O_WRONLY, O_CREAT, O_EXCL]}
- {path: /app/config/gpg/keys/.argocd-generated, flags: [O_WRONLY, O_CREAT, O_TRUNC, O_CLOEXEC]}
- {path: /app/config/gpg/keys/gpg-agent.conf, flags: [O_RDONLY]}
- {path: /app/config/gpg/keys/gpg.conf, flags: [O_RDONLY]}
- {path: /app/config/gpg/keys/pubring.gpg, flags: [O_RDONLY]}
- {path: /app/config/gpg/keys/pubring.kbx, flags: [O_RDONLY, O_WRONLY, O_CREAT, O_TRUNC]}
- {path: /app/config/gpg/keys/pubring.kbx.lock, flags: [O_RDONLY]}
- {path: /app/config/gpg/keys/pubring.kbx.tmp, flags: [O_WRONLY, O_CREAT, O_TRUNC]}
- {path: /app/config/gpg/keys/trustdb.gpg, flags: [O_RDWR, O_WRONLY, O_CREAT, O_TRUNC]}
- {path: /app/config/gpg/keys/trustdb.gpg.lock, flags: [O_RDONLY]}
- {path: /app/config/gpg/keys/openpgp-revocs.d/⋯, flags: [O_CREAT, O_TRUNC, O_WRONLY]}
- {path: /app/config/gpg/keys/private-keys-v1.d/⋯, flags: [O_CREAT, O_EXCL, O_TRUNC, O_RDONLY, O_WRONLY]}
- {path: /app/config/gpg/source, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /app/config/gpg/source/⋯, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /tmp/⋯, flags: [O_RDWR, O_CREAT, O_EXCL, O_CLOEXEC, O_RDONLY]}
- {path: /etc/gcrypt/hwf.deny, flags: [O_RDONLY]}
- {path: /etc/gcrypt/random.conf, flags: [O_RDONLY]}
- {path: /etc/gnupg/gpg-agent.conf, flags: [O_RDONLY]}
- {path: /etc/gnupg/gpg.conf, flags: [O_RDONLY]}
- {path: /dev/urandom, flags: [O_RDONLY]}
- {path: /dev/null, flags: [O_RDONLY, O_CLOEXEC, O_WRONLY]}
- {path: /etc/mime.types, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/apache/mime.types, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/apache2/mime.types, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/httpd/conf/mime.types, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/local/share/mime/globs2, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/share/mime/globs2, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/ld.so.cache, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/passwd, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/hosts, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/nsswitch.conf, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /etc/resolv.conf, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/share/zoneinfo/Etc/UTC, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /home/argocd, flags: [O_RDONLY, O_NONBLOCK, O_DIRECTORY, O_CLOEXEC]}
- {path: /usr/lib/x86_64-linux-gnu/libc.so.6, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/lib/x86_64-linux-gnu/libm.so.6, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/lib/x86_64-linux-gnu/libz.so.1.2.11, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/lib/x86_64-linux-gnu/libbz2.so.1.0.4, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/lib/x86_64-linux-gnu/libassuan.so.0.8.5, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/lib/x86_64-linux-gnu/libgcrypt.so.20.3.4, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/lib/x86_64-linux-gnu/libgpg-error.so.0.32.1, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/lib/x86_64-linux-gnu/libnpth.so.0.1.2, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/lib/x86_64-linux-gnu/libreadline.so.8.1, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/lib/x86_64-linux-gnu/libtinfo.so.6.3, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/local/bin/argocd, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /usr/local/bin/gpg-wrapper.sh, flags: [O_RDONLY]}
- {path: /proc/⋯/net/core/somaxconn, flags: [O_RDONLY, O_CLOEXEC]}
- {path: /proc/⋯/task/1/fd, flags: [O_RDONLY, O_DIRECTORY, O_CLOEXEC]}
- {path: /proc/⋯/fd, flags: [O_RDONLY, O_NONBLOCK, O_DIRECTORY, O_CLOEXEC]}
- {path: /sys/kernel/mm/transparent_hugepage/hpage_pmd_size, flags: [O_RDONLY]}
capabilities: []
endpoints: []
rulePolicies:
R0002:
processAllowed: ["runc:[1:INIT]", "runc:[1:CHILD]", "runc:[2:INIT]", "runc:[3:INIT]"]
R0004:
processAllowed: ["runc:[1:INIT]", "runc:[1:CHILD]", "runc:[2:INIT]", "runc:[3:INIT]"]
matchLabels:
app.kubernetes.io/name: argocd-repo-server
ingress:
- identifier: argocd-grpc-clients
type: internal
ports:
- {name: TCP-8081, port: 8081, protocol: TCP}
- identifier: metrics-scrape
type: internal
ports:
- {name: TCP-8084, port: 8084, protocol: TCP}
egress: null
71 changes: 71 additions & 0 deletions example/mariadb-attacks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -373,3 +373,74 @@ attacks:
successIndicators:
- responseContains: "1"
expectedDetections: []

# ── Full rule-family sweep (scripts/emit-rule-sweep.py) ─────────────────────
# Primitives probed on the live container: staging dir, real mount, interpreter.
# Anything that misses on the dogfood run must be demoted to a probe WITH a reason.
- 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: client }
- 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 }
Comment on lines +386 to +391

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.

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

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.

- name: exec-proc-environ
type: cmdinject
exec: { command: ["cat", "/proc/1/environ"] }
successIndicators: [{ responseContains: "PATH" }]
expectedDetections:
- { ruleID: R0008, ruleName: Read Environment Variables from procfs, containerName: client, command: cat }
- name: exec-etc-shadow
type: cmdinject
exec: { command: ["cat", "/etc/shadow"] }
expectedDetections:
- { ruleID: R0010, ruleName: Unexpected Sensitive File Access, containerName: client, command: cat }
Comment on lines +404 to +408

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.

- name: egress-external-c2
type: cmdinject
exec: { command: ["perl", "-e", "use Socket; socket(my $s,PF_INET,SOCK_STREAM,getprotobyname('tcp')); connect($s,sockaddr_in(80,inet_aton('1.1.1.1'))); close($s); print qq{egress_attempted}"] }
successIndicators: [{ responseContains: "egress_attempted" }]
expectedDetections:
- { ruleID: R0011, ruleName: Unexpected Egress Network Traffic, containerName: client }
- name: drifted-binary-exec
type: cmdinject
exec: { command: ["sh", "-c", "cp /bin/ls /dev/shm/drifted_bob && printf '\\n#bob-drift' >> /dev/shm/drifted_bob && chmod +x /dev/shm/drifted_bob && /dev/shm/drifted_bob / >/dev/null 2>&1 && echo drift_exec_done; rm -f /dev/shm/drifted_bob"] }
successIndicators: [{ responseContains: "drift_exec_done" }]
expectedDetections:
- { ruleID: R1000, ruleName: Process executed from malicious source, containerName: client }
- name: exec-from-volume-mount
type: cmdinject
exec: { command: ["sh", "-c", "cp /bin/echo /var/lib/mysql/mnt_payload && chmod +x /var/lib/mysql/mnt_payload && /var/lib/mysql/mnt_payload mount_exec_done; rm -f /var/lib/mysql/mnt_payload"] }
successIndicators: [{ responseContains: "mount_exec_done" }]
# Verified unreachable on this container: a successful exec from the
# /var/lib/mysql volume mount fires R0001 only, and an exec from /dev/shm
# scores as R1000 on this node-agent build. No path to R1004 here.
expectedDetections: []
- 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 }
Comment on lines +429 to +434

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.

- 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: client }
- name: hardlink-shadow
type: cmdinject
exec: { command: ["sh", "-c", "ln /etc/shadow /dev/shm/hl_probe >/dev/null 2>&1 && echo hardlink_done; rm -f /dev/shm/hl_probe"] }
successIndicators: [{ responseContains: "hardlink_done" }]
expectedDetections:
- { ruleID: R1012, ruleName: Hard link created over sensitive file, containerName: client, command: ln }
Comment on lines +441 to +446

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate example/mariadb-attacks.yaml and relevant lines =="
if [ -f example/mariadb-attacks.yaml ]; then
  wc -l example/mariadb-attacks.yaml
  sed -n '430,455p' example/mariadb-attacks.yaml
  sed -n '620,645p' example/mariadb-attacks.yaml
else
  echo "example/mariadb-attacks.yaml not found"
  fd -a -i 'mariadb-attacks\.yaml' .
fi

echo
echo "== Search for hardlink-shadow / hl_probe / R1012 =="
rg -n "hardlink-shadow|hl_probe|Hard link created|R1012|dev/shm|/tmp/hl_probe|ln /etc/shadow" .

Repository: k8sstormcenter/bob

Length of output: 15142


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Filesystem characteristics on sandbox =="
df -T / /tmp /dev/shm 2>/dev/null || true
stat -f -c 'dev=%t/%T type=%T fstype=%T' / /tmp /dev/shm 2>/dev/null || true
mount | rg '(/tmp|/dev/shm|rootfs|overlay|tmpfs)' || true

echo
echo "== Static filesystem mountpoint in Docker context or container files =="
for f in Dockerfile docker-compose*.yml docker-compose*.yaml example/mariadb-attacks.yaml README.md; do
  if [ -f "$f" ]; then
    echo "-- $f"
    rg -n "/tmp|/dev/shm|shm|tmpfs|volumes:|container_name|environment:" "$f" || true
  fi
done

echo
echo "== Check whether /tmp and root are same mount as observed by df =="
python3 - <<'PY'
from pathlib import Path
root = Path('/mnt') if Path('/mnt').exists() else Path('/')
tmp = Path('/tmp')
shm = Path('/dev/shm')
for p in (root, tmp, shm):
    try:
        print(f"{p}: exists={p.exists()}, stat_dev={p.stat().st_dev}")
    except Exception as e:
        print(f"{p}: {e}")
PY

Repository: k8sstormcenter/bob

Length of output: 1452


Use a same-filesystem target for the hard link probe.

ln /etc/shadow /dev/shm/hl_probe links across the typical root container filesystem and /dev/shm tmpfs, so the probe should fail with EXDEV and never report hardlink_done. Use the same /tmp target pattern as the webapp equivalent probe.

Suggested fix
-    exec: { command: ["sh", "-c", "ln /etc/shadow /dev/shm/hl_probe >/dev/null 2>&1 && echo hardlink_done; rm -f /dev/shm/hl_probe"] }
+    exec: { command: ["sh", "-c", "ln /etc/shadow /tmp/hl_probe >/dev/null 2>&1 && echo hardlink_done; rm -f /tmp/hl_probe"] }
📝 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: hardlink-shadow
type: cmdinject
exec: { command: ["sh", "-c", "ln /etc/shadow /dev/shm/hl_probe >/dev/null 2>&1 && echo hardlink_done; rm -f /dev/shm/hl_probe"] }
successIndicators: [{ responseContains: "hardlink_done" }]
expectedDetections:
- { ruleID: R1012, ruleName: Hard link created over sensitive file, containerName: client, command: ln }
- name: hardlink-shadow
type: cmdinject
exec: { command: ["sh", "-c", "ln /etc/shadow /tmp/hl_probe >/dev/null 2>&1 && echo hardlink_done; rm -f /tmp/hl_probe"] }
successIndicators: [{ responseContains: "hardlink_done" }]
expectedDetections:
- { ruleID: R1012, ruleName: Hard link created over sensitive file, containerName: client, command: ln }
🤖 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 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.

59 changes: 59 additions & 0 deletions example/postgres-attacks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -434,3 +434,62 @@ attacks:
# ═══════════════════════════════════════════════════════════════════════════

# (no probes here — see note above)

# ── Full rule-family sweep (scripts/emit-rule-sweep.py) ─────────────────────
# Primitives probed on the live container: staging dir, real mount, interpreter.
# Anything that misses on the dogfood run must be demoted to a probe WITH a reason.
- 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: pg-client, command: cat }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
- 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: pg-client }
Comment on lines +451 to +456

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.

Same concern as example/mariadb-attacks.yaml Lines 392-397 — 10.43.0.1 assumes the k3s default service CIDR rather than resolving $KUBERNETES_SERVICE_HOST.

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

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

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
fd -a 'postgres-attacks\.yaml$' . || true

echo "== occurrences of exec-etc-shadow =="
rg -n "exec-etc-shadow|cat /etc/shadow|ruleID: R0001|ruleID: R0010" example/postgres-attacks.yaml example/mariadb-attacks.yaml 2>/dev/null || true

echo "== relevant postgres sections =="
sed -n '150,190p' example/postgres-attacks.yaml 2>/dev/null || true
sed -n '445,462p' example/postgres-attacks.yaml 2>/dev/null || true

Repository: k8sstormcenter/bob

Length of output: 4380


Rename the duplicate exec-etc-shadow attack name.

example/postgres-attacks.yaml already has an exec-etc-shadow attack that asserts cat /etc/shadow as R0001; the new entry duplicates that name while asserting R0010. Rename this one to avoid ambiguous per-attack tracking.

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

- name: egress-external-c2
type: cmdinject
exec: { command: ["perl", "-e", "use Socket; socket(my $s,PF_INET,SOCK_STREAM,getprotobyname('tcp')); connect($s,sockaddr_in(80,inet_aton('1.1.1.1'))); close($s); print qq{egress_attempted}"] }
successIndicators: [{ responseContains: "egress_attempted" }]
expectedDetections:
- { ruleID: R0011, ruleName: Unexpected Egress Network Traffic, containerName: pg-client }
- name: drifted-binary-exec
type: cmdinject
exec: { command: ["sh", "-c", "cp /bin/ls /dev/shm/drifted_bob && printf '\\n#bob-drift' >> /dev/shm/drifted_bob && chmod +x /dev/shm/drifted_bob && /dev/shm/drifted_bob / >/dev/null 2>&1 && echo drift_exec_done; rm -f /dev/shm/drifted_bob"] }
successIndicators: [{ responseContains: "drift_exec_done" }]
expectedDetections:
- { ruleID: R1000, ruleName: Process executed from malicious source, containerName: pg-client }
- name: exec-from-volume-mount
type: cmdinject
exec: { command: ["sh", "-c", "cp /bin/echo /var/lib/postgresql/data/mnt_payload && chmod +x /var/lib/postgresql/data/mnt_payload && /var/lib/postgresql/data/mnt_payload mount_exec_done; rm -f /var/lib/postgresql/data/mnt_payload"] }
successIndicators: [{ responseContains: "mount_exec_done" }]
# Verified unreachable on this container: /var/lib/postgresql/data is not
# writable from the client, and a successful exec staged on /dev/shm fires
# R0001 + R1000, never R1004, on this node-agent build. Honest boundary.
expectedDetections: []
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- 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: pg-client }
Comment on lines +482 to +487

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

Hardcoded x86_64 memfd_create syscall number.

Same portability concern as example/mariadb-attacks.yaml Lines 429-434 — syscall(319, ...) is x86_64-specific.

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

- name: symlink-shadow
type: cmdinject
exec: { command: ["ln", "-sf", "/etc/shadow", "/dev/shm/softlink_probe"] }
successIndicators: [{ responseContains: "" }]
expectedDetections:
- { ruleID: R1010, ruleName: Soft link created over sensitive file, containerName: pg-client, command: ln }
- name: hardlink-shadow
type: cmdinject
exec: { command: ["sh", "-c", "ln /etc/shadow /dev/shm/hl_probe >/dev/null 2>&1 && echo hardlink_done; rm -f /dev/shm/hl_probe"] }
successIndicators: [{ responseContains: "hardlink_done" }]
expectedDetections:
- { ruleID: R1012, ruleName: Hard link created over sensitive file, containerName: pg-client, command: ln }
Comment on lines +495 to +499

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate relevant yaml files =="
fd -a 'postgres-attacks.yaml|mariadb-attacks.yaml' . || true

echo "== relevant snippets =="
for f in example/postgres-attacks.yaml example/mariadb-attacks.yaml; do
  if [ -f "$f" ]; then
    echo "--- $f"
    nl -ba "$f" | sed -n '430,510p'
  fi
done

echo "== search for similar hardlink probe and docker/compose devices context =="
rg -n "hl_probe|/dev/shm|/etc/shadow|link|hard link|docker|devices|mnt|mount" example/postgres-attacks.yaml example/mariadb-attacks.yaml || true

echo "== filesystem semantics probe == "
tmpdir="$(mktemp -d)"
trap ' rm -rf "$tmpdir" ' EXIT
mkdir -p "$tmpdir/a"
printf 'test' > "$tmpdir/a/file"
set +e
ln "$tmpdir/a/file" "$tmpdir/link" >/dev/null 2>&1 && echo same_fs_success || echo same_fs_link_failed
mkfs.vfat -n TEST "$tmpdir/vfat_img" >/dev/null 2>&1 || true
if [ -b /dev/loop0 ] && modprobe loop >/dev/null 2>&1; then
  loopdev="$(losetup -f --show "$tmpdir/vfat_img")"
  mkdir -p "$tmpdir/mnt"
  mount "$loopdev" "$tmpdir/mnt"
  set +e
  ln "$tmpdir/a/file" "$tmpdir/mnt/link" >/dev/null 2 >& | sed '' 
  status=$?
  set -e
  if [ $status -eq 0 ]; then echo cross_fs_success; else echo cross_fs_link_failed
fi

Repository: k8sstormcenter/bob

Length of output: 385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant snippets with grep/head/tail =="
for f in example/postgres-attacks.yaml example/mariadb-attacks.yaml; do
  if [ -f "$f" ]; then
    echo "--- $f"
    awk 'NR>=440 && NR<=505 { printf "%6d\t%s\n", NR, $0 }' "$f"
  fi
done

echo "== search for hl_probe and container/device context =="
awk '/hl_probe|\/dev\/shm|\/etc\/shadow|Hard link created|docker:|devices:|container:|pg-client|mariadb|command: ln|type: cmdinject/' \
  example/postgres-attacks.yaml example/mariadb-attacks.yaml || true

echo "== same-file vs cross-file ln semantics probe =="
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/a"
printf 'test' > "$tmp/a/file"
set +e
ln "$tmp/a/file" "$tmp/a/link" >/tmp/ln_same_fs.err 2>&1
status=$?
set -e
if [ $status -eq 0 ]; then
  echo same_filesystem_success
else
  echo same_filesystem_failed "$(cat /tmp/ln_same_fs.err)"
fi

python3 - <<'PY'
import os, tempfile, subprocess, textwrap
with tempfile.TemporaryDirectory() as d:
    loop_file = os.path.join(d, "vfat.img")
    src="src"; dst="other/link"
    os.makedirs(os.path.join(d, src))
    open(os.path.join(d, src, "file"), "w").close()
    # Try to force a different physical device via mountpoint; loop-only fails where loop is unavailable,
    # but the expected EXDEV failure is deterministic across filesystems when mount succeeds.
    loopdev=None
    if subprocess.call(["modprobe", "loop"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)==0:
        subprocess.check_call(["mkfs.vfat", "-n", "TEST", loop_file], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        loopdev=subprocess.check_output(["losetup", "-f", "--show", loop_file], text=True).strip()
        mountdir=os.path.join(d, "mnt")
        os.makedirs(mountdir)
        subprocess.check_call(["mount", loopdev, mountdir],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
        try:
            res=subprocess.run(["ln", os.path.join(d, src, "file"), os.path.join(mountdir, dst)],
                               stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
            print("cross_filesystem_probe:", res.returncode, "stdout="+res.stdout.strip(), "stderr="+res.stderr.strip())
        finally:
            subprocess.call(["umount", mountdir], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    else:
        print("cross_filesystem_probe: loop unavailable")
    if loopdev:
        subprocess.call(["losetup", "-d", loopdev], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
PY

Repository: k8sstormcenter/bob

Length of output: 15006


Use a same-filesystem staging path for the hardlink probe.

ln /etc/shadow /dev/shm/hl_probe links across different filesystems and will fail with EXDEV, so hardlink_done is never printed and this probe cannot exercise rule R1012. Stage the source in /dev/shm first, then link within that directory.

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

Loading
Loading