From 4ce7ae5d29e22175a9cb7c446363735513f76176 Mon Sep 17 00:00:00 2001 From: DTTerastar Date: Sat, 26 Sep 2026 21:59:27 -0400 Subject: [PATCH] Add security workflow Copy security.yml v1 from quantcli/common, per the propagation policy in its CONTRIBUTING.md: govulncheck, osv-scanner and the license allowlist. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Kaq7vVcd9yGgFvWPiq1icY --- .github/workflows/security.yml | 164 +++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 .github/workflows/security.yml diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..aded262 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,164 @@ +# security.yml v1 — source of truth: quantcli/common; sync changes to every *-export-cli. +# Bump the version when this workflow changes materially; a future drift-check job will key off it. +# See quantcli/common CONTRIBUTING.md "Supply-chain and security" for the propagation policy and +# the >5-repos switchover trigger to a reusable workflow_call. + +name: security + +# Supply-chain and license-policy gate for quantcli repos. +# Source of truth lives in quantcli/common; export-clis carry a copy of this file. +# When updating, propagate the change to every *-export-cli repo (see CONTRIBUTING.md). + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +# Default-deny at workflow level; each job re-grants only what it needs. +permissions: {} + +jobs: + govulncheck: + name: govulncheck (Go vuln DB) + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + + - name: Detect Go module + id: detect + run: | + if [ -f go.mod ]; then + echo "has_go=true" >> "$GITHUB_OUTPUT" + else + echo "has_go=false" >> "$GITHUB_OUTPUT" + echo "::notice::No go.mod present; skipping govulncheck." + fi + + - name: Set up Go + if: steps.detect.outputs.has_go == 'true' + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version-file: go.mod + cache: true + + - name: Install govulncheck + if: steps.detect.outputs.has_go == 'true' + run: go install golang.org/x/vuln/cmd/govulncheck@latest + + - name: Run govulncheck + if: steps.detect.outputs.has_go == 'true' + run: govulncheck ./... + + osv-scanner: + name: osv-scanner (transitive vulns) + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + + - name: Detect Go module + id: detect + run: | + # osv-scanner exits non-zero with "No package sources found" if there is + # nothing to scan. The quantcli org is Go-only today, so gate on go.mod. + # If a non-Go manifest (package.json, requirements.txt, Cargo.toml, …) is + # ever introduced, broaden this check. + if [ -f go.mod ]; then + echo "has_manifests=true" >> "$GITHUB_OUTPUT" + else + echo "has_manifests=false" >> "$GITHUB_OUTPUT" + echo "::notice::No go.mod found; skipping osv-scanner." + fi + + - name: Set up Go + if: steps.detect.outputs.has_manifests == 'true' + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: stable + cache: false + + - name: Install osv-scanner + if: steps.detect.outputs.has_manifests == 'true' + run: go install github.com/google/osv-scanner/v2/cmd/osv-scanner@latest + + - name: Run osv-scanner + if: steps.detect.outputs.has_manifests == 'true' + run: osv-scanner scan source --recursive . + + license-policy: + name: license policy (allowlist) + runs-on: ubuntu-latest + permissions: + contents: read + env: + # Policy: every direct + transitive Go dep must resolve to one of these SPDX ids. + # Keep this list sorted by SPDX id; SECURITY.md "License allowlist" must match exactly. + # See SECURITY.md "Supply-chain policy" for the rationale. + ALLOWED_LICENSES: "0BSD,Apache-2.0,BSD-2-Clause,BSD-3-Clause,BSL-1.0,CC0-1.0,ISC,MIT,MPL-2.0,Unlicense" + steps: + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + + - name: Detect Go module + id: detect + run: | + if [ -f go.mod ]; then + echo "has_go=true" >> "$GITHUB_OUTPUT" + else + echo "has_go=false" >> "$GITHUB_OUTPUT" + echo "::notice::No go.mod present; skipping license-policy check." + fi + + - name: Set up Go + if: steps.detect.outputs.has_go == 'true' + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version-file: go.mod + cache: true + + - name: Install go-licenses + if: steps.detect.outputs.has_go == 'true' + run: go install github.com/google/go-licenses@latest + + - name: Check licenses against allowlist + if: steps.detect.outputs.has_go == 'true' + shell: bash + run: | + set -euo pipefail + report=$(mktemp) + # go-licenses csv emits: module,license-url,license-id (one per line). + # Warnings (e.g. license-url not found) go to stderr and are non-fatal here. + go-licenses csv ./... > "$report" + + IFS=',' read -ra ALLOWED <<< "$ALLOWED_LICENSES" + is_allowed() { + local needle="$1" + for a in "${ALLOWED[@]}"; do + if [ "$needle" = "$a" ]; then return 0; fi + done + return 1 + } + + bad=0 + while IFS=',' read -r module url license; do + [ -z "$module" ] && continue + if ! is_allowed "$license"; then + echo "::error::Disallowed license: module=$module license=$license" + bad=1 + fi + done < "$report" + + if [ "$bad" -ne 0 ]; then + echo "::error::License policy violated. Allowlist: ${ALLOWED_LICENSES}" + echo "::error::See SECURITY.md for the policy and how to request an exception." + exit 1 + fi + + echo "All dependency licenses are within policy."