Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
494 changes: 373 additions & 121 deletions .github/workflows/README.md

Large diffs are not rendered by default.

57 changes: 57 additions & 0 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: CodeQL Security Analysis

on:
push:
branches: [ main, develop, feature/* ]
pull_request:
branches: [ main, develop ]
schedule:
- cron: '0 4 * * 0' # Sundays at 4 AM UTC
workflow_call:

permissions:
contents: read
security-events: write

jobs:
analyze:
name: CodeQL Analysis
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
actions: read

strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]

steps:
- name: Checkout repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v4.2.2
with:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@95b1867cf797beb28ce725a6f25268e2d3304672 # v3.27.0
with:
languages: ${{ matrix.language }}
queries: security-extended,security-and-quality

- name: Autobuild
uses: github/codeql-action/autobuild@95b1867cf797beb28ce725a6f25268e2d3304672 # v3.27.0

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@95b1867cf797beb28ce725a6f25268e2d3304672 # v3.27.0
with:
category: "/language:${{ matrix.language }}"

- name: Add job summary
if: always()
run: |
echo "## CodeQL Security Analysis Complete" >> $GITHUB_STEP_SUMMARY
echo "**Language:** ${{ matrix.language }}" >> $GITHUB_STEP_SUMMARY
echo "**Queries:** security-extended, security-and-quality" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "📊 View results in the Security tab under Code Scanning" >> $GITHUB_STEP_SUMMARY
172 changes: 172 additions & 0 deletions .github/workflows/dependency-pinning-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
name: Dependency Pinning Scan

on:
workflow_call:
inputs:
threshold:
description: 'Compliance threshold percentage (0-100)'
required: false
type: number
default: 95
dependency-types:
description: 'Comma-separated list of dependency types to check'
required: false
type: string
default: 'actions,containers'
soft-fail:
description: 'Whether to continue on compliance violations'
required: false
type: boolean
default: false
upload-sarif:
description: 'Whether to upload SARIF results to Security tab'
required: false
type: boolean
default: false
upload-artifact:
description: 'Whether to upload results as artifact'
required: false
type: boolean
default: true
outputs:
compliance-score:
description: 'Compliance score percentage'
value: ${{ jobs.scan.outputs.compliance-score }}
unpinned-count:
description: 'Number of unpinned dependencies found'
value: ${{ jobs.scan.outputs.unpinned-count }}
is-compliant:
description: 'Whether repository meets compliance threshold'
value: ${{ jobs.scan.outputs.is-compliant }}

permissions:
contents: read
security-events: write

jobs:
scan:
name: Validate SHA Pinning Compliance
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
outputs:
compliance-score: ${{ steps.pinning.outputs.compliance-score }}
unpinned-count: ${{ steps.pinning.outputs.unpinned-count }}
is-compliant: ${{ steps.pinning.outputs.is-compliant }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@92c522aaa6f53af082553dedc1596c80b71aba33 # v2.10.2
with:
egress-policy: audit

- name: Checkout code
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v4.2.2
with:
persist-credentials: false

- name: Run SHA Pinning Validation
id: pinning
shell: pwsh
run: |
Write-Host "Validating dependency SHA pinning compliance..."

# Build parameter list
$params = @{
Path = '.'
Format = 'json'
OutputPath = 'logs/dependency-pinning-results.json'
}

if ('${{ inputs.dependency-types }}') {
$params['DependencyTypes'] = '${{ inputs.dependency-types }}'
}

if ('${{ inputs.threshold }}') {
$params['Threshold'] = [int]'${{ inputs.threshold }}'
}

# Run validation script
& scripts/security/Test-DependencyPinning.ps1 @params

# Extract metrics from report
$report = Get-Content logs/dependency-pinning-results.json | ConvertFrom-Json
$complianceScore = $report.ComplianceScore
$unpinnedCount = $report.UnpinnedDependencies
$threshold = [int]'${{ inputs.threshold }}'
$isCompliant = $complianceScore -ge $threshold

"compliance-score=$complianceScore" >> $env:GITHUB_OUTPUT
"unpinned-count=$unpinnedCount" >> $env:GITHUB_OUTPUT
"is-compliant=$($isCompliant.ToString().ToLower())" >> $env:GITHUB_OUTPUT

Write-Host "Compliance Score: $complianceScore%"
Write-Host "Unpinned Dependencies: $unpinnedCount"
Write-Host "Is Compliant (>=$threshold%): $isCompliant"

# Fire GitHub Actions warnings for each violation
if ($unpinnedCount -gt 0) {
foreach ($violation in $report.Violations) {
Write-Output "::warning file=$($violation.File),line=$($violation.Line)::Unpinned $($violation.Type) dependency: $($violation.Name)@$($violation.Version) (Severity: $($violation.Severity))"
}
}

- name: Upload SARIF to Security tab
if: inputs.upload-sarif && always()
uses: github/codeql-action/upload-sarif@95b1867cf797beb28ce725a6f25268e2d3304672 # v3.27.0
with:
sarif_file: logs/dependency-pinning-results.sarif
category: dependency-pinning
continue-on-error: true

- name: Upload validation report
if: inputs.upload-artifact && always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v4.4.3
with:
name: dependency-pinning-results
path: logs/dependency-pinning-results.json
retention-days: 90

- name: Add job summary
if: always()
shell: pwsh
run: |
$complianceScore = '${{ steps.pinning.outputs.compliance-score }}'
$unpinnedCount = '${{ steps.pinning.outputs.unpinned-count }}'
$isCompliant = '${{ steps.pinning.outputs.is-compliant }}'

@"
## Dependency Pinning Scan Results

| Metric | Value |
|--------|-------|
| Compliance Score | $complianceScore% |
| Unpinned Dependencies | $unpinnedCount |
| Status | $(if ($isCompliant -eq 'true') { '✅ Compliant' } else { '⚠️ Non-Compliant' }) |

$(if ($unpinnedCount -ne '0') {
@"

### ⚠️ Action Required

**$unpinnedCount dependencies are not SHA-pinned.**

Review the warnings in the workflow log and pin dependencies to specific SHA commits.

"@
} else {
@"

### ✅ All Dependencies Pinned

All dependencies are properly SHA-pinned.

"@
})
"@ | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding UTF8

- name: Fail job if non-compliant
if: steps.pinning.outputs.is-compliant == 'false' && !inputs.soft-fail
run: |
echo "Dependency pinning scan failed - compliance threshold not met"
exit 1
30 changes: 30 additions & 0 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Dependency Review

on:
pull_request:
branches: [ main, develop ]
workflow_call:

permissions:
contents: read
pull-requests: write

jobs:
dependency-review:
name: Review Dependencies
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write

steps:
- name: Checkout code
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v4.2.2
with:
persist-credentials: false

- name: Dependency Review
uses: actions/dependency-review-action@9129d7d40b8c12c1ed0f60400d00c92d437adcce # v4.3.4
with:
fail-on-severity: moderate
comment-summary-in-pr: always
29 changes: 29 additions & 0 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Security Scan

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

permissions:
contents: read
security-events: write
pull-requests: write

jobs:
codeql:
name: CodeQL Security Analysis
uses: ./.github/workflows/codeql-analysis.yml
permissions:
contents: read
security-events: write
actions: read

dependency-review:
name: Dependency Review
if: github.event_name == 'pull_request'
uses: ./.github/workflows/dependency-review.yml
permissions:
contents: read
pull-requests: write
102 changes: 102 additions & 0 deletions .github/workflows/sha-staleness-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: SHA Staleness Check

on:
workflow_dispatch:
inputs:
max-age-days:
description: 'Maximum SHA age in days before considered stale'
required: false
type: number
default: 30
workflow_call:
inputs:
max-age-days:
description: 'Maximum SHA age in days before considered stale'
required: false
type: number
default: 30
outputs:
stale-count:
description: 'Number of stale SHA pins found'
value: ${{ jobs.check-staleness.outputs.stale-count }}
has-stale:
description: 'Whether any stale SHA pins were found'
value: ${{ jobs.check-staleness.outputs.has-stale }}

permissions:
contents: read

jobs:
check-staleness:
name: Check Action SHA Staleness
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
stale-count: ${{ steps.staleness.outputs.stale-count }}
has-stale: ${{ steps.staleness.outputs.has-stale }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@92c522aaa6f53af082553dedc1596c80b71aba33 # v2.10.2
with:
egress-policy: audit

- name: Checkout code
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v4.2.2
with:
persist-credentials: false

- name: Run SHA Staleness Check
id: staleness
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$thresholdDays = if ('${{ inputs.max-age-days }}') {
[int]'${{ inputs.max-age-days }}'
} else {
30
}

Write-Host "Running SHA staleness check with $thresholdDays day threshold..."

# Run staleness check and capture output
& scripts/security/Test-SHAStaleness.ps1 `
-MaxAge $thresholdDays `
-OutputFormat github `
-OutputPath logs/sha-staleness-results.json

# Extract stale count from JSON report
if (Test-Path logs/sha-staleness-results.json) {
$report = Get-Content logs/sha-staleness-results.json | ConvertFrom-Json
$staleCount = ($report.Dependencies | Where-Object { $_.DaysOld -gt $thresholdDays } | Measure-Object).Count
"stale-count=$staleCount" >> $env:GITHUB_OUTPUT
"has-stale=$(if ($staleCount -gt 0) { 'true' } else { 'false' })" >> $env:GITHUB_OUTPUT

Write-Host "Stale Dependencies Found: $staleCount"

# Fire warnings for each stale dependency
foreach ($staleDep in $report.Dependencies) {
if ($staleDep.DaysOld -gt $thresholdDays) {
Write-Output "::warning file=$($staleDep.File)::Stale SHA detected: $($staleDep.Action)@$($staleDep.CurrentVersion) is $($staleDep.DaysOld) days old (latest: $($staleDep.LatestVersion))"
}
}
} else {
"stale-count=0" >> $env:GITHUB_OUTPUT
"has-stale=false" >> $env:GITHUB_OUTPUT
}

if ($LASTEXITCODE -ne 0) {
Write-Warning "Some GitHub Actions have stale SHA pins. Review the warnings above."
Write-Host "::warning::GitHub Actions with stale SHA pins detected. This is informational only - no action required immediately."
} else {
Write-Host "All GitHub Actions SHA pins are up to date."
}

- name: Upload staleness report
if: always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v4.4.3
with:
name: sha-staleness-results
path: logs/sha-staleness-results.json
retention-days: 90
Loading