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
8 changes: 1 addition & 7 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,6 @@ All workflows in this repository follow security best practices:
* Secrets inherited explicitly with `secrets: inherit`
* No hardcoded tokens or credentials

### Network Hardening

* `step-security/harden-runner` used in all jobs for egress policy auditing
* Egress policy set to `audit` mode for visibility

## Maintenance

### Updating SHA Pins
Expand Down Expand Up @@ -252,7 +247,7 @@ To add a new workflow to the repository:

1. Create `{tool-name}.yml` following existing patterns
2. Implement 4-channel result publishing (annotations, artifacts, SARIF if security, summaries)
3. Add harden-runner and SHA pinning
3. Use SHA pinning for all actions
4. Use minimal permissions
5. Add soft-fail input support
6. Update `pr-validation.yml` and `main.yml` to include new job
Expand Down Expand Up @@ -487,7 +482,6 @@ permissions:

* All actions MUST be pinned to SHA commits (not tags or branches)
* Include SHA comment showing the tag/version (e.g., `# v4.2.2`)
* Use Harden Runner for audit logging
* Disable credential persistence when checking out code: `persist-credentials: false`

## Troubleshooting
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/dependency-pinning-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,6 @@ jobs:
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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v4.2.2
with:
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/frontmatter-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ jobs:
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.10.2
with:
egress-policy: audit

- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v4.2.2
with:
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/link-lang-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ jobs:
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.10.2
with:
egress-policy: audit

- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v4.2.2
with:
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/markdown-link-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ jobs:
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.10.2
with:
egress-policy: audit

- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v4.2.2
with:
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/markdown-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ jobs:
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.10.2
with:
egress-policy: audit

- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v4.2.2
with:
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,26 @@ jobs:
soft-fail: false
upload-sarif: true
upload-artifact: false

npm-audit:
name: npm Security Audit
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v4.2.2
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@633bb92bc0aabcae06e8ea93b85aecddd374c402 # v4.1.0
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run security audit
run: npm audit --audit-level=moderate
5 changes: 0 additions & 5 deletions .github/workflows/ps-script-analyzer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ jobs:
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.10.2
with:
egress-policy: audit

- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v4.2.2
with:
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/sha-staleness-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,6 @@ jobs:
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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v4.2.2
with:
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/spell-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ jobs:
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.10.2
with:
egress-policy: audit

- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v4.2.2
with:
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/table-format.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ jobs:
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.10.2
with:
egress-policy: audit

- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v4.2.2
with:
Expand Down
86 changes: 86 additions & 0 deletions scripts/lib/Get-VerifiedDownload.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<#
.SYNOPSIS
Downloads and verifies artifacts using SHA256 checksums.
.DESCRIPTION
Securely downloads files from URLs and verifies their integrity using
SHA256 checksums before saving or extracting.
.PARAMETER Url
URL to download from.
.PARAMETER ExpectedSHA256
Expected SHA256 checksum of the file.
.PARAMETER OutputPath
Path where the downloaded file will be saved.
.PARAMETER Extract
Extract the archive after verification.
.PARAMETER ExtractPath
Destination directory for extraction.
.EXAMPLE
Get-VerifiedDownload -Url "https://example.com/tool.tar.gz" -ExpectedSHA256 "abc123..." -OutputPath "./tool.tar.gz"
.EXAMPLE
Get-VerifiedDownload -Url "https://example.com/tool.tar.gz" -ExpectedSHA256 "abc123..." -OutputPath "./tool.tar.gz" -Extract -ExtractPath "./tools"
#>

[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Url,

[Parameter(Mandatory = $true)]
[string]$ExpectedSHA256,

[Parameter(Mandatory = $true)]
[string]$OutputPath,

[Parameter(Mandatory = $false)]
[switch]$Extract,

[Parameter(Mandatory = $false)]
[string]$ExtractPath
)

$ErrorActionPreference = 'Stop'

$tempFile = [System.IO.Path]::GetTempFileName()

