actions-warden 0.2.0 → 0.4.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.
Files changed (63) hide show
  1. package/AGENTS.md +199 -0
  2. package/CONTRIBUTING.md +109 -0
  3. package/README.md +284 -224
  4. package/RELEASING.md +338 -0
  5. package/SECURITY.md +30 -3
  6. package/docs/AI-AGENTS.md +474 -0
  7. package/docs/CLI.md +474 -0
  8. package/docs/CONFIGURATION.md +340 -0
  9. package/docs/DEVELOPMENT.md +373 -0
  10. package/docs/GITHUB-ACTION.md +285 -0
  11. package/docs/JAVASCRIPT-API.md +357 -0
  12. package/docs/OUTPUTS.md +414 -0
  13. package/docs/README.md +27 -0
  14. package/examples/org-scan.yml +42 -0
  15. package/examples/upgrade-pr.yml +57 -0
  16. package/llms.txt +38 -0
  17. package/package.json +32 -10
  18. package/skills/actions-warden/SKILL.md +151 -40
  19. package/src/action.js +306 -0
  20. package/src/cli.js +494 -56
  21. package/src/commands/audit.js +189 -36
  22. package/src/commands/org-scan.js +544 -0
  23. package/src/commands/pin.js +59 -56
  24. package/src/commands/report.js +122 -10
  25. package/src/commands/upgrade.js +102 -62
  26. package/src/commands/verify.js +193 -0
  27. package/src/index.js +21 -4
  28. package/src/lib/action-status.js +27 -0
  29. package/src/lib/agent-mode.js +174 -0
  30. package/src/lib/annotations.js +250 -0
  31. package/src/lib/baseline.js +103 -0
  32. package/src/lib/cache.js +47 -10
  33. package/src/lib/concurrency.js +27 -0
  34. package/src/lib/config.js +185 -0
  35. package/src/lib/execution.js +71 -0
  36. package/src/lib/formatter.js +127 -8
  37. package/src/lib/github-org.js +374 -0
  38. package/src/lib/identity.js +62 -0
  39. package/src/lib/ignore.js +7 -6
  40. package/src/lib/org-checkpoint.js +461 -0
  41. package/src/lib/org-progress.js +60 -0
  42. package/src/lib/parser.js +326 -52
  43. package/src/lib/patcher.js +199 -0
  44. package/src/lib/path-equality.js +30 -0
  45. package/src/lib/paths.js +35 -12
  46. package/src/lib/redact.js +65 -4
  47. package/src/lib/resolver.js +225 -43
  48. package/src/lib/targets.js +28 -0
  49. package/src/lib/triggers.js +12 -0
  50. package/src/lib/writer.js +48 -8
  51. package/src/rules/excessive-permissions.js +24 -33
  52. package/src/rules/index.js +19 -1
  53. package/src/rules/pull-request-target-checkout.js +149 -18
  54. package/src/rules/reusable-workflow-secrets.js +32 -0
  55. package/src/rules/script-injection.js +77 -12
  56. package/src/rules/secrets-in-env.js +101 -18
  57. package/src/rules/unpinned-action.js +3 -2
  58. package/src/rules/unpinned-container-image.js +39 -0
  59. package/src/rules/unpinned-docker-action.js +30 -0
  60. package/src/rules/untrusted-self-hosted-runner.js +109 -0
  61. package/src/rules/workflow-run-artifact-execution.js +122 -0
  62. package/src/rules/workflow-structure.js +396 -0
  63. 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)