Commit 594ee84
feat(security): enforce GitHub-hosted Ubuntu runner policy in CI (microsoft#2700)
# Pull Request
## Description
Adds CI enforcement for the GitHub Actions `runs-on` runner policy,
which was previously documented as a MUST rule in
`workflows.instructions.md` but had no validator checking it.
**This PR takes a different approach than the one proposed in the linked
issue.** The issue was filed based on a PR review comment that flagged
`ubuntu-slim` as a non-compliant runner label. During investigation, we
confirmed `ubuntu-slim` is actually a GA (Jan 2026), GitHub-hosted,
lightweight Ubuntu runner — it is compliant with the intended policy,
not a violation. The real violation in that PR was `windows-latest`,
which is genuinely disallowed. Rather than build a validator around the
`ubuntu-slim` premise, this PR:
* Clarifies `workflows.instructions.md` § Runners with an explicit
**allow-list** (`ubuntu-latest`, dated `ubuntu-24.04`/`ubuntu-22.04`
labels and their `-arm` variants, `ubuntu-slim`) and **disallow-list**
(`windows-*`, `macos-*`, `self-hosted`, custom labels), so the rule is
unambiguous about what is and isn't compliant.
* Adds `scripts/security/Test-WorkflowRunner.ps1`, mirroring the
existing `Test-WorkflowPermissions.ps1` validator pattern, to enforce
that allow-list in CI against every job's `runs-on` value (including
list-form and matrix-expression values, which are conservatively treated
as violations since they can't be statically resolved).
* Adds 28 Pester unit/integration tests for the new script.
* Adds a reusable `workflow-runner-scan.yml` (mirroring
`workflow-permissions-scan.yml`) and wires a `workflow-runner-check` job
into `pr-validation.yml`'s required `pr-validation-success` gate.
* Adds `npm run lint:workflow-runner` to `package.json` and the
`validate:local` aggregate.
This closes the broader gap the issue was really pointing at (an
unenforced MUST rule) while fixing the underlying documentation
ambiguity rather than special-casing `ubuntu-slim`.
## Related Issue(s)
Closes microsoft#2696
## Type of Change
**Code & Documentation:**
* [x] Documentation update
**Infrastructure & Configuration:**
* [x] GitHub Actions workflow
* [x] Security configuration
**AI Artifacts:**
* [x] Reviewed contribution with `hve-builder` and addressed all
actionable findings
* [x] Copilot instructions (`.github/instructions/*.instructions.md`)
**Other:**
* [x] Script/automation (`.ps1`, `.sh`, `.py`)
## Sample Prompts (for AI Artifact Contributions)
**User Request:**
"Add a new job to this workflow with `runs-on: windows-latest`."
**Execution Flow:**
A Copilot session authoring/editing any `.github/workflows/*.yml` file
has `workflows.instructions.md` in context (via its `applyTo` glob). It
reads the clarified Runners allow/disallow list, flags `windows-latest`
as non-compliant, and proposes an allow-listed replacement (e.g.,
`ubuntu-latest`). If asked what would catch this in CI, it cites
`scripts/security/Test-WorkflowRunner.ps1`.
**Output Artifacts:**
No files are generated by the instructions themselves; they shape any
workflow YAML a contributor authors. The `Test-WorkflowRunner.ps1`
script produces JSON/SARIF/console reports, e.g.:
```json
{
"violations": [
{
"file": ".github/workflows/example.yml",
"job": "package-installer",
"type": "NonUbuntuRunner",
"value": "windows-latest"
}
]
}
```
**Success Indicators:**
`npm run lint:workflow-runner` exits 0 with no violations; the
`workflow-runner-check` job in `pr-validation.yml` passes and is
included in the required `pr-validation-success` gate.
## Testing
* `npm run test:ps -- -TestPath
"scripts/tests/security/Test-WorkflowRunner.Tests.ps1"` — 28/28 passed.
* Full `scripts/tests/security/` Pester suite — 643/643 passed.
* Full repo Pester suite (`npm run test:ps`) — 4242 passed, 1
pre-existing/unrelated failure (environment-specific
devcontainer-detection test on `main`, reproduced via `git stash`), 7
skipped.
* `npm run lint:workflow-runner` against the real repo — 147/147 jobs
pass.
* `npm run lint:permissions`, `lint:dangerous-workflow`, `lint:pr-gate`,
`lint:yaml`, `lint:json`, `lint:md`, `lint:frontmatter` — all pass.
* Reviewed `.github/instructions/workflows.instructions.md` with
`hve-builder` (review mode): static review found and fixed one High
finding (Enforcement Statement was missing the new script); behavior
gate executed via `hve-builder-tester` (simulation, Medium profile) —
Pass, confirming a Copilot session correctly accepts `ubuntu-slim`,
rejects/corrects `windows-latest`, and cites the enforcement script.
## Checklist
### Required Checks
* [x] Documentation is updated (if applicable)
* [x] Files follow existing naming conventions
* [x] Changes are backwards compatible (if applicable)
* [x] Tests added for new functionality (if applicable)
### AI Artifact Contributions
* [x] Used `hve-builder` review mode to review contribution
* [x] Addressed all actionable findings from the `hve-builder` review
* [x] Verified contribution follows common standards and type-specific
requirements
### Required Local Checks
* [x] Local validation aggregate: `npm run validate:local`
* [ ] Documentation validation (if docs changed): `npm run
validate:docs`
* [x] Spell checking: `npm run spell-check`
* [x] Link validation: `npm run lint:md-links`
## Security Considerations
* [x] This PR does not contain any sensitive or NDA information
* [x] Any new dependencies have been reviewed for security issues
* [x] Security-related scripts follow the principle of least privilege
## Additional Notes
The issue as filed suggested the problem was `ubuntu-slim` being
non-compliant. That premise is incorrect — `ubuntu-slim` is a valid
GitHub-hosted Ubuntu runner. The actual gap (no CI enforcement of the
runner policy at all) is real and is what this PR fixes, alongside
clarifying the docs so this kind of misattribution doesn't recur.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Bill Berry <wberry@microsoft.com>1 parent d1d6f21 commit 594ee84
7 files changed
Lines changed: 1206 additions & 3 deletions
File tree
- .github
- instructions
- workflows
- scripts
- security
- Modules
- tests/security
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
98 | 98 | | |
99 | 99 | | |
100 | 100 | | |
101 | | - | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
102 | 110 | | |
103 | 111 | | |
104 | 112 | | |
| |||
268 | 276 | | |
269 | 277 | | |
270 | 278 | | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
271 | 285 | | |
272 | 286 | | |
273 | 287 | | |
| |||
336 | 350 | | |
337 | 351 | | |
338 | 352 | | |
| 353 | + | |
339 | 354 | | |
340 | 355 | | |
341 | 356 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
420 | 420 | | |
421 | 421 | | |
422 | 422 | | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
423 | 434 | | |
424 | 435 | | |
425 | 436 | | |
| |||
686 | 697 | | |
687 | 698 | | |
688 | 699 | | |
| 700 | + | |
689 | 701 | | |
690 | 702 | | |
691 | 703 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| 28 | + | |
28 | 29 | | |
29 | 30 | | |
30 | 31 | | |
| |||
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
45 | | - | |
| 46 | + | |
46 | 47 | | |
47 | 48 | | |
48 | 49 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| 39 | + | |
| 40 | + | |
39 | 41 | | |
40 | 42 | | |
41 | 43 | | |
| |||
47 | 49 | | |
48 | 50 | | |
49 | 51 | | |
50 | | - | |
| 52 | + | |
51 | 53 | | |
52 | 54 | | |
53 | 55 | | |
| |||
0 commit comments