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.
- subcheck-0.2.0/.gitattributes +10 -0
- subcheck-0.2.0/.github/dependabot.yml +40 -0
- subcheck-0.2.0/.github/workflows/ci.yml +34 -0
- subcheck-0.2.0/.github/workflows/release.yml +44 -0
- subcheck-0.2.0/.gitignore +11 -0
- subcheck-0.2.0/ARCHITECTURE.md +129 -0
- subcheck-0.2.0/BACKLOG.md +141 -0
- subcheck-0.2.0/CHANGELOG.md +58 -0
- subcheck-0.2.0/CONTRIBUTING.md +28 -0
- subcheck-0.2.0/LICENSE +21 -0
- subcheck-0.2.0/PKG-INFO +264 -0
- subcheck-0.2.0/README.md +234 -0
- subcheck-0.2.0/action.yml +47 -0
- subcheck-0.2.0/examples/claims-immutable.json +14 -0
- subcheck-0.2.0/examples/claims-main.json +12 -0
- subcheck-0.2.0/examples/claims-pull-request.json +11 -0
- subcheck-0.2.0/examples/policy-immutable.json +11 -0
- subcheck-0.2.0/examples/policy.json +11 -0
- subcheck-0.2.0/examples/policy.yaml +19 -0
- subcheck-0.2.0/pyproject.toml +57 -0
- subcheck-0.2.0/src/subcheck/__init__.py +23 -0
- subcheck-0.2.0/src/subcheck/__main__.py +5 -0
- subcheck-0.2.0/src/subcheck/cli.py +80 -0
- subcheck-0.2.0/src/subcheck/decoder.py +98 -0
- subcheck-0.2.0/src/subcheck/policy.py +115 -0
- subcheck-0.2.0/src/subcheck/py.typed +0 -0
- subcheck-0.2.0/src/subcheck/report.py +106 -0
- subcheck-0.2.0/src/subcheck/validator.py +64 -0
- subcheck-0.2.0/tests/conftest.py +34 -0
- subcheck-0.2.0/tests/fixtures/github_subjects.json +72 -0
- subcheck-0.2.0/tests/test_cli.py +57 -0
- subcheck-0.2.0/tests/test_decoder.py +68 -0
- subcheck-0.2.0/tests/test_decoder_vectors.py +25 -0
- subcheck-0.2.0/tests/test_report.py +66 -0
- subcheck-0.2.0/tests/test_validator.py +63 -0
|
@@ -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,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.
|