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.
- package/AGENTS.md +199 -0
- package/CONTRIBUTING.md +109 -0
- package/README.md +284 -224
- package/RELEASING.md +338 -0
- package/SECURITY.md +30 -3
- package/docs/AI-AGENTS.md +474 -0
- package/docs/CLI.md +474 -0
- package/docs/CONFIGURATION.md +340 -0
- package/docs/DEVELOPMENT.md +373 -0
- package/docs/GITHUB-ACTION.md +285 -0
- package/docs/JAVASCRIPT-API.md +357 -0
- package/docs/OUTPUTS.md +414 -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 +151 -40
- package/src/action.js +306 -0
- package/src/cli.js +494 -56
- package/src/commands/audit.js +189 -36
- package/src/commands/org-scan.js +544 -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 +174 -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 +461 -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/path-equality.js +30 -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 +48 -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)
|