Skip to content
Merged
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
30 changes: 30 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,36 @@ jobs:
- run: pip install ruff
- run: ruff check tests/

osv-scanner:
# maestro#14 / data-boar-site#91: dependency vulns (osv-scanner or trivy) on every repo.
# Binary pin + sha256 copied from keen-platypus security.yml job osv-scanner (v2.6.0).
# Default branch confirmed: main.
# Checkout SHA matches the other jobs in this file (and KP production), not the
# drifted actions/checkout@v6 pin that landed in the 2026-09-16 issue comment.
# Site has no lockfiles today: `scan source -r .` exits 128 ("No package sources
# found"). --allow-no-lockfiles makes that OK; when a manifest appears, scan runs.
name: OSV dependency scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false

- name: Download and verify osv-scanner
run: |
set -euo pipefail
VER="2.6.0"
BIN="osv-scanner_linux_amd64"
EXPECTED_SHA256="ca69b3d3cd08f889a49dc0a383122f71cc528b83803671df5fd874d97485b108"
BASE="https://github.com/google/osv-scanner/releases/download/v${VER}"
curl -sSfL "${BASE}/${BIN}" -o osv-scanner
echo "${EXPECTED_SHA256} osv-scanner" | sha256sum -c -
chmod +x ./osv-scanner

- name: Scan dependency manifests
run: ./osv-scanner scan source -r . --allow-no-lockfiles

# CodeQL (JavaScript/TypeScript) is enabled via GitHub's managed **default setup**
# (Settings → Code security), NOT a committed workflow — no Action SHA to verify/maintain.
# Free on this public repo. See docs/adr/ADR-0001.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ __pycache__/
.DS_Store
.ruff_cache/
.semgrep/
scripts/.cache/
65 changes: 65 additions & 0 deletions scripts/check-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,25 @@
# check-all — FULL local gate for data-boar-site (run before opening a PR / before merge).
# Mirrors the data-boar `check-all` discipline: green locally BEFORE push (ADR-0080 spirit).
# NOT a toy project — this gate is inviolable (docs/adr/ADR-0001). Add checks, never remove.
# --skip-osv-scanner skip OSV dependency scan (same flag as keen-platypus check-all)
set -uo pipefail
cd "$(dirname "$0")/.." || exit 2
fail=0
SKIP_OSV=0
for arg in "$@"; do
case "$arg" in
--skip-osv-scanner) SKIP_OSV=1 ;;
-h|--help)
echo "uso: $0 [--skip-osv-scanner]"
exit 0
;;
*)
echo "check-all: argumento desconhecido: $arg" >&2
echo "uso: $0 [--skip-osv-scanner]" >&2
exit 2
;;
esac
done

run() { echo; echo "── $1 ──"; shift; if "$@"; then echo " ✅ ok"; else echo " ❌ FALHOU"; fail=1; fi; }

Expand Down Expand Up @@ -55,6 +71,55 @@ sys.exit(bad)
PY
fi

# 4) OSV dependency scan — mirrors CI job osv-scanner (#91). Pin = keen-platypus 2.6.0.
OSV_VER="2.6.0"
OSV_SHA256="ca69b3d3cd08f889a49dc0a383122f71cc528b83803671df5fd874d97485b108"
OSV_CACHE="$(pwd)/scripts/.cache"
ensure_osv_scanner() {
mkdir -p "$OSV_CACHE"
if command -v osv-scanner >/dev/null 2>&1 && osv-scanner --version 2>/dev/null | grep -qF "$OSV_VER"; then
return 0
fi
if [ -x "$OSV_CACHE/osv-scanner" ] && "$OSV_CACHE/osv-scanner" --version 2>/dev/null | grep -qF "$OSV_VER"; then
PATH="$OSV_CACHE:$PATH"
export PATH
return 0
fi
echo "check-all: baixando osv-scanner v${OSV_VER} para ${OSV_CACHE}..." >&2
curl -sSfL "https://github.com/google/osv-scanner/releases/download/v${OSV_VER}/osv-scanner_linux_amd64" \
-o "$OSV_CACHE/osv-scanner.download" || {
echo "check-all: download osv-scanner falhou" >&2
rm -f "$OSV_CACHE/osv-scanner.download"
return 1
}
# set -uo pipefail without -e: sha256sum -c must be checked or we mv a bad blob.
if ! echo "${OSV_SHA256} $OSV_CACHE/osv-scanner.download" | sha256sum -c -; then
echo "check-all: sha256 osv-scanner nao bate — recusando o download (nao vai pro cache)" >&2
rm -f "$OSV_CACHE/osv-scanner.download"
return 1
fi
mv "$OSV_CACHE/osv-scanner.download" "$OSV_CACHE/osv-scanner"
chmod +x "$OSV_CACHE/osv-scanner"
PATH="$OSV_CACHE:$PATH"
export PATH
}

