Skip to content

PostgreSQL packaging-distro contrast (oss, bitnami, CloudNativePG) - #182

Draft
ConstanzeTU wants to merge 27 commits into
mainfrom
postgres-distros
Draft

ConstanzeTU wants to merge 27 commits into
mainfrom
postgres-distros

Conversation

@ConstanzeTU

Copy link
Copy Markdown
Contributor

PostgreSQL packaging-distro contrast

Extends the distro-contrast pattern (previously done for the redis protocol family)
to PostgreSQL across three packagings, each with a bound SBoB verified live:

fork installer benign FP attack rules
oss (postgres:17) plain Deployment 0 14
bitnami helm bitnami/postgresql 0 14
CloudNativePG operator 1.24.1 0 14

Every fork runs the SAME postgres:17 pg-client pod, so a single portable
client SBoB
(sbobs/cp-pg-client.yaml) is the contrast leg across all three
backends; each backend also gets its own server SBoB.

Why the server SBoB allowlists R0002 by comm

A database's data dir is inherently multi-level dynamic — WAL segments, per-OID
relfiles, sort/hash spill filesets — and these cannot be expressed with the
matcher's single-segment wildcard (a dynamic node terminates the tree). So the
server SBoB constrains what the server executes (R0001) and allowlists R0002
(file access) for the server's own comm(s): postgres for oss, the shell
init/probe toolchain for bitnami, and the mounted instance manager (also R1004)
for CloudNativePG. Attack execs run as other comms, so R0001/R1000 and
R0002-on-attack-files all still fire — the contrast is untouched.

Contents

  • example/postgres/distros/ — 3 distro manifests, deploy-distros.sh [oss|bitnami|cnpg|all] [sbob] (deploy via native installer + bind server+client
    profiles), sbobs/, per-fork functional/ + attacks/ suites, DEMO.md
  • kubescape/rule-coverage.yaml — three distro cards + generated killchain GIFs

Identical 14-rule attack contrast on every fork: R0001, R0002, R0005–R0008,
R0010, R0011, R1000, R1001, R1005, R1008, R1010, R1012.

entlein added 4 commits August 3, 2026 17:21
…ePG)

One portable postgres:17 pg-client SBoB (the contrast leg, shared across all
three backends) plus a per-fork server SBoB. Server SBoBs constrain what the
server EXECUTES (R0001) and allowlist R0002 by the server own comms: a database
data dir is inherently multi-level dynamic (WAL segments, per-OID relfiles, sort
spill filesets) and cannot be expressed with single-segment path matching.
Attack execs run as other comms, so R0001/R1000 and R0002-on-attack-files still
fire and the contrast is untouched.

CNPG additionally allowlists its mounted instance manager for R1004; bitnami
allowlists its shell init/probe toolchain for R0002.

Each fork verified live: server+client bound, 0 benign false positives under the
full functional suite, identical 14-rule attack contrast (R0001, R0002,
R0005-R0008, R0010, R0011, R1000, R1001, R1005, R1008, R1010, R1012).

deploy-distros.sh installs each fork via its native installer with an
[oss|bitnami|cnpg|all] [sbob] toggle that binds the server + client profiles;
per-fork functional + attack suites and DEMO.md included.
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e401e821-6a89-4987-ab6d-c1a0ae3a4249

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This change adds OSS, Bitnami, and CloudNativePG PostgreSQL examples. It includes Kubernetes deployments, SBoB ContainerProfiles, functional test suites, attack suites, rule coverage, deployment automation, and a demonstration guide.

Changes

PostgreSQL distro resources

Layer / File(s) Summary
Distro resources and deployment control
example/postgres/distros/postgres-*.yaml, example/postgres/distros/deploy-distros.sh
Adds Kubernetes resources for three PostgreSQL distributions. The deployment script installs each distro, waits for readiness, applies optional SBoB bindings, and supports individual or all-distro deployment.

SBoB profile policies

Layer / File(s) Summary
Client and server ContainerProfiles
example/postgres/distros/sbobs/*
Adds executable, filesystem, network, ingress, and process policies for the shared client and each PostgreSQL server variant.

Functional PostgreSQL suites

Layer / File(s) Summary
Functional command coverage
example/postgres/distros/functional/*
Adds readiness, SQL, schema, CRUD, pgbench, administration, dump and restore, maintenance, and cleanup tests for all three distributions. Privilege-dependent database and role tests omit exit-code assertions.

Attack detection suites

Layer / File(s) Summary
Attack probes and rule sweeps
example/postgres/distros/attacks/*
Adds reconnaissance, execution, credential-access, discovery, egress, impact, endpoint, and rule-family probes. Privileged PostgreSQL and CVE probes are documented as separate-suite coverage.

Coverage and demonstration

Layer / File(s) Summary
Rule coverage and demo workflow
kubescape/rule-coverage.yaml, example/postgres/distros/DEMO.md
Adds coverage entries for the three distributions and documents deployment, profile binding, test execution, Alertmanager comparison, and verified detection results.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant deploy_distros_sh
  participant Kubernetes
  participant FunctionalTests
  participant AttackSuites
  participant Alertmanager

  Operator->>deploy_distros_sh: Select PostgreSQL distro and optional sbob binding
  deploy_distros_sh->>Kubernetes: Deploy resources and apply ContainerProfiles
  Kubernetes-->>deploy_distros_sh: Return ready workloads
  Operator->>FunctionalTests: Run PostgreSQL functional suite
  Operator->>AttackSuites: Run PostgreSQL attack suite
  AttackSuites->>Alertmanager: Emit expected rule alerts
  Alertmanager-->>Operator: Report functional and attack alert results
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the PostgreSQL contrast across OSS, Bitnami, and CloudNativePG packaging variants.
Description check ✅ Passed The description accurately explains the three PostgreSQL distributions, shared client SBoB, server SBoBs, testing, and attack coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch postgres-distros

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

🤖 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/postgres/distros/DEMO.md`:
- Around line 5-8: Add the sh language identifier to the fenced code blocks
around the make commands and other shell snippets referenced by the review,
including the blocks near lines 5, 35, 43, 51, and 62. Insert a blank line after
the deployment fence before the following parenthetical text to satisfy Markdown
spacing rules.
- Around line 16-20: Update the distro documentation table to show CNPG version
1.24.1 and add the exact Bitnami PostgreSQL chart version. In deploy-distros.sh,
modify the Bitnami helm install command to pass the same pinned version via
--version, keeping the documented and installed versions aligned.
- Around line 33-55: Update the command paths in the deployment and test
instructions so they work from the documented repository root: prefix
deploy-distros.sh, functional suite files, and attacks suite files with
example/postgres/distros, or explicitly change into that directory before Step 1
and keep all commands consistent.
- Around line 59-72: Update the alert-splitting command around the Python
pipeline to fail closed when T0 is unavailable: require T0 to be set and
exported before invoking the pipeline, or pass it explicitly to Python, rather
than defaulting to an empty string in os.environ.get. Preserve the existing
namespace filtering and timestamp classification once a valid T0 is supplied.

In `@example/postgres/distros/deploy-distros.sh`:
- Around line 91-94: Update do_distro so the conditional bind-function
invocation does not use “|| true” to mask failures; when SBOB equals sbob,
execute "$2" in an explicit conditional while allowing its non-zero status to
propagate and preserve the no-op behavior otherwise.
- Around line 28-37: Update deploy_bitnami() to pass an explicit --version for
the Bitnami PostgreSQL OCI chart, using the exact chart version that generated
sbobs/cp-postgres-bitnami.yaml; keep the existing installation and deployment
steps unchanged.

In `@example/postgres/distros/postgres-cnpg.yaml`:
- Around line 25-35: Move the CREATE EXTENSION IF NOT EXISTS pg_stat_statements
statement from postInitSQL to postInitApplicationSQL so it runs in the app
database used by pg-client and the functional test, while preserving the
existing table initialization statements.

In `@example/postgres/distros/postgres-oss.yaml`:
- Around line 29-48: Add a readinessProbe to the postgres container definition
using an exec check that runs pg_isready against the configured database user,
so Kubernetes reports readiness only after PostgreSQL accepts connections. Keep
the existing container configuration, ports, and volume mounts unchanged.

In `@example/postgres/distros/sbobs/cp-pg-client.yaml`:
- Around line 136-141: Update the shared client profile entries around the
PostgreSQL command arguments to include every fork-specific service host: pg-rw,
pg-bitnami-postgresql, and postgres. Preserve the existing recorded arguments
while adding the Bitnami and OSS host values, or split the profile so each fork
records its actual -h value.
- Around line 31-42: Update the pg-client egress configuration to cover each
PostgreSQL fork used by deploy-distros.sh: retain the OSS selector
app.kubernetes.io/name: postgres, add a separate entry for Bitnami’s
app.kubernetes.io/name: postgresql selector, and add an entry matching CNPG
instance pods using their actual server selector. Do not leave the shared egress
limited to the OSS label.

In `@example/postgres/distros/sbobs/cp-postgres-bitnami.yaml`:
- Around line 278-317: The three server SBoB profiles contain invalid leaf path
fragments and an overly broad root wildcard instead of full filesystem paths.
Re-learn the opens entries for
example/postgres/distros/sbobs/cp-postgres-bitnami.yaml lines 278-317,
cp-postgres-cnpg.yaml lines 174-278, and cp-postgres-oss.yaml lines 200-260, or
replace each fragment with its full container path; remove the Bitnami path: /*
entry and use valid per-segment DynamicPathDetector wildcards (⋯) where needed.

In `@example/postgres/distros/sbobs/cp-postgres-cnpg.yaml`:
- Around line 29-30: Document in DEMO.md that this CNPG profile assumes the
default k3s service CIDR 10.43.0.0/24 and pod CIDR 10.42.0.0/24, and state that
the profile must be re-learned when deploying on a cluster with different CIDRs.

In `@kubescape/rule-coverage.yaml`:
- Around line 72-101: Add a metrics or metrics_glob configuration to each
PostgreSQL coverage entry: postgres-oss, postgres-bitnami, and postgres-cnpg.
Point each setting to its corresponding per-distro verifier output so the
renderer uses tuner detections and preserves the documented 14-rule verified
result instead of relying only on suite claims.
🪄 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: a8908166-050b-4e2d-a360-5198796620ea

📥 Commits

Reviewing files that changed from the base of the PR and between bdbcb93 and 86a7a76.

⛔ Files ignored due to path filters (3)
  • example/postgres/distros/rule-coverage/postgres-bitnami-killchain.gif is excluded by !**/*.gif
  • example/postgres/distros/rule-coverage/postgres-cnpg-killchain.gif is excluded by !**/*.gif
  • example/postgres/distros/rule-coverage/postgres-oss-killchain.gif is excluded by !**/*.gif
