subcheck 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. subcheck-0.2.0/.gitattributes +10 -0
  2. subcheck-0.2.0/.github/dependabot.yml +40 -0
  3. subcheck-0.2.0/.github/workflows/ci.yml +34 -0
  4. subcheck-0.2.0/.github/workflows/release.yml +44 -0
  5. subcheck-0.2.0/.gitignore +11 -0
  6. subcheck-0.2.0/ARCHITECTURE.md +129 -0
  7. subcheck-0.2.0/BACKLOG.md +141 -0
  8. subcheck-0.2.0/CHANGELOG.md +58 -0
  9. subcheck-0.2.0/CONTRIBUTING.md +28 -0
  10. subcheck-0.2.0/LICENSE +21 -0
  11. subcheck-0.2.0/PKG-INFO +264 -0
  12. subcheck-0.2.0/README.md +234 -0
  13. subcheck-0.2.0/action.yml +47 -0
  14. subcheck-0.2.0/examples/claims-immutable.json +14 -0
  15. subcheck-0.2.0/examples/claims-main.json +12 -0
  16. subcheck-0.2.0/examples/claims-pull-request.json +11 -0
  17. subcheck-0.2.0/examples/policy-immutable.json +11 -0
  18. subcheck-0.2.0/examples/policy.json +11 -0
  19. subcheck-0.2.0/examples/policy.yaml +19 -0
  20. subcheck-0.2.0/pyproject.toml +57 -0
  21. subcheck-0.2.0/src/subcheck/__init__.py +23 -0
  22. subcheck-0.2.0/src/subcheck/__main__.py +5 -0
  23. subcheck-0.2.0/src/subcheck/cli.py +80 -0
  24. subcheck-0.2.0/src/subcheck/decoder.py +98 -0
  25. subcheck-0.2.0/src/subcheck/policy.py +115 -0
  26. subcheck-0.2.0/src/subcheck/py.typed +0 -0
  27. subcheck-0.2.0/src/subcheck/report.py +106 -0
  28. subcheck-0.2.0/src/subcheck/validator.py +64 -0
  29. subcheck-0.2.0/tests/conftest.py +34 -0
  30. subcheck-0.2.0/tests/fixtures/github_subjects.json +72 -0
  31. subcheck-0.2.0/tests/test_cli.py +57 -0
  32. subcheck-0.2.0/tests/test_decoder.py +68 -0
  33. subcheck-0.2.0/tests/test_decoder_vectors.py +25 -0
  34. subcheck-0.2.0/tests/test_report.py +66 -0
  35. subcheck-0.2.0/tests/test_validator.py +63 -0
