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."