Gatekeep evals / eval-floor (push) Successful in 8s
Add .github/workflows/gatekeep-pr.yml with uses: ./ and OPENROUTER_API_KEY from secrets. Forge-agnostic API helper for Gitea.
83 lines
2.9 KiB
Bash
83 lines
2.9 KiB
Bash
#!/usr/bin/env bash
|
|
# Deterministic security checklist seed scanner (paths + diff hunks).
|
|
# Hits here are hard-fail regardless of model agreement (see policy.security_agreement).
|
|
set -euo pipefail
|
|
|
|
FILES_LIST="${1:?changed-files list}"
|
|
OUT="${2:?out json}"
|
|
ROOT="${GATEKEEP_ROOT:-.}"
|
|
REPO="${GITHUB_REPOSITORY:-}"
|
|
EVENT="${GITHUB_EVENT_PATH:-}"
|
|
|
|
hits=()
|
|
|
|
while IFS= read -r f; do
|
|
[[ -z "$f" ]] && continue
|
|
case "$f" in
|
|
.github/workflows/*|.github/actions/*)
|
|
hits+=("CI workflow tampering surface: $f")
|
|
;;
|
|
*secret*|*credential*|.env*|.env)
|
|
hits+=("Secrets/credential path touched: $f")
|
|
;;
|
|
esac
|
|
done < "$FILES_LIST"
|
|
|
|
# Diff scan for high-signal patterns (best-effort; forge-agnostic)
|
|
if [[ -n "$EVENT" && -f "$EVENT" && -n "$REPO" ]]; then
|
|
PR=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["pull_request"]["number"])' "$EVENT")
|
|
ACTION_DIR="${GATEKEEP_ACTION:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
TMP=$(mktemp)
|
|
# Prefer .diff endpoint (works on Gitea + GitHub)
|
|
SERVER="${GITHUB_SERVER_URL:-}"
|
|
TOKEN="${GITHUB_TOKEN:-${GITEA_TOKEN:-}}"
|
|
if [[ -n "$SERVER" ]]; then
|
|
curl -sS -H "Authorization: token ${TOKEN}" \
|
|
"${SERVER%/}/${REPO}/pulls/${PR}.diff" -o "$TMP" 2>/dev/null \
|
|
|| curl -sS -H "Authorization: token ${TOKEN}" \
|
|
"${GITHUB_API_URL%/}/repos/${REPO}/pulls/${PR}.diff" -o "$TMP" 2>/dev/null \
|
|
|| true
|
|
fi
|
|
if [[ ! -s "$TMP" ]]; then
|
|
python3 "$ACTION_DIR/scripts/forge_api.py" GET "/repos/${REPO}/pulls/${PR}.diff" >"$TMP" 2>/dev/null || true
|
|
fi
|
|
DIFF=$(cat "$TMP" 2>/dev/null || true)
|
|
# Only look at added lines
|
|
ADDED=$(grep -E '^\+' "$TMP" | grep -vE '^\+\+\+' || true)
|
|
check() {
|
|
local name="$1" pat="$2"
|
|
if printf '%s\n' "$ADDED" | grep -Eiq -- "$pat"; then
|
|
hits+=("$name")
|
|
fi
|
|
}
|
|
check "Hardcoded secrets/tokens pattern" '(api[_-]?key|secret|token|password|private[_-]?key)\s*[:=]\s*['\''\"]?[A-Za-z0-9_\-]{16,}'
|
|
check "Prompt injection via repo instruction files" '(ignore (all )?(previous|above) instructions|you are now|system prompt)'
|
|
check "Possible SQL injection concat" '(execute\(|query\(|raw\().*\+|(SELECT|DROP|UNION).*\+'
|
|
check "Possible command injection" '(exec\(|spawn\(|os\.system\(|subprocess\.[a-z]+\([^)]*shell\s*=\s*True)'
|
|
check "Unsafe deserialization" '(pickle\.loads|yaml\.load\(|unserialize\(|ObjectInputStream)'
|
|
rm -f "$TMP"
|
|
fi
|
|
|
|
HARD=false
|
|
if ((${#hits[@]} > 0)); then
|
|
HARD=true
|
|
fi
|
|
|
|
python3 - "$OUT" "$HARD" "${hits[@]}" <<'PY'
|
|
import json,sys
|
|
out=sys.argv[1]
|
|
hard=sys.argv[2]=="true"
|
|
hits=sys.argv[3:]
|
|
json.dump({"hard_fail": hard, "hits": hits, "checklist": [
|
|
"hardcoded secrets/tokens",
|
|
"prompt injection via repo files",
|
|
"auth bypass",
|
|
"unauthorized API access",
|
|
"billing logic bypass",
|
|
"CI workflow tampering",
|
|
"unsafe deserialization",
|
|
"SQL/command injection"
|
|
]}, open(out,"w"), indent=2)
|
|
print(f"security-scan hard_fail={hard} hits={len(hits)}", file=sys.stderr)
|
|
PY
|