Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions .github/instructions/workflows.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ Local reusable workflows referenced via relative paths are excluded from SHA pin

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.

Every job MUST declare its own `permissions:` block whenever the workflow-level block grants any scope. A job with no block silently inherits the workflow grant, which is neither explicit nor auditable. The one exception is a workflow-level `permissions: {}`, which grants nothing, so a job beneath it that declares no block inherits an empty set and holds no scope.

An empty workflow-level block is a default, not a ceiling. A job that does declare its own block still receives what it declares, because job-level permissions replace the workflow-level set rather than being capped by it.

**Required pattern:**

```yaml
Expand All @@ -58,7 +62,16 @@ jobs:
echo "Running validation steps"
```

**Enforcement:** Violations are detected by `scripts/security/Test-WorkflowPermissions.ps1`. CI will fail on workflows missing a top-level `permissions:` block. The `copilot-setup-steps.yml` workflow is excluded by default.
**Enforcement:** Violations are detected by `scripts/security/Test-WorkflowPermissions.ps1`, which classifies each workflow and then each of its jobs:

| Workflow-level block | Job-level block | Effective scopes | Verdict |
|----------------------|-----------------|------------------------------------|-----------|
| absent | absent | repository or organization default | violation |
| `permissions: {}` | absent | none | pass |
| populated | absent | inherits the workflow grant | violation |
| any | present | job-declared | pass |

CI will fail on either a missing top-level block or a job that omits its own block under a populated workflow-level block. The `copilot-setup-steps.yml` workflow is excluded by default.

## Credentials and Secrets

Expand Down Expand Up @@ -252,7 +265,7 @@ All workflows MUST pass the following validation checks:
### Workflow Permissions Validation

* **Script:** `scripts/security/Test-WorkflowPermissions.ps1`
* **What it enforces:** All workflows declare a top-level `permissions:` block
* **What it enforces:** Every workflow declares a top-level `permissions:` block, and every job declares its own block unless the workflow-level block is empty
* **CI blocking:** Failures block CI when configured to enforce compliance

## Security Requirements
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/beval.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30

permissions:
contents: read

env:
AGENT_REPO_ROOT: ${{ github.workspace }}

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/workflow-permissions-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ jobs:
with:
persist-credentials: false

- name: Setup PowerShell modules
uses: ./.github/actions/setup-ps-modules

- name: Run Workflow Permissions Validation
id: permissions
shell: pwsh
Expand Down
4 changes: 2 additions & 2 deletions docs/security/security-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Security Assurance Case and Security Model
description: Comprehensive security model and security assurance documentation demonstrating enterprise security practices
sidebar_position: 2
author: Microsoft
ms.date: 2026-07-03
ms.date: 2026-07-28
ms.topic: reference
keywords:
- security
Expand Down Expand Up @@ -338,7 +338,7 @@ Affected workflow jobs:

Defense-in-depth controls:

* All workflows declare job-level permissions, not workflow-level
* Workflows declare a top-level `permissions:` block, and every job under a populated block declares its own permissions rather than inheriting implicitly; `Test-WorkflowPermissions.ps1` enforces both
* `persist-credentials: false` set on all checkout steps
* Inline YAML comments document each `security-events: write` declaration
* SARIF upload is the only write operation performed under this permission
Expand Down
3 changes: 2 additions & 1 deletion scripts/security/Modules/SecurityClasses.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class DependencyViolation {
- VersionMismatch: Version comment does not match the resolved pinned reference
- MissingVersionComment: Dependency is pinned but lacks a human-readable version comment
- MissingPermissions: Workflow file lacks required permissions declarations
- MissingJobPermissions: Workflow job lacks its own permissions declaration and inherits an implicit grant
- Empty string: Default or unclassified violation
#>

Expand All @@ -46,7 +47,7 @@ class DependencyViolation {
[string]$CurrentRef
[ValidateSet('High', 'Medium', 'Low', 'Info')]
[string]$Severity
[ValidateSet('Unpinned', 'Stale', 'VersionMismatch', 'MissingVersionComment', 'MissingPermissions', '')]
[ValidateSet('Unpinned', 'Stale', 'VersionMismatch', 'MissingVersionComment', 'MissingPermissions', 'MissingJobPermissions', '')]
[string]$ViolationType
[string]$Description
[string]$Remediation
Expand Down
29 changes: 24 additions & 5 deletions scripts/security/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Security Scripts
description: PowerShell scripts for dependency pinning validation, SHA staleness monitoring, supply chain security, and centralized PS module installation
author: HVE Core Team
ms.date: 2026-06-26
ms.date: 2026-07-28
ms.topic: reference
keywords:
- powershell
Expand Down Expand Up @@ -230,15 +230,34 @@ reach production.

### `Test-WorkflowPermissions.ps1`

Validates that GitHub Actions workflow files include a top-level `permissions` block.
Validates that GitHub Actions workflow files declare permissions at the workflow
and job level.

Purpose: Ensure workflows explicitly declare token permissions to prevent
OpenSSF Scorecard Token-Permissions failures.
OpenSSF Scorecard Token-Permissions failures, and ensure each job declares the
scope it holds rather than inheriting the workflow grant implicitly.

#### Classification

| Workflow-level block | Job-level block | Effective scopes | Verdict |
|----------------------|-----------------|------------------------------------|-----------|
| absent | absent | repository or organization default | violation |
| `permissions: {}` | absent | none | pass |
| populated | absent | inherits the workflow grant | violation |
| any | present | job-declared | pass |

A job beneath an empty workflow-level block that declares no block of its own
inherits an empty set and therefore holds no scope, so that case passes. An empty
workflow-level block is a default, not a ceiling: a job that does declare its own
block still receives what it declares, because job-level permissions replace the
workflow-level set rather than being capped by it.

#### Features

* Scans `.github/workflows/*.yml` and `.yaml` files
* Uses regex-based detection (`^permissions:`) with zero false positives
* Parses each workflow with `ConvertFrom-Yaml`, so job indentation and the
`permissions` value shape do not affect detection
* Reports workflow-level and job-level compliance as separate metrics
* Outputs results in JSON, SARIF, or console format
* Configurable workflow exclusions
* Integrates with `npm run lint:permissions`
Expand All @@ -248,7 +267,7 @@ OpenSSF Scorecard Token-Permissions failures.
* `-Path` - Directory containing workflow YAML files (default: `.github/workflows`)
* `-Format` - Output format: `json`, `sarif`, or `console` (default: `json`)
* `-OutputPath` - Path for result output file (default: `logs/workflow-permissions-results.json`)
* `-FailOnViolation` (switch) - Exit with non-zero code if any workflow is missing permissions
* `-FailOnViolation` (switch) - Exit with non-zero code if any workflow or job is missing permissions
* `-ExcludePaths` - Workflow filenames to exclude (default: `copilot-setup-steps.yml`)

#### Usage
Expand Down
Loading
Loading