Skip to content

feat(security): OpenSSF Best Practices + Scorecard scaffolding (RAN-55, RAN-60) #3

feat(security): OpenSSF Best Practices + Scorecard scaffolding (RAN-55, RAN-60)

feat(security): OpenSSF Best Practices + Scorecard scaffolding (RAN-55, RAN-60) #3

Workflow file for this run

# Consolidated OSS-CLI security stack — RAN-55.
#
# Mirrors the (B) OSS-CLI stack from the codeiq sibling, language-adapted for
# Vigil's PowerShell + WPF tree:
#
# * Semgrep (SAST) — `p/default` + `p/security-audit` registry packs;
# covers GHA YAML, generic secret-audit, and miscellaneous rules. The
# PowerShell ruleset is small upstream so most signal here will come from
# the workflow / config files in the repo.
# * OSV-Scanner (deps) — Vigil ships no language lockfile, so this scan is
# largely a no-op today. Kept in place so coverage flips on automatically
# if a `*.lock` is ever added (e.g., a Node tooling subpackage).
# * Trivy (filesystem CVEs + IaC misconfig) — language-agnostic; main
# value here is GitHub Actions YAML and any binary asset we ever drop in.
# * Gitleaks (secret scan) — full git history.
# * jscpd (copy-paste detection) — supports PowerShell via tokenization.
# * anchore/sbom-action (SBOM) — Syft → SPDX over the source tree;
# populates the GitHub Dependency Submission API on push to `main`.
#
# Each tool publishes its findings as SARIF to GitHub code scanning where
# supported and uploads the raw report as a workflow artifact regardless,
# so the Security tab plus the artifact tarball are both first-class
# observability surfaces. SARIF upload jobs use `continue-on-error: true`
# during the OSS-CLI bootstrap window because GitHub may already have a
# default-setup CodeQL category bound (same trap that bit codeiq's PR #74).
# Findings still appear in the workflow logs and artifacts; promotion to
# gate-blocking happens once the baseline is clean.
#
# Cron: Mondays 06:00 UTC, same window as `scorecard.yml` so the weekly
# observability sweep runs together. All third-party actions and Docker
# images are pinned by commit SHA / digest-equivalent tag, in line with
# OpenSSF Scorecard `Pinned-Dependencies`.
name: Security supply-chain scan
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
# Restrict default GITHUB_TOKEN to read-only; jobs opt into narrower writes.
permissions: read-all
jobs:
# ------------------------------------------------------------------
# Semgrep — SAST. Runs the official `semgrep` PyPI distribution rather
# than the deprecated `semgrep/semgrep-action` (last release 2023). The
# `p/default` registry pack covers YAML / GHA / generic rules; PowerShell
# coverage is light upstream but the security-audit pack still catches
# secret-shaped strings and dangerous patterns in shell + workflow files.
# ------------------------------------------------------------------
semgrep:
name: Semgrep SAST
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Harden runner egress
# step-security/harden-runner v2.19.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40
with:
egress-policy: audit
- name: Checkout code
# actions/checkout v6.0.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
- name: Set up Python
# actions/setup-python v5
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"
- name: Install Semgrep
run: python -m pip install --no-cache-dir 'semgrep==1.140.0'
- name: Run Semgrep
# `--error` would fail the job; we report-only at bootstrap and
# gate later. SARIF goes to code scanning, JSON to artifact.
run: |
semgrep scan \
--config=p/default \
--config=p/security-audit \
--sarif --output=semgrep.sarif \
--metrics=off || true
- name: Upload Semgrep SARIF (artifact)
# actions/upload-artifact v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
if: always()
with:
name: semgrep-sarif
path: semgrep.sarif
retention-days: 7
- name: Upload Semgrep SARIF to GitHub code-scanning
# github/codeql-action/upload-sarif v3.35.2
uses: github/codeql-action/upload-sarif@ce64ddcb0d8d890d2df4a9d1c04ff297367dea2a
if: always()
continue-on-error: true
with:
sarif_file: semgrep.sarif
category: semgrep
# ------------------------------------------------------------------
# OSV-Scanner — second-source CVE feed. Installed from the official
# GitHub release as a binary; we do NOT use `google/osv-scanner-action`
# because its `action.yml` is composite-only and missing the top-level
# `runs:` block, which causes the action to fail with "Top level 'runs:'
# section is required" when invoked as a job step. Same fix codeiq landed
# on its own RAN-52 follow-up. Using the preinstalled `gh` CLI avoids
# any external `curl`/`wget` per /home/dev/.claude/CLAUDE.md.
#
# Vigil currently has no language lockfile to scan, so the recursive
# source scan exits with no findings today. Coverage flips on
# automatically the moment a `*.lock` lands in-tree.
# ------------------------------------------------------------------
osv-scanner:
name: OSV-Scanner deps
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
env:
OSV_SCANNER_VERSION: 2.3.5
# `gh release download` needs an auth token; the default GITHUB_TOKEN
# (read-only, contents:read) is sufficient for public-asset downloads.
GH_TOKEN: ${{ github.token }}
steps:
- name: Harden runner egress
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
- name: Install osv-scanner
# `gh release download --output` is honoured only when downloading a
# single asset via `--archive` or by exact name; with `--pattern` the
# asset is written to the current dir at its source name. Download
# then move to a stable name. The `--version` smoke step makes any
# future regression surface as a clear error rather than `exit 127`.
run: |
gh release download "v${OSV_SCANNER_VERSION}" \
--repo google/osv-scanner \
--pattern 'osv-scanner_linux_amd64' \
--clobber
mv osv-scanner_linux_amd64 osv-scanner
chmod +x osv-scanner
./osv-scanner --version
- name: Run osv-scanner
# Recursive source scan. Vigil has no language lockfile today, so
# this exits cleanly with no findings; coverage activates the
# moment a `*.lock` is added in-tree. Observation-only at the
# OSS-CLI bootstrap window.
run: |
./osv-scanner \
--recursive \
--skip-git \
--format=sarif \
--output=osv.sarif \
./ || true
- name: Upload OSV SARIF (artifact)
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
if: always()
with:
name: osv-sarif
path: osv.sarif
retention-days: 7
- name: Upload OSV SARIF to GitHub code-scanning
uses: github/codeql-action/upload-sarif@ce64ddcb0d8d890d2df4a9d1c04ff297367dea2a
if: always()
continue-on-error: true
with:
sarif_file: osv.sarif
category: osv-scanner
# ------------------------------------------------------------------
# Trivy — filesystem CVE + IaC misconfig scan. We scan `.` rather
# than a container image because Vigil does not ship images.
# ------------------------------------------------------------------
trivy-fs:
name: Trivy filesystem
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Harden runner egress
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
- name: Run Trivy filesystem scan
# aquasecurity/trivy-action v0.36.0
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25
with:
scan-type: fs
scan-ref: .
format: sarif
output: trivy.sarif
ignore-unfixed: true
severity: CRITICAL,HIGH
exit-code: "0"
env:
# Mirror the public DB; harden-runner audits egress.
TRIVY_DISABLE_VEX_NOTICE: "true"
- name: Upload Trivy SARIF (artifact)
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
if: always()
with:
name: trivy-sarif
path: trivy.sarif
retention-days: 7
- name: Upload Trivy SARIF to GitHub code-scanning
uses: github/codeql-action/upload-sarif@ce64ddcb0d8d890d2df4a9d1c04ff297367dea2a
if: always()
continue-on-error: true
with:
sarif_file: trivy.sarif
category: trivy-fs
# ------------------------------------------------------------------
# Gitleaks — secret scan over full git history. Run via the upstream
# Docker image rather than the GHA wrapper because gitleaks-action
# requires a GITLEAKS_LICENSE for organization-owned repos
# (RandomCodeSpace is an org). Image is pinned by tag; image digest
# pinning is a follow-up once Scorecard `Pinned-Dependencies` flags it.
# ------------------------------------------------------------------
gitleaks:
name: Gitleaks secret scan
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Harden runner egress
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
# Need full history so gitleaks can scan every commit.
fetch-depth: 0
persist-credentials: false
- name: Run Gitleaks (Docker image)
run: |
docker run --rm \
-v "${{ github.workspace }}":/repo \
-w /repo \
zricethezav/gitleaks:v8.21.2 \
detect \
--source=/repo \
--report-format=sarif \
--report-path=/repo/gitleaks.sarif \
--redact \
--no-banner || EXIT=$?
# Exit 1 = leaks found; 2 = error. Surface the result without
# gating CI yet — promote to gate once we have a clean baseline.
echo "gitleaks-exit=${EXIT:-0}" >> "$GITHUB_OUTPUT"
id: gitleaks-run
- name: Upload Gitleaks SARIF (artifact)
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
if: always()
with:
name: gitleaks-sarif
path: gitleaks.sarif
retention-days: 7
# ------------------------------------------------------------------
# jscpd — copy-paste detection across the PowerShell tree. Reports
# are uploaded as artifacts; jscpd does not emit SARIF natively
# so we surface markdown + json in artifacts only.
# ------------------------------------------------------------------
jscpd:
name: jscpd duplication
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Harden runner egress
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
- name: Set up Node
# actions/setup-node v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: "20"
- name: Run jscpd
run: |
npx --yes jscpd@4.0.5 \
--silent \
--reporters json,markdown \
--output reports/jscpd \
--languages 'powershell,yaml,markdown' \
--ignore '**/.git/**,**/.vigil/**,**/node_modules/**,**/dist/**,**/*.min.*' \
. || true
- name: Upload jscpd report (artifact)
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
if: always()
with:
name: jscpd-report
path: reports/jscpd
retention-days: 7
# ------------------------------------------------------------------
# SBOM — Syft via anchore/sbom-action. Generates SPDX JSON for the
# source tree. `dependency-snapshot: true` pushes to the GitHub
# Dependency Submission API on push to `main` only — keeps PR runs
# quiet and lets the scheduled run on `main` populate the graph.
# ------------------------------------------------------------------
sbom:
name: Generate SBOM (Syft)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Harden runner egress
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
- name: Generate SBOM
# anchore/sbom-action v0.24.0
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610
with:
path: .
format: spdx-json
artifact-name: vigil.spdx.json
dependency-snapshot: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}