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

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

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions