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
52 changes: 46 additions & 6 deletions .github/workflows/hypatia-scan-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
workflow_call:
inputs:
block-on-high:
description: Refuse any high or critical finding, without requiring a baseline
description: Refuse current high or critical findings after validated baseline filtering; no baseline is required
type: boolean
required: false
default: false
Expand Down Expand Up @@ -175,13 +175,16 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: hyperpolymath/standards
ref: main
# Includes the filter's completion output and literal-glob matching.
# Keep the helper immutable and compatible with the strict gate.
ref: 6b0bfce648cf9b306693cea0af0bd5f6fc9fea0f
path: .standards-checkout
sparse-checkout: |
scripts
sparse-checkout-cone-mode: false

- name: Filter SARIF through the baseline before upload
id: filter_baseline
if: always()
run: |
# ⚠ WITHOUT THIS, ACKNOWLEDGING A FINDING DOES NOT UNBLOCK ANYTHING.
Expand Down Expand Up @@ -246,7 +249,7 @@ jobs:
retention-days: 90

- name: Gate on baseline (blocking when a baseline is committed)
if: steps.scan.outputs.findings_count > 0
if: '!inputs.block-on-high && steps.scan.outputs.findings_count > 0'
run: |
# Wave-8 gate promotion: when the calling repo commits a
# .hypatia-baseline.json (schema: .machine_readable/hypatia-baseline.
Expand All @@ -269,14 +272,51 @@ jobs:
- name: Block high and critical findings when requested
id: blocking-findings
if: inputs.block-on-high
env:
HYPATIA_BASELINE_FILTERED: ${{ steps.filter_baseline.outputs.filtered }}
run: |
set -euo pipefail
count=$(jq '[.[] | select(.severity == "high" or .severity == "critical")] | length' hypatia-findings.json)
# Use the scanner's authoritative SARIF projection, after the same
# baseline filter used for the Security tab. Raw JSON also contains
# code_scanning_alerts: historical GitHub alert summaries, including
# alerts fixed by this PR that cannot close until it reaches main.
# Hypatia already excludes those meta-rules in its SARIF renderer.
# Counting them here creates a circular merge dependency. Raw JSON
# remains in the artifact for the fleet's historical-debt review.
# Validate a present baseline even when there are no current errors.
# Advisory mode validates without gating historical meta-findings;
# the final filtered SARIF below supplies the blocking decision.
if [ -f .hypatia-baseline.json ]; then
if [ ! -f scripts/apply-baseline.sh ]; then
echo "::error::A committed baseline requires scripts/apply-baseline.sh"
exit 2
fi
bash scripts/apply-baseline.sh hypatia-findings.relativized.json .hypatia-baseline.json advisory >/dev/null
if [ "${HYPATIA_BASELINE_FILTERED:-}" != "true" ]; then
echo "::error::Baseline validation passed but SARIF filtering did not complete"
exit 2
fi
fi
# Missing/truncated output must never become a clean security gate.
if ! jq -se '
length == 1 and (.[0] |
.version == "2.1.0" and (.runs | type == "array" and length > 0) and
all(.runs[];
.tool.driver.name == "Hypatia" and
(.results | type == "array") and
all(.results[];
type == "object" and (.ruleId | type == "string" and length > 0) and
(.level as $level | ["error", "warning", "note", "none"] | index($level) != null))))
' hypatia.sarif >/dev/null; then
echo "::error::Hypatia did not produce one valid SARIF findings document"
exit 2
fi
count=$(jq '[.runs[].results[] | select(.level == "error")] | length' hypatia.sarif)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if [ "$count" -gt 0 ]; then
echo "::error::Hypatia found $count high or critical finding(s); see the scan artifact"
echo "::error::Hypatia found $count current high or critical finding(s); see the SARIF artifact"
exit 1
fi
echo "Hypatia blocking gate: no high or critical findings"
echo "Hypatia blocking gate: no current high or critical findings after baseline filtering"

- name: Check for critical issues (ADVISORY — does not gate)
if: '!inputs.block-on-high && steps.scan.outputs.critical > 0'
Expand Down
22 changes: 18 additions & 4 deletions scripts/apply-baseline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ FINDINGS_FILE="${1:-}"
BASELINE_FILE="${2:-}"
MODE="${3:-advisory}"
BLOCKING_THRESHOLD="${BLOCKING_THRESHOLD:-high}"
case "$MODE" in
advisory|blocking) ;;
*) echo "error: invalid baseline mode: $MODE" >&2; exit 2 ;;
esac
case "$BLOCKING_THRESHOLD" in
info|low|medium|high|critical) ;;
*) echo "error: invalid blocking threshold: $BLOCKING_THRESHOLD" >&2; exit 2 ;;
esac
TODAY="$(date -u +%Y-%m-%d)"

if [[ -z "$FINDINGS_FILE" || -z "$BASELINE_FILE" ]]; then
Expand Down Expand Up @@ -152,6 +160,15 @@ EXPIRED_COUNT=$((EXPIRED_COUNT - ACTIVE_COUNT))
ANNOTATED="$(jq -n \
--argjson findings "$FINDINGS_JSON" \
--argjson baseline "$ACTIVE_BASELINE" '
# Tokenise the two supported wildcards; every other character is literal.
# No sentinel substitution: a real filename may contain DOUBLESTAR.
def glob_regex:
[scan("\\*\\*|\\*|[^*]")
| if . == "**" then ".*"
elif . == "*" then "[^/]*"
elif inside(".\\+?^$()[]{}|") then "\\" + .
else . end]
| "\\A" + join("") + "\\z";
# Two captures are essential here:
# `f as $finding` — without this, references like `f.file` inside the
# select() get re-evaluated against the current baseline entry (the
Expand All @@ -177,10 +194,7 @@ ANNOTATED="$(jq -n \
| $pat != null
and ($finding.file | test(
$pat
| gsub("\\*\\*"; "DOUBLESTAR")
| gsub("\\*"; "[^/]*")
| gsub("DOUBLESTAR"; ".*")
| "^" + . + "$"
| glob_regex
))
)
)
Expand Down
Binary file modified scripts/filter-sarif-by-baseline.sh
Binary file not shown.
33 changes: 30 additions & 3 deletions scripts/tests/apply-baseline-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APPLY="$SCRIPT_DIR/../apply-baseline.sh"
APPLY="${APPLY_BASELINE_TARGET:-$SCRIPT_DIR/../apply-baseline.sh}"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT

Expand All @@ -25,7 +25,7 @@
assert_status() {
local label="$1" findings="$2" baseline="$3" expected="$4"
local got
got=$("$APPLY" "$findings" "$baseline" advisory 2>/dev/null \
got=$(bash "$APPLY" "$findings" "$baseline" advisory \
| jq -r '"\(.findings_suppressed | length),\(.findings_kept | length)"')
if [ "$got" = "$expected" ]; then
echo "PASS: $label (suppressed,kept=$got)"
Expand Down Expand Up @@ -98,14 +98,41 @@
cat > "$WORK/baseline6-invalid.json" <<'EOF'
[{"severity":"medium","rule_module":"implementation_inside_canon","type":"HYP--S009","file":"spec/Cargo.toml"}]
EOF
if "$APPLY" "$WORK/findings6.json" "$WORK/baseline6-invalid.json" advisory >/dev/null 2>&1; then
if bash "$APPLY" "$WORK/findings6.json" "$WORK/baseline6-invalid.json" advisory >/dev/null 2>&1; then
echo "FAIL: malformed HYP--S009 rule type was accepted"
fail=$((fail + 1))
else
echo "PASS: malformed HYP--S009 rule type is rejected"
pass=$((pass + 1))
fi

# Regex metacharacters and the former sentinel must remain literal glob text.
for literal in 'src/foo.rs' 'src/a+b(1)[2]{x}^$?.rs' 'src/DOUBLESTAR.rs'; do
jq -n --arg file "$literal" '[{severity:"high",rule_module:"cicd_rules",type:"banned_language_file",file:$file}]' > "$WORK/literal.json"
jq -n --arg pattern "$literal" '[{severity:"high",rule_module:"cicd_rules",type:"banned_language_file",file_pattern:$pattern}]' > "$WORK/literal-baseline.json"
assert_status "literal glob matches itself: $literal" "$WORK/literal.json" "$WORK/literal-baseline.json" "1,0"
done
jq -n '[{severity:"high",rule_module:"cicd_rules",type:"banned_language_file",file_pattern:"src/foo.rs"}]' > "$WORK/literal-baseline.json"
for unrelated in 'src/fooXrs' $'src/foo.rs\nother'; do
jq -n --arg file "$unrelated" '[{severity:"high",rule_module:"cicd_rules",type:"banned_language_file",file:$file}]' > "$WORK/unrelated.json"
assert_status "literal glob rejects unrelated path" "$WORK/unrelated.json" "$WORK/literal-baseline.json" "0,1"
done

assert_invalid_option() {
local label="$1" mode="$2" threshold="$3" status=0
BLOCKING_THRESHOLD="$threshold" bash "$APPLY" "$WORK/findings1.json" "$WORK/empty.json" "$mode" > "$WORK/invalid.out" 2> "$WORK/invalid.err" || status=$?
if [ "$status" -eq 2 ]; then

Check failure on line 124 in scripts/tests/apply-baseline-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaB7A0VwjHwcUD3oTfzD&open=AaB7A0VwjHwcUD3oTfzD&pullRequest=742
echo "PASS: $label rejected as invalid configuration"
pass=$((pass + 1))
else
echo "FAIL: $label returned $status (expected 2)"
cat "$WORK/invalid.err"
fail=$((fail + 1))
fi
}
assert_invalid_option "invalid mode" bypass high
assert_invalid_option "invalid threshold" blocking nonsense

echo
echo "Total: $pass passed, $fail failed"
[ "$fail" -eq 0 ]
16 changes: 16 additions & 0 deletions scripts/tests/filter-sarif-by-baseline-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,21 @@ echo '[{"severity":"low","rule_module":"code_safety","type":"unwrap_without_chec
bash "$S" "$T/in.sarif" "$T/f.json" "$T/b.json" "$T/out.sarif" >/dev/null 2>&1
ck "an empty SARIF survives filtering" 0 "$(count "$T/out.sarif")"

# The blocking workflow needs affirmative evidence that filtering completed.
export GITHUB_OUTPUT="$T/filter-output"
: > "$GITHUB_OUTPUT"
mk_sarif; mk_findings
bash "$S" "$T/in.sarif" "$T/f.json" "$T/b.json" "$T/out.sarif" > "$T/filter.log" 2>&1
ck "successful filtering is reported" 'filtered=true' "$(cat "$GITHUB_OUTPUT")"
: > "$GITHUB_OUTPUT"
bash "$S" "$T/in.sarif" "$T/f.json" "$T/bad.json" "$T/out.sarif" > "$T/filter.log" 2>&1
ck "invalid baseline cannot report filtering success" '' "$(cat "$GITHUB_OUTPUT")"
: > "$GITHUB_OUTPUT"
APPLY_BASELINE="$T/missing-validator" bash "$S" "$T/in.sarif" "$T/f.json" "$T/b.json" "$T/out.sarif" > "$T/filter.log" 2>&1
ck "missing validator cannot report filtering success" '' "$(cat "$GITHUB_OUTPUT")"
: > "$GITHUB_OUTPUT"
bash "$S" "$T/in.sarif" "$T/empty.json" "$T/b.json" "$T/out.sarif" > "$T/filter.log" 2>&1
ck "completed zero-match filtering is reported" 'filtered=true' "$(cat "$GITHUB_OUTPUT")"

printf '\n %d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]
43 changes: 43 additions & 0 deletions scripts/tests/hypatia-blocking-gate-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ check() {
printf '%s' "$payload" > hypatia-findings.json
fi
if bash validate.sh >result.log 2>&1; then
# Fixture for the authoritative renderer's severity projection. The
# separate controls below exercise malformed SARIF and historical echoes.
jq '{version:"2.1.0", runs:[{tool:{driver:{name:"Hypatia"}}, results:
[.[] | {ruleId:"hypatia/control/planted", level:
(if .severity == "critical" or .severity == "high" then "error"
elif .severity == "medium" then "warning" else "note" end)}]}]}' \
hypatia-findings.json > hypatia.sarif
if bash gate.sh >>result.log 2>&1; then actual=0; else actual=$?; fi
else
actual=$?
Expand All @@ -48,3 +55,39 @@ check 'null is not a findings array' 2 'null'
check 'unknown severity refuses' 2 '[{"severity":"unknown"}]'
check 'missing severity refuses' 2 '[{}]'
check 'multiple JSON documents refuse' 2 '[] []'