@@ -0,0 +1,10 @@
1
+ # Normalize line endings to LF in the repo (kills the CRLF churn on Windows).
2
+ * text=auto eol=lf
3
+
4
+ # Explicitly text
5
+ *.py text eol=lf
6
+ *.json text eol=lf
7
+ *.toml text eol=lf
8
+ *.md text eol=lf
9
+ *.yml text eol=lf
10
+ *.yaml text eol=lf
@@ -0,0 +1,40 @@
1
+ # Dependabot configuration - deliberately low-noise.
2
+ #
3
+ # Non-major updates arrive as ONE grouped PR per ecosystem per month.
4
+ # Security updates stay ungrouped and uncooled so they land fast.
5
+ # Rationale and the estate-wide posture: github.com/Dashtid/estate
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: "pip"
9
+ directory: "/"
10
+ schedule:
11
+ interval: "monthly"
12
+ open-pull-requests-limit: 5
13
+ cooldown:
14
+ default-days: 7
15
+ semver-major-days: 30
16
+ groups:
17
+ python-non-major:
18
+ applies-to: version-updates
19
+ patterns:
20
+ - "*"
21
+ update-types:
22
+ - "minor"
23
+ - "patch"
24
+ labels:
25
+ - "dependencies"
26
+
27
+ - package-ecosystem: "github-actions"
28
+ directory: "/"
29
+ schedule:
30
+ interval: "monthly"
31
+ open-pull-requests-limit: 3
32
+ cooldown:
33
+ default-days: 7
34
+ groups:
35
+ actions-all:
36
+ applies-to: version-updates
37
+ patterns:
38
+ - "*"
39
+ labels:
40
+ - "dependencies"
@@ -0,0 +1,34 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
18
+ steps:
19
+ - uses: actions/checkout@v7
20
+ - uses: actions/setup-python@v7
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+ - name: Install
24
+ run: |
25
+ python -m pip install --upgrade pip
26
+ pip install -e ".[dev]"
27
+ - name: Lint (ruff)
28
+ run: ruff check .
29
+ - name: Type check (mypy)
30
+ run: mypy
31
+ - name: Security lint (bandit)
32
+ run: bandit -q -r src
33
+ - name: Tests (pytest)
34
+ run: pytest -q --cov=subcheck --cov-report=term-missing
@@ -0,0 +1,44 @@
1
+ name: Release
2
+
3
+ # Publishes to PyPI via OIDC trusted publishing (no stored API token) - the same
4
+ # short-lived-token mechanism this tool exists to inspect. Fires when a GitHub
5
+ # Release is published. Requires the PyPI side to be configured once:
6
+ # pypi.org -> project "subcheck" (or a pending publisher before first release) ->
7
+ # owner Dashtid, repository subcheck, workflow release.yml, environment pypi.
8
+
9
+ on:
10
+ release:
11
+ types: [published]
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ build:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+ - uses: actions/setup-python@v7
22
+ with:
23
+ python-version: "3.12"
24
+ - name: Build sdist and wheel
25
+ run: |
26
+ python -m pip install --upgrade pip build
27
+ python -m build
28
+ - uses: actions/upload-artifact@v6
29
+ with:
30
+ name: dist
31
+ path: dist/
32
+
33
+ publish:
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ environment: pypi
37
+ permissions:
38
+ id-token: write # OIDC trusted publishing
39
+ steps:
40
+ - uses: actions/download-artifact@v7
41
+ with:
42
+ name: dist
43
+ path: dist/
44
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ *.egg-info/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
@@ -0,0 +1,129 @@
1
+ # How subcheck works
2
+
3
+ A plain-language tour, for picking the project back up after time away.
4
+
5
+ ## The one-sentence version
6
+
7
+ When a GitHub Actions job authenticates to a cloud, it gets a **token**. subcheck reads that
8
+ token's claims and compares them against a **policy file you wrote**. If they don't match, it exits
9
+ non-zero and your CI step fails.
10
+
11
+ ## The problem it exists for
12
+
13
+ A GitHub Actions job that needs AWS credentials gets a short-lived token. Inside it is a claim
14
+ called `sub` — a single line of text:
15
+
16
+ ```
17
+ repo:acme/payments-api:ref:refs/heads/main
18
+ ```
19
+
20
+ Read it as: *"this job is running on the main branch of acme/payments-api."*
21
+
22
+ On the cloud side, an admin writes a **trust condition** — a rule saying which of those strings is
23
+ acceptable. That string-versus-rule comparison **is the entire security boundary**. Write the rule
24
+ too loosely (say `repo:acme/payments-api:*`) and every branch in the repo can reach your
25
+ production credentials.
26
+
27
+ subcheck sits on the **workflow side**. It doesn't know or care what the cloud rule says. It asks
28
+ one question: *"is the token this job received the one I expected?"* Answering that early turns a
29
+ later, cryptic `AccessDenied` into a readable diff — and catches the case where your token silently
30
+ changed shape (see the immutable-claims migration in the README).
31
+
32
+ ## The flow, end to end
33
+
34
+ ```
35
+ --token <jwt> --policy policy.yaml
36
+ | |
37
+ v v
38
+ decoder.py policy.py "what did I get?" vs "what did I expect?"
39
+ decode_claims() load_policy_file()
40
+ | |
41
+ {claims dict} Policy(rules=[ClaimRule, ...])
42
+ \ /
43
+ \ /
44
+ v v
45
+ validator.py validate()
46
+ |
47
+ [Result, Result, ...] one per rule: PASS / FAIL / MISSING
48
+ |
49
+ v
50
+ report.py build_report() -> adds summary counts + advisory notes
51
+ |
52
+ v
53
+ to_text() or to_json()
54
+ |
55
+ v
56
+ cli.py prints, returns 0 / 1 / 2
57
+ ```
58
+
59
+ ## The files
60
+
61
+ All under `src/subcheck/`. Roughly 400 lines total — small enough to read in one sitting.
62
+
63
+ | File | What it does |
64
+ |---|---|
65
+ | **`cli.py`** | The entry point. Parses arguments, decides where claims come from, calls everything else in order, prints, and returns the exit code. Start here. |
66
+ | **`decoder.py`** | Turns a JWT string into a claims dictionary. A JWT is three base64 chunks joined by dots; this splits it, decodes the middle one, and parses the JSON. Also holds `parse_github_sub`, which breaks a `sub` string into its parts and detects the legacy vs immutable format. |
67
+ | **`policy.py`** | Reads your YAML/JSON policy file and turns it into a list of `ClaimRule` objects. Also holds `CLAIM_SEVERITY` — the table deciding which claims are "high" severity. |
68
+ | **`validator.py`** | The comparison engine. For each rule, look up the claim and check it (`equals` / `in` / `matches` / `glob`). Produces one `Result` per rule. ~60 lines; the real logic is `_matches()`. |
69
+ | **`report.py`** | Assembles results into a report dict, adds the summary counts and the advisory `notes`, and formats it as text or JSON. |
70
+
71
+ Tests mirror this one-to-one: `tests/test_decoder.py`, `test_validator.py`, `test_cli.py`,
72
+ `test_report.py`, plus `test_decoder_vectors.py` (see below).
73
+
74
+ ## Two concepts worth knowing
75
+
76
+ **Exit codes are the product.** `0` = everything matched, `1` = a claim didn't match (this is what
77
+ fails your CI step), `2` = you gave it bad input. A CI gate is just a program with a meaningful
78
+ exit code.
79
+
80
+ **Notes are advisory, not gating.** `report.py` produces `notes` — hints about the immutable-claims
81
+ migration. They never change pass/fail. They exist because a silently-changed `sub` format is the
82
+ failure mode people don't see coming.
83
+
84
+ ## The subvectors connection
85
+
86
+ `tests/fixtures/github_subjects.json` holds real GitHub subject strings copied from the
87
+ [subvectors](https://github.com/Dashtid/subvectors) vector suite (CC0, so copying is free).
88
+ `test_decoder_vectors.py` runs each one through `parse_github_sub` and asserts the result matches
89
+ what subvectors says it should be.
90
+
91
+ **This is test data only.** subcheck does not import subvectors at runtime and never will — the
92
+ dependency is one-way and exists so the two projects can't silently disagree about what a subject
93
+ string means.
94
+
95
+ ## What it deliberately does NOT do
96
+
97
+ These are decisions, not gaps. See `BACKLOG.md` → Non-goals.
98
+
99
+ - **It does not verify the token's signature.** It decodes and inspects. Verifying that GitHub
100
+ really issued the token is the cloud provider's job at role-assumption time. So subcheck is a
101
+ *misconfiguration catcher, not an authentication control* — someone who controls the workflow can
102
+ just skip the step.
103
+ - **It does not simulate cloud trust conditions.** Your policy's `glob` is not "what AWS would do."
104
+ Grading real cloud rules is subvectors' job. Blurring that line risks being wrong in exactly the
105
+ way subvectors exists to catch.
106
+ - **It is not released.** No PyPI package, no git tag, no `action.yml` — on purpose. The launch is
107
+ parked until the flagship and the companion article are ready.
108
+
109
+ ## Running it
110
+
111
+ ```bash
112
+ pip install -e ".[dev]"
113
+
114
+ # decode a token and just look at it
115
+ subcheck --token-file token.txt
116
+
117
+ # check it against a policy (this is the CI gate)
118
+ subcheck --claims examples/claims-pull-request.json --policy examples/policy.json
119
+ echo $? # 1 — the sub doesn't match
120
+
121
+ # see the immutable-migration advisory
122
+ subcheck --claims examples/claims-immutable.json --policy examples/policy.json
123
+
124
+ # the full check suite (what CI runs)
125
+ pytest -q --cov=subcheck && ruff check . && python -m mypy && bandit -q -r src
126
+ ```
127
+
128
+ > On this machine use `python -m mypy` — a bare `mypy` resolves to a different Python install that
129
+ > lacks the type stubs.
@@ -0,0 +1,141 @@
1
+ # Backlog
2
+
3
+ Granular, current task list for subcheck. Complements [`README.md`](README.md) (what it is) and
4
+ [`CHANGELOG.md`](CHANGELOG.md) (what shipped).
5
+
6
+ **What subcheck is — and is not.** subcheck decodes the OIDC token a GitHub Actions job actually
7
+ received and asserts its claims against an expectation *you* write. It is the token-author's
8
+ expectation language and a fail-fast CI gate. It is **not** a cloud-trust-condition simulator and
9
+ **not** an authentication control — grading whether a trust *condition* (AWS StringLike / Azure FIC
10
+ / GCP CEL) is well-formed, matching, and safe lives in the sibling
11
+ [subvectors](https://github.com/Dashtid/subvectors); verifying the JWT signature is the cloud
12
+ provider's job at assume-time. Keep that line sharp (see Non-goals).
13
+
14
+ **Sequencing.** subvectors is the flagship; subcheck is the finished companion. The original plan
15
+ was to keep subcheck parked until a subvectors v0.1 + article #1 launch burst — but as of
16
+ 2026-08-16 that gate has partly fired on its own (subcheck is pinned at public slot #2) while both
17
+ gate conditions remain outside your control. See the DECISION in Phase 4 before doing more here.
18
+
19
+ Status keys: `[ ]` todo · `[~]` in progress · `[x]` done this cycle.
20
+
21
+ ## Phase 0 — correctness & honesty (done this cycle)
22
+
23
+ - `[x]` Fix the stale sibling description: subvectors is a conformance test-vector suite, not a
24
+ "PR gate that maps claims to reachable IAM roles" (the killed `oidc-reach` scanner). Corrected in
25
+ README tagline + Why section + CONTRIBUTING.
26
+ - `[x]` Fix the README quickstart output to match real output (7 rows, `5 pass, 1 fail, 1 missing`).
27
+ - `[x]` Document that `matches` is unanchored (`re.search`) and that `equals`/`in` are
28
+ JSON-type-sensitive while `matches`/`glob` stringify.
29
+ - `[x]` Note the `--token <jwt>` argv-leak footgun; steer to `--token -` / `--token-file`.
30
+ - `[x]` Add a "Related tools" cross-link to subvectors (the ecosystem-link half of the family).
31
+ - `[x]` CI: add Python 3.13, add `mypy` (green), add coverage report; ship `py.typed` + add
32
+ `types-PyYAML` to dev deps.
33
+ - `[x]` Commit + push the above (done; tree clean and in sync).
34
+ - `[ ]` Open follow-up issues for the good-first-issue items below (`gh issue list` returns empty).
35
+
36
+ ## Phase 1 — immutable subject-claims awareness (done this cycle)
37
+
38
+ The immutable `sub` format (`repo:owner@<owner_id>/repo@<repo_id>:...`) became automatic for
39
+ new/renamed/transferred repos on **2026-07-15**; name-based policies silently stop matching.
40
+
41
+ - `[x]` Immutable-aware decoder: `parse_github_sub` parses both formats, exposes
42
+ `repository_id`/`repository_owner_id` and a `format` field. Mirrors the subvectors subject grammar.
43
+ - `[x]` Wire `parse_github_sub` into the engine — used for report **advisories** (format detection +
44
+ migration hints), *not* a `sub.<component>` DSL. `repository`/`repository_owner`/`ref`/`environment`
45
+ are already top-level claims, so a parsed-sub DSL would be redundant; revisit only on concrete need.
46
+ - `[x]` Migration advisories (`report["notes"]`): flag a name-based `sub` pin that will break, and
47
+ hint when an immutable token fails a name-based pattern.
48
+ - `[x]` `repository_id`/`repository_owner_id` ranked high severity (the durable trust anchors).
49
+ - `[x]` Example pair: `examples/claims-immutable.json` + `examples/policy-immutable.json`.
50
+ - `[ ]` (optional) `job_workflow_ref` pinning example + severity — the reusable-workflow supply-chain
51
+ anchor AWS now exposes as a first-class condition key.
52
+
53
+ ## Phase 2 — first consumer of subvectors (done this cycle)
54
+
55
+ - `[x]` Vendored subvectors' CC0 GitHub `subject` strings as decoder fixtures
56
+ (`tests/fixtures/github_subjects.json`, cited); `tests/test_decoder_vectors.py` asserts
57
+ `parse_github_sub` agrees with the subvectors subject grammar on all 9 (legacy, both immutable ID
58
+ forms, case-sensitivity, nested-branch ref, tag ref, customized multi-segment sub). One-way,
59
+ test-time, self-contained (CI-safe) — subvectors is never a runtime dependency.
60
+ - `[ ]` (upstream, separate subvectors session) record subcheck as the corpus's first consumer in
61
+ subvectors' BACKLOG "Consumer-adoption" item — the adoption datapoint its success metric tracks.
62
+
63
+ ## Phase 3 — ship it properly (DE-GATED 2026-08-22 — decision taken, executed)
64
+
65
+ - `[x]` `release.yml` — PyPI **trusted publishing (OIDC)**, fires on a published GitHub Release
66
+ (build job + publish job, `environment: pypi`, `id-token: write`). Version bumped **0.2.0**
67
+ (immutable-claims support is a feature); CHANGELOG rolled; tag `v0.2.0` pushed (inert until the
68
+ Release is created).
69
+ - `[x]` `action.yml` composite wrapper — `uses: Dashtid/subcheck@v0.2.0` (or `@main`, which works
70
+ already: it installs from the action path, no PyPI needed). Inputs passed via `env`, never
71
+ interpolated into `run:` (the template-injection footgun).
72
+ - `[x]` Topics: added `aws-iam`, `cicd-security`, `supply-chain-security`, `least-privilege`
73
+ (11 total). Seeded 5 `good first issue` tickets (#2-#6: forbidden rule, SARIF, GitLab sub,
74
+ `--fail-on`, `--claim-map`).
75
+ - `[ ]` **[HUMAN — the two remaining clicks]** (1) pypi.org → Publishing → add a *pending
76
+ publisher* for project `subcheck`: owner `Dashtid`, repo `subcheck`, workflow `release.yml`,
77
+ environment `pypi`. (2) `gh release create v0.2.0 --title "subcheck 0.2.0" --notes-from-tag`
78
+ (or via UI) — the release event fires the publish. Optionally repeat the pending-publisher step
79
+ for `subvectors` to reserve that name too.
80
+
81
+ ## Phase 4 — launch (the gate has partly fired on its own)
82
+
83
+ - `[x]` **Set the GitHub pin** — done, and at slot **#2** (verified 2026-08-16), ahead of its gate.
84
+ - `[ ]` Record the demo GIF/asciinema (a PR failing on `sub=...:pull_request`).
85
+ - `[ ]` Coordinated burst *with subvectors*: article -> Show HN -> one subreddit -> LinkedIn.
86
+
87
+ > [!] **DECISION NEEDED — the gate no longer holds.** subcheck is pinned on the public profile at
88
+ > slot #2 while its README still says `pip install subcheck # once published` and PyPI 404s. It is
89
+ > *featured but not installable*, which is the worst of both. Both original gate conditions (a
90
+ > Checkov merge, article #1) are outside your control and still unmet after a month, and the pin
91
+ > already spent the launch signal. **Either de-gate and ship Phase 3, or unpin.** Recommendation:
92
+ > de-gate. Cheap either way: reserve the `subcheck` and `subvectors` PyPI names now (5 min, both
93
+ > currently unregistered and squattable).
94
+
95
+ ## Phase 2.5 — technical-soundness pass (done this cycle)
96
+
97
+ Driven by a research fan-out (article-saturation + primary-source verification + adversarial
98
+ refutation) before any article gets published. Findings that were *facts*, fixed here:
99
+
100
+ - `[x]` **The fork claim was wrong.** A fork's `pull_request` cannot mint a token for the upstream
101
+ repo — `id-token: write` is downgraded and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` is never injected.
102
+ README now lists the real paths. This error is endemic to the published literature; do not
103
+ reintroduce it, and it is worth an article of its own (see below).
104
+ - `[x]` Half-immutable subjects (`@id` on one segment) reported as `malformed`, not `immutable`.
105
+ - `[x]` Migration hints suppressed for non-github.com issuers (GHES keeps mutable names).
106
+ - `[x]` Legacy hint no longer implies format follows from repo age (org/repo opt-in exists).
107
+ - `[x]` `repository_id`/`repository_owner_id` provenance corrected (since Jan 2023, pin them now).
108
+ - `[x]` `glob` vs IAM `StringLike` divergence documented (POSIX character classes).
109
+ - `[x]` `job_workflow_ref` promoted to high severity.
110
+ - `[x]` Documented *why* subcheck exists: `runner_environment`, `event_name`, `head_ref`,
111
+ `base_ref`, `workflow_ref` are **not expressible in an AWS trust policy at all**.
112
+
113
+ Open follow-ups from the same pass:
114
+
115
+ - `[ ]` **subvectors carries the same half-immutable tolerance** — `src/subvectors/github.py:56`
116
+ `RepoSegment.immutable` uses `owner_id is not None or repo_id is not None`. Fix in a subvectors
117
+ session so the two grammars genuinely agree (subcheck's decoder docstring claims they do).
118
+ - `[ ]` Re-vendor the subject fixtures if subvectors adds a malformed/asymmetric subject vector.
119
+
120
+ ## Correctness / quality parking lot
121
+
122
+ - `[ ]` `--fail-on <severity>` threshold gating — today any single required-but-missing medium claim
123
+ fails the whole gate (`report.py` `passed = all(PASS)`); no way to gate on high only.
124
+ - `[ ]` Optional `exp`/`iat`/`nbf` time checks — flag an expired or not-yet-valid token.
125
+ - `[ ]` `forbidden` rule (assert a claim is NOT one of a set). *(good first issue)*
126
+ - `[ ]` SARIF output so findings land in the GitHub Security tab. *(good first issue)*
127
+ - `[ ]` GitLab CI `sub` format support. *(good first issue)*
128
+ - `[ ]` Decide `equals`/`in` type handling: coerce, or keep type-strict + documented (currently the
129
+ latter).
130
+ - `[ ]` Close test-coverage holes (92% now): `glob` branch, `--token-file`, `--token -` stdin,
131
+ `load_policy_file` suffix logic, `to_json`/summary counts, and the `rc=2` bad-policy/bad-JSON paths.
132
+ - `[ ]` Cosmetic: rephrase the `# nosec B105` comments so bandit stops emitting "Test in comment"
133
+ warnings (prose after `# nosec` is parsed as test IDs).
134
+
135
+ ## Non-goals (hold the line)
136
+
137
+ - No cloud-trust-condition *simulation* (AWS StringLike / Azure FIC exact / GCP CEL matching) — that
138
+ collides with subvectors and risks being wrong, the exact bug class subvectors exists to grade.
139
+ - No JWT signature/issuer verification — keep the honest "misconfiguration catcher, not auth control"
140
+ boundary explicit as the tool grows.
141
+ - No scanner / posture / reachability-graph scope creep.
@@ -0,0 +1,58 @@
1
+ # Changelog
2
+
3
+ All notable changes are documented here. Format based on
4
+ [Keep a Changelog](https://keepachangelog.com/); this project follows
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.2.0] - 2026-08-22
8
+
9
+ First published release (PyPI, via OIDC trusted publishing — fittingly, the same mechanism the
10
+ tool inspects). Also ships `action.yml`, so a workflow can gate with
11
+ `uses: Dashtid/subcheck@v0.2.0` instead of a curl+pip snippet.
12
+
13
+ ### Added
14
+ - Immutable subject-claims support: `parse_github_sub` decodes both the legacy and the immutable
15
+ `repo:owner@id/repo@id:...` `sub` formats, exposing owner/repo IDs and a `format` field.
16
+ - Report `notes`: advisory hints about the 2026-07-15 immutable-format migration (a name-based
17
+ `sub` pin that will break; a hint when an immutable token fails a name-based pattern).
18
+ - `repository_id` / `repository_owner_id` ranked as high-severity (immutable) trust anchors.
19
+ - `examples/claims-immutable.json` + `examples/policy-immutable.json` (an id-pinned durable policy).
20
+
21
+ ### Changed
22
+ - CI also runs on Python 3.13, type-checks with `mypy`, and reports coverage; the package now
23
+ ships a `py.typed` marker.
24
+
25
+ ### Fixed
26
+ - **Corrected a factually wrong threat description.** A fork's pull request cannot mint an OIDC
27
+ token for the upstream repo (GitHub downgrades `id-token: write` and never injects
28
+ `ACTIONS_ID_TOKEN_REQUEST_TOKEN` for fork `pull_request` runs). The README now names the real
29
+ paths: push/branch-create access, `pull_request_target` / `workflow_run` with untrusted checkout,
30
+ and compromised third-party actions in a trusted job.
31
+ - `parse_github_sub` no longer reports a subject carrying an ID on only one segment as
32
+ `immutable`; that shape is `malformed` (GitHub emits `@id` on both segments or neither) and is
33
+ surfaced as an advisory note.
34
+ - Migration advisories are suppressed for non-github.com issuers — immutable subject claims are a
35
+ github.com-only feature, so GitHub Enterprise Server must not be told to migrate.
36
+ - The legacy-format advisory no longer implies the format follows from a repo's age; any repo can
37
+ opt in early via the org-level or repo-level immutable-subject setting.
38
+ - Corrected `repository_id` / `repository_owner_id` provenance: they have existed since January
39
+ 2023 and work on legacy-format tokens, so they can be pinned today rather than after migrating.
40
+ - Documented that `glob` honours POSIX character classes while IAM `StringLike` treats `[`/`]` as
41
+ literals, and that `repository_owner` is not an AWS condition key (only `repository_owner_id`).
42
+ - `job_workflow_ref` promoted to high severity — the only claim constraining which workflow code
43
+ minted the token, and an AWS-accepted alternative identity-provider control to `sub`.
44
+ - README quickstart output now matches the tool's real output (7 rows, `5 pass, 1 fail, 1 missing`).
45
+ - The sibling **subvectors** is described accurately across README/CONTRIBUTING (a conformance
46
+ test-vector suite, not a reachability PR gate).
47
+ - Documented that `matches` is unanchored (`re.search`) and that `equals`/`in` are JSON-type-sensitive.
48
+
49
+ ## [0.1.0] - 2026-07-15
50
+
51
+ ### Added
52
+ - Decode a GitHub Actions OIDC JWT's claims (`--token` / `--token-file` / `--claims`),
53
+ without signature verification (inspection only).
54
+ - Expected-claims policy in YAML or JSON: `equals`, `in`, `matches`, `glob`, `required`,
55
+ plus `issuer`/`audience` shortcuts.
56
+ - Validation with per-claim severity and `PASS` / `FAIL` / `MISSING` results.
57
+ - Text and JSON reports; non-zero exit on any finding for use as a CI gate.
58
+ - `parse_github_sub` helper for the GitHub `sub` claim.
@@ -0,0 +1,28 @@
1
+ # Contributing
2
+
3
+ Thanks for looking. This is a small, focused tool — decode GitHub Actions OIDC claims and
4
+ validate them against a policy. It stays deliberately small: it asserts a token against an
5
+ expectation *you* write, and deliberately does **not** simulate a cloud's trust-condition
6
+ semantics — grading whether a trust condition is well-formed, matching, and safe lives in the
7
+ sibling [subvectors](https://github.com/Dashtid/subvectors) vector suite.
8
+
9
+ ## Setup
10
+
11
+ ```bash
12
+ pip install -e ".[dev]"
13
+ pytest -q && ruff check . && bandit -r src
14
+ ```
15
+
16
+ ## Good first issues
17
+
18
+ Small, self-contained additions (labelled `good first issue`):
19
+
20
+ - Support the GitLab CI OIDC token `sub` format alongside GitHub's.
21
+ - Add a `forbidden` rule (assert a claim is NOT one of a set of values).
22
+ - Add `--claim-map` to normalise provider-specific claim names before validation.
23
+ - Emit [SARIF](https://sarifweb.azurewebsites.net/) so findings show up in the GitHub Security tab.
24
+
25
+ ## Pull requests
26
+
27
+ Keep the change focused, add a test, and make sure `pytest`, `ruff check`, and `bandit` pass.
28
+ Not sure about scope? Open an issue first — open questions are welcome.
subcheck-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David Dashti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.