Compare commits

...
Author SHA1 Message Date
james.maesandClaude Opus 5.5 4aeccb801e fix(scan): make the security-scan and sast-scan gates fail on findings
- run_trivy.sh: `trivy fs` ran without --exit-code, so it always exited 0
  and security-scan passed with HIGH/CRITICAL findings in the report. Add
  --exit-code 1 and print the findings table on failure.
- Semgrep 1.61.1 cannot parse the current `auto` ruleset, so sast-scan ran
  zero rules on zero files and passed. Move the executor to
  semgrep/semgrep:1.181.0, and fail the job when the report shows no files
  scanned or a rule configuration error (even in warn-only mode). Print a
  findings summary in the job output.
- tests/test_scan_gates.sh covers both gates with tool stubs.

Consumers pick this up when they bump their pinned orb version; repos with
existing HIGH/CRITICAL or Semgrep findings will need to triage them then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:42:51 -05:00
4 changed files with 133 additions and 13 deletions

No files matched your search

+2 -2
View File
@@ -3,8 +3,8 @@ description: >
parameters:
version:
type: string
default: "1.61.1"
default: "1.181.0"
description: Semgrep image version tag.
docker:
- image: returntocorp/semgrep:<< parameters.version >>
- image: semgrep/semgrep:<< parameters.version >>
resource_class: medium
+34 -11
View File
@@ -15,22 +15,45 @@ munitor_header "run_semgrep"
CONFIG="${SEMGREP_CONFIG:-auto}"
FAIL_ON_FINDINGS="${SEMGREP_FAIL_ON_FINDINGS:-true}"
REPORT="${SEMGREP_REPORT:-/tmp/semgrep-results.json}"
echo "Running Semgrep SAST scan (config: ${CONFIG}, fail_on_findings: ${FAIL_ON_FINDINGS})..."
EXIT_CODE=0
semgrep ci \
--config "${CONFIG}" \
--json \
--output /tmp/semgrep-results.json || {
EXIT_CODE=$?
if [[ "${FAIL_ON_FINDINGS}" == "true" ]]; then
echo "ERROR: Semgrep found issues."
echo "Review the report at: /tmp/semgrep-results.json"
exit "${EXIT_CODE}"
else
echo "WARNING: Semgrep found issues (warn-only mode, not failing build)."
echo "Review the report at: /tmp/semgrep-results.json"
fi
}
--output "${REPORT}" || EXIT_CODE=$?
# Summarise the report, and fail a scan that ran no rules or scanned no files:
# semgrep exits 0 when the downloaded rules fail to parse (an outdated semgrep),
# which used to pass this job without scanning anything.
python3 - "${REPORT}" <<'PY'
import json, sys
report = json.load(open(sys.argv[1]))
scanned = len(report.get("paths", {}).get("scanned", []))
results = report.get("results", [])
errors = report.get("errors", [])
rule_errors = [e for e in errors if "rule" in str(e.get("type", "")).lower()
or "config" in str(e.get("message", "")).lower()]
print(f"Semgrep: {scanned} files scanned, {len(results)} findings, {len(errors)} errors.")
for r in results[:50]:
print(f" {r.get('check_id')} {r.get('path')}:{r.get('start', {}).get('line')}")
if scanned == 0 or rule_errors:
for e in rule_errors[:5]:
print(f" error: {str(e.get('message', e))[:300]}")
print("ERROR: Semgrep did not run its rules (no files scanned or invalid rule configuration).")
sys.exit(2)
PY
if [[ "${EXIT_CODE}" -ne 0 ]]; then
if [[ "${FAIL_ON_FINDINGS}" == "true" ]]; then
echo "ERROR: Semgrep found issues."
echo "Review the report at: ${REPORT}"
exit "${EXIT_CODE}"
fi
echo "WARNING: Semgrep found issues (warn-only mode, not failing build)."
echo "Review the report at: ${REPORT}"
fi
echo "Semgrep scan completed."
+4
View File
@@ -36,13 +36,17 @@ fi
echo "Running Trivy filesystem scan..."
# --exit-code 1: without it trivy exits 0 even when it reports HIGH/CRITICAL
# findings, so this job could never fail.
trivy fs \
--format json \
--output /tmp/trivy-results.json \
--severity HIGH,CRITICAL \
--exit-code 1 \
. || {
EXIT_CODE=$?
echo "Trivy found vulnerabilities. See /tmp/trivy-results.json"
trivy fs --severity HIGH,CRITICAL . || true
exit "${EXIT_CODE}"
}
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
# The filesystem Trivy scan and the Semgrep scan must fail when they find
# something, and Semgrep must fail when it ran no rules. Local tool stubs; no
# network needed.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC_SCRIPTS="$(cd "${SCRIPT_DIR}/../src/scripts" && pwd)"
WORK=$(mktemp -d)
trap 'rm -rf "${WORK}"' EXIT
PASS=0
FAIL=0
pass() { echo "PASS"; PASS=$((PASS + 1)); }
fail() { echo "FAIL"; FAIL=$((FAIL + 1)); }
mkdir -p "${WORK}/home/bin" "${WORK}/bin" "${WORK}/repo"
cat > "${WORK}/home/bin/trivy" <<'EOF'
#!/usr/bin/env bash
printf 'trivy %s\n' "$*" >> "${CALL_LOG}"
# Real trivy exits 0 on findings unless --exit-code is given.
for a in "$@"; do [[ "$a" == --exit-code ]] && exit "${SCAN_EXIT:-0}"; done
exit 0
EOF
cat > "${WORK}/bin/semgrep" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "${SEMGREP_FAKE_REPORT}" > "${SEMGREP_REPORT}"
exit "${SEMGREP_FAKE_EXIT:-0}"
EOF
chmod +x "${WORK}/home/bin/trivy" "${WORK}/bin/semgrep"
export PATH="${WORK}/bin:${PATH}" HOME="${WORK}/home" CALL_LOG="${WORK}/calls"
export SEMGREP_REPORT="${WORK}/semgrep.json"
cd "${WORK}/repo"
echo "=== Scan gate tests ==="
echo -n " TEST: trivy fs fails the job on HIGH/CRITICAL findings... "
if ! SCAN_EXIT=1 bash "${SRC_SCRIPTS}/run_trivy.sh" > "${WORK}/trivy.log" 2>&1 &&
grep -q '^trivy fs .*--severity HIGH,CRITICAL --exit-code 1 ' "${CALL_LOG}"; then
pass
else
fail
fi
echo -n " TEST: trivy fs passes a clean scan... "
if SCAN_EXIT=0 bash "${SRC_SCRIPTS}/run_trivy.sh" > "${WORK}/trivy.log" 2>&1; then
pass
else
fail
fi
if command -v python3 &>/dev/null; then
CLEAN='{"results":[],"errors":[],"paths":{"scanned":["a.java","b.java"]}}'
FOUND='{"results":[{"check_id":"java.lang.security.x","path":"a.java","start":{"line":3}}],"errors":[],"paths":{"scanned":["a.java"]}}'
NORULES='{"results":[],"errors":[{"type":"InvalidRuleSchemaError","message":"invalid configuration file found (1 configs were invalid)"}],"paths":{"scanned":[]}}'
echo -n " TEST: semgrep passes a clean scan... "
if SEMGREP_FAKE_REPORT="${CLEAN}" bash "${SRC_SCRIPTS}/run_semgrep.sh" > "${WORK}/sg.log" 2>&1 &&
grep -q '2 files scanned, 0 findings' "${WORK}/sg.log"; then
pass
else
fail
fi
echo -n " TEST: semgrep fails on findings and lists them... "
if ! SEMGREP_FAKE_REPORT="${FOUND}" SEMGREP_FAKE_EXIT=1 bash "${SRC_SCRIPTS}/run_semgrep.sh" > "${WORK}/sg.log" 2>&1 &&
grep -q 'java.lang.security.x a.java:3' "${WORK}/sg.log"; then
pass
else
fail
fi
echo -n " TEST: semgrep warn-only mode passes with findings... "
if SEMGREP_FAIL_ON_FINDINGS=false SEMGREP_FAKE_REPORT="${FOUND}" SEMGREP_FAKE_EXIT=1 \
bash "${SRC_SCRIPTS}/run_semgrep.sh" > "${WORK}/sg.log" 2>&1; then
pass
else
fail
fi
echo -n " TEST: semgrep fails when no rules ran, even with exit 0 and warn-only... "
if ! SEMGREP_FAIL_ON_FINDINGS=false SEMGREP_FAKE_REPORT="${NORULES}" SEMGREP_FAKE_EXIT=0 \
bash "${SRC_SCRIPTS}/run_semgrep.sh" > "${WORK}/sg.log" 2>&1 &&
grep -q 'did not run its rules' "${WORK}/sg.log"; then
pass
else
fail
fi
else
echo " SKIP: semgrep gate tests need python3"
fi
echo ""
echo "Results: ${PASS} passed, ${FAIL} failed"
[[ "${FAIL}" -eq 0 ]]