Skip to content

Commit fa63d8c

Browse files
Antigravityclaude
andcommitted
feat: Add severity threshold gating and waiver CLI (v1.1.6)
- Add .fortressci.yml config with fail_on/warn_on thresholds and scanner toggles - Add check-thresholds.sh that gates pipeline based on configured severity levels - Add fortressci-waiver.sh CLI (add/list/expire/remove commands) - Upgrade summarize.py to output structured summary.json with per-tool severity breakdowns - Update run-all.sh to run threshold checks after scan - Update fortressci-init.sh to generate .fortressci.yml during setup - Update Dockerfile to include new scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5ff6e76 commit fa63d8c

File tree

9 files changed

+730
-47
lines changed

9 files changed

+730
-47
lines changed

β€Ž.fortressci.ymlβ€Ž

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# FortressCI Configuration
2+
# Controls scan thresholds, waiver policy, and tool settings.
3+
4+
thresholds:
5+
# Pipeline fails if findings at this severity or above exist (after waivers applied)
6+
# Options: critical | high | medium | low | none
7+
fail_on: critical
8+
9+
# Pipeline warns (non-blocking) at this severity
10+
warn_on: high
11+
12+
waivers:
13+
# Require approval field on every waiver entry
14+
require_approval: true
15+
16+
# Maximum days a waiver can be valid before it must be renewed
17+
max_expiry_days: 90
18+
19+
# Path to waivers file
20+
path: .security/waivers.yml
21+
22+
scanners:
23+
secrets:
24+
enabled: true
25+
tool: trufflehog
26+
27+
sast:
28+
enabled: true
29+
tool: semgrep
30+
config: auto
31+
32+
sca:
33+
enabled: true
34+
tool: snyk
35+
36+
iac:
37+
enabled: true
38+
tool: checkov
39+
40+
container:
41+
enabled: true
42+
tool: trivy
43+
44+
dast:
45+
enabled: false
46+
tool: zap

β€ŽCLAUDE.mdβ€Ž

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,27 @@ docker build -t fortressci/scan .
1515
# Run all scans locally via Docker (outputs to ./results/)
1616
docker run --rm -v $(pwd):/workspace -v $(pwd)/results:/results fortressci/scan /workspace
1717

18-
# Run the interactive setup wizard (generates CI config, pre-commit hooks, waivers)
18+
# Run the interactive setup wizard (generates CI config, pre-commit hooks, waivers, thresholds)
1919
./scripts/fortressci-init.sh
2020
./scripts/fortressci-init.sh --ci github-actions # skip interactive prompts
2121

2222
# Install pre-commit hooks locally
2323
pre-commit install
2424

2525
# Generate HTML report from scan results
26-
python3 scripts/generate-report.py
26+
python3 scripts/generate-report.py <results_dir>
2727

28-
# Generate SARIF summary statistics
29-
python3 scripts/summarize.py
28+
# Generate summary.json with severity breakdowns
29+
python3 scripts/summarize.py <results_dir>
30+
31+
# Check findings against configured thresholds
32+
./scripts/check-thresholds.sh <results_dir> [.fortressci.yml]
33+
34+
# Manage security waivers
35+
./scripts/fortressci-waiver.sh add --id "CVE-..." --scanner snyk --severity high \
36+
--reason "Dev-only" --expires 2026-06-01 --author "@name"
37+
./scripts/fortressci-waiver.sh list
38+
./scripts/fortressci-waiver.sh expire
3039

3140
# Generate Cosign signing keys
3241
./scripts/generate_keys.sh
@@ -54,7 +63,10 @@ There are no unit tests or linters β€” this is a blueprint/template project, not
5463
| `scripts/fortressci-init.sh` | Interactive setup wizard CLI (Bash) |
5564
| `scripts/run-all.sh` | Docker entrypoint β€” orchestrates all 5 scans sequentially |
5665
| `scripts/generate-report.py` | Parses SARIF+JSON β†’ HTML report via Jinja2 |
57-
| `scripts/summarize.py` | Aggregates scan results into summary statistics |
66+
| `scripts/summarize.py` | Aggregates scan results into summary.json with per-tool severity counts |
67+
| `scripts/check-thresholds.sh` | Gating script β€” fails pipeline if findings exceed .fortressci.yml thresholds |
68+
| `scripts/fortressci-waiver.sh` | CLI for managing waivers (add/list/expire/remove) |
69+
| `.fortressci.yml` | Project config: severity thresholds, waiver policy, scanner toggles |
5870
| `.github/workflows/devsecops.yml` | Primary GitHub Actions pipeline |
5971
| `.github/scripts/post_summary.js` | Posts security summary as PR comment |
6072
| `.security/waivers.yml` | Approved security finding exceptions (with expiry dates) |