sarif_check() {
local name=$1 expected=$2 payload=$3 actual
if [[ "$payload" = MISSING ]]; then
rm -f hypatia.sarif
else
printf '%s' "$payload" > hypatia.sarif
fi
if bash gate.sh >result.log 2>&1; then actual=0; else actual=$?; fi
if [[ "$actual" -ne "$expected" ]]; then
printf 'FAIL: %s: expected %s, got %s\n' "$name" "$expected" "$actual"
cat result.log
exit 1
fi
printf 'PASS: %s\n' "$name"
}
clean='{"version":"2.1.0","runs":[{"tool":{"driver":{"name":"Hypatia"}},"results":[]}]}'
printf '%s' '[{"rule_module":"code_scanning_alerts","type":"CSA003","severity":"high"}]' > hypatia-findings.json
sarif_check 'historical echo does not block a clean current scan' 0 "$clean"
sarif_check 'missing SARIF refuses' 2 MISSING
sarif_check 'empty SARIF refuses' 2 ''
sarif_check 'truncated SARIF refuses' 2 '{"version":'
sarif_check 'multiple SARIF documents refuse' 2 "$clean $clean"
sarif_check 'empty runs refuse' 2 '{"version":"2.1.0","runs":[]}'
sarif_check 'missing results refuse' 2 '{"version":"2.1.0","runs":[{"tool":{"driver":{"name":"Hypatia"}}}]}'
sarif_check 'unknown result level refuses' 2 '{"version":"2.1.0","runs":[{"tool":{"driver":{"name":"Hypatia"}},"results":[{"ruleId":"control","level":"unknown"}]}]}'
printf '%s' '[]' > .hypatia-baseline.json
sarif_check 'baseline without validator refuses' 2 "$clean"
mkdir scripts
cp "$repo/scripts/apply-baseline.sh" scripts/apply-baseline.sh
printf '%s' '[]' > hypatia-findings.relativized.json
sarif_check 'unconfirmed baseline filtering refuses' 2 "$clean"
export HYPATIA_BASELINE_FILTERED=true
sarif_check 'valid baseline accepted' 0 "$clean"
printf '%s' '{}' > .hypatia-baseline.json
sarif_check 'malformed baseline refuses even without findings' 2 "$clean"
Loading