📒 Files selected for processing (16)
  • example/postgres/distros/DEMO.md
  • example/postgres/distros/attacks/bitnami.yaml
  • example/postgres/distros/attacks/cnpg.yaml
  • example/postgres/distros/attacks/oss.yaml
  • example/postgres/distros/deploy-distros.sh
  • example/postgres/distros/functional/bitnami.yaml
  • example/postgres/distros/functional/cnpg.yaml
  • example/postgres/distros/functional/oss.yaml
  • example/postgres/distros/postgres-bitnami.yaml
  • example/postgres/distros/postgres-cnpg.yaml
  • example/postgres/distros/postgres-oss.yaml
  • example/postgres/distros/sbobs/cp-pg-client.yaml
  • example/postgres/distros/sbobs/cp-postgres-bitnami.yaml
  • example/postgres/distros/sbobs/cp-postgres-cnpg.yaml
  • example/postgres/distros/sbobs/cp-postgres-oss.yaml
  • kubescape/rule-coverage.yaml
👮 Files not reviewed due to content moderation or server errors (6)
  • example/postgres/distros/functional/oss.yaml
  • example/postgres/distros/functional/bitnami.yaml
  • example/postgres/distros/functional/cnpg.yaml
  • example/postgres/distros/attacks/oss.yaml
  • example/postgres/distros/attacks/bitnami.yaml
  • example/postgres/distros/attacks/cnpg.yaml

Comment on lines +5 to +8
```
make kubescape
make alertmanager
```

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

Fix the Markdown lint errors in the fenced blocks.

markdownlint-cli2 reports MD040 for the fences at Lines 5, 35, 43, 51, and 62. Add sh to each fence. It also reports MD031 after the deployment fence. Add a blank line before the parenthetical text.

Also applies to: 35-39, 43-47, 51-55, 62-73

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 5-5: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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/distros/DEMO.md` around lines 5 - 8, Add the sh language
identifier to the fenced code blocks around the make commands and other shell
snippets referenced by the review, including the blocks near lines 5, 35, 43,
51, and 62. Insert a blank line after the deployment fence before the following
parenthetical text to satisfy Markdown spacing rules.

Source: Linters/SAST tools

Comment thread example/postgres/distros/DEMO.md Outdated
Comment on lines +16 to +20
| distro | installer | ns | server profile | client profile |
|---------|------------------------------|------------------|------------------------------|-------------------------|
| oss | plain Deployment | postgres-oss | sbobs/cp-postgres-oss.yaml | sbobs/cp-pg-client.yaml |
| bitnami | helm bitnami/postgresql | postgres-bitnami | sbobs/cp-postgres-bitnami.yaml | sbobs/cp-pg-client.yaml |
| cnpg | CloudNativePG operator 1.24 | postgres-cnpg | sbobs/cp-postgres-cnpg.yaml | sbobs/cp-pg-client.yaml |

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:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 5 \
  'helm install|--version|cnpg-1\.24\.1|release-1\.24' \
  example/postgres/distros/deploy-distros.sh \
  example/postgres/distros/DEMO.md

Repository: k8sstormcenter/bob

Length of output: 2229


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== relevant files =="
git ls-files example/postgres/distros || true

echo
echo "== deploy-distros.sh =="
cat -n example/postgres/distros/deploy-distros.sh

echo
echo "== DEMO.md relevant lines =="
sed -n '1,80p' example/postgres/distros/DEMO.md | cat -n

echo
echo "== postgres Bitnami manifests mentioning chart/image/release/version =="
rg -n -i 'chart|postgres|bitnami|registry|image:|tag:|dependency|refRepo|appVersion|--version|version' example/postgres/distros -g '*.yaml' -g '*.md'

Repository: k8sstormcenter/bob

Length of output: 50376


🌐 Web query:

Bitnami PostgreSQL Helm chart OCI registry release tags latest version

💡 Result:

As of August 3, 2026, the Bitnami PostgreSQL Helm chart is distributed via the OCI registry at oci://registry-1.docker.io/bitnamicharts/postgresql [1][2][3]. The latest version of the chart is 18.8.4, released on July 29, 2026 [4]. Key information regarding OCI registry tags and versioning: - Discovery: Because Bitnami OCI charts do not provide a traditional index.yaml file, you can discover available versions via the registry UI (e.g., Docker Hub) or by using Helm commands to inspect the repository [5][3]. - Versioning: You can pull or install a specific version by using the --version flag with the Helm CLI [5][6][3]. Example: helm pull oci://registry-1.docker.io/bitnamicharts/postgresql --version 18.8.4 [5] - Best Practice: It is strongly recommended to use specific, immutable version tags in production environments rather than relying on a latest tag, which can change unexpectedly [7]. For the most up-to-date tracking, you can check the official Artifact Hub page for the Bitnami PostgreSQL chart [8][6].

Citations:


Pin and document the exact Bitnami chart version.

deploy-distros.sh installs oci://registry-1.docker.io/bitnamicharts/postgresql without --version, while the CNPG install already uses cnpg-1.24.1.yaml. Update DEMO.md to CNPG 1.24.1, then add a pinned --version to the Bitnami helm install and record that chart version in DEMO.md.

🤖 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/distros/DEMO.md` around lines 16 - 20, Update the distro
documentation table to show CNPG version 1.24.1 and add the exact Bitnami
PostgreSQL chart version. In deploy-distros.sh, modify the Bitnami helm install
command to pass the same pinned version via --version, keeping the documented
and installed versions aligned.

Comment on lines +33 to +55
## 1. Deploy (+ optionally bind the SBoB)

```
./deploy-distros.sh oss # deploy only
./deploy-distros.sh oss sbob # deploy AND bind the server + client SBoBs
```
(swap `oss` for `bitnami` | `cnpg` | `all`)

## 2. Functional (benign) suite — expect no detections

```
bobctl test --functional-tests functional/oss.yaml -n postgres-oss
bobctl test --functional-tests functional/bitnami.yaml -n postgres-bitnami
bobctl test --functional-tests functional/cnpg.yaml -n postgres-cnpg
```

## 3. Attack suite — expect detections

```
bobctl attack --attack-suite attacks/oss.yaml -n postgres-oss
bobctl attack --attack-suite attacks/bitnami.yaml -n postgres-bitnami
bobctl attack --attack-suite attacks/cnpg.yaml -n postgres-cnpg
```

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

Make the commands consistent with the documented working directory.

The document says to start from the repository root, but ./deploy-distros.sh, functional/..., and attacks/... are paths under example/postgres/distros. The deployment command and all suite commands fail as written.

Either change directory before Step 1 or use repository-root-relative paths.

Proposed path fix
-./deploy-distros.sh oss
+./example/postgres/distros/deploy-distros.sh oss

-./deploy-distros.sh oss sbob
+./example/postgres/distros/deploy-distros.sh oss sbob

-bobctl test --functional-tests functional/oss.yaml     -n postgres-oss
-bobctl test --functional-tests functional/bitnami.yaml -n postgres-bitnami
-bobctl test --functional-tests functional/cnpg.yaml    -n postgres-cnpg
+bobctl test --functional-tests example/postgres/distros/functional/oss.yaml     -n postgres-oss
+bobctl test --functional-tests example/postgres/distros/functional/bitnami.yaml -n postgres-bitnami
+bobctl test --functional-tests example/postgres/distros/functional/cnpg.yaml    -n postgres-cnpg

