actions-warden 0.2.0 → 0.3.0
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.
- package/AGENTS.md +189 -0
- package/CONTRIBUTING.md +109 -0
- package/README.md +272 -224
- package/RELEASING.md +338 -0
- package/SECURITY.md +25 -3
- package/docs/AI-AGENTS.md +458 -0
- package/docs/CLI.md +421 -0
- package/docs/CONFIGURATION.md +340 -0
- package/docs/DEVELOPMENT.md +350 -0
- package/docs/GITHUB-ACTION.md +281 -0
- package/docs/JAVASCRIPT-API.md +355 -0
- package/docs/OUTPUTS.md +409 -0
- package/docs/README.md +27 -0
- package/examples/org-scan.yml +42 -0
- package/examples/upgrade-pr.yml +57 -0
- package/llms.txt +38 -0
- package/package.json +32 -10
- package/skills/actions-warden/SKILL.md +140 -38
- package/src/action.js +317 -0
- package/src/cli.js +267 -22
- package/src/commands/audit.js +189 -36
- package/src/commands/org-scan.js +549 -0
- package/src/commands/pin.js +59 -56
- package/src/commands/report.js +122 -10
- package/src/commands/upgrade.js +102 -62
- package/src/commands/verify.js +193 -0
- package/src/index.js +21 -4
- package/src/lib/action-status.js +27 -0
- package/src/lib/agent-mode.js +175 -0
- package/src/lib/annotations.js +250 -0
- package/src/lib/baseline.js +103 -0
- package/src/lib/cache.js +47 -10
- package/src/lib/concurrency.js +27 -0
- package/src/lib/config.js +185 -0
- package/src/lib/execution.js +71 -0
- package/src/lib/formatter.js +127 -8
- package/src/lib/github-org.js +374 -0
- package/src/lib/identity.js +62 -0
- package/src/lib/ignore.js +7 -6
- package/src/lib/org-checkpoint.js +378 -0
- package/src/lib/org-progress.js +60 -0
- package/src/lib/parser.js +326 -52
- package/src/lib/patcher.js +199 -0
- package/src/lib/paths.js +35 -12
- package/src/lib/redact.js +65 -4
- package/src/lib/resolver.js +225 -43
- package/src/lib/targets.js +28 -0
- package/src/lib/triggers.js +12 -0
- package/src/lib/writer.js +45 -8
- package/src/rules/excessive-permissions.js +24 -33
- package/src/rules/index.js +19 -1
- package/src/rules/pull-request-target-checkout.js +149 -18
- package/src/rules/reusable-workflow-secrets.js +32 -0
- package/src/rules/script-injection.js +77 -12
- package/src/rules/secrets-in-env.js +101 -18
- package/src/rules/unpinned-action.js +3 -2
- package/src/rules/unpinned-container-image.js +39 -0
- package/src/rules/unpinned-docker-action.js +30 -0
- package/src/rules/untrusted-self-hosted-runner.js +109 -0
- package/src/rules/workflow-run-artifact-execution.js +122 -0
- package/src/rules/workflow-structure.js +396 -0
- package/src/version.js +3 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Configuration, ignores, and baselines
|
|
2
|
+
|
|
3
|
+
actions-warden can run with built-in defaults, or load a versioned policy from
|
|
4
|
+
the repository being scanned.
|
|
5
|
+
|
|
6
|
+
## Policy discovery
|
|
7
|
+
|
|
8
|
+
The default filenames are:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
.actions-warden.yml
|
|
12
|
+
.actions-warden.yaml
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
If both exist, `.actions-warden.yml` wins. Select another file with `--config`,
|
|
16
|
+
or skip repository policy with `--ignore-config`:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
actions-warden audit --config=security/actions-warden.yml
|
|
20
|
+
actions-warden audit --ignore-config
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The policy path must resolve inside `--cwd`, including after symlink
|
|
24
|
+
resolution. `--config` and `--ignore-config` are mutually exclusive.
|
|
25
|
+
|
|
26
|
+
Configuration is strict by design. Unknown top-level keys, unknown rule IDs,
|
|
27
|
+
unknown nested keys, invalid severities, and values of the wrong type stop the
|
|
28
|
+
scan. This prevents a typo from silently weakening policy.
|
|
29
|
+
|
|
30
|
+
## Complete policy shape
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
version: 1
|
|
34
|
+
baseline: .actions-warden-baseline.json
|
|
35
|
+
|
|
36
|
+
ignore-paths:
|
|
37
|
+
- .github/workflows/generated/**
|
|
38
|
+
- .github/actions/vendor/**
|
|
39
|
+
|
|
40
|
+
rules:
|
|
41
|
+
excessive-permissions:
|
|
42
|
+
enabled: true
|
|
43
|
+
severity: high
|
|
44
|
+
unpinned-container-image:
|
|
45
|
+
enabled: false
|
|
46
|
+
|
|
47
|
+
runner-policy:
|
|
48
|
+
self-hosted-labels:
|
|
49
|
+
- private-*
|
|
50
|
+
- arc-*
|
|
51
|
+
trusted-groups:
|
|
52
|
+
- github-hosted-*
|
|
53
|
+
flag-unknown-groups: true
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| key | type | default | meaning |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| `version` | integer | `1` | Policy schema; if present, must be `1` |
|
|
59
|
+
| `baseline` | non-empty string | none | Baseline path relative to `--cwd` |
|
|
60
|
+
| `ignore-paths` | string array | `[]` | Picomatch globs excluded before parsing |
|
|
61
|
+
| `rules` | mapping | `{}` | Per-rule enablement and severity overrides |
|
|
62
|
+
| `runner-policy` | mapping | built-in defaults | Additional self-hosted runner selectors and group trust |
|
|
63
|
+
|
|
64
|
+
Rule policies accept only:
|
|
65
|
+
|
|
66
|
+
| key | type | default |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `enabled` | boolean | `true` |
|
|
69
|
+
| `severity` | `low`, `medium`, `high`, or `critical` | rule default |
|
|
70
|
+
|
|
71
|
+
A CLI `--baseline` overrides the policy's `baseline`. A CLI `--severity`
|
|
72
|
+
filters after any configured severity override.
|
|
73
|
+
|
|
74
|
+
## Rule IDs
|
|
75
|
+
|
|
76
|
+
Use the installed version as the source of truth:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
actions-warden rules --format=json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The current built-in rules are:
|
|
83
|
+
|
|
84
|
+
| ID | default severity | detects |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `unpinned-action` | high | External actions and reusable workflows without a full commit SHA |
|
|
87
|
+
| `unpinned-docker-action` | high | Mutable `docker://` action images |
|
|
88
|
+
| `unpinned-container-image` | high | Mutable job, service, or Docker action images |
|
|
89
|
+
| `excessive-permissions` | medium | Broadly writable workflow or job `GITHUB_TOKEN` permissions |
|
|
90
|
+
| `secrets-in-env` | medium | Named job secrets exposed broadly; workflow-wide or dynamic all-secret exposure is high |
|
|
91
|
+
| `script-injection` | critical | Untrusted GitHub context, including tainted `env`, interpolated into a shell or script action |
|
|
92
|
+
| `pull-request-target-checkout` | critical | Privileged pull-request code retrieval without an active checkout guard |
|
|
93
|
+
| `reusable-workflow-secrets-inherit` | high | External reusable workflows receiving every caller secret |
|
|
94
|
+
| `untrusted-self-hosted-runner` | high | Untrusted pull-request code reaching self-hosted infrastructure |
|
|
95
|
+
| `workflow-run-artifact-execution` | critical | Privileged `workflow_run` jobs executing cross-run artifacts |
|
|
96
|
+
| `workflow-structure` | medium | Structurally invalid workflow or composite-action syntax |
|
|
97
|
+
|
|
98
|
+
Rule findings may adjust severity based on the recognized pattern. A configured
|
|
99
|
+
severity override is applied to every finding produced by that rule.
|
|
100
|
+
|
|
101
|
+
## Rule accuracy and analysis boundaries
|
|
102
|
+
|
|
103
|
+
The audit is static and offline. It parses YAML and follows a small amount of
|
|
104
|
+
local data flow, but it does not evaluate expressions, call referenced actions,
|
|
105
|
+
inspect organization settings, or execute workflow code. Findings therefore
|
|
106
|
+
describe a recognized risk pattern and its evidence, not a proof that every
|
|
107
|
+
runtime path is exploitable.
|
|
108
|
+
|
|
109
|
+
The current rule boundaries are deliberate:
|
|
110
|
+
|
|
111
|
+
- `unpinned-action` requires a full 40-character commit SHA for external
|
|
112
|
+
actions and reusable workflows. Workspace-relative `./` references and
|
|
113
|
+
GitHub's `$/` self-repository references are not external dependencies. A
|
|
114
|
+
`$/` reference follows the exact workflow commit and requires runner 2.336.0
|
|
115
|
+
or newer, cannot include an `@ref` suffix, and is not available on GitHub
|
|
116
|
+
Enterprise Server; see GitHub's [self-repository syntax announcement](https://github.blog/changelog/2026-07-30-reference-same-repository-actions-with-self-repository-syntax/).
|
|
117
|
+
- `excessive-permissions` treats a missing top-level declaration as a low
|
|
118
|
+
advisory because token defaults are configurable. It reports `write-all` and
|
|
119
|
+
maps with at least three writable scopes, but does not guess that one
|
|
120
|
+
purpose-specific write scope is unnecessary. Invalid permission values are
|
|
121
|
+
reported by `workflow-structure` instead.
|
|
122
|
+
- `secrets-in-env` reports a named secret at job scope as medium, and raises
|
|
123
|
+
workflow-wide scope, dynamic indexing, and `toJSON(secrets)` to high. GitHub
|
|
124
|
+
may need to expose every available secret to resolve a dynamic reference.
|
|
125
|
+
Job scope can be necessary for a secret-dependent `if` condition; in that
|
|
126
|
+
case every job step should be trusted and the secret should carry minimal
|
|
127
|
+
privilege. A named step-scoped secret is outside this rule, but dynamic or
|
|
128
|
+
all-secret access is reported even at step scope.
|
|
129
|
+
- `script-injection` follows known attacker-controlled GitHub fields directly
|
|
130
|
+
into `run` and `actions/github-script`, and through workflow, job, or step
|
|
131
|
+
`env` when the script re-interpolates `${{ env.NAME }}`. The safe boundary is
|
|
132
|
+
native shell syntax such as `"$NAME"` or JavaScript `process.env.NAME`, as in
|
|
133
|
+
GitHub's [CodeQL recommendation](https://codeql.github.com/codeql-query-help/actions/actions-code-injection-critical/).
|
|
134
|
+
- `pull-request-target-checkout` understands GitHub's June 2026 checkout
|
|
135
|
+
protection and its July 20 backport for floating `actions/checkout` v2-v7
|
|
136
|
+
tags, protected release
|
|
137
|
+
versions (`v2.8.0`, `v3.7.0`, `v4.4.0`, `v5.1.0`, `v6.1.0`, and v7+), and the
|
|
138
|
+
exact known release SHAs. It still reports an explicit or expression-driven
|
|
139
|
+
`allow-unsafe-pr-checkout`, older versions, direct `git`/`gh` fetches, and
|
|
140
|
+
unknown pinned SHAs. Version comments are not trusted as security evidence.
|
|
141
|
+
A protected floating major can therefore avoid this finding while still
|
|
142
|
+
producing `unpinned-action`; the two rules cover different risks. See
|
|
143
|
+
GitHub's [checkout protection announcement](https://github.blog/changelog/2026-06-18-safer-pull_request_target-defaults-for-github-actions-checkout/).
|
|
144
|
+
- `workflow-run-artifact-execution` treats the official
|
|
145
|
+
`actions/download-artifact` action as cross-run only when `run-id` selects
|
|
146
|
+
another run. A same-run download is not enough to trigger the rule. Artifact
|
|
147
|
+
paths under `${{ runner.temp }}` remain isolated from unrelated workspace
|
|
148
|
+
commands, but execution or loading from that exact path is still reported.
|
|
149
|
+
- `untrusted-self-hosted-runner` reports ordinary `pull_request` jobs on
|
|
150
|
+
recognized self-hosted selectors. For `pull_request_target`, it requires a
|
|
151
|
+
recognized unsafe PR checkout or direct fetch instead of assuming that a
|
|
152
|
+
trusted labeling job executes fork code. Literal matrix dimensions and
|
|
153
|
+
`include` entries are followed when `runs-on` selects a matrix key. Matrices
|
|
154
|
+
generated by expressions still require manual review. Use `runner-policy`
|
|
155
|
+
for organization-specific labels and groups.
|
|
156
|
+
- `workflow-structure` checks required trigger/job/step shapes, reusable-job
|
|
157
|
+
conflicts, permission values, and composite run-step shells. It recognizes
|
|
158
|
+
GitHub's current `background`, `wait`, `wait-all`, `cancel`, and `parallel`
|
|
159
|
+
step syntax, validates the documented control shapes, and recursively scans
|
|
160
|
+
actions and scripts inside `parallel` groups. Background and parallel steps
|
|
161
|
+
authored inside composite actions are reported because GitHub does not
|
|
162
|
+
support them there. See GitHub's [current concurrent-step
|
|
163
|
+
syntax](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepsbackground).
|
|
164
|
+
This rule is not a full replacement for GitHub's server-side workflow
|
|
165
|
+
validator.
|
|
166
|
+
|
|
167
|
+
Execution recognition is intentionally conservative but finite: it covers
|
|
168
|
+
common shell scripts, package managers, build/test commands, local actions, and
|
|
169
|
+
build-like third-party actions. A custom action or unusual interpreter can
|
|
170
|
+
consume untrusted files without being recognized. Review privileged
|
|
171
|
+
`pull_request_target` and `workflow_run` jobs manually, and use GitHub Actions
|
|
172
|
+
CodeQL alongside this audit when complete interprocedural analysis is needed.
|
|
173
|
+
|
|
174
|
+
## Runner policy
|
|
175
|
+
|
|
176
|
+
The self-hosted runner rule always recognizes the literal `self-hosted` label.
|
|
177
|
+
Policy can add organization-specific labels and group handling:
|
|
178
|
+
|
|
179
|
+
```yaml
|
|
180
|
+
runner-policy:
|
|
181
|
+
self-hosted-labels:
|
|
182
|
+
- linux-prod-*
|
|
183
|
+
- arc-runner-*
|
|
184
|
+
trusted-groups:
|
|
185
|
+
- github-hosted-*
|
|
186
|
+
flag-unknown-groups: true
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`self-hosted-labels` and `trusted-groups` use Picomatch glob syntax.
|
|
190
|
+
|
|
191
|
+
GitHub runner groups are not assumed unsafe by default because a group may
|
|
192
|
+
select GitHub-hosted infrastructure. When `flag-unknown-groups` is `true`, a
|
|
193
|
+
pull-request job using a group is reported unless the group matches
|
|
194
|
+
`trusted-groups`.
|
|
195
|
+
|
|
196
|
+
## Path exclusions
|
|
197
|
+
|
|
198
|
+
`ignore-paths` is applied before a workflow is parsed:
|
|
199
|
+
|
|
200
|
+
```yaml
|
|
201
|
+
ignore-paths:
|
|
202
|
+
- .github/workflows/generated/**
|
|
203
|
+
- .github/actions/third-party/**
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
For a local audit, paths are relative to `--cwd`. For `org-scan`, the local
|
|
207
|
+
organization policy applies to every repository and paths begin with
|
|
208
|
+
`owner/repository/`:
|
|
209
|
+
|
|
210
|
+
```yaml
|
|
211
|
+
ignore-paths:
|
|
212
|
+
- my-org/generated-repo/.github/workflows/**
|
|
213
|
+
- my-org/archive-*/.github/actions/**
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
A path exclusion removes the file from coverage. Use it for generated or
|
|
217
|
+
vendored sources whose ownership and review process is understood, and protect
|
|
218
|
+
policy changes like code.
|
|
219
|
+
|
|
220
|
+
## Inline ignore directives
|
|
221
|
+
|
|
222
|
+
Use an inline directive when a specific finding has been reviewed and the file
|
|
223
|
+
should remain in scan coverage.
|
|
224
|
+
|
|
225
|
+
Ignore one rule on the same line:
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
- uses: owner/action@v1 # actions-warden-ignore: unpinned-action
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Ignore the next non-empty, non-comment line:
|
|
232
|
+
|
|
233
|
+
```yaml
|
|
234
|
+
# actions-warden-ignore-next-line: excessive-permissions
|
|
235
|
+
permissions: write-all
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Ignore a block:
|
|
239
|
+
|
|
240
|
+
```yaml
|
|
241
|
+
# actions-warden-ignore-start: workflow-structure
|
|
242
|
+
permissions:
|
|
243
|
+
# actions-warden-ignore-end
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Ignore a rule for the whole file:
|
|
247
|
+
|
|
248
|
+
```yaml
|
|
249
|
+
# actions-warden-ignore-file: unpinned-action
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The short `aw-` prefix is also accepted, for example
|
|
253
|
+
`# aw-ignore-next-line: script-injection`. Multiple rule IDs may be separated by
|
|
254
|
+
commas or whitespace. Omitting rule IDs suppresses every rule in that scope;
|
|
255
|
+
prefer naming the narrowest rule so future checks remain active.
|
|
256
|
+
|
|
257
|
+
Unmatched `ignore-start` directives extend to the end of the file. An
|
|
258
|
+
`ignore-end` without an open block has no effect.
|
|
259
|
+
|
|
260
|
+
## Baselines
|
|
261
|
+
|
|
262
|
+
A baseline accepts existing, reviewed findings while allowing new findings to
|
|
263
|
+
fail the scan.
|
|
264
|
+
|
|
265
|
+
Create one from the current unfiltered finding set:
|
|
266
|
+
|
|
267
|
+
```sh
|
|
268
|
+
actions-warden audit \
|
|
269
|
+
--create-baseline=.actions-warden-baseline.json
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The command writes a deterministic document:
|
|
273
|
+
|
|
274
|
+
```json
|
|
275
|
+
{
|
|
276
|
+
"schemaVersion": "1.0",
|
|
277
|
+
"generatedBy": "actions-warden",
|
|
278
|
+
"findings": [
|
|
279
|
+
{
|
|
280
|
+
"id": "18b82e86d7c14fe2",
|
|
281
|
+
"fingerprint": "a5c4f4a2797d17d9",
|
|
282
|
+
"ruleId": "unpinned-action",
|
|
283
|
+
"severity": "high",
|
|
284
|
+
"file": ".github/workflows/ci.yml",
|
|
285
|
+
"line": 14
|
|
286
|
+
}
|
|
287
|
+
]
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Load it explicitly:
|
|
292
|
+
|
|
293
|
+
```sh
|
|
294
|
+
actions-warden audit \
|
|
295
|
+
--baseline=.actions-warden-baseline.json
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
or through policy:
|
|
299
|
+
|
|
300
|
+
```yaml
|
|
301
|
+
version: 1
|
|
302
|
+
baseline: .actions-warden-baseline.json
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Each finding has two matching identities:
|
|
306
|
+
|
|
307
|
+
- `id` identifies the exact repository-relative source occurrence and is stable
|
|
308
|
+
across clones;
|
|
309
|
+
- `fingerprint` ignores line movement while retaining semantic fields, file,
|
|
310
|
+
rule, and source-order ordinal for equivalent duplicates.
|
|
311
|
+
|
|
312
|
+
A baseline match suppresses the finding from `findings`. The summary still
|
|
313
|
+
reports `totalFindings` and `suppressed`. Parser failures cannot be baselined.
|
|
314
|
+
|
|
315
|
+
Review baseline diffs carefully. Deleting a finding from the baseline makes it
|
|
316
|
+
active again; adding one accepts that risk without changing the workflow.
|
|
317
|
+
|
|
318
|
+
## Policy ownership
|
|
319
|
+
|
|
320
|
+
Treat these files as security controls:
|
|
321
|
+
|
|
322
|
+
```text
|
|
323
|
+
.actions-warden.yml
|
|
324
|
+
.actions-warden.yaml
|
|
325
|
+
.actions-warden-baseline.json
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Recommended controls include:
|
|
329
|
+
|
|
330
|
+
- require review from a security or platform owner through `CODEOWNERS`;
|
|
331
|
+
- prevent unreviewed direct pushes with branch protection;
|
|
332
|
+
- show policy and baseline diffs in dependency-maintenance pull requests;
|
|
333
|
+
- avoid granting a scanning pull request permission to rewrite its own policy.
|
|
334
|
+
|
|
335
|
+
## Related guides
|
|
336
|
+
|
|
337
|
+
- [CLI reference](./CLI.md)
|
|
338
|
+
- [Output contracts](./OUTPUTS.md)
|
|
339
|
+
- [GitHub Action](./GITHUB-ACTION.md)
|
|
340
|
+
- [Security policy](../SECURITY.md)
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# Developer guide
|
|
2
|
+
|
|
3
|
+
This guide is for contributors changing actions-warden itself. For public CLI
|
|
4
|
+
usage, start with the [CLI reference](./CLI.md).
|
|
5
|
+
|
|
6
|
+
## Prerequisites
|
|
7
|
+
|
|
8
|
+
- Node.js 20 or newer;
|
|
9
|
+
- npm with the lockfile committed by the repository;
|
|
10
|
+
- optional: `prek` or Python `pre-commit` for local hooks.
|
|
11
|
+
|
|
12
|
+
Install exactly the locked dependency graph:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm ci
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The repository's `.npmrc` pins dependencies exactly and disables package
|
|
19
|
+
lifecycle scripts during installation.
|
|
20
|
+
|
|
21
|
+
Install hooks with either runner:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
prek install --hook-type pre-commit --hook-type pre-push
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
or:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pre-commit install --hook-type pre-commit
|
|
31
|
+
pre-commit install --hook-type pre-push
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Validation commands
|
|
35
|
+
|
|
36
|
+
| command | purpose |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `npm test` | Run the complete Vitest suite once |
|
|
39
|
+
| `npm run test:watch` | Run Vitest in watch mode |
|
|
40
|
+
| `npm run lint` | Lint source, scripts, and tests |
|
|
41
|
+
| `npm run check:yaml` | Parse repository YAML files |
|
|
42
|
+
| `npm run check:docs` | Validate local documentation links |
|
|
43
|
+
| `npm run check:package` | Inspect the exact npm tarball manifest, required files, executable mode, and size bounds |
|
|
44
|
+
| `npm run verify-deps` | Require exact runtime and development versions |
|
|
45
|
+
| `npm run verify-version-sync` | Keep package, lockfile, bundled runtime, plugin, and public invocations aligned |
|
|
46
|
+
| `npm run build:action` | Rebuild the committed Action bundle |
|
|
47
|
+
| `npm run check:action-bundle` | Compare a clean rebuild with working and staged bundles |
|
|
48
|
+
| `npm run audit` | Run the configured npm vulnerability threshold |
|
|
49
|
+
| `npm run release:prepare -- X.Y.Z` | Update all local stable-version sources without committing or publishing |
|
|
50
|
+
| `npm run release:check` | Gate a staged release candidate against npm state and every release validation |
|
|
51
|
+
|
|
52
|
+
Before opening a pull request, run:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npm run verify-version-sync
|
|
56
|
+
npm run verify-deps
|
|
57
|
+
npm run check:yaml
|
|
58
|
+
npm run check:docs
|
|
59
|
+
npm run check:package
|
|
60
|
+
npm run lint
|
|
61
|
+
npm test
|
|
62
|
+
npm run audit
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
If code reachable from `src/action.js` changed, also run:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
npm run build:action
|
|
69
|
+
npm run check:action-bundle
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Repository map
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
src/
|
|
76
|
+
cli.js CLI parsing, exit codes, and output routing
|
|
77
|
+
action.js GitHub Action input/output adapter
|
|
78
|
+
index.js public JavaScript exports
|
|
79
|
+
version.js runtime version embedded in the Action bundle
|
|
80
|
+
commands/ audit, pin, verify, upgrade, report, org-scan
|
|
81
|
+
lib/
|
|
82
|
+
parser.js YAML-to-normalized-workflow model
|
|
83
|
+
targets.js, paths.js repository target discovery
|
|
84
|
+
config.js, baseline.js policy and accepted-finding controls
|
|
85
|
+
resolver.js, cache.js GitHub API, caching, ref and ownership checks
|
|
86
|
+
github-org.js read-only organization/tree/blob access
|
|
87
|
+
agent-mode.js explicit bounded AI-agent CLI defaults
|
|
88
|
+
org-checkpoint.js validated atomic organization resume state
|
|
89
|
+
org-progress.js human rendering of structured scan progress
|
|
90
|
+
identity.js stable IDs and semantic fingerprints
|
|
91
|
+
patcher.js, writer.js source-range rewrites and guarded atomic writes
|
|
92
|
+
formatter.js, redact.js structured output and credential redaction
|
|
93
|
+
annotations.js GitHub workflow annotations
|
|
94
|
+
concurrency.js bounded async work
|
|
95
|
+
rules/ one module per audit rule
|
|
96
|
+
|
|
97
|
+
test/ Vitest suites and hostile/edge-case fixtures
|
|
98
|
+
scripts/ repository validation and release helpers
|
|
99
|
+
docs/ user, integration, AI, and developer guides
|
|
100
|
+
examples/ copyable GitHub workflow examples
|
|
101
|
+
skills/actions-warden/ Claude Code skill
|
|
102
|
+
dist/ generated, committed GitHub Action bundle
|
|
103
|
+
action.yml public Action inputs, outputs, and runtime
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Data flow
|
|
107
|
+
|
|
108
|
+
A local audit follows this path:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
targets → source read → YAML parser → normalized workflow model
|
|
112
|
+
→ ignore directives → rules → stable identities
|
|
113
|
+
→ severity/baseline filtering → renderer
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Pin and upgrade add:
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
normalized action refs → GitHub resolution and ownership verification
|
|
120
|
+
→ source-range patch plan → reparse
|
|
121
|
+
→ guarded atomic writer only when dryRun=false
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
An organization scan follows:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
repository listing → default-branch Git tree → bounded YAML blob reads
|
|
128
|
+
→ in-memory auditSources → repository aggregation
|
|
129
|
+
|
|
130
|
+
optional checkpoint → identity validation → fresh tree SHA comparison
|
|
131
|
+
→ reuse unchanged error-free result or rescan
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Remote repositories are never cloned, checked out, imported, or executed.
|
|
135
|
+
|
|
136
|
+
## Safety invariants
|
|
137
|
+
|
|
138
|
+
Changes must preserve these boundaries:
|
|
139
|
+
|
|
140
|
+
- `pin` and `upgrade` default to dry-run at the command and API layers.
|
|
141
|
+
- CLI and Action mutation require an explicit write option.
|
|
142
|
+
- A resolver failure must not fall back to a guessed tag or SHA.
|
|
143
|
+
- Commits must be verified as belonging to the referenced repository.
|
|
144
|
+
- Workflow rewrites operate on parsed scalar ranges and reparse before write.
|
|
145
|
+
- Writes and configured paths remain inside the real repository root.
|
|
146
|
+
- Symlink escapes and explicit targets that match nothing fail.
|
|
147
|
+
- Credentials are redacted in JSON, TOON, text, SARIF, annotations, and
|
|
148
|
+
top-level errors.
|
|
149
|
+
- Finding and change IDs exclude absolute checkout paths.
|
|
150
|
+
- Parser errors remain findings and cannot be baselined away.
|
|
151
|
+
- Organization coverage failures remain visible and make status fail.
|
|
152
|
+
- Organization source reads remain count- and size-bounded and bypass disk
|
|
153
|
+
cache.
|
|
154
|
+
- Organization resume never trusts a stale result without fresh discovery and
|
|
155
|
+
a matching repository identity, default branch, and tree SHA. Failed results
|
|
156
|
+
are never reused.
|
|
157
|
+
- Checkpoints use guarded atomic writes, remain inside the working directory,
|
|
158
|
+
omit tokens and raw YAML, and cannot replace active policy or baseline files.
|
|
159
|
+
- Progress stays outside deterministic report serialization; CLI progress is
|
|
160
|
+
stderr-only and Action progress cannot form workflow commands.
|
|
161
|
+
- Agent mode is an explicit opt-in, never a TTY, parent-process, CI, or
|
|
162
|
+
vendor-environment heuristic. Explicit CLI output and progress options win.
|
|
163
|
+
Its automatic artifact key uses the validated checkpoint identity, and its
|
|
164
|
+
stdout receipt never includes findings or repository result arrays.
|
|
165
|
+
- Attacker-controlled values cannot forge TOON lines or GitHub annotations.
|
|
166
|
+
|
|
167
|
+
Tests should demonstrate the fail-closed behavior for any change touching these
|
|
168
|
+
boundaries.
|
|
169
|
+
|
|
170
|
+
## Adding or changing a rule
|
|
171
|
+
|
|
172
|
+
A rule module exports:
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
export const id = 'example-rule';
|
|
176
|
+
export const severity = 'high';
|
|
177
|
+
export const description = 'Short sentence describing the risk.';
|
|
178
|
+
|
|
179
|
+
export function check(workflow, context = {}) {
|
|
180
|
+
return [{
|
|
181
|
+
id,
|
|
182
|
+
severity,
|
|
183
|
+
line: 1,
|
|
184
|
+
fields: {
|
|
185
|
+
type: id,
|
|
186
|
+
sev: severity,
|
|
187
|
+
evidence: 'structured-value',
|
|
188
|
+
},
|
|
189
|
+
explain: 'one concise remediation',
|
|
190
|
+
}];
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Then:
|
|
195
|
+
|
|
196
|
+
1. register the module in `src/rules/index.js`;
|
|
197
|
+
2. add focused tests for safe and unsafe cases;
|
|
198
|
+
3. test trigger, expression, quoting, and malformed-input boundaries relevant
|
|
199
|
+
to the rule;
|
|
200
|
+
4. when behavior depends on a moving GitHub or first-party action contract,
|
|
201
|
+
verify it against a primary source, record the date/version boundary in code,
|
|
202
|
+
and test the last unsafe plus first safe version;
|
|
203
|
+
5. test at least one legitimate pattern that must remain clean so remediation
|
|
204
|
+
guidance does not become a false-positive generator;
|
|
205
|
+
6. update the rule table and accuracy boundaries in `docs/CONFIGURATION.md`,
|
|
206
|
+
plus the Claude skill;
|
|
207
|
+
7. run `actions-warden rules --format=json` and ensure the ID is not redacted;
|
|
208
|
+
8. rebuild the Action bundle.
|
|
209
|
+
|
|
210
|
+
Keep `fields` structured and concise. They become JSON evidence, TOON fields,
|
|
211
|
+
SARIF messages, and Action annotations. Never place raw secret values in them.
|
|
212
|
+
Suggestions must name a safe end state, preserve legitimate use cases, and
|
|
213
|
+
avoid claiming that one mitigation closes risks outside the rule's evidence.
|
|
214
|
+
|
|
215
|
+
## Adding or changing a command
|
|
216
|
+
|
|
217
|
+
A command normally needs changes in:
|
|
218
|
+
|
|
219
|
+
1. `src/commands/<name>.js` for the API result and renderer;
|
|
220
|
+
2. `src/cli.js` for public CLI flags and exit semantics;
|
|
221
|
+
3. `src/index.js` and `package.json` exports;
|
|
222
|
+
4. `src/action.js` and `action.yml` if the Action exposes it;
|
|
223
|
+
5. command, CLI, Action, annotation, and output-format tests;
|
|
224
|
+
6. `docs/CLI.md`, `docs/OUTPUTS.md`, API docs, and relevant examples;
|
|
225
|
+
7. the Claude skill and version-sync checker when its command surface changes;
|
|
226
|
+
8. the committed Action bundle.
|
|
227
|
+
|
|
228
|
+
Return operational problems as structured `errors` when useful work can
|
|
229
|
+
continue. Throw when top-level input or discovery makes the requested scope
|
|
230
|
+
undefined or unsafe.
|
|
231
|
+
|
|
232
|
+
Every non-JSON renderer must expose enough information to explain a `FAIL`
|
|
233
|
+
status. Every JSON renderer must include `schemaVersion`, command data, and
|
|
234
|
+
top-level `status`.
|
|
235
|
+
|
|
236
|
+
## Parser and patcher changes
|
|
237
|
+
|
|
238
|
+
The parser retains both a normalized model for rules and source locations for
|
|
239
|
+
precise rewrites. When changing it:
|
|
240
|
+
|
|
241
|
+
- cover workflow files and composite `action.yml` metadata;
|
|
242
|
+
- preserve and recursively inspect steps inside `parallel` groups, while
|
|
243
|
+
retaining background/control-step declarations for structure checks;
|
|
244
|
+
- include quoted, unquoted, inline, multiline, and malformed YAML cases;
|
|
245
|
+
- avoid evaluating expressions;
|
|
246
|
+
- preserve source offsets for patchable `uses` values;
|
|
247
|
+
- test Windows and POSIX path normalization where relevant.
|
|
248
|
+
|
|
249
|
+
The patcher should make the smallest source-range replacement. Do not reserialize
|
|
250
|
+
the entire YAML document: that would destroy comments, formatting, anchors, and
|
|
251
|
+
reviewable diffs.
|
|
252
|
+
|
|
253
|
+
Property tests in `test/patcher-properties.test.js` exercise rewrite stability.
|
|
254
|
+
|
|
255
|
+
## GitHub API changes
|
|
256
|
+
|
|
257
|
+
All runtime requests belong in the resolver or organization GitHub layer and
|
|
258
|
+
must remain pinned to `https://api.github.com`.
|
|
259
|
+
|
|
260
|
+
For new endpoints:
|
|
261
|
+
|
|
262
|
+
- validate status and response shape;
|
|
263
|
+
- paginate boundedly;
|
|
264
|
+
- validate identities, SHAs, sizes, encodings, and UTF-8 as applicable;
|
|
265
|
+
- set timeouts and bounded retries through the shared fetch layer;
|
|
266
|
+
- preserve authentication-isolated cache keys;
|
|
267
|
+
- decide explicitly whether private content may be cached;
|
|
268
|
+
- accumulate scoped errors when continued reporting is safe;
|
|
269
|
+
- add tests with mocked responses for truncation and malformed data.
|
|
270
|
+
|
|
271
|
+
Never execute content to determine what it contains.
|
|
272
|
+
|
|
273
|
+
When changing organization resume behavior, cover interrupted writes,
|
|
274
|
+
checkpoint schema and identity mismatches, hostile fields, changed tree SHAs,
|
|
275
|
+
previous repository errors, output equivalence, and concurrent completions.
|
|
276
|
+
Persist each completed result before emitting its completion event so an
|
|
277
|
+
observer failure or process interruption loses at most in-flight work.
|
|
278
|
+
|
|
279
|
+
## Output compatibility
|
|
280
|
+
|
|
281
|
+
JSON is the public machine interface. Raw command results and serialized JSON
|
|
282
|
+
are deliberately different: renderers add repository-relative paths,
|
|
283
|
+
redaction, and `schemaVersion`.
|
|
284
|
+
|
|
285
|
+
When changing output:
|
|
286
|
+
|
|
287
|
+
- retain top-level `status`;
|
|
288
|
+
- treat new fields as additive where possible;
|
|
289
|
+
- update `docs/OUTPUTS.md`;
|
|
290
|
+
- test all four formats;
|
|
291
|
+
- prevent embedded control characters from forging records;
|
|
292
|
+
- ensure error details are visible in every format;
|
|
293
|
+
- verify stable IDs remain stable unless their semantic input changed.
|
|
294
|
+
|
|
295
|
+
## GitHub Action bundle
|
|
296
|
+
|
|
297
|
+
Never edit `dist/index.js` or `dist/package.json` by hand. They are generated:
|
|
298
|
+
|
|
299
|
+
```sh
|
|
300
|
+
npm run build:action
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Commit source and its rebuilt bundle together when the Action runtime changes.
|
|
304
|
+
`npm run check:action-bundle` performs a clean build in a temporary directory
|
|
305
|
+
and compares both working-tree and staged bundle files.
|
|
306
|
+
|
|
307
|
+
A docs-only, test-only, or CLI-only change that is unreachable from
|
|
308
|
+
`src/action.js` does not require a bundle rebuild.
|
|
309
|
+
|
|
310
|
+
## Tests and fixtures
|
|
311
|
+
|
|
312
|
+
Tests use Vitest and should be deterministic and network-independent. Mock
|
|
313
|
+
GitHub responses rather than relying on live repositories. Use temporary
|
|
314
|
+
directories for writes and assert both file content and failure behavior.
|
|
315
|
+
|
|
316
|
+
Useful focused runs:
|
|
317
|
+
|
|
318
|
+
```sh
|
|
319
|
+
npx vitest --run test/audit.test.js
|
|
320
|
+
npx vitest --run test/org-scan.test.js
|
|
321
|
+
npx vitest --run -t "stable id"
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Keep hostile input in fixtures or inline strings when it clarifies the threat
|
|
325
|
+
being tested. Tests should not contain real credentials.
|
|
326
|
+
|
|
327
|
+
## Documentation maintenance
|
|
328
|
+
|
|
329
|
+
README is the landing page, not the full manual. Put durable detail in the
|
|
330
|
+
focused guide that owns it, then link from README.
|
|
331
|
+
|
|
332
|
+
When CLI, Action, config, output, or API behavior changes:
|
|
333
|
+
|
|
334
|
+
1. update the owning reference document;
|
|
335
|
+
2. update examples and AI guidance that depend on it;
|
|
336
|
+
3. run `npm run check:docs`;
|
|
337
|
+
4. run live `--help` and at least one documented example against
|
|
338
|
+
`node src/cli.js`.
|
|
339
|
+
|
|
340
|
+
Keep examples copyable, use exact package versions for `npx`, and use full
|
|
341
|
+
commit placeholders for the Action itself.
|
|
342
|
+
|
|
343
|
+
## Releases
|
|
344
|
+
|
|
345
|
+
The authoritative [release runbook](../RELEASING.md) defines maintainer and
|
|
346
|
+
agent authorization, SemVer selection, live-state preflight, preparation,
|
|
347
|
+
trusted publication, monitoring, verification, and partial-failure recovery.
|
|
348
|
+
Do not bump versions as part of an unrelated contribution. `release:prepare`
|
|
349
|
+
is local-only; `release:check` is intended for a reviewed, staged candidate and
|
|
350
|
+
will fail a stale or already-published version.
|