remit-actions 1__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 (90) hide show
  1. remit_actions-1/CHANGELOG.md +51 -0
  2. remit_actions-1/CONTRIBUTING.md +85 -0
  3. remit_actions-1/LICENSE +21 -0
  4. remit_actions-1/MANIFEST.in +9 -0
  5. remit_actions-1/PKG-INFO +260 -0
  6. remit_actions-1/README.md +232 -0
  7. remit_actions-1/SECURITY.md +24 -0
  8. remit_actions-1/SETUP.md +169 -0
  9. remit_actions-1/action.yml +63 -0
  10. remit_actions-1/benchmarks/README.md +51 -0
  11. remit_actions-1/benchmarks/expected-summary.json +18 -0
  12. remit_actions-1/benchmarks/manifest.jsonl +74 -0
  13. remit_actions-1/benchmarks/manifest.sha256 +1 -0
  14. remit_actions-1/docs/ARCHITECTURE.md +95 -0
  15. remit_actions-1/docs/RULES.md +256 -0
  16. remit_actions-1/examples/association-base/.github/workflows/deploy.yml +15 -0
  17. remit_actions-1/examples/association-head/.github/workflows/deploy.yml +15 -0
  18. remit_actions-1/examples/base/.github/workflows/release.yml +8 -0
  19. remit_actions-1/examples/github-action-template.yml +45 -0
  20. remit_actions-1/examples/head/.github/workflows/release.yml +13 -0
  21. remit_actions-1/examples/sample-report.json +70 -0
  22. remit_actions-1/examples/sample-report.md +33 -0
  23. remit_actions-1/pyproject.toml +54 -0
  24. remit_actions-1/requirements.txt +75 -0
  25. remit_actions-1/scripts/action_entry.py +55 -0
  26. remit_actions-1/scripts/action_runner.py +210 -0
  27. remit_actions-1/scripts/compare_zizmor.py +94 -0
  28. remit_actions-1/scripts/hosted_fixture.py +52 -0
  29. remit_actions-1/scripts/offline_smoke.py +125 -0
  30. remit_actions-1/scripts/replay_benchmark.py +96 -0
  31. remit_actions-1/scripts/run-action.sh +5 -0
  32. remit_actions-1/scripts/validate.py +45 -0
  33. remit_actions-1/scripts/vendor_pyyaml.py +68 -0
  34. remit_actions-1/setup.cfg +4 -0
  35. remit_actions-1/src/remit/__init__.py +3 -0
  36. remit_actions-1/src/remit/__main__.py +3 -0
  37. remit_actions-1/src/remit/cli.py +101 -0
  38. remit_actions-1/src/remit/contexts.py +279 -0
  39. remit_actions-1/src/remit/expressions.py +7 -0
  40. remit_actions-1/src/remit/flows.py +483 -0
  41. remit_actions-1/src/remit/report.py +142 -0
  42. remit_actions-1/src/remit/sarif.py +74 -0
  43. remit_actions-1/src/remit/scan.py +644 -0
  44. remit_actions-1/src/remit/sources.py +223 -0
  45. remit_actions-1/src/remit/yaml_limits.py +52 -0
  46. remit_actions-1/src/remit_actions.egg-info/PKG-INFO +260 -0
  47. remit_actions-1/src/remit_actions.egg-info/SOURCES.txt +88 -0
  48. remit_actions-1/src/remit_actions.egg-info/dependency_links.txt +1 -0
  49. remit_actions-1/src/remit_actions.egg-info/entry_points.txt +2 -0
  50. remit_actions-1/src/remit_actions.egg-info/requires.txt +1 -0
  51. remit_actions-1/src/remit_actions.egg-info/top_level.txt +1 -0
  52. remit_actions-1/tests/alias_cases.json +199 -0
  53. remit_actions-1/tests/cases.json +394 -0
  54. remit_actions-1/tests/hosted_cases.json +168 -0
  55. remit_actions-1/tests/test_action_outcomes.py +228 -0
  56. remit_actions-1/tests/test_aliases.py +121 -0
  57. remit_actions-1/tests/test_associations.py +233 -0
  58. remit_actions-1/tests/test_audit.py +181 -0
  59. remit_actions-1/tests/test_contexts.py +178 -0
  60. remit_actions-1/tests/test_expression_identifiers.py +84 -0
  61. remit_actions-1/tests/test_flows.py +353 -0
  62. remit_actions-1/tests/test_hosted_fixtures.py +45 -0
  63. remit_actions-1/tests/test_indirect_flows.py +132 -0
  64. remit_actions-1/tests/test_installation.py +72 -0
  65. remit_actions-1/tests/test_mvp.py +579 -0
  66. remit_actions-1/tests/test_offline_runtime.py +58 -0
  67. remit_actions-1/tests/test_report.py +139 -0
  68. remit_actions-1/tests/test_review_fixes.py +131 -0
  69. remit_actions-1/tests/test_sarif.py +69 -0
  70. remit_actions-1/tests/test_vendor_refresh.py +73 -0
  71. remit_actions-1/vendor/README.md +23 -0
  72. remit_actions-1/vendor/pyyaml/LICENSE +20 -0
  73. remit_actions-1/vendor/pyyaml/manifest.json +28 -0
  74. remit_actions-1/vendor/pyyaml/yaml/__init__.py +390 -0
  75. remit_actions-1/vendor/pyyaml/yaml/composer.py +139 -0
  76. remit_actions-1/vendor/pyyaml/yaml/constructor.py +748 -0
  77. remit_actions-1/vendor/pyyaml/yaml/cyaml.py +101 -0
  78. remit_actions-1/vendor/pyyaml/yaml/dumper.py +62 -0
  79. remit_actions-1/vendor/pyyaml/yaml/emitter.py +1137 -0
  80. remit_actions-1/vendor/pyyaml/yaml/error.py +75 -0
  81. remit_actions-1/vendor/pyyaml/yaml/events.py +86 -0
  82. remit_actions-1/vendor/pyyaml/yaml/loader.py +63 -0
  83. remit_actions-1/vendor/pyyaml/yaml/nodes.py +49 -0
  84. remit_actions-1/vendor/pyyaml/yaml/parser.py +589 -0
  85. remit_actions-1/vendor/pyyaml/yaml/reader.py +185 -0
  86. remit_actions-1/vendor/pyyaml/yaml/representer.py +389 -0
  87. remit_actions-1/vendor/pyyaml/yaml/resolver.py +227 -0
  88. remit_actions-1/vendor/pyyaml/yaml/scanner.py +1435 -0
  89. remit_actions-1/vendor/pyyaml/yaml/serializer.py +111 -0
  90. remit_actions-1/vendor/pyyaml/yaml/tokens.py +104 -0
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ ## Version 1 — 2026-11-04
4
+
5
+ First public release.
6
+
7
+ - **Ten rules** for privilege changes between a pull request's base and head:
8
+ token scope increases (WF001), jobs relying on or leaving the repository's
9
+ default token permissions (WF002), review-sensitive triggers such as
10
+ `pull_request_target` (WF003), new secret references and forwarding (WF004),
11
+ self-hosted, custom or group runners (WF005), environment changes (WF007),
12
+ runner/environment re-pairing in matrices (WF008), new third-party code in a
13
+ privileged job (WF009), pull request content brought into a
14
+ `pull_request_target`, `workflow_run` or `issue_comment` job (WF010), and
15
+ mutable pull request references checked out after a label, comment or
16
+ environment approval (WF011, time-of-check to time-of-use).
17
+ - **Compares from the merge-base.** When the base comes from the pull request
18
+ event, the Action compares from where the PR branched off, as GitHub's Files
19
+ changed view does, so a stale branch doesn't show base-branch changes reversed.
20
+ - **Indirect references are followed:** WF010 and WF011 trace pull request
21
+ references through environment variables, `$GITHUB_ENV`, step outputs,
22
+ `needs` job outputs and action inputs, and treat runtime lookups such as
23
+ `gh pr view` as pull request content. Besides checkouts, WF010 recognizes
24
+ clones of the head repository, `gh pr diff | git apply`, archive downloads of
25
+ the head commit, and artifact downloads with `gh run download` or
26
+ `github-script`.
27
+ - **Comment commands count as approval only when checked:** an `issue_comment`
28
+ job is treated as gated (WF011) only when it, or a job it needs, checks who
29
+ commented. A check of the comment's text alone is WF010, since anyone can
30
+ type it.
31
+ - **`remit audit`** reports WF010 and WF011 in a repository's current state,
32
+ for a directory or any Git commit, as JSON, Markdown or SARIF, with an
33
+ optional `--fail-on-findings` exit code for scheduled checks.
34
+ - **Local code is followed:** steps inside local composite actions and local
35
+ reusable workflows (`./path` and `$/path`) count, and a change to them is
36
+ caught even when no workflow file changed. A local file that can't be read or
37
+ parsed makes the analysis `incomplete` for every job that uses it.
38
+ - **Reports** as pull request annotations on the line that introduced each
39
+ finding (an incomplete analysis first, then the most serious rules, with a
40
+ job's WF001 scopes combined so GitHub's 10-annotation limit doesn't hide them), a Markdown job summary with a review question per rule, JSON, and
41
+ SARIF for GitHub code scanning.
42
+ - **GitHub Action** with `advisory` and `require-complete` modes. Findings never
43
+ fail the step; incomplete or failed analysis is reported explicitly.
44
+ - **Safe on untrusted pull requests:** the head is read as Git data and never
45
+ executed, the analysis makes no network calls, and the Action ships a
46
+ hash-verified PyYAML bundle. Git reads ignore caller redirection and
47
+ replacement refs, and YAML anchors are bounded before parsing.
48
+ - **CLI** `remit` (`pip install remit-actions`), for directories or local Git commits.
49
+ - **Validation:** the test suite on Python 3.11–3.14, an end-to-end Action run
50
+ with networking disabled, a hosted contract of 18 cases in both modes, a
51
+ replay of 74 historical public PRs, and a reproducible comparison with zizmor.
@@ -0,0 +1,85 @@
1
+ # Contributing
2
+
3
+ ## Development
4
+
5
+ Python 3.11+ and Git are required.
6
+
7
+ ```sh
8
+ python3 -m venv .venv
9
+ . .venv/bin/activate
10
+ pip install -e .
11
+ python scripts/validate.py
12
+ python scripts/offline_smoke.py
13
+ pip install ruff==0.16.9 && ruff check
14
+ ```
15
+
16
+ To run one test file: `python -m unittest discover -s tests -p 'test_contexts.py'`.
17
+
18
+ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for how the code is organised.
19
+
20
+ ## Tests
21
+
22
+ | Command | What it covers |
23
+ |---|---|
24
+ | `python scripts/validate.py` | The full unit and integration suite: rules, parser limits, source readers, CLI, the Action controller, and the template's fetch step against local Git repositories. Writes `validation-results.json` (git-ignored) with versions and source hashes. |
25
+ | `python scripts/offline_smoke.py` | Runs the real Action controller against local Git fixtures in both modes. In CI, `--network-namespace` runs it with networking disabled. |
26
+ | `scripts/replay_benchmark.py` | Replays [74 historical public PRs](benchmarks/README.md) and checks the expected WF001/WF003/WF004 findings. |
27
+ | `scripts/compare_zizmor.py` | Needs `zizmor` and a replay cache. Measures how many findings zizmor's new-findings view also surfaces. |
28
+ | *Action outcome contract* workflow | Manually dispatched. Runs the published, pinned Action against the fixtures in `tests/hosted_cases.json` in both modes. |
29
+
30
+ CI runs `ruff check`, plus `validate.py` and the network-isolated smoke test on
31
+ Python 3.11–3.14. The repository also runs its own Action on every PR
32
+ ([`privilege-review.yml`](.github/workflows/privilege-review.yml)), loaded from
33
+ the PR's base commit.
34
+
35
+ ## Changing behaviour
36
+
37
+ - Add a regression test that fails without the change, plus a nearby case that
38
+ should stay quiet. Cover `incomplete`/`error` outcomes too.
39
+ - Findings already established must survive uncertainty elsewhere in the file.
40
+ - Analysis must stay local: no executing workflow content, no network access, no
41
+ querying live repository settings.
42
+ - Keep rule IDs stable. Bump `schemaVersion` only for incompatible changes to the
43
+ JSON report.
44
+ - Update [docs/RULES.md](docs/RULES.md) and the `Unreleased` section of the
45
+ [changelog](CHANGELOG.md).
46
+ - If report output changes, regenerate `examples/sample-report.md` and
47
+ `examples/sample-report.json` (a test checks them).
48
+ - For rule changes, run the replay and look at any new findings. Don't change an
49
+ expected result just to make the replay pass.
50
+ - Changes to the Action controller, template or bundled dependency need the
51
+ offline smoke test, and the hosted contract before release.
52
+
53
+ ## Releases
54
+
55
+ 1. Update the version in `pyproject.toml` and `src/remit/__init__.py`,
56
+ and move `Unreleased` entries in the changelog under the new version.
57
+ 2. Run CI, the offline smoke test, the replay and the hosted contract on the
58
+ release candidate.
59
+ 3. Merge to `main`, then run [`publish.yml`](.github/workflows/publish.yml)
60
+ from the Actions tab (Run workflow, branch `main`). It refuses other
61
+ branches, builds with hash-pinned tools, tests the wheel and uploads to PyPI
62
+ through trusted publishing. Record the CI and contract runs in the
63
+ changelog entry.
64
+ 4. Update the pinned SHA in `README.md`, `examples/github-action-template.yml`
65
+ and the contract workflow.
66
+
67
+ PyPI trusted publishing needs a one-time setup on pypi.org: add a pending
68
+ publisher for project `remit-actions`, owner `adi271213`, repository
69
+ `remit`, workflow `publish.yml`, environment `pypi`.
70
+
71
+ Tags are never moved or deleted after publication.
72
+
73
+ ## Feedback
74
+
75
+ If you've tried the check on a repository, the *Trial feedback* issue template
76
+ is the quickest way to say what was useful, what was noise and what it missed.
77
+
78
+ ## Pull requests
79
+
80
+ Describe the problem, the new behaviour and the checks you ran. Include a small
81
+ public or synthetic example for bugs. Don't include credentials or private logs.
82
+ Report vulnerabilities privately; see [SECURITY.md](SECURITY.md).
83
+
84
+ `main` is protected: changes go through a pull request with passing CI on all
85
+ four Python versions.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Remit contributors
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.
@@ -0,0 +1,9 @@
1
+ include action.yml requirements.txt CHANGELOG.md CONTRIBUTING.md SECURITY.md SETUP.md
2
+ recursive-include scripts *.py *.sh
3
+ recursive-include tests *.py *.json
4
+ graft examples
5
+ graft docs
6
+ graft benchmarks
7
+ graft vendor
8
+ prune .benchmark-cache
9
+ global-exclude __pycache__ *.py[cod] .DS_Store
@@ -0,0 +1,260 @@
1
+ Metadata-Version: 2.4
2
+ Name: remit-actions
3
+ Version: 1
4
+ Summary: See what a pull request changes about what your GitHub Actions workflows are allowed to do.
5
+ License-Expression: MIT
6
+ Project-URL: Source, https://github.com/adi271213/remit
7
+ Project-URL: Issues, https://github.com/adi271213/remit/issues
8
+ Project-URL: Documentation, https://github.com/adi271213/remit/blob/main/SETUP.md
9
+ Keywords: github-actions,security,ci,workflows,permissions,supply-chain,code-review
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Requires-Python: >=3.11
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: PyYAML==6.0.3
27
+ Dynamic: license-file
28
+
29
+ <div align="center">
30
+
31
+ # Remit
32
+
33
+ **Know when a pull request widens your workflows' authority.**
34
+
35
+ [![CI](https://github.com/adi271213/remit/actions/workflows/ci.yml/badge.svg)](https://github.com/adi271213/remit/actions/workflows/ci.yml)
36
+ [![PyPI](https://img.shields.io/pypi/v/remit-actions)](https://pypi.org/project/remit-actions/)
37
+ [![Python](https://img.shields.io/pypi/pyversions/remit-actions)](https://pypi.org/project/remit-actions/)
38
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/adi271213/remit/blob/main/LICENSE)
39
+
40
+ </div>
41
+
42
+ The changes that matter in a workflow PR are often a single line: `contents: read`
43
+ becomes `write`, a `pull_request_target` trigger appears, a release job starts
44
+ running a new third-party action with your secrets. Remit compares the base and
45
+ head of every pull request and puts each of those changes on the line that
46
+ introduced it, so reviewers can't miss them.
47
+
48
+ - **Only what changed.** Unchanged configuration stays quiet, so ordinary PRs get no noise.
49
+ - **Flags common pwn-request patterns.** Privileged workflows that start checking out, fetching or downloading pull request content, including the time-of-check to time-of-use trap after a label or comment approval.
50
+ - **Right on the diff.** Findings appear as annotations in the PR's *Files changed* tab.
51
+ - **Safe on untrusted PRs.** The PR is read as data and never executed; the analysis makes no network calls.
52
+ - **Nothing to install.** A pinned Action with a hash-verified bundled parser; no secrets, `contents: read` only.
53
+
54
+ ## What it catches
55
+
56
+ A PR edits a release workflow:
57
+
58
+ ```diff
59
+ name: Release
60
+ -on: push
61
+ +on: [push, pull_request_target]
62
+ permissions: {}
63
+ jobs:
64
+ publish:
65
+ runs-on: ubuntu-latest
66
+ permissions:
67
+ - contents: read
68
+ + contents: write
69
+ + id-token: write
70
+ steps:
71
+ - uses: actions/checkout@v4
72
+ - run: make dist
73
+ + - uses: someone/release-action@v1
74
+ + with:
75
+ + token: ${{ secrets.RELEASE_TOKEN }}
76
+ ```
77
+
78
+ Remit annotates five lines of that diff:
79
+
80
+ | Line | Rule | Finding |
81
+ |---|---|---|
82
+ | 2 | WF003 | Review-sensitive trigger `pull_request_target` introduced. |
83
+ | 8 | WF001 | Job publish is newly configured to use `contents: write`. |
84
+ | 9 | WF001 | Job publish is newly configured to request an OIDC identity token. |
85
+ | 13 | WF009 | Job publish now runs third-party code from `someone/release-action@v1`, which can use `contents: write`; `id-token: write`; secrets: `RELEASE_TOKEN`. |
86
+ | 15 | WF004 | Job publish adds a reference or forwarding path to `RELEASE_TOKEN`. |
87
+
88
+ The job summary groups the same findings by workflow and job, with a question
89
+ for the reviewer on each ([sample](https://github.com/adi271213/remit/blob/main/examples/sample-report.md)).
90
+ JSON and SARIF reports carry the same data for tooling and GitHub code scanning.
91
+
92
+ ## Quick start
93
+
94
+ 1. Copy [`examples/github-action-template.yml`](https://github.com/adi271213/remit/blob/main/examples/github-action-template.yml)
95
+ (45 lines) to `.github/workflows/remit.yml`.
96
+ 2. Merge it.
97
+ 3. Open a pull request that touches a workflow and look at *Files changed*.
98
+
99
+ The template checks out the base commit, fetches the PR head without checking it
100
+ out, and runs the Action pinned to a full commit SHA:
101
+
102
+ ```yaml
103
+ - uses: adi271213/remit@3db5c481c527a7f60bf92c0499083fe15f7829be # version 1
104
+ with:
105
+ mode: advisory
106
+ ```
107
+
108
+ Use the whole template rather than this step alone: the step needs both commits
109
+ in the local clone. The [setup guide](https://github.com/adi271213/remit/blob/main/SETUP.md)
110
+ covers inputs, outputs, code scanning and troubleshooting.
111
+
112
+ ## Rules
113
+
114
+ | Rule | Reports |
115
+ |---|---|
116
+ | WF001 | A job's token scope increases (`none` → `read` → `write`), including new `id-token: write` (OIDC) |
117
+ | WF002 | A job starts relying on the repository's default token permissions, or replaces them with permissions that may exceed the default |
118
+ | WF003 | A newly added `pull_request_target`, `workflow_run`, `issue_comment`, `issues`, `discussion` or `discussion_comment` trigger |
119
+ | WF004 | A job newly references a secret, or forwards all secrets (`secrets: inherit`, `toJSON(secrets)`) |
120
+ | WF005 | A job's runner changes to a self-hosted, custom-label, runner-group or unresolvable selection |
121
+ | WF007 | A job's deployment environment is added, removed or changed |
122
+ | WF008 | A matrix job pairs a runner with a different environment while the overall runner and environment sets stay the same |
123
+ | WF009 | Third-party code (an action or reusable workflow) newly runs in a job with write scopes, OIDC, secrets or an environment |
124
+ | WF010 | A `pull_request_target`, `workflow_run` or `issue_comment` job newly checks out, fetches or downloads pull request content: the "pwn request" |
125
+ | WF011 | An approval-gated privileged job checks out a pull request reference that can change after approval: time-of-check to time-of-use |
126
+
127
+ Rules follow steps into local composite actions and local reusable workflows
128
+ (`./` and `$/` references), so a change inside `.github/actions/` is caught even
129
+ when no workflow file changed.
130
+
131
+ [docs/RULES.md](https://github.com/adi271213/remit/blob/main/docs/RULES.md) describes the exact behaviour of each rule.
132
+
133
+ ## Remit and zizmor
134
+
135
+ [zizmor](https://github.com/zizmorcore/zizmor) audits what your workflows *are*
136
+ for risky patterns. Remit reports what a pull request *changed*, including
137
+ increases that never become a zizmor alert because the end state isn't "bad"
138
+ by a state rule: a job gaining `id-token: write`, a new secret reference, a
139
+ changed environment, or a new action receiving write access.
140
+
141
+ On [74 historical public PRs](https://github.com/adi271213/remit/blob/main/benchmarks/README.md),
142
+ Remit reported 72 findings. For 46 of them (64%), zizmor 1.32 (pedantic,
143
+ offline) reported no new finding of a related audit in the same workflow and
144
+ job, including all 8 new secret references. The PRs were selected and labelled
145
+ by this project and the matching is approximate, so read this as an indication,
146
+ not a benchmark. Run zizmor for what is dangerous, and Remit for what got wider.
147
+
148
+ ## Results
149
+
150
+ Every run reports an **analysis** status and a **review** status:
151
+
152
+ | Analysis | Meaning | CLI exit | `require-complete` (default) | `advisory` |
153
+ |---|---|---|---|---|
154
+ | `complete` | All changed workflows were compared | 0 | pass | pass |
155
+ | `incomplete` | Some context is unknown, e.g. a dynamic expression | 2 | fail | pass with warning |
156
+ | `error` | Some input couldn't be parsed or read | 2 | fail | pass with warning |
157
+
158
+ Review is `suggested` when there are findings, `none` when analysis is complete
159
+ with no findings, and (in the Action) `unknown` when analysis is incomplete and
160
+ nothing was found. Known findings are reported alongside incomplete or failed parts.
161
+
162
+ **Findings never fail the step.** Remit is a review aid, not a merge gate: a
163
+ clean report doesn't mean a PR is safe, and a PR can edit the workflow that runs
164
+ the check.
165
+
166
+ <details>
167
+ <summary><b>Inherited token permissions</b></summary>
168
+
169
+ A job without a `permissions` block gets the repository or organization default,
170
+ which Remit can't see. When a PR adds such a job, or removes a job's
171
+ `permissions` block, WF002 asks the author to declare permissions explicitly.
172
+ When a job that used the default starts declaring permissions, only levels above
173
+ GitHub's narrowest possible default (`contents: read`, `packages: read`) are
174
+ reported, so ordinary hardening PRs stay quiet. To compare against your real
175
+ default instead, pass it in:
176
+
177
+ ```yaml
178
+ with:
179
+ default-permissions: '{"contents": "read"}'
180
+ ```
181
+
182
+ </details>
183
+
184
+ <details>
185
+ <summary><b>Limitations</b></summary>
186
+
187
+ - Workflows are read from `.github/workflows/`, and local composite actions and
188
+ reusable workflows are followed. Actions and reusable workflows from other
189
+ repositories, and the contents of scripts, are not analysed.
190
+ - Repository and organization settings are not visible: default token
191
+ permissions, runner inventory, environment protection rules and secrets.
192
+ - WF003 looks at trigger names only. Filter changes on existing triggers
193
+ (branches, types, paths) are not analysed.
194
+ - Expressions are not evaluated. Only direct `${{ matrix.<field> }}` references
195
+ into a static matrix of up to 256 rows are resolved; anything else is reported
196
+ as unresolved.
197
+ - YAML merge keys (`<<`) and recursive aliases are rejected as errors.
198
+ - A renamed file is matched only if its content is identical; an edited rename
199
+ is treated as a new file and can produce findings for unchanged settings.
200
+ - Input limits: 200 workflow files and 4 MiB per commit; per file, 256 KiB and
201
+ 60,000 YAML tokens (a very dense file can reach the token limit first).
202
+ - WF010 and WF011 follow pull request references through environment variables,
203
+ `$GITHUB_ENV`, step and job outputs, and action inputs within the repository.
204
+ Values computed in other ways (a script that builds a ref from pieces, files
205
+ passed between jobs) are not traced.
206
+
207
+ </details>
208
+
209
+ ## Command line
210
+
211
+ The package is `remit-actions` on PyPI; the command it installs is `remit`.
212
+
213
+ ```sh
214
+ pip install remit-actions
215
+ remit scan --repo . --base main --head my-branch --format markdown
216
+ remit scan --dirs path/to/before path/to/after --format sarif --output remit.sarif
217
+ ```
218
+
219
+ Output is JSON by default; `--format markdown` and `--format sarif` are also
220
+ available. `--summary FILE` appends the Markdown summary and `--sarif FILE`
221
+ also writes SARIF. The exit code is 0 for complete analysis and 2 otherwise.
222
+
223
+ ## Audit an existing repository
224
+
225
+ Pull request checks only see what a PR changes. `remit audit` reports what is
226
+ already there: every place a `pull_request_target`, `workflow_run` or
227
+ `issue_comment` job brings in pull request content (WF010), and every mutable
228
+ checkout after a label, comment or environment approval (WF011), including
229
+ through local actions and reusable workflows.
230
+
231
+ ```sh
232
+ remit audit --repo . # the current commit
233
+ remit audit --repo . --ref v2.3.0 # any commit or tag
234
+ remit audit --dir path/to/checkout --format markdown
235
+ remit audit --repo . --format sarif --output remit-audit.sarif
236
+ ```
237
+
238
+ Like `scan`, it reads committed Git objects (or a directory) as data and runs
239
+ nothing. The exit code is 0 for a complete audit, 2 when something couldn't be
240
+ analysed, and with `--fail-on-findings`, 1 when the audit found something. The
241
+ [setup guide](https://github.com/adi271213/remit/blob/main/SETUP.md#scheduled-audit)
242
+ has a weekly scheduled audit workflow.
243
+
244
+ ## Status
245
+
246
+ Rule IDs, Action inputs and outputs, and the JSON report format stay compatible
247
+ within a version; an incompatible change gets a new version number. Each release
248
+ runs the test suite on Python 3.11–3.14, runs the Action end to end with networking
249
+ disabled, and replays 74 historical public PRs. The replay's expected results
250
+ were labelled by the project, so treat it as a regression suite rather than an
251
+ accuracy measurement. Feedback from real repositories is the most useful thing
252
+ you can send: open a [trial feedback issue](https://github.com/adi271213/remit/issues/new?template=trial_feedback.md).
253
+
254
+ [Setup](https://github.com/adi271213/remit/blob/main/SETUP.md) ·
255
+ [Rules](https://github.com/adi271213/remit/blob/main/docs/RULES.md) ·
256
+ [Architecture](https://github.com/adi271213/remit/blob/main/docs/ARCHITECTURE.md) ·
257
+ [Contributing](https://github.com/adi271213/remit/blob/main/CONTRIBUTING.md) ·
258
+ [Security](https://github.com/adi271213/remit/blob/main/SECURITY.md) ·
259
+ [Changelog](https://github.com/adi271213/remit/blob/main/CHANGELOG.md) ·
260
+ [MIT license](https://github.com/adi271213/remit/blob/main/LICENSE)
@@ -0,0 +1,232 @@
1
+ <div align="center">
2
+
3
+ # Remit
4
+
5
+ **Know when a pull request widens your workflows' authority.**
6
+
7
+ [![CI](https://github.com/adi271213/remit/actions/workflows/ci.yml/badge.svg)](https://github.com/adi271213/remit/actions/workflows/ci.yml)
8
+ [![PyPI](https://img.shields.io/pypi/v/remit-actions)](https://pypi.org/project/remit-actions/)
9
+ [![Python](https://img.shields.io/pypi/pyversions/remit-actions)](https://pypi.org/project/remit-actions/)
10
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/adi271213/remit/blob/main/LICENSE)
11
+
12
+ </div>
13
+
14
+ The changes that matter in a workflow PR are often a single line: `contents: read`
15
+ becomes `write`, a `pull_request_target` trigger appears, a release job starts
16
+ running a new third-party action with your secrets. Remit compares the base and
17
+ head of every pull request and puts each of those changes on the line that
18
+ introduced it, so reviewers can't miss them.
19
+
20
+ - **Only what changed.** Unchanged configuration stays quiet, so ordinary PRs get no noise.
21
+ - **Flags common pwn-request patterns.** Privileged workflows that start checking out, fetching or downloading pull request content, including the time-of-check to time-of-use trap after a label or comment approval.
22
+ - **Right on the diff.** Findings appear as annotations in the PR's *Files changed* tab.
23
+ - **Safe on untrusted PRs.** The PR is read as data and never executed; the analysis makes no network calls.
24
+ - **Nothing to install.** A pinned Action with a hash-verified bundled parser; no secrets, `contents: read` only.
25
+
26
+ ## What it catches
27
+
28
+ A PR edits a release workflow:
29
+
30
+ ```diff
31
+ name: Release
32
+ -on: push
33
+ +on: [push, pull_request_target]
34
+ permissions: {}
35
+ jobs:
36
+ publish:
37
+ runs-on: ubuntu-latest
38
+ permissions:
39
+ - contents: read
40
+ + contents: write
41
+ + id-token: write
42
+ steps:
43
+ - uses: actions/checkout@v4
44
+ - run: make dist
45
+ + - uses: someone/release-action@v1
46
+ + with:
47
+ + token: ${{ secrets.RELEASE_TOKEN }}
48
+ ```
49
+
50
+ Remit annotates five lines of that diff:
51
+
52
+ | Line | Rule | Finding |
53
+ |---|---|---|
54
+ | 2 | WF003 | Review-sensitive trigger `pull_request_target` introduced. |
55
+ | 8 | WF001 | Job publish is newly configured to use `contents: write`. |
56
+ | 9 | WF001 | Job publish is newly configured to request an OIDC identity token. |
57
+ | 13 | WF009 | Job publish now runs third-party code from `someone/release-action@v1`, which can use `contents: write`; `id-token: write`; secrets: `RELEASE_TOKEN`. |
58
+ | 15 | WF004 | Job publish adds a reference or forwarding path to `RELEASE_TOKEN`. |
59
+
60
+ The job summary groups the same findings by workflow and job, with a question
61
+ for the reviewer on each ([sample](https://github.com/adi271213/remit/blob/main/examples/sample-report.md)).
62
+ JSON and SARIF reports carry the same data for tooling and GitHub code scanning.
63
+
64
+ ## Quick start
65
+
66
+ 1. Copy [`examples/github-action-template.yml`](https://github.com/adi271213/remit/blob/main/examples/github-action-template.yml)
67
+ (45 lines) to `.github/workflows/remit.yml`.
68
+ 2. Merge it.
69
+ 3. Open a pull request that touches a workflow and look at *Files changed*.
70
+
71
+ The template checks out the base commit, fetches the PR head without checking it
72
+ out, and runs the Action pinned to a full commit SHA:
73
+
74
+ ```yaml
75
+ - uses: adi271213/remit@3db5c481c527a7f60bf92c0499083fe15f7829be # version 1
76
+ with:
77
+ mode: advisory
78
+ ```
79
+
80
+ Use the whole template rather than this step alone: the step needs both commits
81
+ in the local clone. The [setup guide](https://github.com/adi271213/remit/blob/main/SETUP.md)
82
+ covers inputs, outputs, code scanning and troubleshooting.
83
+
84
+ ## Rules
85
+
86
+ | Rule | Reports |
87
+ |---|---|
88
+ | WF001 | A job's token scope increases (`none` → `read` → `write`), including new `id-token: write` (OIDC) |
89
+ | WF002 | A job starts relying on the repository's default token permissions, or replaces them with permissions that may exceed the default |
90
+ | WF003 | A newly added `pull_request_target`, `workflow_run`, `issue_comment`, `issues`, `discussion` or `discussion_comment` trigger |
91
+ | WF004 | A job newly references a secret, or forwards all secrets (`secrets: inherit`, `toJSON(secrets)`) |
92
+ | WF005 | A job's runner changes to a self-hosted, custom-label, runner-group or unresolvable selection |
93
+ | WF007 | A job's deployment environment is added, removed or changed |
94
+ | WF008 | A matrix job pairs a runner with a different environment while the overall runner and environment sets stay the same |
95
+ | WF009 | Third-party code (an action or reusable workflow) newly runs in a job with write scopes, OIDC, secrets or an environment |
96
+ | WF010 | A `pull_request_target`, `workflow_run` or `issue_comment` job newly checks out, fetches or downloads pull request content: the "pwn request" |
97
+ | WF011 | An approval-gated privileged job checks out a pull request reference that can change after approval: time-of-check to time-of-use |
98
+
99
+ Rules follow steps into local composite actions and local reusable workflows
100
+ (`./` and `$/` references), so a change inside `.github/actions/` is caught even
101
+ when no workflow file changed.
102
+
103
+ [docs/RULES.md](https://github.com/adi271213/remit/blob/main/docs/RULES.md) describes the exact behaviour of each rule.
104
+
105
+ ## Remit and zizmor
106
+
107
+ [zizmor](https://github.com/zizmorcore/zizmor) audits what your workflows *are*
108
+ for risky patterns. Remit reports what a pull request *changed*, including
109
+ increases that never become a zizmor alert because the end state isn't "bad"
110
+ by a state rule: a job gaining `id-token: write`, a new secret reference, a
111
+ changed environment, or a new action receiving write access.
112
+
113
+ On [74 historical public PRs](https://github.com/adi271213/remit/blob/main/benchmarks/README.md),
114
+ Remit reported 72 findings. For 46 of them (64%), zizmor 1.32 (pedantic,
115
+ offline) reported no new finding of a related audit in the same workflow and
116
+ job, including all 8 new secret references. The PRs were selected and labelled
117
+ by this project and the matching is approximate, so read this as an indication,
118
+ not a benchmark. Run zizmor for what is dangerous, and Remit for what got wider.
119
+
120
+ ## Results
121
+
122
+ Every run reports an **analysis** status and a **review** status:
123
+
124
+ | Analysis | Meaning | CLI exit | `require-complete` (default) | `advisory` |
125
+ |---|---|---|---|---|
126
+ | `complete` | All changed workflows were compared | 0 | pass | pass |
127
+ | `incomplete` | Some context is unknown, e.g. a dynamic expression | 2 | fail | pass with warning |
128
+ | `error` | Some input couldn't be parsed or read | 2 | fail | pass with warning |
129
+
130
+ Review is `suggested` when there are findings, `none` when analysis is complete
131
+ with no findings, and (in the Action) `unknown` when analysis is incomplete and
132
+ nothing was found. Known findings are reported alongside incomplete or failed parts.
133
+
134
+ **Findings never fail the step.** Remit is a review aid, not a merge gate: a
135
+ clean report doesn't mean a PR is safe, and a PR can edit the workflow that runs
136
+ the check.
137
+
138
+ <details>
139
+ <summary><b>Inherited token permissions</b></summary>
140
+
141
+ A job without a `permissions` block gets the repository or organization default,
142
+ which Remit can't see. When a PR adds such a job, or removes a job's
143
+ `permissions` block, WF002 asks the author to declare permissions explicitly.
144
+ When a job that used the default starts declaring permissions, only levels above
145
+ GitHub's narrowest possible default (`contents: read`, `packages: read`) are
146
+ reported, so ordinary hardening PRs stay quiet. To compare against your real
147
+ default instead, pass it in:
148
+
149
+ ```yaml
150
+ with:
151
+ default-permissions: '{"contents": "read"}'
152
+ ```
153
+
154
+ </details>
155
+
156
+ <details>
157
+ <summary><b>Limitations</b></summary>
158
+
159
+ - Workflows are read from `.github/workflows/`, and local composite actions and
160
+ reusable workflows are followed. Actions and reusable workflows from other
161
+ repositories, and the contents of scripts, are not analysed.
162
+ - Repository and organization settings are not visible: default token
163
+ permissions, runner inventory, environment protection rules and secrets.
164
+ - WF003 looks at trigger names only. Filter changes on existing triggers
165
+ (branches, types, paths) are not analysed.
166
+ - Expressions are not evaluated. Only direct `${{ matrix.<field> }}` references
167
+ into a static matrix of up to 256 rows are resolved; anything else is reported
168
+ as unresolved.
169
+ - YAML merge keys (`<<`) and recursive aliases are rejected as errors.
170
+ - A renamed file is matched only if its content is identical; an edited rename
171
+ is treated as a new file and can produce findings for unchanged settings.
172
+ - Input limits: 200 workflow files and 4 MiB per commit; per file, 256 KiB and
173
+ 60,000 YAML tokens (a very dense file can reach the token limit first).
174
+ - WF010 and WF011 follow pull request references through environment variables,
175
+ `$GITHUB_ENV`, step and job outputs, and action inputs within the repository.
176
+ Values computed in other ways (a script that builds a ref from pieces, files
177
+ passed between jobs) are not traced.
178
+
179
+ </details>
180
+
181
+ ## Command line
182
+
183
+ The package is `remit-actions` on PyPI; the command it installs is `remit`.
184
+
185
+ ```sh
186
+ pip install remit-actions
187
+ remit scan --repo . --base main --head my-branch --format markdown
188
+ remit scan --dirs path/to/before path/to/after --format sarif --output remit.sarif
189
+ ```
190
+
191
+ Output is JSON by default; `--format markdown` and `--format sarif` are also
192
+ available. `--summary FILE` appends the Markdown summary and `--sarif FILE`
193
+ also writes SARIF. The exit code is 0 for complete analysis and 2 otherwise.
194
+
195
+ ## Audit an existing repository
196
+
197
+ Pull request checks only see what a PR changes. `remit audit` reports what is
198
+ already there: every place a `pull_request_target`, `workflow_run` or
199
+ `issue_comment` job brings in pull request content (WF010), and every mutable
200
+ checkout after a label, comment or environment approval (WF011), including
201
+ through local actions and reusable workflows.
202
+
203
+ ```sh
204
+ remit audit --repo . # the current commit
205
+ remit audit --repo . --ref v2.3.0 # any commit or tag
206
+ remit audit --dir path/to/checkout --format markdown
207
+ remit audit --repo . --format sarif --output remit-audit.sarif
208
+ ```
209
+
210
+ Like `scan`, it reads committed Git objects (or a directory) as data and runs
211
+ nothing. The exit code is 0 for a complete audit, 2 when something couldn't be
212
+ analysed, and with `--fail-on-findings`, 1 when the audit found something. The
213
+ [setup guide](https://github.com/adi271213/remit/blob/main/SETUP.md#scheduled-audit)
214
+ has a weekly scheduled audit workflow.
215
+
216
+ ## Status
217
+
218
+ Rule IDs, Action inputs and outputs, and the JSON report format stay compatible
219
+ within a version; an incompatible change gets a new version number. Each release
220
+ runs the test suite on Python 3.11–3.14, runs the Action end to end with networking
221
+ disabled, and replays 74 historical public PRs. The replay's expected results
222
+ were labelled by the project, so treat it as a regression suite rather than an
223
+ accuracy measurement. Feedback from real repositories is the most useful thing
224
+ you can send: open a [trial feedback issue](https://github.com/adi271213/remit/issues/new?template=trial_feedback.md).
225
+
226
+ [Setup](https://github.com/adi271213/remit/blob/main/SETUP.md) ·
227
+ [Rules](https://github.com/adi271213/remit/blob/main/docs/RULES.md) ·
228
+ [Architecture](https://github.com/adi271213/remit/blob/main/docs/ARCHITECTURE.md) ·
229
+ [Contributing](https://github.com/adi271213/remit/blob/main/CONTRIBUTING.md) ·
230
+ [Security](https://github.com/adi271213/remit/blob/main/SECURITY.md) ·
231
+ [Changelog](https://github.com/adi271213/remit/blob/main/CHANGELOG.md) ·
232
+ [MIT license](https://github.com/adi271213/remit/blob/main/LICENSE)