PostgreSQL packaging-distro contrast (oss, bitnami, CloudNativePG) - #182
ConstanzeTU wants to merge 27 commits into
Conversation
…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.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis 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. ChangesPostgreSQL distro resources
SBoB profile policies
Functional PostgreSQL suites
Attack detection suites
Coverage and demonstration
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 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
⛔ Files ignored due to path filters (3)
example/postgres/distros/rule-coverage/postgres-bitnami-killchain.gifis excluded by!**/*.gifexample/postgres/distros/rule-coverage/postgres-cnpg-killchain.gifis excluded by!**/*.gifexample/postgres/distros/rule-coverage/postgres-oss-killchain.gifis excluded by!**/*.gif
📒 Files selected for processing (16)
example/postgres/distros/DEMO.mdexample/postgres/distros/attacks/bitnami.yamlexample/postgres/distros/attacks/cnpg.yamlexample/postgres/distros/attacks/oss.yamlexample/postgres/distros/deploy-distros.shexample/postgres/distros/functional/bitnami.yamlexample/postgres/distros/functional/cnpg.yamlexample/postgres/distros/functional/oss.yamlexample/postgres/distros/postgres-bitnami.yamlexample/postgres/distros/postgres-cnpg.yamlexample/postgres/distros/postgres-oss.yamlexample/postgres/distros/sbobs/cp-pg-client.yamlexample/postgres/distros/sbobs/cp-postgres-bitnami.yamlexample/postgres/distros/sbobs/cp-postgres-cnpg.yamlexample/postgres/distros/sbobs/cp-postgres-oss.yamlkubescape/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
| ``` | ||
| make kubescape | ||
| make alertmanager | ||
| ``` |
There was a problem hiding this comment.
📐 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
| | 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 | |
There was a problem hiding this comment.
🗄️ 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.mdRepository: 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:
- 1: https://github.com/bitnami/charts/tree/main/bitnami/postgresql
- 2: https://docs.bitnami.com/general/how-to/troubleshoot-helm-chart-issues/
- 3: https://oneuptime.com/blog/post/2026-03-05-helmrepository-bitnami-charts-flux/view
- 4: https://artifacthub.io/packages/helm/bitnami/postgresql?af1115ab_page=3
- 5: https://hub.docker.com/r/bitnamicharts/postgresql/tags
- 6: https://artifacthub.io/packages/helm/bitnami/postgresql?d8b851d8_page=2%3Futm_campaign%3DKubeWeekly
- 7: https://hub.docker.com/r/bitnamicharts/postgresql
- 8: https://artifacthub.io/packages/helm/bitnami/postgresql
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.
| ## 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 | ||
| ``` |
There was a problem hiding this comment.
🎯 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.
| ## 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.
| 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)' |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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 | ||
| } |
There was a problem hiding this comment.
🗄️ 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/postgresRepository: 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))
PYRepository: 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:
- 1: https://github.com/bitnami/charts/blob/main/bitnami/postgresql/CHANGELOG.md
- 2: https://github.com/bitnami/charts/blob/466dbcda/bitnami/postgresql/CHANGELOG.md
- 3: https://github.com/bitnami/charts/blob/HEAD/bitnami/postgresql/CHANGELOG.md
- 4: https://hub.docker.com/r/bitnamicharts/postgresql/tags
- 5: https://github.com/bitnami/charts/tree/main/bitnami/postgresql
- 6: https://github.com/bitnami/charts
- 7: chore(deps): update postgresql docker tag to v16.7.27 goauthentik/helm#393
- 8: https://blog.bitnami.com/2024/05/enhancing-bitnami-helm-charts.html
- 9: https://artifacthub.io/packages/helm/bitnami/postgresql/16.7.27
🏁 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
doneRepository: 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.
| 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.
| 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'); |
There was a problem hiding this comment.
🎯 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 -SRepository: 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 -SRepository: 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:
- 1: https://github.com/cloudnative-pg/cloudnative-pg/blob/main/docs/src/bootstrap.md
- 2: https://cloudnative-pg.io/documentation/1.24/bootstrap/
- 3: https://cloudnative-pg.io/documentation/1.20/bootstrap/
- 4: https://github.com/cloudnative-pg/cloudnative-pg/blob/af12d5da/docs/src/bootstrap.md
- 5: Missing documentation on how to enable pg extensions cloudnative-pg/cloudnative-pg#2660
- 6: https://cloudnative-pg.io/documentation/1.21/bootstrap/
🌐 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:
- 1: https://cloudnative-pg.github.io/docs/1.28/postgresql_conf/
- 2: https://cloudnative-pg.github.io/docs/1.25/postgresql_conf
- 3: https://cloudnative-pg.io/documentation/1.24/postgresql_conf/
- 4: https://github.com/cloudnative-pg/cloudnative-pg/blob/main/docs/src/samples/cluster-example-full.yaml
- 5: [Bug]: Database CRD doesn't handle changes to
shared_preload_librariescloudnative-pg/cloudnative-pg#9930 - 6: https://docs.pgedge.com/cloudnativepg/development/postgresql_conf/
- 7: https://cloudnative-pg.io/documentation/1.22/postgresql_conf/
🏁 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.yamlRepository: 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.
| 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: /⋯ |
There was a problem hiding this comment.
🔒 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 -40Repository: 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 -200Repository: 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-L278example/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.
| ipAddresses: | ||
| - 10.43.0.0/24 |
There was a problem hiding this comment.
📐 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.
| # 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] |
There was a problem hiding this comment.
🗄️ 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'; }
doneRepository: 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)
PYRepository: 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}')
PYRepository: 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: /* |
| - runc:[2:INIT] | ||
| - runc:[3:INIT] | ||
| - postgres | ||
| - sh |
…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.
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]} |
There was a problem hiding this comment.
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
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.
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).
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:
bitnami/postgresqlEvery fork runs the SAME
postgres:17pg-client pod, so a single portableclient SBoB (
sbobs/cp-pg-client.yaml) is the contrast leg across all threebackends; 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):
postgresfor oss, the shellinit/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+clientprofiles),
sbobs/, per-forkfunctional/+attacks/suites,DEMO.mdkubescape/rule-coverage.yaml— three distro cards + generated killchain GIFsIdentical 14-rule attack contrast on every fork: R0001, R0002, R0005–R0008,
R0010, R0011, R1000, R1001, R1005, R1008, R1010, R1012.