try {
Write-Host "Downloading: $Url"
Invoke-WebRequest -Uri $Url -OutFile $tempFile -UseBasicParsing

Write-Host "Verifying SHA256: $ExpectedSHA256"
$actualHash = (Get-FileHash -Path $tempFile -Algorithm SHA256).Hash

if ($actualHash -ne $ExpectedSHA256.ToUpper()) {
Write-Error "Checksum verification failed!`nExpected: $ExpectedSHA256`nActual: $actualHash"
exit 1
}

Comment on lines +61 to +64
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script uses exit 1 which will terminate the entire PowerShell session when run as a script. This is problematic if the script is dot-sourced or used as a function. Consider using throw instead to allow proper error handling by the caller, or use return with a proper error status if this is intended to be a standalone script.

Suggested change
Write-Error "Checksum verification failed!`nExpected: $ExpectedSHA256`nActual: $actualHash"
exit 1
}
throw "Checksum verification failed!`nExpected: $ExpectedSHA256`nActual: $actualHash"
}

Copilot uses AI. Check for mistakes.
if ($Extract -and $ExtractPath) {
Write-Host "Extracting to: $ExtractPath"
if (-not (Test-Path $ExtractPath)) {
New-Item -ItemType Directory -Path $ExtractPath -Force | Out-Null
}
Expand-Archive -Path $tempFile -DestinationPath $ExtractPath -Force
}
Comment on lines +65 to +71
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition if ($Extract -and $ExtractPath) means that extraction will silently fail if -Extract is specified but -ExtractPath is not provided. Consider making ExtractPath mandatory when Extract is used, or provide a better error message, or default to extracting in the same directory as OutputPath.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +71
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expand-Archive is only available for .zip files in PowerShell. This will fail for .tar.gz files (like the gitleaks download mentioned in tool-checksums.json). Consider using platform-appropriate extraction commands (e.g., tar on Linux/macOS) or checking the file extension and choosing the appropriate extraction method.

Suggested change
Expand-Archive -Path $tempFile -DestinationPath $ExtractPath -Force
}
$fileExt = [System.IO.Path]::GetExtension($tempFile).ToLowerInvariant()
$fileName = [System.IO.Path]::GetFileName($tempFile).ToLowerInvariant()
if ($fileExt -eq ".zip") {
Expand-Archive -Path $tempFile -DestinationPath $ExtractPath -Force
}
elseif ($fileName -match "\.tar\.gz$" -or $fileExt -eq ".tgz") {
# Use tar for .tar.gz and .tgz
Write-Host "Extracting tar.gz/tgz archive using tar"
$tarCmd = "tar"
$tarArgs = "xf `"$tempFile`" -C `"$ExtractPath`""
$process = Start-Process -FilePath $tarCmd -ArgumentList $tarArgs -NoNewWindow -Wait -PassThru
if ($process.ExitCode -ne 0) {
Write-Error "Extraction failed for tar.gz/tgz archive."
exit 1
}
}
elseif ($fileExt -eq ".tar") {
# Use tar for .tar
Write-Host "Extracting tar archive using tar"
$tarCmd = "tar"
$tarArgs = "xf `"$tempFile`" -C `"$ExtractPath`""
$process = Start-Process -FilePath $tarCmd -ArgumentList $tarArgs -NoNewWindow -Wait -PassThru
if ($process.ExitCode -ne 0) {
Write-Error "Extraction failed for tar archive."
exit 1
}
}
else {
Write-Error "Unsupported archive format: $fileExt. Only .zip, .tar.gz, .tgz, and .tar are supported."
exit 1
}

Copilot uses AI. Check for mistakes.
else {
$outputDir = Split-Path -Parent $OutputPath
if ($outputDir -and -not (Test-Path $outputDir)) {
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
}
Move-Item -Path $tempFile -Destination $OutputPath -Force
}

Write-Host "Download verified and complete" -ForegroundColor Green
}
finally {
if (Test-Path $tempFile) {
Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue
}
}
69 changes: 68 additions & 1 deletion scripts/security/Test-DependencyPinning.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ $DependencyPatterns = @{
RemediationUrl = 'https://registry.npmjs.org/{0}/{1}'
}