-bobctl attack --attack-suite attacks/oss.yaml     -n postgres-oss
-bobctl attack --attack-suite attacks/bitnami.yaml -n postgres-bitnami
-bobctl attack --attack-suite attacks/cnpg.yaml    -n postgres-cnpg
+bobctl attack --attack-suite example/postgres/distros/attacks/oss.yaml     -n postgres-oss
+bobctl attack --attack-suite example/postgres/distros/attacks/bitnami.yaml -n postgres-bitnami
+bobctl attack --attack-suite example/postgres/distros/attacks/cnpg.yaml    -n postgres-cnpg
📝 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
## 1. Deploy (+ optionally bind the SBoB)
```
./deploy-distros.sh oss # deploy only
./deploy-distros.sh oss sbob # deploy AND bind the server + client SBoBs
```
(swap `oss` for `bitnami` | `cnpg` | `all`)
## 2. Functional (benign) suite — expect no detections
```
bobctl test --functional-tests functional/oss.yaml -n postgres-oss
bobctl test --functional-tests functional/bitnami.yaml -n postgres-bitnami
bobctl test --functional-tests functional/cnpg.yaml -n postgres-cnpg
```
## 3. Attack suite — expect detections
```
bobctl attack --attack-suite attacks/oss.yaml -n postgres-oss
bobctl attack --attack-suite attacks/bitnami.yaml -n postgres-bitnami
bobctl attack --attack-suite attacks/cnpg.yaml -n postgres-cnpg
```
## 1. Deploy (+ optionally bind the SBoB)
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 35-35: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 38-38: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 43-43: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 51-51: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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/distros/DEMO.md` around lines 33 - 55, Update the command
paths in the deployment and test instructions so they work from the documented
repository root: prefix deploy-distros.sh, functional suite files, and attacks
suite files with example/postgres/distros, or explicitly change into that
directory before Step 1 and keep all commands consistent.

Comment on lines +59 to +72
Port-forward alertmanager and split alerts by time (`$T0` = a timestamp taken
right before step 3; `NS` = the distro namespace):

```
kubectl -n honey port-forward svc/alertmanager 9093:9093 &
curl -s localhost:9093/api/v2/alerts | python3 -c '
import json,sys,os
from collections import Counter
a=json.load(sys.stdin); ns=os.environ["NS"]; t0=os.environ.get("T0","")
al=[x for x in a if x["labels"].get("namespace")==ns]
fp=[x for x in al if x.get("startsAt","")<t0]
tp=sorted({x["labels"].get("rule_id") for x in al if x.get("startsAt","")>=t0})
print("functional FPs:", len(fp), dict(Counter(x["labels"].get("rule_id") for x in fp)) or "CLEAN")
print("attack TPs (distinct rules):", len(tp), tp)'

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

Fail closed when T0 is missing.

os.environ.get("T0", "") converts a missing or unexported T0 into an empty string. Every alert with a non-empty startsAt then satisfies startsAt >= t0, so the script can report no functional false positives and count all namespace alerts as attack true positives.

Require and export T0 before running the pipeline, or pass it as an explicit argument.

Proposed validation
+ : "${NS:?Set NS to the distro namespace}"
+ : "${T0:?Set T0 to the timestamp before step 3}"
+ export NS T0
+
  curl -s localhost:9093/api/v2/alerts | python3 -c '
  import json,sys,os
  from collections import Counter
- a=json.load(sys.stdin); ns=os.environ["NS"]; t0=os.environ.get("T0","")
+ a=json.load(sys.stdin); ns=os.environ["NS"]; t0=os.environ["T0"]
📝 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
Port-forward alertmanager and split alerts by time (`$T0` = a timestamp taken
right before step 3; `NS` = the distro namespace):
```
kubectl -n honey port-forward svc/alertmanager 9093:9093 &
curl -s localhost:9093/api/v2/alerts | python3 -c '
import json,sys,os
from collections import Counter
a=json.load(sys.stdin); ns=os.environ["NS"]; t0=os.environ.get("T0","")
al=[x for x in a if x["labels"].get("namespace")==ns]
fp=[x for x in al if x.get("startsAt","")<t0]
tp=sorted({x["labels"].get("rule_id") for x in al if x.get("startsAt","")>=t0})
print("functional FPs:", len(fp), dict(Counter(x["labels"].get("rule_id") for x in fp)) or "CLEAN")
print("attack TPs (distinct rules):", len(tp), tp)'
Port-forward alertmanager and split alerts by time (`$T0` = a timestamp taken
right before step 3; `NS` = the distro namespace):
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 62-62: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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/distros/DEMO.md` around lines 59 - 72, Update the
alert-splitting command around the Python pipeline to fail closed when T0 is
unavailable: require T0 to be set and exported before invoking the pipeline, or
pass it explicitly to Python, rather than defaulting to an empty string in
os.environ.get. Preserve the existing namespace filtering and timestamp
classification once a valid T0 is supplied.

Comment on lines +28 to +37
deploy_bitnami() {
helm install pg-bitnami oci://registry-1.docker.io/bitnamicharts/postgresql \
--set auth.database=app --set auth.username=app \
--set auth.password=bobtest --set auth.postgresPassword=bobtest \
--set primary.persistence.enabled=false \
-n postgres-bitnami --create-namespace
kubectl apply -f postgres-bitnami.yaml
kubectl -n postgres-bitnami rollout status statefulset/pg-bitnami-postgresql --timeout=180s
kubectl -n postgres-bitnami wait --for=condition=ready pod/pg-client --timeout=60s
}

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
# Confirm the documented Bitnami chart version and check whether other scripts pin chart versions.
rg -n -C2 'helm (install|upgrade)' --glob '*.sh' --glob '*.md' .
rg -n 'bitnami/postgresql|chart version|--version' example/postgres

Repository: k8sstormcenter/bob

Length of output: 8258


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== deploy-distros relevant lines =="
sed -n '1,80p' example/postgres/distros/deploy-distros.sh | cat -n

echo
echo "== Bitnami profile file header/relevant paths =="
sed -n '1,120p' example/postgres/distros/sbobs/cp-postgres-bitnami.yaml | cat -n

echo
echo "== README context =="
sed -n '1,80p' example/postgres/distros/DEMO.md | cat -n

echo
echo "== Check Helm availability and chart metadata if available at OCI registry =="
if command -v helm >/dev/null 2>&1; then
  VERSIONS_OUT="$(helm search oci registry-1.docker.io/bitnamicharts/postgresql --versions 2>/dev/null || true)"
  echo "HELM_VERSIONS_SAMPLE"
  printf '%s\n' "$VERSIONS_OUT" | head -80 || true
  echo "LATEST_VERSION"
  printf '%s\n' "$VERSIONS_OUT" | awk 'NR>1 {print $1; exit}' || true
else
  echo "no helm binary"
fi

echo
echo "== Deterministic source fact: does Bitnami install command contain --version? =="
python3 - <<'PY'
from pathlib import Path
p = Path("example/postgres/distros/deploy-distros.sh")
text = p.read_text()
block = []
inside = False
for line in text.splitlines():
    if "deploy_bitnami() {" in line:
        inside = True
    if inside:
        block.append(line)
        if line.strip().startswith("}"):
            break
print("\n".join(block))
print("has_version_flag=", any("--version" in line for line in block))
PY

Repository: k8sstormcenter/bob

Length of output: 14386


🌐 Web query:

Bitnami postgresql chart versions registry-1.docker.io/bitnamicharts/postgresql version 16.7.27

💡 Result:

Bitnami Helm chart version 16.7.27 for PostgreSQL was released on August 26, 2025 [1][2][3]. This version is part of the Bitnami Helm charts collection, which is hosted as Open Container Initiative (OCI) artifacts on the Docker Hub registry at registry-1.docker.io/bitnamicharts/postgresql [4][5][6]. Key information regarding this release: - Release Date: August 26, 2025 [1][3]. - Changelog: The primary change for this version was an update to dependency references (GitHub pull request #36195) [1][2][7]. - Source Tracking: Bitnami maintains detailed CHANGELOG.md files for each chart, which can be found in the official bitnami/charts GitHub repository [1][3][8]. - Verification: You can view specific release details or verify the chart version via platforms like Artifact Hub [9]. To install or pull this specific version of the chart, you can use the following Helm command: helm pull oci://registry-1.docker.io/bitnamicharts/postgresql --version 16.7.27

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate all install scripts =="
git ls-files '*deploy-distros.sh' || true

echo
echo "== Search pinning/version details in installed chart/config sources =="
rg -n 'bitnami/postgresql|16\.7\.27|bitnami/postgresql@|version:' example README.md .github 2>/dev/null || true

echo
echo "== Inspect remaining distros around chart installs =="
git ls-files '*deploy-distros.sh' | while read -r f; do
  echo "--- $f"
  sed -n '1,55p' "$f" | cat -n
done

Repository: k8sstormcenter/bob

Length of output: 17313


Pin the exact Bitnami PostgreSQL chart version.

deploy_bitnami() installs from the OCI chart without --version, while the repo intent is to install each distro exactly pinned and the documented version is only 16.x. Add the chart version used to generate sbobs/cp-postgres-bitnami.yaml, or the install can diverge between runs.

🔧 Proposed fix
   helm install pg-bitnami oci://registry-1.docker.io/bitnamicharts/postgresql \
+    --version 16.7.27 \
     --set auth.database=app --set auth.username=app \
📝 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
deploy_bitnami() {
helm install pg-bitnami oci://registry-1.docker.io/bitnamicharts/postgresql \
--set auth.database=app --set auth.username=app \
--set auth.password=bobtest --set auth.postgresPassword=bobtest \
--set primary.persistence.enabled=false \
-n postgres-bitnami --create-namespace
kubectl apply -f postgres-bitnami.yaml
kubectl -n postgres-bitnami rollout status statefulset/pg-bitnami-postgresql --timeout=180s
kubectl -n postgres-bitnami wait --for=condition=ready pod/pg-client --timeout=60s
}
deploy_bitnami() {
helm install pg-bitnami oci://registry-1.docker.io/bitnamicharts/postgresql \
--version 16.7.27 \
--set auth.database=app --set auth.username=app \
--set auth.password=bobtest --set auth.postgresPassword=bobtest \
--set primary.persistence.enabled=false \
-n postgres-bitnami --create-namespace
kubectl apply -f postgres-bitnami.yaml
kubectl -n postgres-bitnami rollout status statefulset/pg-bitnami-postgresql --timeout=180s
kubectl -n postgres-bitnami wait --for=condition=ready pod/pg-client --timeout=60s
}
🤖 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/distros/deploy-distros.sh` around lines 28 - 37, Update
deploy_bitnami() to pass an explicit --version for the Bitnami PostgreSQL OCI
chart, using the exact chart version that generated
sbobs/cp-postgres-bitnami.yaml; keep the existing installation and deployment
steps unchanged.

Comment thread example/postgres/distros/deploy-distros.sh
Comment on lines +25 to +35
postInitSQL:
- CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
postInitApplicationSQL:
- |
CREATE TABLE IF NOT EXISTS bobtest (
id SERIAL PRIMARY KEY,
key TEXT NOT NULL,
value TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
- INSERT INTO bobtest (key, value) VALUES ('init', 'baseline');

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how the functional suites use pg_stat_statements and \dx across the three forks.
fd -t f -e yaml . example/postgres/distros/functional --exec rg -n 'pg_stat_statements|\\dx|shared_preload' {} +
rg -n 'pg_stat_statements|shared_preload_libraries' example/postgres --glob '!**/sbobs/**'

Repository: k8sstormcenter/bob

Length of output: 863


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- postgres-cnpg relevant lines ---\n'
sed -n '1,120p' example/postgres/distros/postgres-cnpg.yaml | cat -n

printf '\n--- postgres-oss relevant lines ---\n'
sed -n '1,120p' example/postgres/distros/postgres-oss.yaml | cat -n

printf '\n--- cluster relevant lines ---\n'
sed -n '1,80p' example/postgres/cluster.yaml | cat -n

printf '\n--- functional cnpg relevant lines ---\n'
sed -n '100,130p' example/postgres/distros/functional/cnpg.yaml | cat -n

printf '\n--- search for extension check context ---\n'
rg -n -C 3 "\\\\dx|pg_stat_statements|PostgreSQL|app_db|PGDATABASE|database:" example/postgres -S

Repository: k8sstormcenter/bob

Length of output: 247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- postgres-cnpg relevant lines ---'
sed -n '1,120p' example/postgres/distros/postgres-cnpg.yaml | cat -n

echo
echo '--- postgres-oss relevant lines ---'
sed -n '1,120p' example/postgres/distros/postgres-oss.yaml | cat -n

echo
echo '--- cluster relevant lines ---'
sed -n '1,80p' example/postgres/cluster.yaml | cat -n

echo
echo '--- functional cnpg relevant lines ---'
sed -n '100,130p' example/postgres/distros/functional/cnpg.yaml | cat -n

echo
echo '--- search for extension check context ---'
rg -n -C 3 '\\\\dx|pg_stat_statements|PostgreSQL|app_db|PGDATABASE|database:' example/postgres -S

Repository: k8sstormcenter/bob

Length of output: 33824


🌐 Web query:

CloudNativePG Cluster bootstrap initdb postInitSQL executed on clusterdb documentation

💡 Result:

In CloudNativePG, when using the initdb bootstrap method, the postInitSQL stanza allows you to execute custom SQL queries immediately after a new PostgreSQL cluster is initialized [1][2]. These queries are executed as the postgres superuser [1][3]. CloudNativePG provides three specific stanzas for executing post-initialization SQL, which run in this specific order against their respective target databases [1][4]: 1. postInitSQL: Executed against the postgres database [1][4]. 2. postInitTemplateSQL: Executed against the template1 database [1][4]. Queries here are often used to enable extensions so they are automatically available in all subsequently created databases [5]. 3. postInitApplicationSQL: Executed against the application database (defined in the initdb.database field) [1][4]. You can provide these queries in two ways: * Directly in the cluster definition as a list of SQL queries [1][2]. * Via references to Kubernetes Secrets or ConfigMaps using the corresponding Refs stanzas (e.g., postInitSQLRefs, postInitTemplateSQLRefs, and postInitApplicationSQLRefs) [1][4]. When using Refs, Secrets are processed before ConfigMaps [1][4]. Important Considerations: * Execution Failure: If any of the provided SQL queries fail, the bootstrap phase is interrupted, which may leave the cluster in an incomplete or non-functional state [3][4]. * Permissions: Queries are executed with superuser privileges; use these options with extreme caution to avoid disrupting the cluster [3][6]. * Scope: Ensure your queries are targeting the correct database, as the scope of postInitSQL (the postgres database) differs from postInitApplicationSQL (the application-specific database) [5].

Citations:


🌐 Web query:

CloudNativePG pg_stat_statements shared_preload_libraries configure postgresql.conf

💡 Result:

In CloudNativePG, you do not need to manually configure shared_preload_libraries to enable pg_stat_statements. The operator automatically manages the shared_preload_libraries setting for well-known extensions, including pg_stat_statements [1][2][3]. To enable and configure pg_stat_statements, you should define the relevant pg_stat_statements parameters within the postgresql.parameters section of your Cluster resource [1][2][4]. The CloudNativePG operator detects these parameters and automatically adds pg_stat_statements to the shared_preload_libraries list [1][5]. Example configuration in your Cluster resource: apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: my-cluster spec: #... postgresql: parameters: pg_stat_statements.max: "10000" pg_stat_statements.track: all #... Once these parameters are set, the operator will automatically add the library to shared_preload_libraries and execute the CREATE EXTENSION IF NOT EXISTS pg_stat_statements command on each database [1][2][3]. Important Considerations: - Manual overriding: While you can manually add libraries to.spec.postgresql.shared_preload_libraries, the operator will merge your manual entries with those it manages automatically [1][2][3]. - Database CRD limitations: Note that adding pg_stat_statements via the Database CRD alone will not trigger the automatic management of shared_preload_libraries; you must configure the parameters in the Cluster resource [5]. - Immutability: You should not attempt to manually edit the postgresql.conf file on the pods, as CloudNativePG uses a declarative approach where the operator maintains the configuration [6][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- cnpg 1.24 bootstrap source reference ---'
curl -fsSL https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/v1.24.0/internal/management/controller/bootstrap/bootstrap.go | rg -n -C 4 'postInitSQL|postInitTemplateSQL|postInitApplicationSQL|Execute script ' || true

echo
echo '--- cnpg 1.24 bootstrap test/source constants ---'
curl -fsSL https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/v1.24.0/api/v1/cluster_webhook.go | rg -n -C 3 'postInitTemplateSQL|postInitApplicationSQL|postgres' || true

echo
echo '--- all CNPG configs containing CREATE EXTENSION pg_stat_statements ---'
rg -n -C 3 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements' example/postgres/distros/postgres-cnpg.yaml example/postgres/cluster.yaml

Repository: k8sstormcenter/bob

Length of output: 17143


Move extension creation to the app database.

CNPG runs postInitSQL against postgres, while pg-client connects to PGDATABASE: app. Since CREATE EXTENSION creates a database-local extension and the functional test runs \dx against app, create pg_stat_statements with postInitApplicationSQL to match the OSS initialization behavior.

🤖 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/distros/postgres-cnpg.yaml` around lines 25 - 35, Move the
CREATE EXTENSION IF NOT EXISTS pg_stat_statements statement from postInitSQL to
postInitApplicationSQL so it runs in the app database used by pg-client and the
functional test, while preserving the existing table initialization statements.

Comment on lines +278 to +317
opens:
- flags:
- O_CLOEXEC
- O_CREAT
- O_DIRECTORY
- O_EXCL
- O_NOFOLLOW
- O_NONBLOCK
- O_RDONLY
- O_RDWR
- O_TRUNC
- O_WRONLY
path: /*
- flags:
- O_CLOEXEC
- O_CREAT
- O_RDONLY
- O_RDWR
- O_TRUNC
path: /*/0.0
- flags:
- O_CLOEXEC
- O_CREAT
- O_RDONLY
- O_RDWR
- O_TRUNC
path: /*/1.0
- flags:
- O_CLOEXEC
- O_RDONLY
path: /*/libpq.so.5
- flags:
- O_CLOEXEC
- O_DIRECTORY
- O_NOCTTY
- O_NOFOLLOW
- O_NONBLOCK
- O_RDONLY
- O_RDWR
path: /⋯

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# List the suspicious short `opens` paths per profile and search the repo for documented path compaction rules.
for f in $(fd -t f -e yaml . example/postgres/distros/sbobs); do
  echo "== $f"; rg -n '^\s+path: /[^/]{0,14}$' "$f"
done
rg -n -C3 'truncat|compact|dynamic path|⋯' --glob '*.md' --glob '*.go' . | head -40

Repository: k8sstormcenter/bob

Length of output: 3632


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant files =="
git ls-files 'example/postgres/distros/sbobs/*.yaml' 'example/postgres/distros/DEMO.md'

echo
echo "== DEMO context =="
cat -n example/postgres/distros/DEMO.md | sed -n '1,80p'

echo
echo "== dynamicpathdetector files =="
git ls-files 'storage/pkg/registry/file/*dynamicpathdetector*' || true
for f in $(git ls-files 'storage/pkg/registry/file/*dynamicpathdetector*' 2>/dev/null); do
  echo "--- $f"
  wc -l "$f"
  ast-grep outline "$f" || true
  sed -n '1,240p' "$f"
done

echo
echo "== SBoB parser/compiler matches =="
rg -n -C3 'sbobs|SBoB|opens:|path: \\*\*|O_CLOEXEC|dynamicpathdetector|MatchPath|wildcard|wildcard' --glob '*.go' --glob '*.md' . | head -200

Repository: k8sstormcenter/bob

Length of output: 20316


Re-learn the server SBoBs from full filesystem paths.

The opens lists contain leaf fragments such as /449, /so, /o, /mp, /eckpoint, and /0000001 instead of real container paths. The Bitnami profile also contains path: /*, which allows any open on the root filesystem. The DynamicPathDetector guidance documents per segment , but these short paths are not valid segment wildcards, so these entries do not describe the workload baseline. Re-learn the three server profiles, or replace the fragments with the full paths.

📍 Affects 3 files
  • example/postgres/distros/sbobs/cp-postgres-bitnami.yaml#L278-L317 (this comment)
  • example/postgres/distros/sbobs/cp-postgres-cnpg.yaml#L174-L278
  • example/postgres/distros/sbobs/cp-postgres-oss.yaml#L200-L260
🤖 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/distros/sbobs/cp-postgres-bitnami.yaml` around lines 278 -
317, The three server SBoB profiles contain invalid leaf path fragments and an
overly broad root wildcard instead of full filesystem paths. Re-learn the opens
entries for example/postgres/distros/sbobs/cp-postgres-bitnami.yaml lines
278-317, cp-postgres-cnpg.yaml lines 174-278, and cp-postgres-oss.yaml lines
200-260, or replace each fragment with its full container path; remove the
Bitnami path: /* entry and use valid per-segment DynamicPathDetector wildcards
(⋯) where needed.

Comment on lines +29 to +30
ipAddresses:
- 10.43.0.0/24

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 | 🟡 Minor | ⚡ Quick win

The CNPG profile pins k3s-specific CIDRs.

Line 30 allows egress to 10.43.0.0/24 and Line 160 allows ingress from 10.42.0.0/24. These are the default k3s service CIDR and pod CIDR. On a cluster with different CIDRs the apiserver egress and the operator ingress fall outside the allowlist, and the demo then reports benign false positives.

The second ingress entry also sets podSelector: null with type: external, so it admits port 8000 from the whole pod CIDR.

Document the cluster assumption in DEMO.md, or note that the profile must be re-learned on a cluster with other CIDRs.

Also applies to: 149-160

🤖 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/distros/sbobs/cp-postgres-cnpg.yaml` around lines 29 - 30,
Document in DEMO.md that this CNPG profile assumes the default k3s service CIDR
10.43.0.0/24 and pod CIDR 10.42.0.0/24, and state that the profile must be
re-learned when deploying on a cluster with different CIDRs.

Comment on lines +72 to +101
# PostgreSQL packaging distros — ONE portable pg-client SBoB (the contrast leg,
# postgres:17 across all three) plus a per-fork server SBoB. Every fork: server
# AND client bound, 0 benign false positives under the full functional suite,
# and the SAME 14-rule attack contrast. A database's data dir is inherently
# dynamic (WAL/OID relfiles/temp spill), so the server SBoB constrains what the
# server EXECUTES (R0001) and allowlists R0002 for the server's own comms rather
# than chasing unmatchable file paths — attack execs run as other comms, so the
# contrast is untouched.
- name: postgres-oss
title: postgres (docker library 17)
out: example/postgres/distros/rule-coverage/postgres-oss-killchain.gif
suites: [example/postgres/distros/attacks/oss.yaml]
note: official postgres:17 Deployment; server+client SBoB bound, 0 benign FP
# R1004: the manager/staged exec fires R0001+R1000 here, never R1004. R1006:
# unshare is cluster-config-dependent; R0001 catches the exec unconditionally.
probe: [R1004, R1006]

- name: postgres-bitnami
title: postgres (bitnami)
out: example/postgres/distros/rule-coverage/postgres-bitnami-killchain.gif
suites: [example/postgres/distros/attacks/bitnami.yaml]
note: bitnami/postgresql helm chart; server SBoB allowlists the shell init toolchain
probe: [R1004, R1006]

- name: postgres-cnpg
title: postgres (CloudNativePG)
out: example/postgres/distros/rule-coverage/postgres-cnpg-killchain.gif
suites: [example/postgres/distros/attacks/cnpg.yaml]
note: CloudNativePG operator; server SBoB allowlists the mounted instance `manager` (R1004)
probe: [R1004, R1006]

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:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 4 \
  'name: postgres-(oss|bitnami|cnpg)|metrics(_glob)?:|suites:' \
  kubescape/rule-coverage.yaml

fd -t f -e json . results/legs | sort | rg 'postgres'

Repository: k8sstormcenter/bob

Length of output: 4694


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^kubescape/rule-coverage\.yaml$|^scripts/render-rule-coverage-gif\.py$|distros|results|DEMO\.md)' | sed -n '1,220p'

echo
echo "== postgres coverage entries =="
python3 - <<'PY'
from pathlib import Path
p = Path('kubescape/rule-coverage.yaml')
lines = p.read_text().splitlines()
for i,line in enumerate(lines,1):
    if 'name: postgres-' in line or line.strip() in ('metrics:', 'metrics_glob:', 'suites:'):
        start=max(1,i-8); end=min(len(lines),i+12)
        print(f'--- context {start}-{end} ---')
        for n in range(start,end+1):
            print(f'{n:4}: {lines[n-1]}')
PY

echo
echo "== render script relevant sections =="
if [ -f scripts/render-rule-coverage-gif.py ]; then
  ast-grep outline scripts/render-rule-coverage-gif.py --view compact | sed -n '1,220p' || true
  rg -n -C 6 'run_config|metrics|verified_rules|glob|matches' scripts/render-rule-coverage-gif.py
  sed -n '1,260p' scripts/render-rule-coverage-gif.py
fi

echo
echo "== candidate result directories =="
for d in results example example/postgres/distros results/legs; do
  [ -d "$d" ] && { echo "--- $d"; find "$d" -maxdepth 3 -type f | sed -n '1,120p'; }
done

Repository: k8sstormcenter/bob

Length of output: 32445


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== DEMO relevant lines =="
sed -n '60,90p' example/postgres/distros/DEMO.md

echo
echo "== postgres distros result/output-related files =="
files=(
  example/postgres/distros/postgres-oss.yaml
  example/postgres/distros/postgres-bitnami.yaml
  example/postgres/distros/postgres-cnpg.yaml
  example/postgres/distros/rule-coverage/
)
for f in "${files[@]}"; do
  [ -e "$f" ] || continue
  echo "--- $f ---"
  if [ -f "$f" ]; then
    rg -n -C 5 'metrics|results|rule-coverage|killchain|coverage' "$f" || true
  else
    find "$f" -maxdepth 3 -type f -print | sed -n '1,80p'
  fi
done

echo
echo "== behavioral probe: run_config metrics source for pg distros =="
python3 - <<'PY'
import glob
from pathlib import Path
import yaml

path = Path('kubescape/rule-coverage.yaml')
apps = yaml.safe_load(path.read_text()).get('apps', [])
for a in apps:
    if (a.get('name') or '').startswith('postgres-'):
        metrics_file = a.get('metrics') or []
        metrics_glob = a.get('metrics_glob')
        globbed = sorted(glob.glob(metrics_glob)) if metrics_glob else []
        print(a['name'])
        print('  metrics', metrics_file)
        print('  metrics_glob', metrics_glob)
        print('  files_matching_glob', globbed)
PY

Repository: k8sstormcenter/bob

Length of output: 1637


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== run_config metrics assignment =="
sed -n '274,303p' scripts/render-rule-coverage-gif.py

echo
echo "== postgres distro entry fields =="
python3 - <<'PY'
from pathlib import Path
import re
text = Path('kubescape/rule-coverage.yaml').read_text().splitlines()
entries = re.split(r'\n(?=  - name: )', text)
target_names = ['postgres-oss', 'postgres-bitnami', 'postgres-cnpg']
for entry in entries:
    hit = next((n for n in target_names if n in entry), None)
    if not hit:
        continue
    fields = {m: entry.splitlines()[i].strip() for i, m in enumerate(entry.splitlines(), 1) if any(m.strip().lower().startswith(x) for x in ['name:', 'metrics:', 'metrics_glob:', 'suites:'])}
    print(hit)
    for k,v in fields.items():
        print(f'  {k} {v}')
PY

Repository: k8sstormcenter/bob

Length of output: 1843


Add metrics or metrics_glob for each PostgreSQL distro.

run_config only passes metrics and metrics_glob into the renderer. These three entries only define suites, so the generated cards use the suite claim instead of tuner detections and cannot honor the documented 14-rule verified result unless the per-distro verifier output is wired in separately.

🤖 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 `@kubescape/rule-coverage.yaml` around lines 72 - 101, Add a metrics or
metrics_glob configuration to each PostgreSQL coverage entry: postgres-oss,
postgres-bitnami, and postgres-cnpg. Point each setting to its corresponding
per-distro verifier output so the renderer uses tuner detections and preserves
the documented 14-rule verified result instead of relying only on suite claims.

- O_RDWR
- O_TRUNC
- O_WRONLY
path: /*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

not again....

- runc:[2:INIT]
- runc:[3:INIT]
- postgres
- sh

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

you learnt the attacks...

@entlein
entlein marked this pull request as draft August 4, 2026 06:59
entlein added 2 commits August 4, 2026 17:57
…lowlist

The three server profiles allowlisted R0002 by the server's own comm, because
the learned profiles collapsed their root path segment and were unusable. That
allowlist gives the named comms free reign over the filesystem -- cp-postgres-
bitnami.yaml carried 30 of them, including sh, bash, find, cat, ls and rm, and
held only 5 opens, so file-access detection on that container was effectively
off.

The collapse had a cause outside these files. node-agent emitted open paths that
were fragments of unrelated events, non-absolute, which NormalizePath turned
into well-formed absolute paths. Each distinct fragment became its own
root-level segment; crossing OpenDynamicThreshold (50) marked the root position
dynamic and rewrote every entry to a wildcard root. With that fixed upstream of
these files, the raw learned profiles are usable and the allowlist is no longer
needed.

Relearned all three against their pinned installers on a 2-node k3s cluster,
then regenerated from the raw learned profiles:

  profile             opens  execs  caps  distinct roots
  postgres-oss          228     39     5              15
  postgres-bitnami      497     76     7              19
  postgres-cnpg         289      7     3              13

R0002/R0004 processAllowed now lists only the container-init processes, matching
the redis SBoBs. Every open carries a literal first segment; there is no bare
"/*" and no leading single-segment wildcard, either of which makes storage treat
the root as dynamic and collapse the profile.

Verified by round-trip: applying each file and reading it back from storage
returns the same entry count with no wildcard root and no "/*"-leading rewrite,
so the committed form is what is enforced.

Entries whose first segment is a data-dir child (base, global, pg_wal,
pg_logical, pg_stat, backup_label) are kept deliberately. postgres chdirs into
PGDATA and opens relative to it, and speculatively opens fork files that do not
exist yet (_fsm, _vm, _init). A failed open has no descriptor, so the full-path
resolver is never called and the relative name is recorded with a leading slash.
Resolving those needs the open gadget to resolve the dirfd or cwd for a relative
name; until then these entries are what the workload actually produces, and at
13-19 distinct roots they stay well below the collapse threshold.
postgres-distros carried pkg at 2a2b337, which predates entlein/bob#61, so none
of the volatile-path normalisation, --container retargeting, network tuning or
R0012 grading was reachable from this branch. The postgres SBoBs are raw learns
full of literal OIDs and shm segments, which is exactly what that work fixes.

Independent of PR #181: that carries the same bump for the flux work, but this
branch should not have to wait on it.
@k8sstormcenter k8sstormcenter deleted a comment from coderabbitai Bot Aug 24, 2026
@k8sstormcenter k8sstormcenter deleted a comment from coderabbitai Bot Aug 24, 2026
@k8sstormcenter k8sstormcenter deleted a comment from coderabbitai Bot Aug 24, 2026
Relearned on node-agent p85-cbdbb392 + storage svcsel-6f3a2ee6, recorded and
tuned through local-ci (Total score: 0 — 27/27 detections, zero false
positives), then generalised.

                opens  execs  ingress  volatile literals
  committed       228     39     null                 13
  proposed        206     18        1                  0

The committed file was learned on an agent that truncated paths, so it carried
fragments that can never match, plus 13 literal OIDs and shm segments that match
on exactly one run of one cluster. The new learn has 1238 opens because the
truncation fix means full paths now arrive intact; generalising collapses that to
206 with zero volatile residue and zero leading wildcards.

ingress is the storage#42 form: entity: host for the readiness probe, which has
no pod identity and which no podSelector can ever match. Previously that required
listing the pod CIDR, admitting every pod on the node as a side effect.

Kept alongside as cp-postgres-oss.raw-learn.yaml so the generalisation can be
diffed rather than taken on trust.

Placed under sbobs/proposed/ rather than replacing the committed file, so the two
can be compared before anything is swapped.

Supporting fixes, all found by running this through local-ci instead of ad-hoc:

  - local-ci profile discovery ran `grep -i pg-client | grep -v client`, which
    cancels itself out and left PROFILE empty; set -e then killed the run with no
    message. The client exclusion now only applies when the target is not itself
    a client. Broke both the discovery and the learn-poll paths.
  - three postgres distro legs added to local-ci, plus --container wiring and
    APP_DEPLOY_TARGET so a leg can deploy via deploy-distros.sh.
  - generalise-sbob.py deduped execs BEFORE collapsing args, so entries differing
    only by args survived as duplicates — dash appeared five times, chmod twice.
  - kubescape/post-render.sh had no shebang; helm died with "exec format error".
…ration

Replaces the file in place rather than adding a parallel copy — git already
holds the previous version.

               opens  execs  ingress  volatile literals
  was            228     39     null                 13
  now            206     18        1                  0

The old file was learned on an agent that truncated paths, so it shipped
fragments that can never match plus 13 literal OIDs and shm segments valid for
exactly one run. Relearned on p85-cbdbb392 + svcsel-6f3a2ee6 (1238 opens now that
full paths arrive intact), then generalised to 206 with no volatile residue and
no leading wildcards. ingress is entity: host — the readiness probe has no pod
identity, so no podSelector can match it.

Two generaliser bugs fixed first, both of which would have shipped:

  - ingress/egress and capabilities were not deduped after generalisation.
    Generalisation rewrites peers, so several learned IPs of one Service collapse
    to one serviceRef and several CIDR probe entries to one entity — duplicates
    created by the pass itself, the same class of bug as the exec duplication.
    Dedup ignores the identifier (a per-entry hash that says nothing about what
    is admitted) and unions ports so collapsing never narrows the allowance.

  - pod-CIDR probe entries were left in place ALONGSIDE the new entity: host
    entry, because removal keyed on the identifier "kubelet-probes" while learned
    entries carry hash identifiers. The profile kept admitting every pod on the
    node, making the conversion cosmetic. Removal is now by shape: sole peer spec
    is a known node/pod CIDR, with no selector, serviceRef, DNS or entity. A
    specific pod IP or a podSelector entry is untouched.

Postgres has no network entries so neither bug shows here; both matter for flux
and argo, which do.
…-suffix fix

Three defects in the previous generalisation, all found by review.

EXEC ARGS were collapsed to [path, ⋯⋯] for every binary. That discards all
argument discrimination — a profile permitting `psql -c "SELECT 1"` equally
permits `psql -c "COPY x TO PROGRAM 'sh'"`. It also forced args[0] to the binary
path, which is simply wrong here: dash runs as /bin/sh, env as
docker-entrypoint.sh, perl as psql via pg_wrapper. Matching is anchored, so the
falsified args[0] only went unnoticed because ⋯⋯ swallowed the mismatch.

Now: real args[0] preserved; binaries whose arguments can encode a command
(shells, perl, python, env, gosu, psql, xargs, nsenter) are never merged and keep
literal invocations; everything else merges position-wise within one arity only,
refusing when a wildcard would exceed a third of the vector or land straight
after -c/-e/--command/-exec. Never across arities, and no trailing ⋯⋯ — that
catch-all subsumes every narrower sibling, the exec-side twin of a leading
wildcard in opens.

Result for the six dangerous binaries: zero command-position wildcards.

  bash  docker-entrypoint.sh postgres        (was: anything)
  gosu  postgres docker-entrypoint.sh postgres
  find  ⋯ ! -user postgres -exec chown postgres {} +
  psql/perl/dash  literal invocations only

find keeps a wildcard at the path position but the -exec action stays pinned to
`chown postgres`, so `find … -exec sh` still alerts.

OPENS were undercollapsed: 20 extension files, 36 sonames and 20 locale
catalogues listed individually. Directory collapse now folds a directory to
<dir>/* when all three hold: under a static-content root (/usr/share, /usr/lib,
/lib), NOT under NEVER_COLLAPSE (/etc, /root, /home, /run/secrets, /proc, /sys,
/dev, the postgres data dirs), and every observed access read-only. A single
write means the directory is state rather than content, and collapsing it would
baseline writes too. That is why the 54 pgdata entries stay individual.

PID-SUFFIXED files were missed: postgres rewrites pg_internal.init as
pg_internal.init.<pid>, so the learn captured .58 .62 .71 .72 .73 and every
restart invents more. The rule only matched pure-numeric segments. Any
<name>.<digits> is now volatile — except sonames, since libc.so.6 and
libicudata.so.76.1 look identical but are stable ABI versions, not per-run
values; those are handled by directory collapse instead.

  opens 228 -> 98, execs 39 -> 37, volatile residue 0, catch-all args 0

Contrast unchanged at 22 Separable / 9 Ambiguous / 0 Blind — the collapse cost no
detection, which is the check that matters.
- {path: /usr/lib/postgresql/17/bin/postgres, args: [/usr/lib/postgresql/⋯/bin/postgres, -C, password_encryption]}
- {path: /usr/lib/postgresql/17/bin/postgres, args: [/usr/lib/postgresql/⋯/bin/postgres, --boot, -F, -c, log_checkpoints=false, -X, '16777216']}
- {path: /usr/lib/postgresql/17/bin/postgres, args: [/usr/lib/postgresql/⋯/bin/postgres, -D, /var/lib/postgresql/data/pgdata, -c, listen_addresses=, -p, '5432']}
- {path: /usr/lib/postgresql/17/bin/postgres, args: [/usr/lib/postgresql/⋯/bin/postgres, --check, -F, -c, log_checkpoints=false, -c, max_connections=100, -c, shared_buffers=1000, -c, dynamic_shared_memory_type=posix]}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

still too specific, the buffer size is hardly fixed. for the postgres binary, we can probably accept an actual wildcard. we just didnt want bash, cat, dash, chown etc be fully wildcarded

entlein and others added 7 commits August 24, 2026 19:23
Per review on #182: "for the postgres binary, we can probably accept an actual
wildcard. we just didnt want bash, cat, dash, chown etc be fully wildcarded."

I had applied the no-merge rule to everything, which inverted the intent. The
postgres binary is invoked eight ways here and several arguments are
node-dependent — it emits shared_buffers=16384 on one machine and 1000 on
another — so pinning them literally is a portability false positive waiting to
happen. Its identity is the discriminant: an attacker who runs the postgres
binary with different flags has achieved nothing, whereas an attacker who runs
bash has. So --core-binary marks the workload's own executable and collapses its
args to ⋯⋯, while shells, interpreters and coreutils keep literal invocations.

  oss       228 opens 39 execs  ->   98 opens 29 execs  (3 with ⋯⋯)
  bitnami   497 opens 76 execs  ->  203 opens 55 execs  (3 with ⋯⋯)
  cnpg      289 opens  7 execs  ->   89 opens  6 execs  (2 with ⋯⋯)

The ⋯⋯ entries are exactly postgres, initdb, pg_ctl (and cnpg's controller
manager). bash, cat, chown, dash, perl, psql, find and gosu remain literal, so
none of them gained a command-position wildcard.

Also made the exec path normalisation consistent with its arguments. args[0] was
being ellipsised at the version segment while path kept a literal 17, so the two
fields disagreed about the same binary — version-portable in one, pinned in the
other.

Validated rather than assumed: the oss SBoB was bound to the running server and
exercised with the 76-test benign suite. 76 passed, ZERO false positives on the
bound container. Contrast unchanged at 22 Separable / 9 Ambiguous / 0 Blind for
all three, so the wildcarding cost no detection.

Known residual, not fixed here: the dash -c invocations embed shared_buffers
numbers INSIDE a single argument. There is no intra-argument globbing, so the
only covering pattern would be a wildcard straight after -c, which is a
command-injection hole. Literal is the right trade, but those entries will drift
on a machine with different memory.
Both images built today from the same net-v2 line; drift against their branch
HEADs is chart-only for node-agent and tests+fixtures only for storage, so the
runtime code in each is current.

Deployed and rolled out clean before pinning. The three postgres SBoBs on this
branch were learned on the previous agent (p85-cbdbb392), so they are being
relearned and revalidated against this pair rather than carried over.
…erbatim

#208 supersedes the net-v2-70ebf6d0 pin I had just added. It is not merely a tag
bump: the rc1 images are an hour newer with different digests, and the ruleset
changes materially.

  R0011  net.is_private_ip gate  ->  was_address_port_protocol_in_egress
                                     + was_selector_in_egress   (port-aware)
  R0012  ingress twin added
  both   IPv6 loopback (::1) excluded alongside 127.

Taken byte-for-byte from chore/net-v2-rc1 rather than re-derived, so whichever PR
merges second is a no-op on these two files instead of a conflict — the same
approach used for #187.

Deployed and verified before committing: both images rolled out clean, 31 rules
loaded, 30 bound including R0012.

The three postgres SBoBs on this branch were learned under p85-cbdbb392 and are
therefore stale against this pair. They are being relearned and revalidated, not
carried over.
All three distro suites carried target: pg-client and asserted every detection
on the pg-client container. pg-client is the same postgres:17 image in every
leg, so the three "packaging distro contrasts" were one identical client
profile measured three times, and the oss/bitnami/cnpg servers — the workloads
the shipped SBoBs actually describe — were never attacked at all. That is why
all three graded identically.

Each suite now targets its own server (postgres, pg-bitnami-postgresql, pg-rw)
and asserts on that vendor's container. The plain postgres and neighbour legs
keep their client target: there the client IS the subject and ships
cp-pg-client.yaml.

Three assertions were only ever valid against the client and are re-based on
what the server actually emits, measured from alertmanager:
  - exec-etc-shadow      duplicated, one copy asserting R0001; dropped. cat is
                         in the server's own baseline so R0001 cannot fire.
                         The surviving copy asserts R0010, which does.
  - exec-sa-token-attempt R0001 -> R0006. The token is mounted in the server pod
                         and the open is what betrays the read.
  - exec-etc-passwd      no assertion. The entrypoint reads /etc/passwd for its
                         own uid lookup, so it is baseline and nothing is
                         anomalous. Kept as a behaviour probe.

local-ci gained two ordering fixes without which none of this can be measured:

  - rollout status ds/node-agent runs BEFORE the app is deployed. `kubectl wait
    pod -l app=node-agent --for=ready` matches the OUTGOING pod when helm has
    just updated the DaemonSet, so it returned while the roll had not started,
    the app deployed into that gap, and node-agent restarted underneath it.

  - the app's workloads are unbound and its pods recreated before learning. A
    previous demo-mode run patches kubescape.io/user-defined-profile onto the
    Deployment/StatefulSet/Cluster and it persists there; while it is set
    node-agent enforces the supplied SBoB instead of learning, so no
    ContainerProfile is produced and the learn poll waits out its timeout
    against stale profiles.

KS_RUNC_MNT is expressed as --set rather than through the post-renderer, which
is no longer in the critical path. Without that hostPath the runc named by
global.overrideRuntimePath is invisible under node-agent's non-recursive bind
of "/", the fanotify mark fails, and the agent sees no container starts at all.

postgres-oss on the server: missed=0 fp=0, contrast 27 Separable / 4 Ambiguous
/ 0 Blind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reported as a bug in the incoming net-v2 PR. With it enabled the learned
profiles carry network state that the SBoBs then have to unpick; ingress and
egress are declared explicitly instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Regenerated from a profile learned with the server as the subject, on a tuner
that no longer wildcards exec basenames. 25 execs, 45 opens, no wildcarded
binary name, no over-broad open.

Network is declared rather than learned (networkEventsStreaming is off): ingress
admits only pg-client in this namespace on 5432/TCP, egress is null because a
database initiates none. No entity:host entry — this Deployment declares no
probes, so nothing arrives from the kubelet.

rulePolicies.R0002.processAllowed lists exactly one comm, postgres. The data dir
is multi-level dynamic (per-OID relfiles, WAL segments, spill filesets) and
bound literally it produced 1233 R0002 alerts on a benign run. Wildcarding
pgdata would clear those and also blind R0002/R0010 in the directory an attacker
most wants to write to, for every process; allowlisting the server's own comm
clears them while attack execs (sh, cat, ln, nc) still trip it.

Validated bound, not just tuned:
  benign  76/76 pass, 0 false positives on the server
  attack  298 alerts, 17 distinct rules incl. R0002 — the allowlist blinds
          nothing the attacks reach
  tune    missed=0 fp=0
  contrast 22 Separable / 9 Ambiguous / 0 Blind (type database)

local-ci deletes Failed/Succeeded pods before deploying. kubectl apply does not
restart a pod left Error by an eviction, and a bare Pod has no controller to
replace it, so the deploy target's readiness wait times out on a corpse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two detection tests that read bobtest and ship it off the DB pod, so the
evidence graph links a DB read to an egress under one anomaly:

- exfil-http-dbdump: SELECT -> base64 -> POST to an in-cluster nginx sink on
  plain :80 (bash /dev/tcp; sh/dash has no /dev/tcp). Fires R0011 egress.
- exfil-dns-dbdump: SELECT -> base64url -> the bytes ARE the DNS query name,
  40-char labels < 63B. Fires R0005. Payload recovers from dns_events.req_body
  (aW5pdDpiYXNlbGluZQo -> "init:baseline").

Contained: exfil-sink.yaml is in-cluster nginx, data never leaves; the DNS
domain never resolves, the lookup itself is the signal.

Validated on a live rig (net-v2-rc1, postgres-oss): R0011=40, R0005=8, sink
nginx logged the POST, base64 payload recovered from the captured query name.
…untime FPs

runc:[2:INIT] file access (/proc/*/setgroups, cgroup, mountinfo) fired R0002 on
every container exec — 580/run, pure container-runtime noise that drowned the
attack signal and inflated dc_snoop evidence. Add runc:[1:INIT]/[2:INIT] to
R0002.processAllowed (oss had only postgres; bitnami/cnpg had empty
rulePolicies).

