### Local setup commands (activate-hermit, make setup, make install-hooks) Source: https://github.com/0hardik1/kubesplaining/blob/main/CONTRIBUTING.md Activates the Hermit environment, downloads Go modules, and installs pre-commit and commit-msg hooks. The install-hooks step is often skipped but is required to match CI checks. ```bash . ./bin/activate-hermit # adds ./bin/ to PATH for this shell make setup # downloads Go modules into ./.tmp make install-hooks # activates pre-commit + commit-msg hooks (one-time per clone) ``` -------------------------------- ### Install via Go install Source: https://github.com/0hardik1/kubesplaining/blob/main/README.md Installs the latest Kubesplaining binary directly from the Go module path. ```bash go install github.com/0hardik1/kubesplaining/cmd/kubesplaining@latest ``` -------------------------------- ### Install via kubectl krew and scan Source: https://github.com/0hardik1/kubesplaining/blob/main/README.md Installs Kubesplaining as a kubectl plugin using Krew, then runs a scan. All commands work the same way behind kubectl, e.g. kubectl kubesplaining scan --input-file snapshot.json. ```bash kubectl krew install kubesplaining kubectl kubesplaining scan ``` -------------------------------- ### Verify checksum and install binary Source: https://github.com/0hardik1/kubesplaining/blob/main/README.md Verifies the SHA-256 checksum of the downloaded release archive, then installs the binary to /usr/local/bin/. ```bash shasum -a 256 -c kubesplaining__checksums.txt sudo install kubesplaining /usr/local/bin/ ``` -------------------------------- ### Quick start: run focused least-privilege scan with audit log Source: https://github.com/0hardik1/kubesplaining/blob/main/README.md Capture audit logs from your cluster (see docs/audit-logs.md for kubeadm / kind / EKS setup; audit policy level 'Metadata' is enough). Then run the focused mode against a snapshot (or live cluster) plus your audit log using the make target or the direct CLI command. The direct command requires --audit-log, --audit-source native, --audit-window-days 30, and --least-privilege-only. ```bash # 1. Capture audit logs from your cluster - see docs/audit-logs.md for # kubeadm / kind / EKS setup. Audit policy level "Metadata" is enough. # 2. Run the focused mode against a snapshot (or live cluster) + your audit log: make scan-lp AUDIT_LOG=./audit.log ARGS="--input-file snapshot.json" # or directly: kubesplaining scan \ --input-file snapshot.json \ --audit-log ./audit.log \ --audit-source native \ --audit-window-days 30 \ --least-privilege-only ``` -------------------------------- ### Install from fresh clone with make scan Source: https://github.com/0hardik1/kubesplaining/blob/main/README.md Clones the repository and runs `make scan`, which builds the binary (Hermit auto-downloads the pinned Go toolchain) and scans the current kubectl context in one step. ```bash git clone https://github.com/0hardik1/kubesplaining cd kubesplaining make scan ``` -------------------------------- ### Get EKS token and set kubectl context for assumed role Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/eks-demo-walkthrough.md Uses `aws eks get-token` with current assumed-role credentials to obtain a bearer token, then configures kubectl credentials and context. Do not pass `--role-arn` because the role cannot assume itself. After setup, `auth can-i '*' '*' --all-namespaces` returns `yes` and `get pods -A` lists all pods cluster-wide. ```bash K8S_TOKEN=$(aws eks get-token --cluster-name holy-splain --region us-east-1 \ --output text --query 'status.token') kubectl config set-credentials holy-splain-via-prod-irsa --token="$K8S_TOKEN" kubectl config set-context holy-splain-via-prod-irsa \ --cluster=holy-splain.us-east-1.eksctl.io \ --user=holy-splain-via-prod-irsa kubectl --context=holy-splain-via-prod-irsa auth can-i '*' '*' --all-namespaces kubectl --context=holy-splain-via-prod-irsa get pods -A | head -8 ``` -------------------------------- ### Complete exclusions YAML configuration Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/exclusions.md Full YAML example showing all exclusion sections: global, rbac, pod_security, and network_policy. Includes comments for reasons and inline notes. Use as a template for configuring exclusions in kubesplaining. ```yaml global: exclude_namespaces: - kube-system - kube-public - prometheus exclude_service_accounts: - system:* - argocd:argocd-application-controller exclude_cluster_roles: - system:* - kubeadm:* exclude_finding_ids: - KUBE-PRIVESC-005 # secret reads accepted in this cluster, see ticket-1234 exclude_subjects: - kind: Group name: system:* reason: Built-in Kubernetes group rbac: exclude_subjects: - kind: ServiceAccount name: cert-manager namespace: cert-manager reason: Vendor SA, reviewed Q1 2026 pod_security: exclude_workloads: - kind: DaemonSet name_pattern: node-exporter-* namespace: monitoring reason: Prometheus node exporter, hostPath required exclude_checks: - check: hostNetwork namespace: kube-system reason: System networking components require host networking network_policy: exclude_namespaces: - kube-system - kube-public - kube-node-lease ``` -------------------------------- ### fluxSnapshot and Confused-Deputy Tests (TestConfusedDeputyBridgesToControllerSA, TestConfusedDeputyRequiresInstalledController) Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-24-privesc-deep-chains.md Builds a Kubernetes snapshot where a tenant ServiceAccount can create Flux Kustomizations, optionally installing the kustomize-controller SA with cluster-admin. The first test expects a path from dev-deployer to cluster_admin via an operator_reconcile edge; the second verifies no such edge is emitted when the controller is absent. Run with GOCACHE and GOMODCACHE set to local .tmp directories and the -run TestConfusedDeputy flag; the first test fails and the second passes vacuously. ```go package privesc import ( "testing" "github.com/0hardik1/kubesplaining/internal/models" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // fluxSnapshot builds a snapshot where a tenant SA can create Flux Kustomizations. // installController controls whether the reconciling controller SA actually exists, // which is the precision gate for the whole edge family. func fluxSnapshot(installController bool) models.Snapshot { snapshot := models.Snapshot{} snapshot.Resources.Namespaces = []corev1.Namespace{ {ObjectMeta: metav1.ObjectMeta{Name: "tenant"}}, {ObjectMeta: metav1.ObjectMeta{Name: "flux-system"}}, } snapshot.Resources.Roles = []rbacv1.Role{{ ObjectMeta: metav1.ObjectMeta{Name: "gitops", Namespace: "tenant"}, Rules: []rbacv1.PolicyRule{{ APIGroups: []string{"kustomize.toolkit.fluxcd.io"}, Resources: []string{"kustomizations"}, Verbs: []string{"create", "patch"}, }}, }} snapshot.Resources.RoleBindings = []rbacv1.RoleBinding{{ ObjectMeta: metav1.ObjectMeta{Name: "gitops-rb", Namespace: "tenant"}, RoleRef: rbacv1.RoleRef{Kind: "Role", Name: "gitops"}, Subjects: []rbacv1.Subject{{Kind: "ServiceAccount", Name: "dev-deployer", Namespace: "tenant"}}, }} if installController { snapshot.Resources.ServiceAccounts = []corev1.ServiceAccount{ {ObjectMeta: metav1.ObjectMeta{Name: "kustomize-controller", Namespace: "flux-system"}}, } snapshot.Resources.ClusterRoleBindings = []rbacv1.ClusterRoleBinding{{ ObjectMeta: metav1.ObjectMeta{Name: "flux-crb"}, RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", Name: "cluster-admin"}, Subjects: []rbacv1.Subject{ {Kind: "ServiceAccount", Name: "kustomize-controller", Namespace: "flux-system"}, }, }} } return snapshot } func TestConfusedDeputyBridgesToControllerSA(t *testing.T) { graph := BuildGraph(fluxSnapshot(true)) paths := FindPaths(graph, 5) var found bool for _, p := range paths { if p.Source.Name == "dev-deployer" && p.Target == models.TargetClusterAdmin && len(p.Hops) >= 2 && p.Hops[0].Action == "operator_reconcile" { found = true } } if !found { t.Fatalf("want dev-deployer -> kustomize-controller -> cluster_admin; paths: %+v", paths) } } // TestConfusedDeputyRequiresInstalledController is the precision gate: a stray RBAC // grant on a CRD that no operator serves must produce nothing. func TestConfusedDeputyRequiresInstalledController(t *testing.T) { graph := BuildGraph(fluxSnapshot(false)) for _, edge := range graph.Edges { if edge.Action == "operator_reconcile" { t.Fatalf("emitted an operator_reconcile edge with no controller SA installed: %+v", edge) } } } ``` -------------------------------- ### Sanity Check AWS CLI Commands Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/eks-demo-iam.md Run these three commands to verify your local AWS credentials are configured correctly before running the setup. If `aws sts get-caller-identity` fails, consult the AWS CLI configuration documentation. ```bash aws sts get-caller-identity aws eks list-clusters --region aws s3 ls ``` -------------------------------- ### Deny matching examples Source: https://github.com/0hardik1/kubesplaining/blob/main/testdata/corpus/README.md Illustrates how deny entries match findings: a bare rule ID bans every instance of that rule, while a `rule:subject` prefix bans only that specific subject's path. ```text "KUBE-RBAC-OVERBROAD-001" → bans the rule everywhere "KUBE-PRIVESC-PATH-CLUSTER-ADMIN:ServiceAccount/legacy/loadtester" → bans that one subject's path ``` -------------------------------- ### Generate exclusions starter file Source: https://github.com/0hardik1/kubesplaining/blob/main/README.md Creates a starter exclusions file based on the standard preset. The user file is merged with the preset, so defaults are kept and custom suppressions can be added. ```bash kubesplaining create-exclusions-file --preset standard --output-file exclusions.yml ``` -------------------------------- ### Create exclusions file with preset Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/exclusions.md Generates a user exclusions file starting from the `standard` preset. The second command pre-populates any `kube-*` / `*-system` namespaces discovered in a snapshot. ```bash kubesplaining create-exclusions-file --preset standard --output-file exclusions.yml ``` ```bash kubesplaining create-exclusions-file --preset standard \ --from-snapshot snapshot.json --output-file exclusions.yml ``` -------------------------------- ### EKS demo lifecycle commands (make eks-demo-*) Source: https://github.com/0hardik1/kubesplaining/blob/main/README.md Commands to provision, scan, exploit, and tear down a real EKS cluster named 'holy-splain' in your AWS account. The demo exhibits a cross-namespace + AWS-pivot privilege escalation chain. Provisioning takes ~12 minutes, scanning ~30 seconds, and teardown ~10 minutes. The POC defaults to dry-run; use --execute to step through the attack interactively. Costs approximately $5/day while running. ```bash make eks-demo-up # ~12 min: cluster + IAM + S3 + K8s manifests + aws-auth mapping ``` ```bash make eks-demo-scan # ~30 sec: produces .tmp/eks-demo-report/report.html ``` ```bash make eks-demo-poc # default dry-run; --execute to step through the attack interactively ``` ```bash make eks-demo-down # ~10 min: tears down everything (cluster, IAM role, S3 bucket) ``` -------------------------------- ### Build and scan minimal-risky fixture Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Builds the binary and runs a scan on the minimal-risky fixture, writing HTML output to .tmp/altcheck. Expect no alternate blocks because the fixture has no parallel-binding subject. ```bash make build && ./bin/kubesplaining scan --input-file testdata/snapshots/minimal-risky.json \ --output-dir .tmp/altcheck --formats html ``` -------------------------------- ### Add banned-edge parameter to bfsToSinks Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Modifies the bfsToSinks function signature to accept a banned-edge predicate and adds a guard at the start of the edge loop to skip banned edges. The banned parameter is optional (nil allowed). ```go func bfsToSinks( graph *models.EscalationGraph, adj map[string][]*models.EscalationEdge, sourceID string, maxDepth int, banned func(*models.EscalationEdge) bool, ) map[string][]pathStep { ``` ```go if banned != nil && banned(edge) { continue } ``` ```go if neighbor.IsSink { sinks[edge.To] = nextPath ``` -------------------------------- ### Run full suite, corpus, and lint Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-24-privesc-deep-chains.md Runs make test, make corpus, make lint, and golangci-lint on the whole repo. Expected: all green. ```bash make test 2>&1 | tail -10 && make corpus 2>&1 | tail -20 && make lint && ./bin/golangci-lint run ./... ``` -------------------------------- ### Run the full gate (make lint, test, golangci-lint, corpus, e2e) Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Step 5 of Task 7. Runs the complete validation suite; golangci-lint catches what make lint misses and fails CI. Expected output is all PASS. ```bash make lint && make test && ./bin/golangci-lint run ./... && make corpus && make e2e ``` -------------------------------- ### Build and scan with make scan Source: https://github.com/0hardik1/kubesplaining/blob/main/README.md Builds the binary (Hermit auto-downloads the pinned Go toolchain) and runs it against the current kubectl context. Extra flags can be passed via ARGS, e.g. --severity-threshold high --only-modules privesc. ```bash make scan ``` -------------------------------- ### Delete lingering eksctl CloudFormation stack Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/eks-demo.md Use when `eksctl create cluster` refuses because its CloudFormation stack already exists. If a previous setup partially failed, delete the lingering stack with these commands, then re-run `make eks-demo-up`. ```bash eksctl utils describe-stacks --region $AWS_REGION --cluster holy-splain eksctl delete cluster --name holy-splain --region $AWS_REGION --wait ``` -------------------------------- ### Run e2e and inspect output (make e2e 2>&1 | tail -60) Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-24-privesc-deep-chains.md Runs the e2e suite and shows the last 60 lines. On the first run, the `*.ruleset` set-equality gate is expected to FAIL because `KUBE-CONFUSED-DEPUTY-001` is new and the new fixture may surface other rules. Review the diff to confirm every new rule ID is a true positive attributable to the new fixture or edge families; any rule appearing on a pre-existing fixture is a false positive from Tasks 2-4 and must be investigated before regenerating goldens. ```bash make e2e 2>&1 | tail -60 ``` -------------------------------- ### Run tests to verify they fail (go test) Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Runs the new provenance tests expecting a compile failure because edge.SourceBinding is undefined on *models.EscalationEdge. Uses temporary GOCACHE and GOMODCACHE directories. ```bash GOCACHE=$(pwd)/.tmp/go-build-cache GOMODCACHE=$(pwd)/.tmp/go-mod-cache \ go test ./internal/analyzer/privesc -run 'TestBuildGraphStamps|TestPodEscapeEdgesCarryNoProvenance' -v ``` -------------------------------- ### deputy.go: Confused-Deputy Edge Builder (operatorEntry struct) Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-24-privesc-deep-chains.md Starts the confused-deputy catalog and builder in internal/analyzer/privesc/deputy.go. Defines operatorEntry to map an attacker-writable custom resource to the controller ServiceAccount that reconciles it, with a gains clause used verbatim in edge descriptions. ```go // Package privesc: confused-deputy edges. A subject that cannot escalate directly // may still be able to write a custom resource that a privileged controller then // reconciles on its behalf. The controller is the deputy: it holds the permissions, // the attacker supplies the instructions. package privesc import ( "fmt" "github.com/0hardik1/kubesplaining/internal/models" "github.com/0hardik1/kubesplaining/internal/permissions" ) // operatorEntry maps one attacker-writable custom resource to the controller // ServiceAccount that reconciles it. type operatorEntry struct { group string resources []string saName string saNS string // gains describes, in one clause, what the reconciler will do on the // attacker's behalf. Used verbatim in the edge description. gains string } ``` -------------------------------- ### Check and update EKS cluster accessConfig authenticationMode Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/eks-demo.md Use when `update-kubeconfig --role-arn` fails with 'User ... is not authorized'. The cluster's `accessConfig.authenticationMode` must be `API_AND_CONFIG_MAP` or `CONFIG_MAP`; newer EKS clusters created via the AWS Console default to `API`-only, which silently ignores the aws-auth ConfigMap. The setup script pins `API_AND_CONFIG_MAP` explicitly and re-checks it post-create. ```bash aws eks describe-cluster --name holy-splain --query 'cluster.accessConfig.authenticationMode' aws eks update-cluster-config --name holy-splain \ --access-config authenticationMode=API_AND_CONFIG_MAP ``` -------------------------------- ### EKS demo make targets Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/eks-demo.md Four make targets orchestrate the live EKS demo: up provisions the cluster and resources, scan runs kubesplaining, poc executes the attack (dry-run by default), and down tears everything down. Each target includes its approximate runtime and side effects. ```bash make eks-demo-up # ~12 min: cluster + IAM + S3 + K8s manifests + aws-auth mapping ``` ```bash make eks-demo-scan # ~30 sec: kubesplaining download + scan, opens .tmp/eks-demo-report/report.html ``` ```bash make eks-demo-poc # default dry-run, prints all 10 steps; --execute to actually attack ``` ```bash make eks-demo-down # ~10 min: removes cluster, IAM role, S3 bucket, kubeconfig contexts ``` -------------------------------- ### Development loop commands (make lint, test, build, e2e) Source: https://github.com/0hardik1/kubesplaining/blob/main/CONTRIBUTING.md Runs the full lint, test, build, and end-to-end checks. The e2e target requires Docker and spins up a kind cluster. ```bash make lint # gofmt + go vet (full repo) make test # go test ./... make build # builds ./bin/kubesplaining make e2e # spins up kind, applies risky manifests, asserts findings (needs Docker) ``` -------------------------------- ### Deep-chain privesc fixture YAML Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-24-privesc-deep-chains.md Kubernetes manifest for the deep-chain privesc fixture. It exercises three edge families: confused-deputy operator bridge, namespace-admin continuation into a co-located ServiceAccount, and node-escape continuation into control-plane PKI. No CRDs are installed on purpose; RBAC rules may name resources whose CRD does not exist, which is the shape the confused-deputy catalog matches on. The fixture deliberately needs no pods; every edge is RBAC-derived. ```yaml # Deep-chain privesc fixture. Exercises three edge families that only produce # findings when chained: the confused-deputy operator bridge, namespace-admin # continuation into a co-located ServiceAccount, and (via the single-node kind # control-plane node) the node-escape continuation into control-plane PKI. # # No CRDs are installed on purpose. RBAC rules may name resources whose CRD does # not exist, which is exactly the shape the confused-deputy catalog matches on. apiVersion: v1 kind: Namespace metadata: name: deepchain-tenant --- apiVersion: v1 kind: Namespace metadata: name: flux-system --- # The reconciling controller. Its existence is the precision gate for the # confused-deputy edge: without this ServiceAccount, no bridge is emitted. apiVersion: v1 kind: ServiceAccount metadata: name: kustomize-controller namespace: flux-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: deepchain-flux-admin roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - kind: ServiceAccount name: kustomize-controller namespace: flux-system --- # The tenant. Holds no dangerous Kubernetes verb of its own: its only power is # writing a Flux Kustomization, which the controller then reconciles as itself. apiVersion: v1 kind: ServiceAccount metadata: name: deepchain-deployer namespace: deepchain-tenant --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: deepchain-gitops namespace: deepchain-tenant rules: - apiGroups: ["kustomize.toolkit.fluxcd.io"] resources: ["kustomizations"] verbs: ["create", "patch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: deepchain-gitops namespace: deepchain-tenant roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: deepchain-gitops subjects: - kind: ServiceAccount name: deepchain-deployer namespace: deepchain-tenant --- # Namespace-admin continuation: this SA can only bind roles inside its own # namespace, but that namespace co-hosts a cluster-admin-bound identity. apiVersion: v1 kind: ServiceAccount metadata: name: deepchain-binder namespace: deepchain-tenant --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: deepchain-binder namespace: deepchain-tenant rules: - apiGroups: ["rbac.authorization.k8s.io"] resources: ["rolebindings"] verbs: ["create", "update", "patch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: deepchain-binder namespace: deepchain-tenant roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: deepchain-binder subjects: - kind: ServiceAccount name: deepchain-binder namespace: deepchain-tenant --- # The co-located high-value identity that makes namespace-admin worth chaining. apiVersion: v1 kind: ServiceAccount metadata: name: deepchain-privileged namespace: deepchain-tenant --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: deepchain-privileged roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - kind: ServiceAccount name: deepchain-privileged namespace: deepchain-tenant ``` -------------------------------- ### Run the new technique copy test Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-24-privesc-deep-chains.md Runs only TestNewPrivescActionsHaveTechniqueCopy with verbose output. Expected to fail with four 'no Techniques entry' errors before adding the entries. ```bash GOCACHE=$(pwd)/.tmp/go-build-cache GOMODCACHE=$(pwd)/.tmp/go-mod-cache go test ./internal/report/ -run TestNewPrivescActionsHaveTechniqueCopy -v ``` -------------------------------- ### Export EKS audit log window with aws logs filter-log-events (Linux and macOS/BSD date) Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/audit-logs.md Computes the start time 30 days ago using Linux or macOS/BSD `date` syntax, then exports audit events from CloudWatch Logs to `eks-audit.json`. For long windows on busy clusters, paginate via `--next-token` or use `aws logs tail` and post-process. The kubesplaining EKS parser accepts either the `{"events":[...]}` envelope from `filter-log-events` or one `{"message":"..."}` object per line. ```bash # Linux START_MS=$(($(date -u +%s -d '30 days ago') * 1000)) # macOS / BSD START_MS=$(($(date -u -v-30d +%s) * 1000)) aws logs filter-log-events \ --log-group-name "/aws/eks/my-cluster/cluster" \ --log-stream-name-prefix "kube-apiserver-audit-" \ --start-time "$START_MS" \ --output json \ > eks-audit.json ``` -------------------------------- ### Run TestFinding tests to verify compile failure Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Runs the TestFinding tests in the privesc package with isolated Go caches. Expected to fail compilation because finding.AlternateEscalationPath is undefined until the field is added. ```bash GOCACHE=$(pwd)/.tmp/go-build-cache GOMODCACHE=$(pwd)/.tmp/go-mod-cache \ go test ./internal/analyzer/privesc -run TestFinding -v ``` -------------------------------- ### Run the e2e suite with make e2e Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Executes the end-to-end test suite on a local kind cluster. Expected to pass with 'ok chain KUBE-PRIVESC-PATH-CLUSTER-ADMIN:ServiceAccount/cutres/...' lines for all four assertions. ```bash make e2e ``` -------------------------------- ### S3 Copy: Read Demo Bucket Flag Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/eks-demo-walkthrough.md Uses `aws s3 cp` to download the flag file from the demo bucket `kubesplaining-holysplain-secrets-123456789012-us-east-1`. Demonstrates the minimum data-exfiltration action possible with the assumed role's permissions. ```bash aws s3 cp s3://kubesplaining-holysplain-secrets-123456789012-us-east-1/flag.txt - ``` -------------------------------- ### Set up attacker kubeconfig context with stolen token Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/eks-demo-walkthrough.md Creates a token for the dev-deployer-sa service account, then adds a kubeconfig user and context named holy-splain-attacker that uses that token. The context reuses the existing eksctl cluster definition and sets the namespace to dev-team. This simulates using a leaked token; a real attacker would already possess the token. ```bash TOKEN=$(kubectl create token dev-deployer-sa -n dev-team --duration=1h) kubectl config set-credentials holy-splain-dev-deployer-sa --token="$TOKEN" kubectl config set-context holy-splain-attacker \ --cluster=holy-splain.us-east-1.eksctl.io \ --user=holy-splain-dev-deployer-sa \ --namespace=dev-team ``` -------------------------------- ### Verify EKS demo cleanup with AWS CLI and kubectl Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/eks-demo.md Run these commands after `make eks-demo-down` to confirm no leftover resources. All four should return empty output. ```bash aws eks list-clusters --region $AWS_REGION | grep holy-splain aws iam list-roles --query "Roles[?starts_with(RoleName, \`HolySplain\`)].RoleName" aws s3 ls | grep holysplain kubectl config get-contexts | grep holy-splain ``` -------------------------------- ### Scan current kubectl context and open report Source: https://github.com/0hardik1/kubesplaining/blob/main/README.md Runs a scan against the current kubectl context, writing results to ./kubesplaining-report/. Then opens the generated HTML report (open on macOS, xdg-open on Linux). ```bash kubesplaining scan # writes ./kubesplaining-report/ open kubesplaining-report/report.html # macOS; xdg-open on Linux ``` -------------------------------- ### Run privesc and models tests with isolated Go caches Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Runs the privesc analyzer and models tests using temporary build and module caches under .tmp. Expected to pass, including three new tests and all pre-existing privesc tests. ```bash GOCACHE=$(pwd)/.tmp/go-build-cache GOMODCACHE=$(pwd)/.tmp/go-mod-cache \ go test ./internal/analyzer/privesc/... ./internal/models/... -v ``` -------------------------------- ### Run cut-resilient tests with isolated Go caches Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Runs the targeted tests for alternate paths using temporary build and module caches to avoid polluting the project cache. Expected to fail with 'want a 1-hop alternate ... got 0 hops' before implementing the changes. ```bash GOCACHE=$(pwd)/.tmp/go-build-cache GOMODCACHE=$(pwd)/.tmp/go-mod-cache \ go test ./internal/analyzer/privesc -run 'TestAlternate|TestNoAlternate|TestSyntheticRooted' -v ``` -------------------------------- ### Run privesc pathfinder tests with isolated Go caches Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Runs the privesc analyzer tests with per-run Go build and module caches under .tmp. Expected to pass, including four new tests; TestFindPathsContinuesThroughTraversableSink must still pass unchanged. ```bash GOCACHE=$(pwd)/.tmp/go-build-cache GOMODCACHE=$(pwd)/.tmp/go-mod-cache \ go test ./internal/analyzer/privesc/... -v ``` -------------------------------- ### Fast inner loop for a single package (go test with custom caches) Source: https://github.com/0hardik1/kubesplaining/blob/main/CONTRIBUTING.md Runs tests for a specific package using local build and module caches to speed up iteration. ```bash GOCACHE=$(pwd)/.tmp/go-build-cache GOMODCACHE=$(pwd)/.tmp/go-mod-cache \ go test ./internal/analyzer/rbac/... ``` -------------------------------- ### Run kubesplaining scan with audit log file Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/audit-logs.md Run kubesplaining scan with a single audit log file, using --audit-source native and a 30-day observation window. ```bash kubesplaining scan \ --input-file snapshot.json \ --audit-log ./audit.log \ --audit-source native \ --audit-window-days 30 \ --least-privilege-only ``` -------------------------------- ### Create kind cluster with audit config Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/audit-logs.md Creates the kind cluster using the audit-enabled configuration file kind-audit.yaml. Run this after placing both kind-audit.yaml and audit-policy.yaml in the current directory. ```bash kind create cluster --config kind-audit.yaml ``` -------------------------------- ### Run Confused-Deputy Tests with Local Go Cache Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-24-privesc-deep-chains.md Executes the TestConfusedDeputy tests in the privesc analyzer package using temporary build and module caches under .tmp. Expected outcome: the first test fails and the second passes vacuously. ```bash GOCACHE=$(pwd)/.tmp/go-build-cache GOMODCACHE=$(pwd)/.tmp/go-mod-cache go test ./internal/analyzer/privesc/ -run TestConfusedDeputy -v ``` -------------------------------- ### Wire alternates into FindPaths Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Updates the per-source loop in FindPaths to compute alternates and attach them to each path. The doc comment should be updated to note that a path may now carry an alternate. ```go found := bfsToSinks(graph, adj, sourceID, maxDepth, nil) alternates := alternatesForSource(graph, adj, sourceID, maxDepth, found) for targetID, chain := range found { targetNode := graph.Nodes[targetID] path := buildPath(graph, sourceNode.Subject, targetNode.Target, chain) path.TargetNamespace = targetNode.TargetNamespace if alt, ok := alternates[targetID]; ok { path.AlternateHops = buildPath(graph, sourceNode.Subject, targetNode.Target, alt).Hops } paths = append(paths, path) } ``` -------------------------------- ### Run test command: GOCACHE/GOMODCACHE go test -run TestControlPlaneSAIsTraversableButNotASource Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-24-privesc-deep-chains.md Command to run the failing test. Expected to fail with a compile error on the missing IsControlPlane field. ```bash GOCACHE=$(pwd)/.tmp/go-build-cache GOMODCACHE=$(pwd)/.tmp/go-mod-cache go test ./internal/analyzer/privesc/ -run TestControlPlaneSAIsTraversableButNotASource -v ``` -------------------------------- ### Append EscalationHop with binding provenance in Go Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Builds an EscalationHop for each step in an escalation path, copying edge metadata including SourceBinding and BindingNamespace. Used in the privesc pathfinder to record which RBAC binding granted the hop. ```go hops = append(hops, models.EscalationHop{ Step: i + 1, Action: step.edge.Action, Technique: step.edge.Technique, Difficulty: step.edge.Difficulty, FromSubject: current, ToSubject: next, Permission: step.edge.Permission, Gains: step.edge.Description, SourceBinding: step.edge.SourceBinding, SourceRole: step.edge.SourceRole, BindingNamespace: step.edge.BindingNamespace, }) ``` -------------------------------- ### Prove node access with chroot /host Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/eks-demo-walkthrough.md Runs a chroot into the host filesystem from a pod to verify real node access. The output shows the EC2 instance hostname, the node-only /etc/kubernetes directory, and per-pod directories under /var/lib/kubelet/pods, which are not visible from inside a non-privileged pod. ```bash kubectl --context=holy-splain-attacker exec attacker -n dev-team -- \ chroot /host /bin/sh -c ' cat /etc/hostname # the EC2 instance name ls /etc/kubernetes # only exists on a node ls /var/lib/kubelet/pods | head # one directory per pod ' ``` ```text ip-192-168-13-154.ec2.internal kubelet manifests pki 41ac61e7-d421-408d-b8da-c58bccc34b71 5ba48433-921a-42ea-9921-df345f806d24 7d855b99-e396-4607-9369-892bce1114cd ... ``` -------------------------------- ### renderEscalationPath primary variant wrapper Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Default entry point for rendering the primary escalation chain. Kept as a wrapper so existing callers and the escalationPathHTML template func read unchanged. ```go // renderEscalationPath renders the primary chain. Kept as the default entry point so // existing callers and the escalationPathHTML template func read unchanged. func renderEscalationPath(hops []models.EscalationHop) template.HTML { return renderEscalationPathVariant(hops, "primary") } ``` -------------------------------- ### Run report tests Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Runs the report package tests with a local Go build/module cache. Expected PASS; a template parse error surfaces as a failure in TestBuildHTMLData or the writer tests. ```bash GOCACHE=$(pwd)/.tmp/go-build-cache GOMODCACHE=$(pwd)/.tmp/go-mod-cache \ go test ./internal/report/... -v ``` -------------------------------- ### Run package tests for privesc analyzer Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-24-privesc-deep-chains.md Runs the privesc analyzer tests with isolated Go caches. Expect the two new tests to pass; pre-existing tests asserting exact scores or severities will fail and must be updated by hand from the new formula, not weakened to ranges. ```bash GOCACHE=$(pwd)/.tmp/go-build-cache GOMODCACHE=$(pwd)/.tmp/go-mod-cache go test ./internal/analyzer/privesc/... 2>&1 | tail -25 ``` -------------------------------- ### Run analyzer and models tests with Go cache Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Runs go test on the analyzer and models packages with local build/module caches. Expected: PASS. ```bash GOCACHE=$(pwd)/.tmp/go-build-cache GOMODCACHE=$(pwd)/.tmp/go-mod-cache \ go test ./internal/analyzer/... ./internal/models/... -v ``` -------------------------------- ### Bash: run TestForPrivescPath tests with custom Go caches Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Runs the two ForPrivescPath tests verbosely, using temporary Go build and module caches under .tmp. Expected: TestForPrivescPathCutsGrantingBinding fails with the diff mentioning aaa-harmless-view, while TestForPrivescPathFallsBackWithoutProvenance passes. ```bash GOCACHE=$(pwd)/.tmp/go-build-cache GOMODCACHE=$(pwd)/.tmp/go-mod-cache \ go test ./internal/remediation -run TestForPrivescPath -v ``` -------------------------------- ### Populate AltHops in PrivescPathCard literal Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Adds AltHops and AltHopCount to the PrivescPathCard literal in internal/report/privesc_paths_section.go. Card sorting stays keyed on the primary chain, so tab ordering does not move. ```go AltHops: f.AlternateEscalationPath, AltHopCount: len(f.AlternateEscalationPath), ``` -------------------------------- ### Run full test suite and corpus Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-24-privesc-deep-chains.md Runs the full test suite and corpus generation, piping output to tail. Expected green; corpus snapshots have no Nodes so the gate returns false and nothing changes. ```bash make test 2>&1 | tail -10 && make corpus 2>&1 | tail -20 ``` -------------------------------- ### Run privesc package tests with pinned caches Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-24-privesc-deep-chains.md Use this command to run tests for the privesc analyzer package. It pins GOCACHE and GOMODCACHE under .tmp/ as required by the Makefile. Run it before committing changes to the privesc package. ```bash GOCACHE=$(pwd)/.tmp/go-build-cache GOMODCACHE=$(pwd)/.tmp/go-mod-cache go test ./internal/analyzer/privesc/... ``` -------------------------------- ### Go test helpers and cut-resilient escalation path tests Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/superpowers/plans/2026-07-25-cut-resilient-escalation-paths.md Contains a helper for binding edges and four Go tests verifying alternate escalation paths after cutting a binding. Tests cover parallel bindings, longer routes, no alternate when cut closes everything, and synthetic-rooted paths skipping the cut pass. ```go // bindingEdge is a test helper: an edge carrying binding provenance. func bindingEdge(action, binding string) *models.EscalationEdge { return &models.EscalationEdge{Action: action, SourceBinding: binding, SourceRole: "some-role"} } // TestAlternateViaParallelBinding is the case that proves this feature is about // remediation, not chain length. One subject is bound to cluster-admin twice, so // cutting the first binding leaves the second and the alternate comes back at the // SAME hop count. func TestAlternateViaParallelBinding(t *testing.T) { graph := &models.EscalationGraph{Nodes: map[string]*models.EscalationNode{}} src := models.SubjectRef{Kind: "ServiceAccount", Name: "twice", Namespace: "app"} ensureSubjectNode(graph, src) graph.Nodes[sinkClusterAdmin] = &models.EscalationNode{ ID: sinkClusterAdmin, IsSink: true, Target: models.TargetClusterAdmin, } addEdge(graph, nodeID(src), sinkClusterAdmin, bindingEdge("bound_to_cluster_admin", "admin-a")) addEdge(graph, nodeID(src), sinkClusterAdmin, bindingEdge("bound_to_cluster_admin", "admin-b")) paths := FindPaths(graph, 5) if len(paths) != 1 { t.Fatalf("want 1 path, got %d", len(paths)) } p := paths[0] if len(p.AlternateHops) != 1 { t.Fatalf("want a 1-hop alternate via the second binding, got %d hops", len(p.AlternateHops)) } if p.Hops[0].SourceBinding == p.AlternateHops[0].SourceBinding { t.Errorf("alternate reuses the cut binding %q", p.Hops[0].SourceBinding) } } // TestAlternateViaLongerRoute covers the case the research doc framed as O3: the // only surviving route is a genuinely longer chain through an intermediate. func TestAlternateViaLongerRoute(t *testing.T) { graph := &models.EscalationGraph{Nodes: map[string]*models.EscalationNode{}} src := models.SubjectRef{Kind: "ServiceAccount", Name: "src", Namespace: "app"} mid := models.SubjectRef{Kind: "ServiceAccount", Name: "mid", Namespace: "app"} ensureSubjectNode(graph, src) ensureSubjectNode(graph, mid) graph.Nodes[sinkClusterAdmin] = &models.EscalationNode{ ID: sinkClusterAdmin, IsSink: true, Target: models.TargetClusterAdmin, } addEdge(graph, nodeID(src), sinkClusterAdmin, bindingEdge("bound_to_cluster_admin", "direct")) addEdge(graph, nodeID(src), nodeID(mid), bindingEdge("impersonate", "detour")) addEdge(graph, nodeID(mid), sinkClusterAdmin, bindingEdge("bound_to_cluster_admin", "mid-admin")) paths := FindPaths(graph, 5) if len(paths) != 1 { t.Fatalf("want 1 path to cluster-admin, got %d", len(paths)) } p := paths[0] if len(p.Hops) != 1 { t.Fatalf("primary should stay the 1-hop direct route, got %d hops", len(p.Hops)) } if len(p.AlternateHops) != 2 { t.Fatalf("want a 2-hop alternate through mid, got %d hops", len(p.AlternateHops)) } if p.AlternateHops[0].Action != "impersonate" { t.Errorf("alternate first hop = %q, want impersonate", p.AlternateHops[0].Action) } } // TestNoAlternateWhenCutClosesEverything is the common case: one route, and cutting // its binding closes it. The field must stay empty rather than echoing the primary. func TestNoAlternateWhenCutClosesEverything(t *testing.T) { graph := &models.EscalationGraph{Nodes: map[string]*models.EscalationNode{}} src := models.SubjectRef{Kind: "ServiceAccount", Name: "only", Namespace: "app"} ensureSubjectNode(graph, src) graph.Nodes[sinkClusterAdmin] = &models.EscalationNode{ ID: sinkClusterAdmin, IsSink: true, Target: models.TargetClusterAdmin, } addEdge(graph, nodeID(src), sinkClusterAdmin, bindingEdge("bound_to_cluster_admin", "the-only-one")) paths := FindPaths(graph, 5) if len(paths) != 1 { t.Fatalf("want 1 path, got %d", len(paths)) } if len(paths[0].AlternateHops) != 0 { t.Fatalf("want no alternate, got %d hops", len(paths[0].AlternateHops)) } } // TestSyntheticRootedPathSkipsCutPass proves a chain whose first hop names no // binding is left alone. There is no binding to model cutting, so claiming an // alternate would be meaningless. func TestSyntheticRootedPathSkipsCutPass(t *testing.T) { graph := &models.EscalationGraph{Nodes: map[string]*models.EscalationNode{}} src := models.SubjectRef{Kind: "ServiceAccount", Name: "escaper", Namespace: "app"} ensureSubjectNode(graph, src) graph.Nodes[sinkNodeEscape] = &models.EscalationNode{ ID: sinkNodeEscape, IsSink: true, Target: models.TargetNodeEscape, } // No SourceBinding: a pod-escape edge. addEdge(graph, nodeID(src), sinkNodeEscape, &models.EscalationEdge{Action: "pod_host_escape"}) addEdge(graph, nodeID(src), sinkNodeEscape, &models.EscalationEdge{Action: "pod_host_escape_2"}) paths := FindPaths(graph, 5) if len(paths) != 1 { t.Fatalf("want 1 path, got %d", len(paths)) } if len(paths[0].AlternateHops) != 0 { t.Fatalf("want no alternate for a synthetic-rooted chain, got %d hops", len(paths[0].AlternateHops)) } } ``` -------------------------------- ### Run kubesplaining scan on EKS audit logs Source: https://github.com/0hardik1/kubesplaining/blob/main/docs/audit-logs.md Runs kubesplaining with the exported audit log and a snapshot file. The repo includes fixture exports `testdata/audit/eks/minimal-risky-eks-audit.json` and `testdata/snapshots/minimal-risky.json`; running this command against those files should emit a `KUBE-RBAC-UNUSED-VERB-001` finding for the `reader` ServiceAccount. ```bash kubesplaining scan \ --input-file snapshot.json \ --audit-log ./eks-audit.json \ --audit-source eks \ --audit-window-days 30 \ --least-privilege-only ``` -------------------------------- ### Run corpus with make Source: https://github.com/0hardik1/kubesplaining/blob/main/testdata/corpus/README.md Commands to run the corpus scorecard for every case and to run the same gate as part of `go test ./...`. ```bash make corpus # scorecard for every case make test # runs the same gate as part of `go test ./...` ```