'pip' = @{
'pip' = @{
FilePatterns = @('**/requirements*.txt', '**/Pipfile', '**/pyproject.toml', '**/setup.py')
VersionPatterns = @(
@{
Expand All @@ -155,6 +155,13 @@ $DependencyPatterns = @{
SHAPattern = '^[a-fA-F0-9]{40}$'
RemediationUrl = 'https://pypi.org/pypi/{0}/{1}/json'
}

'shell-downloads' = @{
FilePatterns = @('*.sh')
SearchPaths = @('.devcontainer/scripts', 'scripts')
Comment on lines +160 to +161
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SearchPaths property is specific to shell-downloads but is not used by any other ecosystem in $DependencyPatterns. This inconsistency suggests incomplete implementation. If this is a custom property for the ValidationFunc approach, it should be documented or the design should be revised to match the pattern used by other ecosystems (FilePatterns + VersionPatterns).

Suggested change
FilePatterns = @('*.sh')
SearchPaths = @('.devcontainer/scripts', 'scripts')
FilePatterns = @('**/.devcontainer/scripts/*.sh', '**/scripts/*.sh')

Copilot uses AI. Check for mistakes.
ValidationFunc = 'Test-ShellDownloadSecurity'
Description = 'Shell script downloads must include checksum verification'
}
Comment on lines +159 to +164
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shell-downloads ecosystem is defined with a ValidationFunc property, but this function is never invoked in the main execution flow. The main script only calls Get-DependencyViolation which uses VersionPatterns to scan files. Since shell-downloads doesn't define VersionPatterns, this ecosystem will not be checked. Either integrate Test-ShellDownloadSecurity into the scanning logic or remove this unused configuration.

Suggested change
'shell-downloads' = @{
FilePatterns = @('*.sh')
SearchPaths = @('.devcontainer/scripts', 'scripts')
ValidationFunc = 'Test-ShellDownloadSecurity'
Description = 'Shell script downloads must include checksum verification'
}

Copilot uses AI. Check for mistakes.
}

class DependencyViolation {
Expand Down Expand Up @@ -195,6 +202,66 @@ class ComplianceReport {
}
}

function Test-ShellDownloadSecurity {
<#
.SYNOPSIS
Scans shell scripts for curl/wget downloads lacking checksum verification.

.DESCRIPTION
Analyzes shell scripts to detect download commands (curl/wget) that do not
have corresponding checksum verification (sha256sum/shasum) within the
following lines.

.PARAMETER FilePath
Path to the shell script file to scan.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$FilePath
)

if (-not (Test-Path $FilePath)) {
return @()
}

$lines = Get-Content $FilePath
$violations = @()

# Pattern to match curl/wget download commands
$downloadPattern = '(curl|wget)\s+.*https?://[^\s]+'
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The download pattern '(curl|wget)\s+.*https?://[^\s]+' will match lines with curl or wget commands but may produce false positives. It doesn't account for:

  1. Commented-out commands (lines starting with #)
  2. Commands in strings or heredocs
  3. curl/wget appearing in URLs or variable names

Consider adding a check to exclude commented lines (e.g., if ($line -match '^\s*#')) and potentially using more precise regex patterns.

Copilot uses AI. Check for mistakes.
$checksumPattern = 'sha256sum|shasum'

for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i]
if ($line -match $downloadPattern) {
# Check next 5 lines for checksum verification
$hasChecksum = $false
$searchEnd = [Math]::Min($i + 5, $lines.Count - 1)

for ($j = $i; $j -le $searchEnd; $j++) {
if ($lines[$j] -match $checksumPattern) {
$hasChecksum = $true
break
}
}

if (-not $hasChecksum) {
$violations += [PSCustomObject]@{
File = $FilePath
Line = $i + 1
Pattern = $line.Trim()
Issue = 'Download without checksum verification'
Severity = 'warning'
Ecosystem = 'shell-downloads'
}
}
}
}

return $violations
}

function Write-PinningLog {
param(
[Parameter(Mandatory = $true)]
Expand Down
Loading
Loading