Validated live after profile propagation: runc R0002 580 -> 0. Remaining R0002
(psql/perl/pg_dump/cp/bash) is attack-process file access = signal, left intact.
perl loading its own runtime modules (/usr/lib/x86_64-linux-gnu/perl-base/*.pm,
auto/Socket/Socket.so, warnings/, Tie/, IPC/, Exporter/, auto/{POSIX,Fcntl})
fired R0002 76/run — perl's normal startup, not the attack (the attack is what
perl DOES: socket/dns). Wildcard the 8 perl-base dirs with the ⋯ dynamic marker.

Validated live: perl-base R0002 76 -> 0. Non-perl-base perl access (glibc-hwcaps,
/etc, gconv) left intact as signal.
entlein and others added 8 commits August 25, 2026 12:35
Both re-recorded against their own database rather than the shared client, then
validated bound — benign suite for false positives, attack suite for what still
fires — not just tuned.

  bitnami  76/76 benign, 0 FPs, 93 attack alerts / 15 rules, tune 0/0,
           contrast 22 Separable / 9 Ambiguous / 0 Blind
  cnpg     76/76 benign, 0 FPs, 134 attack alerts / 17 rules, tune 0/0,
           contrast 22 Separable / 9 Ambiguous / 0 Blind

All three now handle runc identically: the tuner emits the runc-init allowlist
and clean-profile.py preserves it. local-ci rebuilds the shipped file from the
per-iteration snapshot, which predates the tuner's emit step, so the last writer
had to apply it too or the allowlist vanished from what actually ships.

Attacks that cannot land are documented rather than asserted or removed:
  bitnami  fileless-memfd-exec — no perl, python or compiler in the image, so
           the raw syscall cannot be issued. k8s-api-unexpected-call rewritten
           to use curl, which the image does have, so it lands and R0007 fires.
  cnpg     exec-sa-token-attempt and k8s-api-unexpected-call — the container is
           /controller/manager instance run, and the instance manager reads the
           service-account token and calls the API as normal operation, so both
           rules are baseline there. exec-drifted-binary — /tmp is mounted
           read-only. All three stay in the suite and report FAILED.

That last group is the contrast result, not a gap: packaging an API-speaking
control process inside the database container costs three detections.

cnpg needed the most declaration, each entry traced to a measured alert:
R0006 allows the manager comm; /bin/dash in execs because the manager forks a
shell in steady state — and the path matters, /bin/sh is a symlink and
node-agent records the resolved target, so an entry naming /bin/sh silently
never matches; /controller/manager, /controller/log/postgres.json, /plugins and
the perl-base tree as literal or anchored opens; and the kubelet-probes ingress
carries ipAddresses alongside entity: host, because entity alone did not
suppress R0012 on this node-agent build.

Fixture fixes found by running it:
  - the shared client moves to postgres:18. bitnami's chart ships server 18.6
    and pg_dump refuses a newer server, which failed 21 benign tests. A newer
    client against oss 17 and cnpg 17.5 is supported.
  - cnpg's bobtest table was owned by postgres, because postInitApplicationSQL
    runs as the superuser, so pg_dump as app could not lock it. It is now
    ALTERed to app, matching the other two distros.
  - deploy-distros.sh uses helm upgrade --install; helm install failed on any
    second run with "cannot re-use a name that is still in use".

cp-pg-client.yaml is NOT re-recorded for the postgres:18 bump and is still the
17-era profile; DEMO.md says so. The server SBoBs are unaffected because the
attacks exec into the server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
We converged on the same two fixes independently — the runc-init allowlist and
the perl-base opens — so the three server SBoBs conflicted. Resolved as a union
rather than a pick, because each side had measured something the other had not.

Theirs contributes what a post-startup measurement cannot see. My validation ran
the benign suite against an already-running pod, so container-start reads never
appeared: cnpg's /controller/certificates/* (54 paths, including the
timestamp-suffixed generations) and bitnami's data-dir literals were missing
from my versions entirely and would have fired R0002 on the next fresh pod.
Their per-subdirectory perl-base entries are also tighter than my
perl-base/⋯/* and replace it.

Mine contributes the network schema (ingress podSelector / entity:host with
ipAddresses, egress null), the app-comm rule policies, and the full four-comm
runc set the tuner now emits — theirs listed two.

Also taken as-is: the exfil sink and the two exfiltration attacks, which read
from the database and push the result out over HTTP and DNS. Both assert on the
server container, consistent with the retarget, and both are contained — the
sink is in-cluster and the DNS name never resolves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kept the wider form wherever the two sides differed, and applied the broad
perl-base wildcards to all three rather than only cnpg. Widening can only cost
detections, so it was checked by re-running the attack suites bound:

  oss      17 -> 18 distinct rules  (+R0004, from the new exfil attacks)
  bitnami  15 -> 15                  identical set
  cnpg     17 -> 17                  identical set

Nothing dropped anywhere, and the benign suites still report 0 false positives
on the server.

generalise-sbob.py's service_ref took a port list and hardcoded protocol TCP, so
it could not express kube-dns at all — a UDP peer emitted as TCP-53 silently
fails to admit the traffic it was written for. It now accepts a protocol and
PORT/PROTO entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A learned exec carries the process environment, and that is where apps keep
their credentials. Nothing shipped today has an envs key, but the field exists
in the schema and the tuner keys on it, so the first app that passes a password
by env would have published it — these files live in a public repo.

envs are dropped outright rather than redacted, and an exec arg shaped like
NAME=value where NAME matches PASS/PWD/SECRET/TOKEN/KEY/CRED aborts the emit
instead of writing the file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All three server SBoBs carried /dev/shm/⋯ with O_CREAT|O_EXCL|O_RDWR. postgres
does use POSIX shared memory, but a bare wildcard there also admits
/dev/shm/malicious_pg and /dev/shm/drifted_bob — and four attacks in this suite
stage exactly there (exec-devshm, drifted-binary-exec, symlink-shadow,
hardlink-shadow, asserting R0001, R1000, R1010, R1012). The profile was
allowlisting the drop directory those attacks depend on.

postgres names its segments PostgreSQL.<n>, so the entry keeps that shape:
/dev/shm/PostgreSQL.⋯. The server's own shm still matches; a file dropped under
any other name does not.

This predates the recent re-recordings — it has been in the shipped profiles
since the original three-fork commit — but it is what "you learnt the attacks"
on PR 182 is pointing at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
collapse_exec_args keyed on the exec path alone and kept the first argv0
it saw, so a binary invoked under several argv0 values ended up with one
entry naming one tool. Debian's pg_wrapper is /usr/bin/perl invoked as
psql, pgbench, pg_dump, pg_isready and eight more; every tool but the
first stopped matching, and since exec-args matching is anchored at both
ends the surviving entry could not match those invocations either.

Key on (path, argv0) and emit one [argv0, wildcard] entry per pair. The
projection maps a path to a LIST of argv vectors and a runtime exec
matches if any vector matches, so several entries per path is the
supported shape, not a workaround.
bobctl is the tool that shapes a shippable SBoB — generalize --collapse
runs the exec-arg and path collapsing. These three scripts did the same
job in a second implementation, so the shape of a shipped SBoB depended
on which tool happened to be run. One authority; the binary wins.

Removed: sbob-from-learned.py, generalise-sbob.py, portable-sbob.py.
None of the three is invoked from a workflow, a Makefile target or
another script. The only mention was a hint string in
check-representativeness.py, repointed at bobctl.

clean-profile.py is deliberately left in place: it is called from
ci-bobctl-autotune.yaml and local-ci.sh, so removing it is a CI change
and not part of this cleanup.
Resolve kubescape/values.yaml toward main's image pins (storage rc-rogue5,
node-agent duckling v0.1.0-rogue2). Keep default-rules.yaml identical to main
(the branch's duplicate R0012 addition dropped — main already defines it; no
rule change). Bump pkg to the latest bobctl (570a3a4: multi-argv0 wrapper +
atomic-write-suffix collapse fixes).
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.

2 participants