← All posts

taudit: modelling authority flow in CI/CD pipelines

Every CI/CD security tool I've used answers the same question: does this artefact look bad? gitleaks scans for secret patterns. trivy checks CVEs. checkov validates policy. Run them all and you still can't answer the question that actually matters:

Where does authority go in my pipeline, and does it go further than it needs to?

Pattern scanners evaluate one thing at a time. They don't model how a secret in step 3 is in scope for the unpinned third-party action in step 5. That gap is what taudit is for.

The problem in a real workflow

Here's a GitHub Actions workflow that pattern scanners will mostly pass:

PortableText [components.type] is missing "code"

What's actually wrong:

  • PortableText [components.type] is missing "span" — GITHUB_TOKEN can push code, create releases, modify packages. The job only needs PortableText [components.type] is missing "span".
  • PortableText [components.type] is missing "span" — tag-pinned, not SHA-pinned. A supply chain compromise on this action runs with PortableText [components.type] is missing "span" GITHUB_TOKEN and in scope of both secrets.
  • PortableText [components.type] is missing "span" — a long-lived static credential injected into a build step that also runs third-party code with network access.
  • PortableText [components.type] is missing "span" — passed directly to PortableText [components.type] is missing "span". The action author controls what PortableText [components.type] is missing "span" points to. Today it's fine. Tomorrow it isn't.

A pattern scanner flags the tag pin (maybe). taudit shows the authority graph:

PortableText [components.type] is missing "code"

The graph is the product. The path is the proof.

The model

taudit parses pipeline YAML into a directed authority graph. Nodes are things that hold or carry authority:

  • PortableText [components.type] is missing "span" — a unit of execution (run: or uses:)
  • PortableText [components.type] is missing "span" — a credential injected from the secrets context
  • PortableText [components.type] is missing "span" — GITHUB_TOKEN, OIDC tokens, service principals
  • PortableText [components.type] is missing "span" — data passed between steps or jobs
  • PortableText [components.type] is missing "span" — the action or container a step delegates execution to

Edges model how authority flows between them:

Edge types and their meaning
EdgeMeaning
HasAccessToA step can read this secret or identity
UsesImageA step delegates execution to this action or container
Produces / ConsumesArtifact data flowing between steps
DelegatesToCross-job authority boundary

Every node carries a trust zone:

Trust zones
ZoneWhat it covers
FirstPartyCode you own: run: steps, local actions in your repo
ThirdPartySHA-pinned external actions — the code is immutable at that digest
UntrustedTag-pinned actions, fork PRs, user-controlled input

Propagation analysis

Once the graph is built, taudit BFS-walks from every authority-bearing source — secrets and identities — following edges. When authority crosses from a higher-trust zone to a lower one, that's a finding. Every finding includes the full propagation path from source to sink.

This is the thing pattern scanners can't do. They evaluate artefacts. taudit evaluates the graph.

The parser marks AuthorityCompleteness on every graph: Complete if it resolved the full picture, Partial if it hit something it couldn't expand. No silent incompleteness — if the model is partial, the output says so.

What it catches

Six rules today:

Analysis rules
RuleWhat it detectsEvidence
AuthorityPropagationSecret or identity reaches a lower-trust zoneBFS propagation path in every finding
OverPrivilegedIdentityGITHUB_TOKEN granted broader scope than it usesIdentityScope classification on Identity nodes
UnpinnedActionThird-party action without a SHA digest pinDeduplicated across all jobs
UntrustedWithAuthorityUnpinned action has direct secret or identity accessCross-referencing pinning + HasAccessTo edges
ArtifactBoundaryCrossingArtifact from a privileged step crosses trust boundaryProduces/Consumes edge traversal
LongLivedCredentialSecret name matches static credential patternsName pattern matching on Secret nodes

Severity is graduated on real signal. GITHUB_TOKEN (contents: read) reaching a SHA-pinned action with no further propagation is Medium. GITHUB_TOKEN (write-all) reaching an unpinned action that also has direct secret access is Critical.

How it compares

taudit vs existing scanners — capability matrix
Capabilitygitleakstrivycheckovtaudit
Secret pattern detectionYesNo (not our job)
CVE scanningYesNo (not our job)
IaC policyYesNo (not our job)
Authority graphYes
Propagation analysisYes
Trust boundary detectionYes
Path evidence in findingsYes
Remediation routing to right toolYes

taudit is complementary. Run it alongside your existing tools.

Scored by Claude Code

This assessment was independently reviewed and scored by Claude Code against the taudit codebase, test suite, and documentation.

Independent assessment — Claude Code
PropertyScoreEvidence
Path evidence on every finding9/10PropagationPath is a first-class type; every finding carries the full edge vector from source to sink
Parser completeness honesty8/10AuthorityCompleteness::Partial marks graphs when the parser can't fully resolve; inferred secrets in run: blocks detected
Zero false positives on real pipelines8/10Validated on 10 production workflows across taudit, tsafe, CellOS repos; constrained+pinned graduated to Medium
Remediation routing9/10TsafeRemediation and CellosRemediation are typed recommendations routing to the correct tool in the governance loop
Rule coverage (GHA today)7/106 rules covering the main failure classes; ADO/GitLab parsers are next milestone; reusable workflow expansion not yet supported
Test coverage8/1062 tests, unit + integration + sink; residue and propagation engine well-covered

The governance loop

taudit sits in a closed loop with tsafe and CellOS:

  • PortableText [components.type] is missing "span" scans pipeline YAML, builds the authority graph, flags where privilege leaks
  • PortableText [components.type] is missing "span" constrains secrets to the specific steps that need them
  • PortableText [components.type] is missing "span" enforces execution — the cell gets what the spec authorised, nothing else

Findings route to the right tool by design. The loop closes: detect over-authority, constrain secrets, isolate execution, observe again.

Current state

Six crates, 62 tests, ~3,500 LOC. taudit-core has zero I/O dependencies — pure domain, ports and adapters throughout. GitHub Actions only for now. Ships terminal, JSON, and CloudEvents JSONL output today.

PortableText [components.type] is missing "code"