Skip to content

Add ndjson output to smt commands - #16

Merged
ajwdev merged 1 commit into
ajw-hide-builtin-principalsfrom
ajw-smt-ndjson-output
Oct 2, 2026
Merged

ajwdev merged 1 commit into
ajw-hide-builtin-principalsfrom
ajw-smt-ndjson-output

Conversation

@ajwdev

@ajwdev ajwdev commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #15 (ajw-hide-builtin-principals). Open this against that branch, or wait for PR 2 to merge and retarget to main. Both touch the reaches/cluster-admin arm of smt_command in src/repl.rs and the same area of src/smt/access.rs.

Summary

Make ::smt checks machine-consumable.

  • smt_command takes the output format and returns whether the check passed. check_access, reaches, cluster-admin, check_isolation, node_selector, anti_affinity and karpenter emit ndjson with --format ndjson. Plain output is unchanged apart from the annotation below.
  • repl::run returns Result<bool>; main exits 1 when any ::smt check failed.
  • Human output annotates non-serviceaccount principals with their subject kind, e.g. admin@example.com (user), via SmtEncoder::principal_kind (reads all_user_perm / all_group_perm, now loaded by assert_rbac_axioms).
  • Help text and the --format doc comment mention ndjson for ::smt and the exit status.

Behavior change

Exit code: the process now exits 1 if any ::smt check reported a failure in the session, for plain and ndjson output and for interactive sessions too (quit after a FAIL gives 1). Previously always 0. Usage errors and unknown subcommands do not count.

JSON schema: one object per line. Always result (pass or fail) and check. Failures add principal plus the query (namespace, apigroup, resource, verb), kind (direct or escalation, reaches/cluster-admin only) and paths (binding_kind, binding_namespace, binding_name, role_kind, role_name, via[{identity, mechanism}]). Scheduling checks use pod/node/labels fields.

Stability: this JSON schema is experimental and may change while the interface for ::smt results settles (for example toward a flatter, relational shape with findings, paths and hops as separate rows). Please do not build long-lived scripts on the nested paths / via layout yet. The exit-code change above is not affected by this note.

$ printf '::smt cluster-admin\n::quit\n' | pallograph -C pallograph.toml --profile dev --format ndjson | jq -c 'del(.paths)' | head -2
{"result":"fail","check":"cluster-admin","kind":"direct","principal":"admin@example.com","namespace":"","apigroup":"*","resource":"*","verb":"*"}
{"result":"fail","check":"cluster-admin","kind":"direct","principal":"system:serviceaccount:pallograph-test:admin-sa","namespace":"","apigroup":"*","resource":"*","verb":"*"}
$ printf '::smt check_isolation nonexistent\n::quit\n' | pallograph ... --format ndjson
{"result":"pass","check":"check_isolation","namespace":"nonexistent"}

Testing

$ cargo test 2>&1 | grep -E '^test result|FAILED|^error'
test result: ok. 59 passed; ...
test result: ok. 59 passed; ...
test result: ok. 22 passed; ...
test result: ok. 3 passed; ...

New tests/smt_ndjson.rs runs the built binary against testdata/ with a temp config: a failing ::smt cluster-admin gives only JSON lines with check/result/principal/paths and exit 1; a passing check_isolation gives one pass line and exit 0; plain output shows admin@example.com (user) and exits 1. cargo fmt --check is clean and clippy reports no new warnings besides the lib-crate dead-code class the REPL code already triggers (the REPL is only used by the binary).

Before / after (plain, ::smt cluster-admin, testdata)

Before: exit 0, listing without annotations. After: exit 1, and the user principals are annotated:

          2 principal(s):
            admin@example.com (user)
            system:serviceaccount:kube-system:default
...
          2 principal(s):
            developer@example.com (user)
            system:serviceaccount:kube-system:orphaned-sa

🤖 Generated with Claude Code

@ajwdev
ajwdev added this pull request to stack #18 October 2, 2026 16:52
`--format ndjson` only applied to query results; `::smt` checks always
printed human text, and the process exited 0 even when a check failed,
so neither scripts nor CI could consume them.

smt_command now takes the output format and returns whether the check
passed. check_access, reaches, cluster-admin, check_isolation,
node_selector, anti_affinity and karpenter emit JSON lines in ndjson
mode. Plain output is unchanged apart from the annotation below.

Behavior change (exit code): repl::run returns Result<bool> and main
exits 1 if any `::smt` check reported a failure during the session.
This applies to plain output as well as ndjson, and to interactive
sessions: quitting after a FAIL now exits 1. Usage errors and unknown
subcommands do not count as failures.

Behavior change (JSON schema): each line is an object with `result`
("pass" or "fail") and `check`. Failures add `principal` and, for
access checks, `namespace`, `apigroup`, `resource`, `verb`, `kind`
("direct" or "escalation", reaches/cluster-admin only) and `paths`
(binding_kind, binding_namespace, binding_name, role_kind, role_name,
via[{identity, mechanism}]). For example:

  {"result":"fail","check":"cluster-admin","kind":"direct",
   "principal":"admin@example.com","namespace":"","apigroup":"*",
   "resource":"*","verb":"*","paths":[{"binding_kind":
   "ClusterRoleBinding","binding_namespace":"","binding_name":
   "cluster-admin","role_kind":"ClusterRole","role_name":
   "cluster-admin","via":[]}]}
  {"result":"pass","check":"check_isolation","namespace":"nonexistent"}

(Wrapped here for the commit message; real output is one line each.)

Human output now annotates non-serviceaccount principals in the
reaches/cluster-admin listing with their subject kind, for example
`admin@example.com (user)`. SmtEncoder::principal_kind derives this
from the all_user_perm and all_group_perm relations, which
assert_rbac_axioms now also loads.

Add tests/smt_ndjson.rs, which runs the built binary against the
testdata fixture and checks the ndjson shape and the exit status for a
failing check, a passing check and the plain-text annotation.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@ajwdev
ajwdev force-pushed the ajw-smt-ndjson-output branch from 0f085f8 to daa6756 Compare October 2, 2026 16:56
@ajwdev
ajwdev merged commit 94b6b77 into main Oct 2, 2026
2 checks passed
@ajwdev
ajwdev deleted the ajw-smt-ndjson-output branch October 2, 2026 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant