125 lines
4.7 KiB
Bash
Executable File
125 lines
4.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# CI license-compliance gate (runs in the security workflow for both dry
|
|
# runs and releases). Two layers:
|
|
# 1) Structural: every vendored component directory must physically carry
|
|
# a license file.
|
|
# 2) ScanCode Toolkit license detection over every vendored license text
|
|
# and all first-party sources; ONE detection outside
|
|
# scripts/license-policy.json fails the gate (see license-gate-check.py).
|
|
#
|
|
# Grammar parser bodies (generated parser.c, no headers) are excluded from
|
|
# the ScanCode pass for runtime; their per-directory license files ARE
|
|
# scanned, and layer 1 guarantees each grammar carries one.
|
|
#
|
|
# Requires: scancode (pipx install scancode-toolkit), python3.
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
# --selftest: plant a violation and assert the structural layer catches it.
|
|
# Run in CI before the real gate so a silently-broken gate cannot pass.
|
|
if [ "${1:-}" = "--selftest" ]; then
|
|
TESTDIR="$ROOT/vendored/.gate-selftest-$$"
|
|
mkdir -p "$TESTDIR"
|
|
echo "int x;" > "$TESTDIR/x.c"
|
|
# The child gate exits 1 on the planted violation (that is the point) —
|
|
# capture its output first so pipefail cannot mask the grep result.
|
|
GATE_OUT="$("$0" 2>/dev/null || true)"
|
|
rm -rf "$TESTDIR"
|
|
if printf '%s' "$GATE_OUT" | grep -q "BLOCKED: vendored code in .*gate-selftest"; then
|
|
echo "OK: gate self-test — planted violation was detected"
|
|
exit 0
|
|
fi
|
|
echo "FAIL: gate self-test — planted unlicensed file was NOT detected"
|
|
exit 1
|
|
fi
|
|
|
|
echo "=== License gate 1/2: structural coverage ==="
|
|
# Rule: every directory under either vendored tree that contains source or
|
|
# data files must have a license file in itself or an ancestor directory
|
|
# WITHIN the vendored tree. No hardcoded component list — newly vendored
|
|
# code with no license is flagged immediately.
|
|
MISS=0
|
|
has_license_dir() {
|
|
ls "$1" 2>/dev/null | grep -qiE '^(LICENSE|LICENCE|COPYING|UNLICENSE|NOTICE)'
|
|
}
|
|
for root in vendored internal/cbm/vendored; do
|
|
find "$root" -type f \
|
|
\( -name '*.c' -o -name '*.h' -o -name '*.cpp' -o -name '*.hpp' \
|
|
-o -name '*.S' -o -name '*.bin' -o -name '*.txt' \) \
|
|
! -iname 'LICENSE*' ! -iname 'COPYING*' ! -iname 'NOTICE*' \
|
|
-exec dirname {} \; | LC_ALL=C sort -u | while IFS= read -r d; do
|
|
cur="$d"
|
|
covered=1
|
|
while :; do
|
|
if has_license_dir "$cur"; then
|
|
covered=0
|
|
break
|
|
fi
|
|
[ "$cur" = "$root" ] && break
|
|
cur="$(dirname "$cur")"
|
|
done
|
|
if [ $covered -ne 0 ]; then
|
|
echo "BLOCKED: vendored code in $d has no license file in itself or any ancestor within $root/"
|
|
fi
|
|
done > /tmp/license-gate-structural.$$
|
|
if [ -s /tmp/license-gate-structural.$$ ]; then
|
|
cat /tmp/license-gate-structural.$$
|
|
MISS=1
|
|
fi
|
|
rm -f /tmp/license-gate-structural.$$
|
|
done
|
|
if [ $MISS -ne 0 ]; then
|
|
echo "=== LICENSE GATE FAILED (structural) ==="
|
|
exit 1
|
|
fi
|
|
echo "OK: every vendored source directory is covered by a license file"
|
|
|
|
echo "=== License gate 2/2: ScanCode detection ==="
|
|
if ! command -v scancode &>/dev/null; then
|
|
echo "FAIL: scancode not installed (pipx install scancode-toolkit)"
|
|
exit 1
|
|
fi
|
|
|
|
STAGE="$(mktemp -d)"
|
|
trap 'rm -rf "$STAGE"' EXIT
|
|
|
|
# Inputs: all vendored license/notice texts + all first-party sources.
|
|
find vendored internal/cbm/vendored -type f \
|
|
\( -iname 'LICENSE*' -o -iname 'COPYING*' -o -iname 'NOTICE*' -o -iname 'UNLICENSE*' \) \
|
|
> "$STAGE/files.txt"
|
|
find src pkg scripts -type f \
|
|
\( -name '*.c' -o -name '*.h' -o -name '*.sh' -o -name '*.js' \
|
|
-o -name '*.py' -o -name '*.rb' -o -name '*.toml' -o -name '*.json' \) \
|
|
>> "$STAGE/files.txt"
|
|
|
|
mkdir -p "$STAGE/tree"
|
|
tar cf - -T "$STAGE/files.txt" | tar xf - -C "$STAGE/tree"
|
|
|
|
scancode --license --quiet --processes 2 --json-pp "$STAGE/scan.json" "$STAGE/tree" \
|
|
> "$STAGE/scancode.log" 2>&1 || {
|
|
echo "FAIL: scancode run failed:"
|
|
tail -20 "$STAGE/scancode.log"
|
|
exit 1
|
|
}
|
|
|
|
python3 scripts/license-gate-check.py "$STAGE/scan.json" scripts/license-policy.json
|
|
|
|
echo "=== License gate 3/3: UI npm production tree ==="
|
|
# The -ui binaries embed the compiled frontend bundle; its production
|
|
# dependency tree must be allow-listed too. --ignore-scripts: no dependency
|
|
# postinstall code runs inside the security job.
|
|
if command -v npm &>/dev/null && [ -f graph-ui/package-lock.json ]; then
|
|
if [ ! -d graph-ui/node_modules ]; then
|
|
(cd graph-ui && npm ci --ignore-scripts --silent)
|
|
fi
|
|
python3 scripts/license-gate-check-npm.py graph-ui scripts/license-policy.json
|
|
else
|
|
echo "FAIL: npm or graph-ui/package-lock.json unavailable — UI tree unchecked"
|
|
exit 1
|
|
fi
|
|
|
|
echo "=== License gate passed ==="
|