Skip to content

feat(ci): add workflow permissions validation to prevent OpenSSF Scorecard Token-Permissions regression #528

@WilliamBerryiii

Description

@WilliamBerryiii

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:

  1. 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."
  2. No enforcement script — The Enforcement Statement section lists Test-DependencyPinning.ps1, Test-SHAStaleness.ps1, and Invoke-YamlLint.ps1, but none validate permissions declarations.
  3. 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 (via using module) and CIHelpers.psm1
  • Extend DependencyViolation.ViolationType ValidateSet in SecurityClasses.psm1 to include 'MissingPermissions'
  • Scan all .github/workflows/*.yml files
  • Two checks per file:
    1. Top-level permissions block — Verify a permissions: key exists at root level (not indented under jobs:)
    2. Job-level permissions block — For every jobs.<id>: entry, verify a permissions: key exists as a direct child
  • Support -FailOnMissing switch for CI enforcement
  • Support JSON and markdown output formats (matching existing patterns)
  • Use Export-CICDArtifact from CIHelpers.psm1 for 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-level permissions: block — even when the job only needs contents: read or no permissions at all (permissions: {}). The default permission set is contents: read. Permissions beyond contents: read MUST 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 block

Deliverable 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

  1. Run npm run lint:permissions locally — should pass with zero violations after the fix issues are resolved.
  2. Introduce a test violation (remove a job-level permissions block) and confirm the script detects it.
  3. PR validation pipeline includes the new check and blocks on violations.

References

  • .github/instructions/hve-core/workflows.instructions.md — Current workflow conventions
  • scripts/security/Test-ActionVersionConsistency.ps1 — Template script pattern
  • scripts/security/Modules/SecurityClasses.psm1 — Shared violation class
  • scripts/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

How to Build This

This is a multi-artifact implementation task using the task-implementor workflow.

Workflow: /task-research/task-plan/task-implement/task-review

Tip

Between each phase, type /clear or start a new chat to reset context.

Phase 1: Research

Source Material

  • This issue body
  • #file:scripts/security/Test-DependencyPinning.ps1 (pattern reference for PowerShell validation scripts)
  • #file:scripts/security/Test-ActionVersionPinning.ps1 (pattern reference)
  • #file:.github/workflows/main.yml (target workflow with permissions blocks)
  • #file:.github/workflows/pr-validation.yml (target workflow)
  • #file:package.json (existing npm scripts)
  • #file:PSScriptAnalyzer.psd1 (PowerShell linting rules)

Steps

  1. Type /clear to start a fresh context.
  2. Attach or open the files listed above.
  3. Copy and run this prompt:
/task-research topic="workflow permissions validation script design"

Research how to create a PowerShell script (Test-WorkflowPermissions.ps1) that validates
GitHub Actions workflow permission blocks. Investigate:

1. How existing Test-DependencyPinning.ps1 and Test-ActionVersionPinning.ps1 scripts are structured
   (parameter blocks, Pester integration, SARIF output, npm script wiring)
2. The permissions model in GitHub Actions YAML (top-level vs job-level, valid permission names,
   read/write/none values)
3. Which of the 25 workflow files in .github/workflows/ have permissions blocks and what patterns
   they follow
4. How to detect missing permissions blocks, overly broad permissions, and write permissions
   that should be read-only
5. SARIF output format for reporting findings to GitHub Security tab
6. How to wire the new script as an npm command in package.json following existing patterns

Output: Research document at .copilot-tracking/research/{{YYYY-MM-DD}}-workflow-permissions-research.md

Phase 2: Plan

Source Material

  • Research document from Phase 1

Steps

  1. Type /clear to start a fresh context.
  2. Open the research document from Phase 1.
  3. Copy and run this prompt:
/task-plan

Create an implementation plan for the workflow permissions validation feature
covering Test-WorkflowPermissions.ps1, npm script wiring, and CI integration.
The plan should address the PowerShell script structure, SARIF output, Pester tests,
and the npm run command registration following patterns from existing security scripts.

Output: Plan at .copilot-tracking/plans/ and details at .copilot-tracking/details/

Phase 3: Implement

Source Material

  • Plan from Phase 2

Steps

  1. Type /clear to start a fresh context.
  2. Open the plan document from Phase 2.
  3. Copy and run this prompt:
/task-implement

Implement the workflow permissions validation plan. Create Test-WorkflowPermissions.ps1
following the patterns established by Test-DependencyPinning.ps1, add the npm script
to package.json, and integrate with CI workflows.

Output: New and modified files, changes log at .copilot-tracking/changes/

Phase 4: Review

Source Material

  • Plan from Phase 2
  • Changes log from Phase 3

Steps

  1. Type /clear to start a fresh context.
  2. Open the plan and changes log.
  3. Copy and run this prompt:
/task-review

Review the workflow permissions validation implementation. Run these validation commands:
- npm run lint:ps (PowerShell linting)
- npm run test:ps (Pester tests)
- npm run lint:yaml (YAML validation for any modified workflows)
Verify the script follows PSScriptAnalyzer rules and matches existing security script patterns.

Output: Review log at .copilot-tracking/reviews/

After Review

  • Pass: All criteria met. Create a PR referencing this issue.
  • Iterate: Review found issues. Run /clear, return to Phase 3 with the review feedback.
  • Escalate: Fundamental design issue discovered. Run /clear, return to Phase 1 to research the gap.

Authoring Standards

  • PowerShell scripts follow PSScriptAnalyzer rules from PSScriptAnalyzer.psd1
  • Include comment-based help blocks
  • Use CIHelpers module patterns from existing scripts
  • SARIF output matches the schema used by Test-DependencyPinning.ps1
  • npm scripts follow the lint: prefix naming convention

Success Criteria

  • Test-WorkflowPermissions.ps1 validates permissions blocks across all workflow files
  • SARIF output uploads to GitHub Security tab
  • npm script lint:permissions registered in package.json
  • Pester tests cover key validation scenarios
  • npm run lint:ps passes
  • npm run test:ps passes

Metadata

Metadata

Assignees

No one assigned

    Labels

    ciContinuous integrationgithub-actionsGitHub Actions workflowsinstruction-fileCopilot instruction files (.instructions.md)ossf-complianceOpenSSF security compliancescriptsPowerShell, Bash, or Python scriptssecuritySecurity-related changes or concerns

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions