From 33c1a4095f459923d9824ba399502cc163810c75 Mon Sep 17 00:00:00 2001 From: Nathan Gillett Date: Thu, 21 May 2026 21:38:34 -0500 Subject: [PATCH 1/7] Add dependency vulnerability scan CI gates Signed-off-by: Nathan Gillett --- .github/dependabot.yml | 23 +++++++++ .github/deps-allowlist.yml | 6 +++ .github/workflows/deps-scan.yml | 55 +++++++++++++++++++++ .osv-scanner.toml | 4 ++ scripts/check-deps-allowlist.sh | 86 +++++++++++++++++++++++++++++++++ 5 files changed, 174 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/deps-allowlist.yml create mode 100644 .github/workflows/deps-scan.yml create mode 100644 .osv-scanner.toml create mode 100755 scripts/check-deps-allowlist.sh diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1c51ab3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,23 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + groups: + patch-updates: + update-types: + - patch + minor-major-updates: + update-types: + - minor + - major + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + github-actions: + patterns: + - "*" diff --git a/.github/deps-allowlist.yml b/.github/deps-allowlist.yml new file mode 100644 index 0000000..ab83a63 --- /dev/null +++ b/.github/deps-allowlist.yml @@ -0,0 +1,6 @@ +# Dependency vulnerability allowlist with expiry model. +# HIGH-severity exceptions require security on-call approval. +# Mirror approved entries into .osv-scanner.toml [[IgnoredVulns]] with +# matching ignoreUntil dates. Expired entries fail CI via +# scripts/check-deps-allowlist.sh. +allowlist: [] diff --git a/.github/workflows/deps-scan.yml b/.github/workflows/deps-scan.yml new file mode 100644 index 0000000..b7ed8e4 --- /dev/null +++ b/.github/workflows/deps-scan.yml @@ -0,0 +1,55 @@ +name: deps-scan + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "0 7 * * 1" + +permissions: + contents: read + +jobs: + allowlist-expiry: + name: "IntentProof Security: Deps Allowlist" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate dependency allowlist expiry dates + run: bash ./scripts/check-deps-allowlist.sh + + pip-audit: + name: "IntentProof Security: pip-audit" + needs: allowlist-expiry + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install package and pip-audit + run: pip install -e ".[dev]" pip-audit + + - name: Run pip-audit + run: pip-audit --desc on + + osv-scanner: + name: "IntentProof Security: OSV-Scanner" + needs: allowlist-expiry + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run OSV-Scanner + uses: google/osv-scanner-action/osv-scanner-action@v2.2.2 + with: + scan-args: |- + --recursive + --config=.osv-scanner.toml + --severity=HIGH,CRITICAL + ./ diff --git a/.osv-scanner.toml b/.osv-scanner.toml new file mode 100644 index 0000000..393cdb0 --- /dev/null +++ b/.osv-scanner.toml @@ -0,0 +1,4 @@ +# IntentProof OSV-Scanner configuration. +# CRITICAL and HIGH findings fail CI via deps-scan workflow severity filter. +# Time-bounded HIGH allowlist entries live in .github/deps-allowlist.yml and +# must be mirrored here under [[IgnoredVulns]] with matching ignoreUntil dates. diff --git a/scripts/check-deps-allowlist.sh b/scripts/check-deps-allowlist.sh new file mode 100755 index 0000000..1ddbb64 --- /dev/null +++ b/scripts/check-deps-allowlist.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Validate .github/deps-allowlist.yml expiry dates. +# Expired entries fail with a clear message for security on-call follow-up. +set -euo pipefail + +ALLOWLIST_FILE="${1:-.github/deps-allowlist.yml}" + +if [[ ! -f "$ALLOWLIST_FILE" ]]; then + echo "No allowlist file at $ALLOWLIST_FILE; skipping expiry check." + exit 0 +fi + +if ! command -v python3 >/dev/null 2>&1; then + echo "python3 is required to validate $ALLOWLIST_FILE" >&2 + exit 1 +fi + +python3 - "$ALLOWLIST_FILE" <<'PY' +import datetime +import re +import sys + +path = sys.argv[1] +text = open(path, encoding="utf-8").read() + +# Minimal parser for our allowlist schema (no PyYAML dependency). +entries = [] +current = None +for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith("- "): + if current: + entries.append(current) + current = {} + item = stripped[2:].strip() + for field in ("id", "rule_id", "cve_id"): + m = re.search(rf"{field}:\s*(\S+)", item) + if m: + current["id"] = m.group(1) + m = re.search(r"expires:\s*(\S+)", item) + if m: + current["expires"] = m.group(1) + continue + if current is None: + continue + m = re.match(r"expires:\s*(\S+)", stripped) + if m: + current["expires"] = m.group(1) + for field in ("id", "rule_id", "cve_id"): + m = re.match(rf"{field}:\s*(\S+)", stripped) + if m: + current["id"] = m.group(1) +if current: + entries.append(current) + +today = datetime.date.today() +expired = [] +for idx, entry in enumerate(entries): + expires_raw = entry.get("expires") + if not expires_raw: + print( + f"{path}: allowlist[{idx}] missing expires date " + "(required for security on-call approval model)", + file=sys.stderr, + ) + sys.exit(1) + try: + expires = datetime.date.fromisoformat(str(expires_raw)) + except ValueError: + print( + f"{path}: allowlist[{idx}] has invalid expires date: {expires_raw!r}", + file=sys.stderr, + ) + sys.exit(1) + if expires < today: + entry_id = entry.get("id", "") + expired.append(f"{entry_id} (expired {expires.isoformat()})") + +if expired: + print("Allowlist expired; contact security on-call to extend or remove:", file=sys.stderr) + for item in expired: + print(f" - {item}", file=sys.stderr) + sys.exit(1) + +print(f"PASS: {len(entries)} allowlist entries are current.") +PY From 7f9f3bc80592402cf92ca76a7f41a3bb2ffe90f9 Mon Sep 17 00:00:00 2001 From: Nathan Gillett Date: Thu, 21 May 2026 22:53:47 -0500 Subject: [PATCH 2/7] Fix deps-scan CI: OSV gate script Signed-off-by: Nathan Gillett --- .github/workflows/deps-scan.yml | 10 +---- scripts/check-deps-allowlist.sh | 30 ++++++++----- scripts/run-osv-scanner-gate.sh | 77 +++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 18 deletions(-) create mode 100755 scripts/run-osv-scanner-gate.sh diff --git a/.github/workflows/deps-scan.yml b/.github/workflows/deps-scan.yml index b7ed8e4..3e4822a 100644 --- a/.github/workflows/deps-scan.yml +++ b/.github/workflows/deps-scan.yml @@ -45,11 +45,5 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Run OSV-Scanner - uses: google/osv-scanner-action/osv-scanner-action@v2.2.2 - with: - scan-args: |- - --recursive - --config=.osv-scanner.toml - --severity=HIGH,CRITICAL - ./ + - name: Run OSV-Scanner gate + run: bash ./scripts/run-osv-scanner-gate.sh diff --git a/scripts/check-deps-allowlist.sh b/scripts/check-deps-allowlist.sh index 1ddbb64..05102d6 100755 --- a/scripts/check-deps-allowlist.sh +++ b/scripts/check-deps-allowlist.sh @@ -29,14 +29,19 @@ current = None for line in text.splitlines(): stripped = line.strip() if stripped.startswith("- "): - if current: + if current is not None: entries.append(current) current = {} item = stripped[2:].strip() - for field in ("id", "rule_id", "cve_id"): - m = re.search(rf"{field}:\s*(\S+)", item) - if m: - current["id"] = m.group(1) + m = re.search(r"(?:^|\s)id:\s*(\S+)", item) + if m: + current["id"] = m.group(1) + m = re.search(r"rule_id:\s*(\S+)", item) + if m: + current["id"] = m.group(1) + m = re.search(r"cve_id:\s*(\S+)", item) + if m: + current["id"] = m.group(1) m = re.search(r"expires:\s*(\S+)", item) if m: current["expires"] = m.group(1) @@ -46,11 +51,16 @@ for line in text.splitlines(): m = re.match(r"expires:\s*(\S+)", stripped) if m: current["expires"] = m.group(1) - for field in ("id", "rule_id", "cve_id"): - m = re.match(rf"{field}:\s*(\S+)", stripped) - if m: - current["id"] = m.group(1) -if current: + m = re.match(r"(?:^|\s)id:\s*(\S+)", stripped) + if m: + current["id"] = m.group(1) + m = re.match(r"rule_id:\s*(\S+)", stripped) + if m: + current["id"] = m.group(1) + m = re.match(r"cve_id:\s*(\S+)", stripped) + if m: + current["id"] = m.group(1) +if current is not None: entries.append(current) today = datetime.date.today() diff --git a/scripts/run-osv-scanner-gate.sh b/scripts/run-osv-scanner-gate.sh new file mode 100755 index 0000000..19fa036 --- /dev/null +++ b/scripts/run-osv-scanner-gate.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Run OSV-Scanner and fail only on CRITICAL or HIGH findings. +# MEDIUM and LOW are reported but non-blocking per SECURITY-PROCESS.md. +set -euo pipefail + +ROOT="${1:-.}" +CONFIG="${2:-.osv-scanner.toml}" +OSV_VERSION="${OSV_SCANNER_VERSION:-v2.2.2}" + +if ! command -v osv-scanner >/dev/null 2>&1; then + arch=linux_amd64 + case "$(uname -m)" in + aarch64 | arm64) arch=linux_arm64 ;; + x86_64 | amd64) arch=linux_amd64 ;; + esac + curl -sSfL \ + "https://github.com/google/osv-scanner/releases/download/${OSV_VERSION}/osv-scanner_${OSV_VERSION#v}_${arch}.tar.gz" \ + | tar -xz + sudo install -m 755 osv-scanner /usr/local/bin/osv-scanner +fi + +args=(scan source --recursive --format=table --no-call-analysis) +if [[ -f "$CONFIG" ]]; then + args+=(--config="$CONFIG") +fi +args+=("$ROOT") + +set +e +output="$(osv-scanner "${args[@]}" 2>&1)" +status=$? +set -e + +printf '%s\n' "$output" + +if [[ "$status" -gt 1 ]]; then + echo "osv-scanner failed unexpectedly (exit $status)" >&2 + exit "$status" +fi + +python3 - "$output" <<'PY' +import re +import sys + +text = sys.argv[1] + +if "No issues found" in text: + print("PASS: no OSV findings") + raise SystemExit(0) + +match = re.search( + r"\((\d+) Critical, (\d+) High, (\d+) Medium, (\d+) Low", + text, +) +if not match: + if re.search(r"\b(GHSA-[a-z0-9-]+|GO-\d{4}-\d+|CVE-\d{4}-\d+)\b", text): + print( + "FAIL: OSV findings present but severity summary missing", + file=sys.stderr, + ) + raise SystemExit(1) + print("PASS: no parseable HIGH/CRITICAL OSV findings") + raise SystemExit(0) + +critical = int(match.group(1)) +high = int(match.group(2)) +if critical or high: + print( + f"FAIL: OSV found {critical} Critical and {high} High findings", + file=sys.stderr, + ) + raise SystemExit(1) + +print( + "PASS: OSV gate " + f"(Critical={critical}, High={high}; Medium/Low non-blocking)" +) +PY From 8c02149454222657d503a725c775d4d2dbc8e7cd Mon Sep 17 00:00:00 2001 From: Nathan Gillett Date: Thu, 21 May 2026 22:56:08 -0500 Subject: [PATCH 3/7] fix deps scan CI: OSV binary download URL Download the raw osv-scanner release binary instead of a missing tarball and allow repos without lockfiles to pass the OSV gate. Signed-off-by: Nathan Gillett --- scripts/run-osv-scanner-gate.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/run-osv-scanner-gate.sh b/scripts/run-osv-scanner-gate.sh index 19fa036..61094b3 100755 --- a/scripts/run-osv-scanner-gate.sh +++ b/scripts/run-osv-scanner-gate.sh @@ -13,13 +13,16 @@ if ! command -v osv-scanner >/dev/null 2>&1; then aarch64 | arm64) arch=linux_arm64 ;; x86_64 | amd64) arch=linux_amd64 ;; esac + tmp="$(mktemp)" curl -sSfL \ - "https://github.com/google/osv-scanner/releases/download/${OSV_VERSION}/osv-scanner_${OSV_VERSION#v}_${arch}.tar.gz" \ - | tar -xz - sudo install -m 755 osv-scanner /usr/local/bin/osv-scanner + "https://github.com/google/osv-scanner/releases/download/${OSV_VERSION}/osv-scanner_${arch}" \ + -o "$tmp" + chmod +x "$tmp" + sudo install -m 755 "$tmp" /usr/local/bin/osv-scanner + rm -f "$tmp" fi -args=(scan source --recursive --format=table --no-call-analysis) +args=(scan source --recursive --format=table --no-call-analysis --allow-no-lockfiles) if [[ -f "$CONFIG" ]]; then args+=(--config="$CONFIG") fi From 3b5b58e50434eae9ded097f83b326c7740b7b60c Mon Sep 17 00:00:00 2001 From: Nathan Gillett Date: Thu, 21 May 2026 23:02:47 -0500 Subject: [PATCH 4/7] fix deps scan: materialize Python lockfile for OSV Freeze installed requirements for OSV-Scanner and harden the gate script for repos without committed lockfiles. Signed-off-by: Nathan Gillett --- .github/workflows/deps-scan.yml | 11 ++++++++++- scripts/run-osv-scanner-gate.sh | 18 ++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deps-scan.yml b/.github/workflows/deps-scan.yml index 3e4822a..f95d160 100644 --- a/.github/workflows/deps-scan.yml +++ b/.github/workflows/deps-scan.yml @@ -45,5 +45,14 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Materialize Python lockfile for OSV + run: | + pip install -e ".[dev]" + pip freeze --exclude-editable > requirements-osv.txt + - name: Run OSV-Scanner gate - run: bash ./scripts/run-osv-scanner-gate.sh + run: bash ./scripts/run-osv-scanner-gate.sh . .osv-scanner.toml requirements-osv.txt diff --git a/scripts/run-osv-scanner-gate.sh b/scripts/run-osv-scanner-gate.sh index 61094b3..6abc55a 100755 --- a/scripts/run-osv-scanner-gate.sh +++ b/scripts/run-osv-scanner-gate.sh @@ -5,6 +5,8 @@ set -euo pipefail ROOT="${1:-.}" CONFIG="${2:-.osv-scanner.toml}" +shift 2 2>/dev/null || true +EXTRA_LOCKFILES=("$@") OSV_VERSION="${OSV_SCANNER_VERSION:-v2.2.2}" if ! command -v osv-scanner >/dev/null 2>&1; then @@ -22,11 +24,18 @@ if ! command -v osv-scanner >/dev/null 2>&1; then rm -f "$tmp" fi -args=(scan source --recursive --format=table --no-call-analysis --allow-no-lockfiles) +args=(scan source --format=table --no-call-analysis=all --allow-no-lockfiles) if [[ -f "$CONFIG" ]]; then args+=(--config="$CONFIG") fi -args+=("$ROOT") + +if ((${#EXTRA_LOCKFILES[@]} > 0)); then + for lockfile in "${EXTRA_LOCKFILES[@]}"; do + args+=(--lockfile="$lockfile") + done +else + args+=(--recursive "$ROOT") +fi set +e output="$(osv-scanner "${args[@]}" 2>&1)" @@ -35,6 +44,11 @@ set -e printf '%s\n' "$output" +if [[ "$status" -eq 128 ]] && grep -q "No package sources found" <<<"$output"; then + echo "PASS: no scannable dependency manifests (OSV skipped)" + exit 0 +fi + if [[ "$status" -gt 1 ]]; then echo "osv-scanner failed unexpectedly (exit $status)" >&2 exit "$status" From 021bb03a3fab6fea929ad8a98dd36cf6b5c91081 Mon Sep 17 00:00:00 2001 From: Nathan Gillett Date: Thu, 21 May 2026 23:24:56 -0500 Subject: [PATCH 5/7] address Bugbot: fix OSV args and dedupe allowlist checks Parse optional lockfiles without a brittle shift, share allowlist expiry validation in one script, and fix codeql allowlist empty-entry handling. Signed-off-by: Nathan Gillett --- scripts/check-allowlist-expiry.sh | 106 ++++++++++++++++++++++++++++++ scripts/check-codeql-allowlist.sh | 83 +---------------------- scripts/check-deps-allowlist.sh | 95 +------------------------- scripts/run-osv-scanner-gate.sh | 7 +- 4 files changed, 113 insertions(+), 178 deletions(-) create mode 100755 scripts/check-allowlist-expiry.sh diff --git a/scripts/check-allowlist-expiry.sh b/scripts/check-allowlist-expiry.sh new file mode 100755 index 0000000..224aff6 --- /dev/null +++ b/scripts/check-allowlist-expiry.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Validate allowlist YAML expiry dates (minimal parser, no PyYAML). +# Usage: check-allowlist-expiry.sh [file] [id-field...] +set -euo pipefail + +ALLOWLIST_FILE="${1:-.github/codeql-allowlist.yml}" +shift || true +ID_FIELDS=("$@") +if ((${#ID_FIELDS[@]} == 0)); then + ID_FIELDS=(rule_id) +fi + +if [[ ! -f "$ALLOWLIST_FILE" ]]; then + echo "No allowlist file at $ALLOWLIST_FILE; skipping expiry check." + exit 0 +fi + +if ! command -v python3 >/dev/null 2>&1; then + echo "python3 is required to validate $ALLOWLIST_FILE" >&2 + exit 1 +fi + +python3 - "$ALLOWLIST_FILE" "${ID_FIELDS[*]}" <<'PY' +import datetime +import re +import sys + +path = sys.argv[1] +id_fields = sys.argv[2].split() +patterns = { + "rule_id": re.compile(r"rule_id:\s*(\S+)"), + "cve_id": re.compile(r"cve_id:\s*(\S+)"), + "id": re.compile(r"(?:^|\s)id:\s*(\S+)"), +} + + +def extract_id(text, anchored=False): + for field in id_fields: + pattern = patterns.get(field) + if pattern is None: + continue + match = pattern.match(text) if anchored else pattern.search(text) + if match: + return match.group(1) + return None + + +text = open(path, encoding="utf-8").read() +entries = [] +current = None +for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith("- "): + if current is not None: + entries.append(current) + current = {} + item = stripped[2:].strip() + entry_id = extract_id(item, anchored=False) + if entry_id: + current["id"] = entry_id + match = re.search(r"expires:\s*(\S+)", item) + if match: + current["expires"] = match.group(1) + continue + if current is None: + continue + match = re.match(r"expires:\s*(\S+)", stripped) + if match: + current["expires"] = match.group(1) + entry_id = extract_id(stripped, anchored=True) + if entry_id: + current["id"] = entry_id +if current is not None: + entries.append(current) + +today = datetime.date.today() +expired = [] +for idx, entry in enumerate(entries): + expires_raw = entry.get("expires") + if not expires_raw: + print( + f"{path}: allowlist[{idx}] missing expires date " + "(required for security on-call approval model)", + file=sys.stderr, + ) + sys.exit(1) + try: + expires = datetime.date.fromisoformat(str(expires_raw)) + except ValueError: + print( + f"{path}: allowlist[{idx}] has invalid expires date: {expires_raw!r}", + file=sys.stderr, + ) + sys.exit(1) + if expires < today: + entry_id = entry.get("id", "") + expired.append(f"{entry_id} (expired {expires.isoformat()})") + +if expired: + print("Allowlist expired; contact security on-call to extend or remove:", file=sys.stderr) + for item in expired: + print(f" - {item}", file=sys.stderr) + sys.exit(1) + +print(f"PASS: {len(entries)} allowlist entries are current.") +PY diff --git a/scripts/check-codeql-allowlist.sh b/scripts/check-codeql-allowlist.sh index 4c9072a..9e58438 100755 --- a/scripts/check-codeql-allowlist.sh +++ b/scripts/check-codeql-allowlist.sh @@ -1,84 +1,3 @@ #!/usr/bin/env bash # Validate .github/codeql-allowlist.yml expiry dates. -# Expired entries fail with a clear message for security on-call follow-up. -set -euo pipefail - -ALLOWLIST_FILE="${1:-.github/codeql-allowlist.yml}" - -if [[ ! -f "$ALLOWLIST_FILE" ]]; then - echo "No allowlist file at $ALLOWLIST_FILE; skipping expiry check." - exit 0 -fi - -if ! command -v python3 >/dev/null 2>&1; then - echo "python3 is required to validate $ALLOWLIST_FILE" >&2 - exit 1 -fi - -python3 - "$ALLOWLIST_FILE" <<'PY' -import datetime -import re -import sys - -path = sys.argv[1] -text = open(path, encoding="utf-8").read() - -# Minimal parser for our allowlist schema (no PyYAML dependency). -entries = [] -current = None -for line in text.splitlines(): - stripped = line.strip() - if stripped.startswith("- "): - if current: - entries.append(current) - current = {} - item = stripped[2:].strip() - m = re.search(r"rule_id:\s*(\S+)", item) - if m: - current["rule_id"] = m.group(1) - m = re.search(r"expires:\s*(\S+)", item) - if m: - current["expires"] = m.group(1) - continue - if current is None: - continue - m = re.match(r"expires:\s*(\S+)", stripped) - if m: - current["expires"] = m.group(1) - m = re.match(r"rule_id:\s*(\S+)", stripped) - if m: - current["rule_id"] = m.group(1) -if current: - entries.append(current) - -today = datetime.date.today() -expired = [] -for idx, entry in enumerate(entries): - expires_raw = entry.get("expires") - if not expires_raw: - print( - f"{path}: allowlist[{idx}] missing expires date " - "(required for security on-call approval model)", - file=sys.stderr, - ) - sys.exit(1) - try: - expires = datetime.date.fromisoformat(str(expires_raw)) - except ValueError: - print( - f"{path}: allowlist[{idx}] has invalid expires date: {expires_raw!r}", - file=sys.stderr, - ) - sys.exit(1) - if expires < today: - rule_id = entry.get("rule_id", "") - expired.append(f"{rule_id} (expired {expires.isoformat()})") - -if expired: - print("Allowlist expired; contact security on-call to extend or remove:", file=sys.stderr) - for item in expired: - print(f" - {item}", file=sys.stderr) - sys.exit(1) - -print(f"PASS: {len(entries)} allowlist entries are current.") -PY +exec "$(dirname "$0")/check-allowlist-expiry.sh" "${1:-.github/codeql-allowlist.yml}" rule_id diff --git a/scripts/check-deps-allowlist.sh b/scripts/check-deps-allowlist.sh index 05102d6..698999e 100755 --- a/scripts/check-deps-allowlist.sh +++ b/scripts/check-deps-allowlist.sh @@ -1,96 +1,3 @@ #!/usr/bin/env bash # Validate .github/deps-allowlist.yml expiry dates. -# Expired entries fail with a clear message for security on-call follow-up. -set -euo pipefail - -ALLOWLIST_FILE="${1:-.github/deps-allowlist.yml}" - -if [[ ! -f "$ALLOWLIST_FILE" ]]; then - echo "No allowlist file at $ALLOWLIST_FILE; skipping expiry check." - exit 0 -fi - -if ! command -v python3 >/dev/null 2>&1; then - echo "python3 is required to validate $ALLOWLIST_FILE" >&2 - exit 1 -fi - -python3 - "$ALLOWLIST_FILE" <<'PY' -import datetime -import re -import sys - -path = sys.argv[1] -text = open(path, encoding="utf-8").read() - -# Minimal parser for our allowlist schema (no PyYAML dependency). -entries = [] -current = None -for line in text.splitlines(): - stripped = line.strip() - if stripped.startswith("- "): - if current is not None: - entries.append(current) - current = {} - item = stripped[2:].strip() - m = re.search(r"(?:^|\s)id:\s*(\S+)", item) - if m: - current["id"] = m.group(1) - m = re.search(r"rule_id:\s*(\S+)", item) - if m: - current["id"] = m.group(1) - m = re.search(r"cve_id:\s*(\S+)", item) - if m: - current["id"] = m.group(1) - m = re.search(r"expires:\s*(\S+)", item) - if m: - current["expires"] = m.group(1) - continue - if current is None: - continue - m = re.match(r"expires:\s*(\S+)", stripped) - if m: - current["expires"] = m.group(1) - m = re.match(r"(?:^|\s)id:\s*(\S+)", stripped) - if m: - current["id"] = m.group(1) - m = re.match(r"rule_id:\s*(\S+)", stripped) - if m: - current["id"] = m.group(1) - m = re.match(r"cve_id:\s*(\S+)", stripped) - if m: - current["id"] = m.group(1) -if current is not None: - entries.append(current) - -today = datetime.date.today() -expired = [] -for idx, entry in enumerate(entries): - expires_raw = entry.get("expires") - if not expires_raw: - print( - f"{path}: allowlist[{idx}] missing expires date " - "(required for security on-call approval model)", - file=sys.stderr, - ) - sys.exit(1) - try: - expires = datetime.date.fromisoformat(str(expires_raw)) - except ValueError: - print( - f"{path}: allowlist[{idx}] has invalid expires date: {expires_raw!r}", - file=sys.stderr, - ) - sys.exit(1) - if expires < today: - entry_id = entry.get("id", "") - expired.append(f"{entry_id} (expired {expires.isoformat()})") - -if expired: - print("Allowlist expired; contact security on-call to extend or remove:", file=sys.stderr) - for item in expired: - print(f" - {item}", file=sys.stderr) - sys.exit(1) - -print(f"PASS: {len(entries)} allowlist entries are current.") -PY +exec "$(dirname "$0")/check-allowlist-expiry.sh" "${1:-.github/deps-allowlist.yml}" rule_id cve_id id diff --git a/scripts/run-osv-scanner-gate.sh b/scripts/run-osv-scanner-gate.sh index 6abc55a..1794160 100755 --- a/scripts/run-osv-scanner-gate.sh +++ b/scripts/run-osv-scanner-gate.sh @@ -5,8 +5,11 @@ set -euo pipefail ROOT="${1:-.}" CONFIG="${2:-.osv-scanner.toml}" -shift 2 2>/dev/null || true -EXTRA_LOCKFILES=("$@") +if (( $# > 2 )); then + EXTRA_LOCKFILES=("${@:3}") +else + EXTRA_LOCKFILES=() +fi OSV_VERSION="${OSV_SCANNER_VERSION:-v2.2.2}" if ! command -v osv-scanner >/dev/null 2>&1; then From 2ac16be8552e4627882253781ec661db3ae0be3d Mon Sep 17 00:00:00 2001 From: Nathan Gillett Date: Thu, 21 May 2026 23:39:24 -0500 Subject: [PATCH 6/7] fix OSV gate: avoid argv size limit on large output Write osv-scanner output to a temp file and pass the path to the severity parser instead of embedding the full table in sys.argv. Signed-off-by: Nathan Gillett --- scripts/run-osv-scanner-gate.sh | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/scripts/run-osv-scanner-gate.sh b/scripts/run-osv-scanner-gate.sh index 1794160..8d1cece 100755 --- a/scripts/run-osv-scanner-gate.sh +++ b/scripts/run-osv-scanner-gate.sh @@ -40,14 +40,17 @@ else args+=(--recursive "$ROOT") fi +output_file="$(mktemp)" +trap 'rm -f "$output_file"' EXIT + set +e -output="$(osv-scanner "${args[@]}" 2>&1)" +osv-scanner "${args[@]}" >"$output_file" 2>&1 status=$? set -e -printf '%s\n' "$output" +cat "$output_file" -if [[ "$status" -eq 128 ]] && grep -q "No package sources found" <<<"$output"; then +if [[ "$status" -eq 128 ]] && grep -q "No package sources found" "$output_file"; then echo "PASS: no scannable dependency manifests (OSV skipped)" exit 0 fi @@ -57,11 +60,12 @@ if [[ "$status" -gt 1 ]]; then exit "$status" fi -python3 - "$output" <<'PY' +python3 - "$output_file" <<'PY' import re import sys -text = sys.argv[1] +with open(sys.argv[1], encoding="utf-8") as fh: + text = fh.read() if "No issues found" in text: print("PASS: no OSV findings") From d47b605702c71a2a371dc867369b31ce91c492c9 Mon Sep 17 00:00:00 2001 From: Nathan Gillett Date: Thu, 21 May 2026 23:52:31 -0500 Subject: [PATCH 7/7] fix allowlist checker: require explicit file path Drop the misleading codeql-allowlist default so invoking the shared script without arguments fails fast instead of silently skipping. Signed-off-by: Nathan Gillett --- scripts/check-allowlist-expiry.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/check-allowlist-expiry.sh b/scripts/check-allowlist-expiry.sh index 224aff6..db93e7c 100755 --- a/scripts/check-allowlist-expiry.sh +++ b/scripts/check-allowlist-expiry.sh @@ -1,10 +1,15 @@ #!/usr/bin/env bash # Validate allowlist YAML expiry dates (minimal parser, no PyYAML). -# Usage: check-allowlist-expiry.sh [file] [id-field...] +# Usage: check-allowlist-expiry.sh [id-field...] set -euo pipefail -ALLOWLIST_FILE="${1:-.github/codeql-allowlist.yml}" -shift || true +if [[ $# -lt 1 || -z "${1:-}" ]]; then + echo "usage: check-allowlist-expiry.sh [id-field...]" >&2 + exit 1 +fi + +ALLOWLIST_FILE="$1" +shift ID_FIELDS=("$@") if ((${#ID_FIELDS[@]} == 0)); then ID_FIELDS=(rule_id)