Tainted CI
The one surface where Tainted's dynamic proof step reliably has a real app to
attack, because the pipeline already builds a preview deployment — a running app
you own. Ownership here is not DNS (the preview host is thrown away after the run, so a
DNS TXT record can't point at it) but OIDC: GitHub Actions and GitLab CI mint
short-lived signed identity tokens whose claims name the repository and run. analyze
reports what it finds, prove runs the attack against the preview, and the job fails
when a high-severity finding is proven.
How it runs
Everything is configured through environment variables (see
tainted_ci/entrypoint.py), so GitHub and GitLab share one runner. The entrypoint:
- runs
analyzeon the checkout, always; - if
TAINTED_TARGET_URLis set, checks ownership (local, or an OIDC repo-claim check viatainted_ci/oidc.py) and, if that passes, runsproveagainst the preview; - writes a markdown report to
$GITHUB_STEP_SUMMARYand to stdout; - exits non-zero when a finding at or above
TAINTED_FAIL_ON(defaulthigh) is proven.
One gap, stated plainly: to bind the verified workflow to the specific preview URL, the workflow has to claim that URL itself. Tainted checks that claim but cannot independently confirm it.
The walkthrough
TAINTED_TUTORIAL=1 prints the setup walkthrough and exits without scanning — useful from
inside the job container while the pipeline is still half-wired. Set it to a topic slug
(analyze-only, prove-the-preview, ownership, the-gate, auto-fix-pr) to print just
that lesson.
Every report also ends with a Next steps block saying how far this particular run reached and what to set to reach further — a summary with no proof column reads as a clean bill of health when it may only mean nothing was tried.
Ownership verification
Tainted always checks the claims (repository must match $GITHUB_REPOSITORY). It
verifies the signature against the provider's JWKS when PyJWT is installed
(it is, as one of this surface's dependencies). If it can't verify the
signature, the report says so instead of pretending it checked.
Auto-fix PR
fix: "true" (or TAINTED_FIX=1) takes every finding this run proved whose repair the code
fully determines, writes it, re-proves it against this same preview, and opens one PR carrying
the evidence. Tool-plane findings are reported, never auto-fixed — their repair depends on
answers only a person has.
It needs permissions: contents: write and a github-token, and it shells out to git and
gh; the action's image installs both.
Deploy
- GitHub Actions:
uses: OWNER/tainted@v0. The manifest isaction.ymlat the repository root, which is where GitHub looks and — because a Docker action's build context is the directory holding its manifest — also the contextDockerfilebelow needs. Seeexamples/github-workflow.ymlfor the whole job (permissions: id-token: writeand the OIDC-token minting step are the two easy things to leave out). - GitLab CI: see
examples/gitlab-ci.yml(uses theid_tokenskeyword). - Container:
docker build -f surfaces/ci/Dockerfile -t tainted-ci .(build context = repo root, so the core is included), then run it with theTAINTED_*environment variables.
PUBLISHING.md §3 covers tagging, the moving v0 tag, the Marketplace listing, and pushing a
pre-built image so consumer jobs stop rebuilding it.
Release files for tainted-ci 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tainted_ci-0.1.0.tar.gz | 20.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tainted_ci-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 39.0 kB
Release files / tainted_ci-0.1.0.tar.gz
| Download URL | tainted_ci-0.1.0.tar.gz |
|---|---|
| Size | 20.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f89b9612e577b7e7b079b673a10b010a8a16c2ec05df4148649ffaf4a66fd72f
|
|
BLAKE2b-256 checksum How to use checksums |
fb7a97a15630405df9de64fc98b4d9ca7d4f90d9f80b313a2195c542d47afa04
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.9
|
Release files / tainted_ci-0.1.0-py3-none-any.whl
| Download URL | tainted_ci-0.1.0-py3-none-any.whl |
|---|---|
| Size | 18.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b649dee37edf1791aed5ec2724c6ca73d5c49114edb8dcc7984579dfde7c9372
|
|
BLAKE2b-256 checksum How to use checksums |
0b9a54204e1563441177200f00c8f0f15ef0e6c365457430a1791b393487b184
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.9
|