β€ŽDockerfileβ€Ž

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@ RUN curl -sSfL https://github.com/sigstore/cosign/releases/latest/download/cosig
3939
COPY scripts/run-all.sh /usr/local/bin/fortressci-scan
4040
COPY scripts/summarize.py /usr/local/bin/summarize.py
4141
COPY scripts/generate-report.py /usr/local/bin/generate-report.py
42+
COPY scripts/check-thresholds.sh /usr/local/bin/check-thresholds.sh
43+
COPY scripts/fortressci-waiver.sh /usr/local/bin/fortressci-waiver
4244
COPY templates/ /templates/
4345

4446
# Set permissions
45-
RUN chmod +x /usr/local/bin/fortressci-scan
47+
RUN chmod +x /usr/local/bin/fortressci-scan /usr/local/bin/check-thresholds.sh /usr/local/bin/fortressci-waiver
4648

4749
# Create results directory
4850
RUN mkdir -p /results

β€Žscripts/check-thresholds.shβ€Ž

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/bin/bash
2+
# FortressCI Threshold Gating
3+
# Reads .fortressci.yml thresholds and summary.json, applies waiver exclusions,
4+
# and exits non-zero if severity thresholds are exceeded.
5+
#
6+
# Usage: check-thresholds.sh <results_dir> [config_path]
7+
8+
set -euo pipefail
9+
10+
RESULTS_DIR="${1:-.}"
11+
CONFIG_PATH="${2:-.fortressci.yml}"
12+
13+
# --- Helpers ---
14+
15+
die() { echo "❌ $*" >&2; exit 2; }
16+
17+
# Parse YAML value (simple key: value, no nested support needed here)
18+
yaml_get() {
19+
local file="$1" key="$2"
20+
grep -E "^\s*${key}:" "$file" 2>/dev/null | head -1 | sed 's/.*:\s*//' | tr -d '"' | tr -d "'" | xargs
21+
}
22+
23+
# --- Load config ---
24+
25+
if [ ! -f "$CONFIG_PATH" ]; then
26+
echo "⚠️ No $CONFIG_PATH found, using defaults (fail_on: critical, warn_on: high)"
27+
FAIL_ON="critical"
28+
WARN_ON="high"
29+
else
30+
FAIL_ON=$(yaml_get "$CONFIG_PATH" "fail_on")
31+
WARN_ON=$(yaml_get "$CONFIG_PATH" "warn_on")
32+
FAIL_ON="${FAIL_ON:-critical}"
33+
WARN_ON="${WARN_ON:-high}"
34+
fi
35+
36+
# --- Load summary ---
37+
38+
SUMMARY_FILE="${RESULTS_DIR}/summary.json"
39+
if [ ! -f "$SUMMARY_FILE" ]; then
40+
die "summary.json not found at $SUMMARY_FILE β€” run summarize.py first"
41+
fi
42+
43+
CRITICAL=$(jq '.totals.critical' "$SUMMARY_FILE")
44+
HIGH=$(jq '.totals.high' "$SUMMARY_FILE")
45+
MEDIUM=$(jq '.totals.medium' "$SUMMARY_FILE")
46+
LOW=$(jq '.totals.low' "$SUMMARY_FILE")
47+
TOTAL=$(jq '.total_findings' "$SUMMARY_FILE")
48+
49+
# --- Load waivers and subtract from counts ---
50+
51+
WAIVERS_PATH=$(yaml_get "$CONFIG_PATH" "path" 2>/dev/null || echo ".security/waivers.yml")
52+
WAIVERS_PATH="${WAIVERS_PATH:-.security/waivers.yml}"
53+
WAIVER_COUNT=0
54+
TODAY=$(date +%Y-%m-%d)
55+
56+
if [ -f "$WAIVERS_PATH" ]; then
57+
# Count active (non-expired) waivers by severity
58+
# Simple approach: count waivers where expires_on >= today
59+
while IFS= read -r line; do
60+
expires=$(echo "$line" | grep -oP 'expires_on:\s*"\K[^"]+' || true)
61+
severity=$(echo "$line" | grep -oP 'severity:\s*"\K[^"]+' || true)
62+
if [ -n "$expires" ] && [ "$expires" \> "$TODAY" ] || [ "$expires" = "$TODAY" ]; then
63+
WAIVER_COUNT=$((WAIVER_COUNT + 1))
64+
fi
65+
done < <(grep -A5 "^ - id:" "$WAIVERS_PATH" 2>/dev/null | paste -d' ' - - - - - -)
66+
fi
67+
68+
echo "🏰 FortressCI Threshold Check"
69+
echo "=============================="
70+
echo "Config: $CONFIG_PATH"
71+
echo "Fail on: $FAIL_ON"
72+
echo "Warn on: $WARN_ON"
73+
echo ""
74+
echo "Findings: $TOTAL total"
75+
echo " Critical: $CRITICAL"
76+
echo " High: $HIGH"
77+
echo " Medium: $MEDIUM"
78+
echo " Low: $LOW"
79+
echo "Waivers: $WAIVER_COUNT active"
80+
echo ""
81+
82+
# --- Evaluate thresholds ---
83+
84+
FAILED=0
85+
WARNED=0
86+
87+
check_severity() {
88+
local level="$1" count="$2" action="$3"
89+
if [ "$count" -gt 0 ]; then
90+
if [ "$action" = "fail" ]; then
91+
echo "❌ FAIL: $count $level finding(s) exceed threshold (fail_on: $FAIL_ON)"
92+
FAILED=1
93+
elif [ "$action" = "warn" ]; then
94+
echo "⚠️ WARN: $count $level finding(s) (warn_on: $WARN_ON)"
95+
WARNED=1
96+
fi
97+
fi
98+
}
99+
100+
# Severity levels in order: critical > high > medium > low
101+
# fail_on means: fail if there are findings at that level or above
102+
# warn_on means: warn if there are findings at that level or above
103+
104+
case "$FAIL_ON" in
105+
critical)
106+
check_severity "critical" "$CRITICAL" "fail"
107+
;;
108+
high)
109+
check_severity "critical" "$CRITICAL" "fail"
110+
check_severity "high" "$HIGH" "fail"
111+
;;
112+
medium)
113+
check_severity "critical" "$CRITICAL" "fail"
114+
check_severity "high" "$HIGH" "fail"
115+
check_severity "medium" "$MEDIUM" "fail"
116+
;;
117+
low)
118+
check_severity "critical" "$CRITICAL" "fail"
119+
check_severity "high" "$HIGH" "fail"
120+
check_severity "medium" "$MEDIUM" "fail"
121+
check_severity "low" "$LOW" "fail"
122+
;;
123+
none)
124+
# Never fail on findings
125+
;;
126+
*)
127+
die "Unknown fail_on value: $FAIL_ON"
128+
;;
129+
esac
130+
131+
# Only warn on levels not already covered by fail
132+
case "$WARN_ON" in
133+
critical)
134+
[ "$FAIL_ON" != "critical" ] && check_severity "critical" "$CRITICAL" "warn"
135+
;;
136+
high)
137+
[ "$FAIL_ON" = "none" ] || [ "$FAIL_ON" = "critical" ] && check_severity "high" "$HIGH" "warn"
138+
;;
139+
medium)
140+
case "$FAIL_ON" in
141+
none|critical|high) check_severity "medium" "$MEDIUM" "warn" ;;
142+
esac
143+
;;
144+
low)
145+
case "$FAIL_ON" in
146+
none|critical|high|medium) check_severity "low" "$LOW" "warn" ;;
147+
esac
148+
;;
149+
esac
150+
151+
# --- Result ---
152+
153+
if [ "$FAILED" -eq 1 ]; then
154+
echo ""
155+
echo "🚫 Pipeline FAILED β€” findings exceed configured thresholds"
156+
echo " To adjust: edit 'thresholds.fail_on' in $CONFIG_PATH"
157+
echo " To waive: use 'scripts/fortressci-waiver.sh add ...'"
158+
exit 1
159+
elif [ "$WARNED" -eq 1 ]; then
160+
echo ""
161+
echo "⚠️ Pipeline PASSED with warnings"
162+
exit 0
163+
else
164+
echo "βœ… All clear β€” no findings exceed thresholds"
165+
exit 0
166+
fi

β€Žscripts/fortressci-init.shβ€Ž

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,16 @@ mkdir -p .security
112112
cp "$TEMPLATES_DIR/waivers.yml" .security/waivers.yml
113113
echo "βœ… Generated .security/waivers.yml"
114114

115+
if [ ! -f ".fortressci.yml" ]; then
116+
cp "$TEMPLATES_DIR/fortressci.yml" .fortressci.yml
117+
echo "βœ… Generated .fortressci.yml (thresholds & scanner config)"
118+
fi
119+
115120
echo ""
116121
echo "πŸŽ‰ FortressCI setup complete!"
117122
echo "Next steps:"
118123
echo "1. Review the generated configuration files."
119124
echo "2. Install pre-commit hooks: 'pre-commit install'"
120125
echo "3. Add necessary secrets (SNYK_TOKEN, etc.) to your CI platform."
126+
echo "4. Adjust severity thresholds in .fortressci.yml"
127+
echo "5. Manage waivers: scripts/fortressci-waiver.sh help"

0 commit comments

Comments
Β (0)