if [ "$SKIP_OSV" = "1" ]; then
echo
echo "── osv-scanner ──"
echo " ⏭️ skip (--skip-osv-scanner)"
else
if ensure_osv_scanner && command -v osv-scanner >/dev/null 2>&1; then
# Static site: no lockfiles today → --allow-no-lockfiles (CI same). Exit 128 without it.
run "osv-scanner scan source -r ." osv-scanner scan source -r . --allow-no-lockfiles
else
echo
echo "── osv-scanner ──"
echo " ❌ FALHOU — osv-scanner v${OSV_VER} ausente (CI security.yml baixa e verifica sha256)"
fail=1
fi
fi

echo
if [ "$fail" = "0" ]; then echo "✅ check-all VERDE — pode abrir PR."; else echo "❌ check-all VERMELHO — NÃO abra PR."; fi
exit "$fail"
92 changes: 92 additions & 0 deletions tests/test_guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import re
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from typing import ClassVar

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
Expand Down Expand Up @@ -74,6 +76,96 @@ def test_pages_deploy_queues_and_avoids_unquoted_secret_heredoc(self):
)
self.assertIn("json.dumps", wf)
self.assertIn("refusing deploy", wf)

def test_security_yml_osv_scanner_job_is_sha256_pinned(self):
"""#91: osv-scanner v2.6.0 checksum pin (keen-platypus / issue comment)."""
wf = _read(os.path.join(ROOT, ".github", "workflows", "security.yml"))
self.assertIn("osv-scanner:", wf)
self.assertIn("2.6.0", wf)
self.assertIn(
"ca69b3d3cd08f889a49dc0a383122f71cc528b83803671df5fd874d97485b108",
wf,
)
self.assertIn("--allow-no-lockfiles", wf)
chk = _read(os.path.join(ROOT, "scripts", "check-all.sh"))
self.assertIn("--skip-osv-scanner", chk)
self.assertIn("--allow-no-lockfiles", chk)

def test_ensure_osv_scanner_does_not_promote_binary_on_bad_sha256(self):
"""PR #94: checksum fail must return 1 before mv into scripts/.cache/osv-scanner."""
src = _read(os.path.join(ROOT, "scripts", "check-all.sh"))
ver = re.search(r'^OSV_VER="([^"]+)"', src, flags=re.MULTILINE)
sha = re.search(r'^OSV_SHA256="([^"]+)"', src, flags=re.MULTILINE)
self.assertTrue(ver and sha)
start = src.index("ensure_osv_scanner() {")
end = src.index('\nif [ "$SKIP_OSV"', start)
fn = src[start:end]
body = fn[fn.index("{") + 1 :].rstrip().removesuffix("}")
idx_if = body.rfind("if ! echo")
idx_sum = body.rfind("sha256sum -c -")
idx_mv = body.find('mv "$OSV_CACHE/osv-scanner.download"')
self.assertGreater(idx_if, -1)
self.assertGreater(idx_sum, idx_if)
self.assertGreater(idx_mv, idx_sum)
between = body[idx_if:idx_mv]
self.assertIn("return 1", between)
self.assertRegex(between, r"if !")

with tempfile.TemporaryDirectory() as td:
cache = Path(td) / "cache"
bindir = Path(td) / "bin"
cache.mkdir()
bindir.mkdir()
curl = bindir / "curl"
curl.write_text(
"#!/bin/sh\n"
"out=\n"
"while [ $# -gt 0 ]; do\n"
' if [ "$1" = "-o" ]; then shift; out=$1; fi\n'
" shift\n"
"done\n"
'printf "tampered-not-osv\\n" > "$out"\n',
encoding="utf-8",
)
curl.chmod(0o755)
# Must shadow a real /usr/bin/osv-scanner so we actually hit curl+sha256.
fake_osv = bindir / "osv-scanner"
fake_osv.write_text(
'#!/bin/sh\necho "osv-scanner version: 0.0.0-not-pinned"\n',
encoding="utf-8",
)
fake_osv.chmod(0o755)
wrapper = Path(td) / "run.sh"
wrapper.write_text(
"#!/usr/bin/env bash\n"
"set -uo pipefail\n"
f"OSV_VER={ver.group(1)!r}\n"
f"OSV_SHA256={sha.group(1)!r}\n"
f"OSV_CACHE={str(cache)!r}\n"
"ensure_osv_scanner() {\n"
f"{body}\n"
"}\n"
"ensure_osv_scanner\n"
"exit $?\n",
encoding="utf-8",
)
env = os.environ.copy()
env["PATH"] = f"{bindir}{os.pathsep}/usr/bin{os.pathsep}/bin"
proc = subprocess.run(
["bash", str(wrapper)],
cwd=td,
env=env,
capture_output=True,
text=True,
check=False,
)
self.assertNotEqual(proc.returncode, 0, proc.stderr)
self.assertFalse((cache / "osv-scanner").exists())
self.assertFalse((cache / "osv-scanner").is_file())

def test_pages_deploy_rejects_unpinned_upload_pages_artifact_v3(self):
"""Keep Pages SHA-pin regression next to the concurrency test above."""
wf = _read(os.path.join(ROOT, ".github", "workflows", "pages-deploy.yml"))
# Transitive pin: v3.0.1 composite used upload-artifact@v4 (org policy fail).
self.assertNotIn(
"56afc609e74202658d3ffba0e8f6dda462b719fa",
Expand Down
Loading