-
Notifications
You must be signed in to change notification settings - Fork 47
Description
Summary
Five Token-Permissions violations were discovered across three workflow files (#456, and two companion issues for extension-publish-prerelease.yml and extension-publish.yml). These went undetected because no CI validation enforces the repository convention that every workflow must have a top-level permissions: block and every job must have a job-level permissions: block.
This issue adds automated enforcement to prevent regression after the fixes are applied.
Problem Analysis
Root Cause
The workflow instructions (.github/instructions/hve-core/workflows.instructions.md) describe the desired permissions pattern, but:
- Ambiguous language — "Additional permissions MUST be granted at the job level" reads as "only when extra permissions are needed," not "every job must declare permissions."
- No enforcement script — The Enforcement Statement section lists
Test-DependencyPinning.ps1,Test-SHAStaleness.ps1, andInvoke-YamlLint.ps1, but none validate permissions declarations. - actionlint gap — The existing YAML linter (
Invoke-YamlLint.ps1) validates syntax and some best practices but does not enforce permissions completeness.
Current Enforcement Coverage
| Script | What It Checks | Permissions? |
|---|---|---|
Test-DependencyPinning.ps1 |
SHA pinning of actions | No |
Test-SHAStaleness.ps1 |
Stale SHA detection | No |
Test-ActionVersionConsistency.ps1 |
Version comment consistency | No |
Invoke-YamlLint.ps1 |
YAML syntax via actionlint | No |
Deliverable 1: Test-WorkflowPermissions.ps1
Create scripts/security/Test-WorkflowPermissions.ps1 modeled after Test-ActionVersionConsistency.ps1.
Requirements
- Import
SecurityClasses.psm1(viausing module) andCIHelpers.psm1 - Extend
DependencyViolation.ViolationTypeValidateSet inSecurityClasses.psm1to include'MissingPermissions' - Scan all
.github/workflows/*.ymlfiles - Two checks per file:
- Top-level permissions block — Verify a
permissions:key exists at root level (not indented underjobs:) - Job-level permissions block — For every
jobs.<id>:entry, verify apermissions:key exists as a direct child
- Top-level permissions block — Verify a
- Support
-FailOnMissingswitch for CI enforcement - Support JSON and markdown output formats (matching existing patterns)
- Use
Export-CICDArtifactfromCIHelpers.psm1for artifact upload
Detection Approach
Parse YAML line-by-line (consistent with existing scripts in scripts/security/):
- Top-level permissions: A line matching
^permissions:(no leading whitespace) - Job block start:
^jobs:followed by^ \w+:(2-space indent) - Job-level permissions: Within a job block,
^ permissions:(4-space indent)
Output Format
JSON array of DependencyViolation objects:
[
{
"File": ".github/workflows/example.yml",
"Line": 1,
"ViolationType": "MissingPermissions",
"Severity": "Error",
"Message": "No top-level permissions block defined",
"Metadata": { "scope": "workflow" }
}
]Deliverable 2: workflow-permissions-scan.yml
Create .github/workflows/workflow-permissions-scan.yml as a reusable workflow following the dependency-pinning-scan.yml pattern:
- Trigger:
workflow_call - Permissions:
contents: read - Jobs: checkout → run
Test-WorkflowPermissions.ps1→ upload results artifact
Deliverable 3: AI Instructions Updates
workflows.instructions.md — Permissions Section
Current:
Workflows MUST declare explicit permissions following the principle of least privilege. The default permission set is
contents: read. Additional permissions MUST be granted at the job level and only when required for a specific capability.
Proposed:
Workflows MUST declare explicit permissions following the principle of least privilege. Every workflow MUST have a top-level
permissions:block that sets a restrictive default. Every job MUST have a job-levelpermissions:block — even when the job only needscontents: reador no permissions at all (permissions: {}). The default permission set iscontents: read. Permissions beyondcontents: readMUST be granted at the job level only when required for a specific capability.
workflows.instructions.md — Enforcement Statement
Add to the existing enforcement tool list:
- `scripts/security/Test-WorkflowPermissions.ps1` — Validates every workflow has a top-level permissions block and every job has a job-level permissions blockDeliverable 4: npm Script and CI Pipeline Integration
package.json
Add:
"lint:permissions": "pwsh -NoProfile -NonInteractive -Command \"& { . ./scripts/security/Test-WorkflowPermissions.ps1 -FailOnMissing }\""Add lint:permissions to the lint:all chain.
pr-validation.yml
Add a new job calling workflow-permissions-scan.yml, following the existing reusable workflow invocation pattern used by dependency-pinning-scan.yml.
Implementation Order
1. SecurityClasses.psm1 — Extend ViolationType ValidateSet
2. Test-WorkflowPermissions.ps1 — New validation script
3. workflow-permissions-scan.yml — New reusable workflow
4. pr-validation.yml — Add permissions validation job
5. package.json — Add lint:permissions, update lint:all
6. workflows.instructions.md — Strengthen permissions rules, add enforcement entry
Verification
- Run
npm run lint:permissionslocally — should pass with zero violations after the fix issues are resolved. - Introduce a test violation (remove a job-level permissions block) and confirm the script detects it.
- PR validation pipeline includes the new check and blocks on violations.
References
.github/instructions/hve-core/workflows.instructions.md— Current workflow conventionsscripts/security/Test-ActionVersionConsistency.ps1— Template script patternscripts/security/Modules/SecurityClasses.psm1— Shared violation classscripts/lib/Modules/CIHelpers.psm1— CI artifact export helpers.github/workflows/dependency-pinning-scan.yml— Template reusable workflow pattern.github/workflows/pr-validation.yml— CI orchestration target