@boyingliu01/xp-gate 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/gate-9.sh +142 -60
- package/bin/xp-gate.js +2 -2
- package/gate-9.sh +142 -59
- package/hooks/gate-9.sh +142 -59
- package/hooks/lib/coverage-total.sh +72 -0
- package/hooks/pre-commit +88 -27
- package/hooks/pre-push +32 -0
- package/lib/__tests__/arch.test.js +403 -0
- package/lib/__tests__/doctor-469-hook-env.test.js +200 -0
- package/lib/arch.js +336 -49
- package/lib/doctor.js +319 -13
- package/lib/mattpocock-license-verdict.json +1 -1
- package/package.json +1 -1
- package/plugins/dsh/src/command.test.ts +14 -2
- package/plugins/dsh/src/command.ts +8 -1
- package/plugins/dsh/src/index.test.ts +43 -26
- package/plugins/dsh/src/index.ts +164 -138
- package/plugins/opencode/README.md +5 -2
- package/plugins/opencode/index.ts +215 -164
package/adapters/gate-9.sh
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
# Detects security vulnerabilities (SQL injection, XSS, etc.) in staged files
|
|
4
4
|
# Tool: semgrep -- https://semgrep.dev
|
|
5
5
|
# ============================================================================
|
|
6
|
+
# #532: this is a legacy gate-9 fallback copy, kept alive by
|
|
7
|
+
# resolveGateScript(9) in TS-less deployments (xp-gate gate sast without the
|
|
8
|
+
# src/gates/gate-9.ts entry). The verdict semantics below are ported from
|
|
9
|
+
# gate-10.sh (the #518 fix, v0.21.0) — the verdict block must stay byte
|
|
10
|
+
# identical across githooks/gate-9.sh, githooks/gates/gate-9-sast-security.sh
|
|
11
|
+
# and githooks/adapters/gate-9.sh; parity is asserted by
|
|
12
|
+
# githooks/__tests__/issue-532-gate9-verdict.test.bats (AC-A-02-04).
|
|
6
13
|
|
|
7
14
|
2>&1 echo ""
|
|
8
15
|
2>&1 echo "→ Gate 9: Semgrep SAST Security Scan..."
|
|
@@ -32,74 +39,149 @@ else
|
|
|
32
39
|
echo " ⏭️ SKIPPED - SAST (no supported language files changed)"
|
|
33
40
|
GATE_9_STATUS="SKIP"
|
|
34
41
|
else
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
# --
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
# One path per line into an array. Quoting the newline-joined list would hand
|
|
43
|
+
# semgrep ONE argument containing newlines -> "Invalid scanning root" once
|
|
44
|
+
# >=2 files are staged (#490). Same defect class as the Gate 4 --files fix
|
|
45
|
+
# (#457): an array is the only form that keeps spaces intact AND splits files.
|
|
46
|
+
#
|
|
47
|
+
# Each element gets a ./ prefix: a tracked path may legitimately start with
|
|
48
|
+
# '-' (`git add -- ./-o.ts`), and bare in argv that element reads as an option
|
|
49
|
+
# and rewrites the scan it was meant to constrain. ./ is a path to every
|
|
50
|
+
# argument parser.
|
|
51
|
+
#
|
|
52
|
+
# The unquoted heredoc below expands $SEMGREP_FILES once, then feeds literal
|
|
53
|
+
# lines to `read`; bash never rescans the result for separators. Its correct
|
|
54
|
+
# reading rests on one property of the producer above, not of the loop:
|
|
55
|
+
# `git diff --cached --name-only` terminates paths with LF and quotes control
|
|
56
|
+
# characters (a CR in a name arrives as the four bytes \215, never as a bare
|
|
57
|
+
# CR), so one line is exactly one path. Asserted for spaces and a leading '-'
|
|
58
|
+
# by AC-490-04.
|
|
59
|
+
SEMGREP_ARGS=()
|
|
60
|
+
while IFS= read -r _semgrep_file; do
|
|
61
|
+
[ -n "$_semgrep_file" ] && SEMGREP_ARGS+=("./$_semgrep_file")
|
|
62
|
+
done <<EOF
|
|
63
|
+
$SEMGREP_FILES
|
|
64
|
+
EOF
|
|
41
65
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});" 2>/dev/null || echo "0")
|
|
66
|
+
# The verdict is parsed from the JSON with node (see below), so node is a
|
|
67
|
+
# hard dependency of this gate — check it BEFORE spending a semgrep run
|
|
68
|
+
# (#518 R2): without it we would silently SKIP every finding as
|
|
69
|
+
# "unparseable" and lose the whole security net on minimal CI images.
|
|
70
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
71
|
+
echo " ⚠️ WARN - node is not installed — Gate 9 needs it to parse semgrep's JSON verdict"
|
|
72
|
+
echo " Install node (https://nodejs.org); without it SAST findings cannot be classified"
|
|
73
|
+
echo " ⏭️ SKIPPED - SAST (node not installed)"
|
|
74
|
+
GATE_9_STATUS="SKIP"
|
|
75
|
+
else
|
|
53
76
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
# Run semgrep with JSON output. stdout and stderr are captured SEPARATELY:
|
|
78
|
+
# --json writes findings to stdout, progress/summary lines to stderr, and the
|
|
79
|
+
# old `2>&1` merge poisoned the very JSON the verdict now depends on (#518).
|
|
80
|
+
# SEMGREP_OUTPUT keeps its name and role — what the tool said, printed on the
|
|
81
|
+
# SKIP path (AC-475-04).
|
|
82
|
+
#
|
|
83
|
+
# #518: the exit code does NOT carry the verdict. semgrep exits 0 even with
|
|
84
|
+
# ERROR-severity findings, and its exit-code semantics have drifted across
|
|
85
|
+
# versions and platforms (upstream currently reports findings WITH --error
|
|
86
|
+
# as exit 1 and failures as 2, but that mapping is not something a gate
|
|
87
|
+
# should bet on). The JSON carries what the exit code never could —
|
|
88
|
+
# per-severity counts — so the verdict is read from the JSON and the exit
|
|
89
|
+
# code is demoted to "did the tool itself run". --error stays OFF: it would
|
|
90
|
+
# add nothing the JSON does not already provide, and keeping it off leaves
|
|
91
|
+
# failure exit codes unambiguous for the #475 diagnostics path.
|
|
92
|
+
SEMGREP_STDOUT_FILE=$(mktemp)
|
|
93
|
+
SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check "${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
|
|
94
|
+
SEMGREP_EXIT=$?
|
|
60
95
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
})
|
|
96
|
+
# One node pass over the JSON: first line = verdict counts, remaining lines =
|
|
97
|
+
# finding details. The severity vocabulary is the UNION of the two scales
|
|
98
|
+
# (#518): semgrep's own ERROR is its most severe tier (it maps to the old
|
|
99
|
+
# CRITICAL/HIGH → BLOCK intent), its WARNING/INFO align with the old
|
|
100
|
+
# MEDIUM/LOW → warnings-only tier. Anything OUTSIDE both lists (missing,
|
|
101
|
+
# empty, or a future/new tier) counts as BLOCKING — a security gate must
|
|
102
|
+
# fail closed on severities it does not recognize (#518 R2). Parsing
|
|
103
|
+
# failure degrades to PARSE=FAIL — a fail-closed SKIP below, never a PASS.
|
|
104
|
+
SEMGREP_VERDICT=$(node -e '
|
|
105
|
+
let d="";process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>{
|
|
106
|
+
let data;try{data=JSON.parse(d);}catch(e){console.log("PARSE=FAIL RESULTS=0 ERRORS=0 BLOCKING=0 WARNINGS=0");return;}
|
|
107
|
+
const results=data.results||[];const errors=data.errors||[];
|
|
108
|
+
const sev=r=>((r.extra&&r.extra.severity)||"").toUpperCase();
|
|
109
|
+
const WARNV=["WARNING","INFO","MEDIUM","LOW"];
|
|
110
|
+
let b=0,w=0;results.forEach(r=>{if(WARNV.indexOf(sev(r))>=0)w++;else b++;});
|
|
111
|
+
console.log("PARSE=OK RESULTS="+results.length+" ERRORS="+errors.length+" BLOCKING="+b+" WARNINGS="+w);
|
|
112
|
+
results.slice(0,5).forEach(r=>{const extra=r.extra||{};const s=sev(r)||"UNKNOWN";
|
|
113
|
+
console.log(" ["+s+"] "+(r.check_id||"unknown"));
|
|
114
|
+
console.log(" "+(r.path||"unknown")+":"+((r.start&&r.start.line)||"?")+" → "+(extra.message||"").slice(0,80));
|
|
115
|
+
console.log("");});
|
|
116
|
+
});' < "$SEMGREP_STDOUT_FILE") || SEMGREP_VERDICT="PARSE=FAIL RESULTS=0 ERRORS=0 BLOCKING=0 WARNINGS=0"
|
|
72
117
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
118
|
+
PARSE_LINE=$(printf '%s\n' "$SEMGREP_VERDICT" | sed -n '1p')
|
|
119
|
+
FINDING_DETAILS=$(printf '%s\n' "$SEMGREP_VERDICT" | sed -n '2,$p')
|
|
120
|
+
V_PARSE=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/^PARSE=\([A-Z]*\) .*/\1/p')
|
|
121
|
+
V_RESULTS=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* RESULTS=\([0-9]*\) .*/\1/p')
|
|
122
|
+
V_ERRORS=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* ERRORS=\([0-9]*\) .*/\1/p')
|
|
123
|
+
V_BLOCKING=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* BLOCKING=\([0-9]*\) .*/\1/p')
|
|
124
|
+
V_WARNINGS=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* WARNINGS=\([0-9]*\)$/\1/p')
|
|
125
|
+
# Keep the raw stdout tail for the unparseable branch's diagnostics BEFORE
|
|
126
|
+
# the temp file goes away (#518 R2: stderr alone often says nothing there).
|
|
127
|
+
SEMGREP_STDOUT_TAIL=$(tail -5 "$SEMGREP_STDOUT_FILE" 2>/dev/null)
|
|
128
|
+
rm -f "$SEMGREP_STDOUT_FILE"
|
|
129
|
+
|
|
130
|
+
# Fail closed: an unparseable payload OR an OK parse with any missing count
|
|
131
|
+
# field (verdict-line format drift) must SKIP, never silently degrade to
|
|
132
|
+
# PASS (#518).
|
|
133
|
+
if [ "$V_PARSE" != "OK" ] || [ -z "$V_RESULTS" ] || [ -z "$V_ERRORS" ] || [ -z "$V_BLOCKING" ] || [ -z "$V_WARNINGS" ]; then
|
|
134
|
+
# Unparseable output: nothing the exit code claims can be trusted (#518).
|
|
135
|
+
# Fail closed — SKIP with diagnostics, never a PASS.
|
|
136
|
+
echo " ⚠️ semgrep output unparseable or verdict fields missing (exit code ${SEMGREP_EXIT}) — cannot determine verdict"
|
|
137
|
+
if [ -n "$SEMGREP_OUTPUT" ]; then
|
|
138
|
+
echo " ── what semgrep reported on stderr ──"
|
|
139
|
+
printf '%s\n' "$SEMGREP_OUTPUT" | tail -10 | sed 's/^/ /'
|
|
140
|
+
fi
|
|
141
|
+
if [ -n "$SEMGREP_STDOUT_TAIL" ]; then
|
|
142
|
+
echo " ── what semgrep wrote to stdout (tail) ──"
|
|
143
|
+
printf '%s\n' "$SEMGREP_STDOUT_TAIL" | sed 's/^/ /'
|
|
144
|
+
fi
|
|
145
|
+
echo " ⏭️ SKIPPED - SAST (semgrep output unparseable)"
|
|
146
|
+
GATE_9_STATUS="SKIP"
|
|
147
|
+
elif [ "$V_RESULTS" -eq 0 ] && { [ "$V_ERRORS" -gt 0 ] || [ "$SEMGREP_EXIT" -ne 0 ]; }; then
|
|
148
|
+
# Nothing was scanned to a verdict: semgrep either failed outright
|
|
149
|
+
# (nonzero exit) or completed but reported scan errors for every input
|
|
150
|
+
# (#518 R2: exit 0 + results:[] + errors:[{...}] used to fall through to
|
|
151
|
+
# PASS). An operational failure must neither block nor claim a pass
|
|
152
|
+
# (#475 kept).
|
|
153
|
+
echo " ⚠️ semgrep exit=${SEMGREP_EXIT}, scan errors=${V_ERRORS}, findings=${V_RESULTS} — nothing to judge"
|
|
154
|
+
if [ -n "$SEMGREP_OUTPUT" ]; then
|
|
155
|
+
echo " ── what semgrep reported ──"
|
|
156
|
+
printf '%s\n' "$SEMGREP_OUTPUT" | tail -10 | sed 's/^/ /'
|
|
95
157
|
fi
|
|
96
|
-
else
|
|
97
|
-
# semgrep runtime error (timeout, config error, etc.)
|
|
98
|
-
echo " ⚠️ semgrep exited with code ${SEMGREP_EXIT} — skipping gate"
|
|
99
158
|
echo " ⏭️ SKIPPED - SAST (semgrep runtime error)"
|
|
100
159
|
GATE_9_STATUS="SKIP"
|
|
160
|
+
elif [ "$V_BLOCKING" -gt 0 ]; then
|
|
161
|
+
echo ""
|
|
162
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
163
|
+
echo " GATE 9: Semgrep Security Gate"
|
|
164
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
165
|
+
echo " CRITICAL/HIGH/ERROR: ${V_BLOCKING} ❌ BLOCKED"
|
|
166
|
+
echo " MEDIUM/LOW/WARNING/INFO: ${V_WARNINGS} ⚠️ warning"
|
|
167
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
168
|
+
echo " ❌ BLOCKED — blocking-severity vulnerability found"
|
|
169
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
170
|
+
echo "$FINDING_DETAILS"
|
|
171
|
+
echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
|
|
172
|
+
GATE_9_STATUS="FAIL"
|
|
173
|
+
exit 1
|
|
174
|
+
else
|
|
175
|
+
echo ""
|
|
176
|
+
echo " ✅ PASSED - No blocking vulnerabilities (files scanned: ${#SEMGREP_ARGS[@]})"
|
|
177
|
+
if [ "$V_WARNINGS" -gt 0 ]; then
|
|
178
|
+
echo " ⚠️ ${V_WARNINGS} non-blocking findings (warnings only)"
|
|
179
|
+
echo "$FINDING_DETAILS"
|
|
180
|
+
fi
|
|
181
|
+
GATE_9_STATUS="PASS"
|
|
101
182
|
fi
|
|
183
|
+
|
|
184
|
+
fi # node available
|
|
102
185
|
fi
|
|
103
186
|
fi
|
|
104
187
|
record_gate_audit "gate-9" "sast-security" "$GATE_9_STATUS" "0" "$GATE_9_START"
|
|
105
|
-
|
package/bin/xp-gate.js
CHANGED
|
@@ -168,9 +168,9 @@ const COMMANDS = {
|
|
|
168
168
|
usage: 'xp-gate principles <file_or_directory> [--format console|json|sarif]'
|
|
169
169
|
},
|
|
170
170
|
'arch': {
|
|
171
|
-
description: 'Run architecture validation (Gate 6 standalone,
|
|
171
|
+
description: 'Run architecture validation (Gate 6 standalone; built-in rules, baseline ratchet when .architecture-baseline.json exists)',
|
|
172
172
|
run: subargs => arch(subargs).then(code => process.exit(code)),
|
|
173
|
-
usage: 'xp-gate arch [--config <path>]'
|
|
173
|
+
usage: 'xp-gate arch [--use-config <path>] (default: pinned local @archlinter/cli built-in rules; --use-config is the opt-in legacy config path)'
|
|
174
174
|
},
|
|
175
175
|
'check-alignment': {
|
|
176
176
|
description: 'Run test-specification alignment check (deterministic, no LLM)',
|
package/gate-9.sh
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
# Detects security vulnerabilities (SQL injection, XSS, etc.) in staged files
|
|
4
4
|
# Tool: semgrep -- https://semgrep.dev
|
|
5
5
|
# ============================================================================
|
|
6
|
+
# #532: this is a legacy gate-9 fallback copy, kept alive by
|
|
7
|
+
# resolveGateScript(9) in TS-less deployments (xp-gate gate sast without the
|
|
8
|
+
# src/gates/gate-9.ts entry). The verdict semantics below are ported from
|
|
9
|
+
# gate-10.sh (the #518 fix, v0.21.0) — the verdict block must stay byte
|
|
10
|
+
# identical across githooks/gate-9.sh, githooks/gates/gate-9-sast-security.sh
|
|
11
|
+
# and githooks/adapters/gate-9.sh; parity is asserted by
|
|
12
|
+
# githooks/__tests__/issue-532-gate9-verdict.test.bats (AC-A-02-04).
|
|
6
13
|
|
|
7
14
|
2>&1 echo ""
|
|
8
15
|
2>&1 echo "→ Gate 9: Semgrep SAST Security Scan..."
|
|
@@ -32,73 +39,149 @@ else
|
|
|
32
39
|
echo " ⏭️ SKIPPED - SAST (no supported language files changed)"
|
|
33
40
|
GATE_9_STATUS="SKIP"
|
|
34
41
|
else
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
# --
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
# One path per line into an array. Quoting the newline-joined list would hand
|
|
43
|
+
# semgrep ONE argument containing newlines -> "Invalid scanning root" once
|
|
44
|
+
# >=2 files are staged (#490). Same defect class as the Gate 4 --files fix
|
|
45
|
+
# (#457): an array is the only form that keeps spaces intact AND splits files.
|
|
46
|
+
#
|
|
47
|
+
# Each element gets a ./ prefix: a tracked path may legitimately start with
|
|
48
|
+
# '-' (`git add -- ./-o.ts`), and bare in argv that element reads as an option
|
|
49
|
+
# and rewrites the scan it was meant to constrain. ./ is a path to every
|
|
50
|
+
# argument parser.
|
|
51
|
+
#
|
|
52
|
+
# The unquoted heredoc below expands $SEMGREP_FILES once, then feeds literal
|
|
53
|
+
# lines to `read`; bash never rescans the result for separators. Its correct
|
|
54
|
+
# reading rests on one property of the producer above, not of the loop:
|
|
55
|
+
# `git diff --cached --name-only` terminates paths with LF and quotes control
|
|
56
|
+
# characters (a CR in a name arrives as the four bytes \215, never as a bare
|
|
57
|
+
# CR), so one line is exactly one path. Asserted for spaces and a leading '-'
|
|
58
|
+
# by AC-490-04.
|
|
59
|
+
SEMGREP_ARGS=()
|
|
60
|
+
while IFS= read -r _semgrep_file; do
|
|
61
|
+
[ -n "$_semgrep_file" ] && SEMGREP_ARGS+=("./$_semgrep_file")
|
|
62
|
+
done <<EOF
|
|
63
|
+
$SEMGREP_FILES
|
|
64
|
+
EOF
|
|
41
65
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});" 2>/dev/null || echo "0")
|
|
66
|
+
# The verdict is parsed from the JSON with node (see below), so node is a
|
|
67
|
+
# hard dependency of this gate — check it BEFORE spending a semgrep run
|
|
68
|
+
# (#518 R2): without it we would silently SKIP every finding as
|
|
69
|
+
# "unparseable" and lose the whole security net on minimal CI images.
|
|
70
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
71
|
+
echo " ⚠️ WARN - node is not installed — Gate 9 needs it to parse semgrep's JSON verdict"
|
|
72
|
+
echo " Install node (https://nodejs.org); without it SAST findings cannot be classified"
|
|
73
|
+
echo " ⏭️ SKIPPED - SAST (node not installed)"
|
|
74
|
+
GATE_9_STATUS="SKIP"
|
|
75
|
+
else
|
|
53
76
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
# Run semgrep with JSON output. stdout and stderr are captured SEPARATELY:
|
|
78
|
+
# --json writes findings to stdout, progress/summary lines to stderr, and the
|
|
79
|
+
# old `2>&1` merge poisoned the very JSON the verdict now depends on (#518).
|
|
80
|
+
# SEMGREP_OUTPUT keeps its name and role — what the tool said, printed on the
|
|
81
|
+
# SKIP path (AC-475-04).
|
|
82
|
+
#
|
|
83
|
+
# #518: the exit code does NOT carry the verdict. semgrep exits 0 even with
|
|
84
|
+
# ERROR-severity findings, and its exit-code semantics have drifted across
|
|
85
|
+
# versions and platforms (upstream currently reports findings WITH --error
|
|
86
|
+
# as exit 1 and failures as 2, but that mapping is not something a gate
|
|
87
|
+
# should bet on). The JSON carries what the exit code never could —
|
|
88
|
+
# per-severity counts — so the verdict is read from the JSON and the exit
|
|
89
|
+
# code is demoted to "did the tool itself run". --error stays OFF: it would
|
|
90
|
+
# add nothing the JSON does not already provide, and keeping it off leaves
|
|
91
|
+
# failure exit codes unambiguous for the #475 diagnostics path.
|
|
92
|
+
SEMGREP_STDOUT_FILE=$(mktemp)
|
|
93
|
+
SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check "${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
|
|
94
|
+
SEMGREP_EXIT=$?
|
|
60
95
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
})
|
|
96
|
+
# One node pass over the JSON: first line = verdict counts, remaining lines =
|
|
97
|
+
# finding details. The severity vocabulary is the UNION of the two scales
|
|
98
|
+
# (#518): semgrep's own ERROR is its most severe tier (it maps to the old
|
|
99
|
+
# CRITICAL/HIGH → BLOCK intent), its WARNING/INFO align with the old
|
|
100
|
+
# MEDIUM/LOW → warnings-only tier. Anything OUTSIDE both lists (missing,
|
|
101
|
+
# empty, or a future/new tier) counts as BLOCKING — a security gate must
|
|
102
|
+
# fail closed on severities it does not recognize (#518 R2). Parsing
|
|
103
|
+
# failure degrades to PARSE=FAIL — a fail-closed SKIP below, never a PASS.
|
|
104
|
+
SEMGREP_VERDICT=$(node -e '
|
|
105
|
+
let d="";process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>{
|
|
106
|
+
let data;try{data=JSON.parse(d);}catch(e){console.log("PARSE=FAIL RESULTS=0 ERRORS=0 BLOCKING=0 WARNINGS=0");return;}
|
|
107
|
+
const results=data.results||[];const errors=data.errors||[];
|
|
108
|
+
const sev=r=>((r.extra&&r.extra.severity)||"").toUpperCase();
|
|
109
|
+
const WARNV=["WARNING","INFO","MEDIUM","LOW"];
|
|
110
|
+
let b=0,w=0;results.forEach(r=>{if(WARNV.indexOf(sev(r))>=0)w++;else b++;});
|
|
111
|
+
console.log("PARSE=OK RESULTS="+results.length+" ERRORS="+errors.length+" BLOCKING="+b+" WARNINGS="+w);
|
|
112
|
+
results.slice(0,5).forEach(r=>{const extra=r.extra||{};const s=sev(r)||"UNKNOWN";
|
|
113
|
+
console.log(" ["+s+"] "+(r.check_id||"unknown"));
|
|
114
|
+
console.log(" "+(r.path||"unknown")+":"+((r.start&&r.start.line)||"?")+" → "+(extra.message||"").slice(0,80));
|
|
115
|
+
console.log("");});
|
|
116
|
+
});' < "$SEMGREP_STDOUT_FILE") || SEMGREP_VERDICT="PARSE=FAIL RESULTS=0 ERRORS=0 BLOCKING=0 WARNINGS=0"
|
|
72
117
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
118
|
+
PARSE_LINE=$(printf '%s\n' "$SEMGREP_VERDICT" | sed -n '1p')
|
|
119
|
+
FINDING_DETAILS=$(printf '%s\n' "$SEMGREP_VERDICT" | sed -n '2,$p')
|
|
120
|
+
V_PARSE=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/^PARSE=\([A-Z]*\) .*/\1/p')
|
|
121
|
+
V_RESULTS=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* RESULTS=\([0-9]*\) .*/\1/p')
|
|
122
|
+
V_ERRORS=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* ERRORS=\([0-9]*\) .*/\1/p')
|
|
123
|
+
V_BLOCKING=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* BLOCKING=\([0-9]*\) .*/\1/p')
|
|
124
|
+
V_WARNINGS=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* WARNINGS=\([0-9]*\)$/\1/p')
|
|
125
|
+
# Keep the raw stdout tail for the unparseable branch's diagnostics BEFORE
|
|
126
|
+
# the temp file goes away (#518 R2: stderr alone often says nothing there).
|
|
127
|
+
SEMGREP_STDOUT_TAIL=$(tail -5 "$SEMGREP_STDOUT_FILE" 2>/dev/null)
|
|
128
|
+
rm -f "$SEMGREP_STDOUT_FILE"
|
|
129
|
+
|
|
130
|
+
# Fail closed: an unparseable payload OR an OK parse with any missing count
|
|
131
|
+
# field (verdict-line format drift) must SKIP, never silently degrade to
|
|
132
|
+
# PASS (#518).
|
|
133
|
+
if [ "$V_PARSE" != "OK" ] || [ -z "$V_RESULTS" ] || [ -z "$V_ERRORS" ] || [ -z "$V_BLOCKING" ] || [ -z "$V_WARNINGS" ]; then
|
|
134
|
+
# Unparseable output: nothing the exit code claims can be trusted (#518).
|
|
135
|
+
# Fail closed — SKIP with diagnostics, never a PASS.
|
|
136
|
+
echo " ⚠️ semgrep output unparseable or verdict fields missing (exit code ${SEMGREP_EXIT}) — cannot determine verdict"
|
|
137
|
+
if [ -n "$SEMGREP_OUTPUT" ]; then
|
|
138
|
+
echo " ── what semgrep reported on stderr ──"
|
|
139
|
+
printf '%s\n' "$SEMGREP_OUTPUT" | tail -10 | sed 's/^/ /'
|
|
140
|
+
fi
|
|
141
|
+
if [ -n "$SEMGREP_STDOUT_TAIL" ]; then
|
|
142
|
+
echo " ── what semgrep wrote to stdout (tail) ──"
|
|
143
|
+
printf '%s\n' "$SEMGREP_STDOUT_TAIL" | sed 's/^/ /'
|
|
144
|
+
fi
|
|
145
|
+
echo " ⏭️ SKIPPED - SAST (semgrep output unparseable)"
|
|
146
|
+
GATE_9_STATUS="SKIP"
|
|
147
|
+
elif [ "$V_RESULTS" -eq 0 ] && { [ "$V_ERRORS" -gt 0 ] || [ "$SEMGREP_EXIT" -ne 0 ]; }; then
|
|
148
|
+
# Nothing was scanned to a verdict: semgrep either failed outright
|
|
149
|
+
# (nonzero exit) or completed but reported scan errors for every input
|
|
150
|
+
# (#518 R2: exit 0 + results:[] + errors:[{...}] used to fall through to
|
|
151
|
+
# PASS). An operational failure must neither block nor claim a pass
|
|
152
|
+
# (#475 kept).
|
|
153
|
+
echo " ⚠️ semgrep exit=${SEMGREP_EXIT}, scan errors=${V_ERRORS}, findings=${V_RESULTS} — nothing to judge"
|
|
154
|
+
if [ -n "$SEMGREP_OUTPUT" ]; then
|
|
155
|
+
echo " ── what semgrep reported ──"
|
|
156
|
+
printf '%s\n' "$SEMGREP_OUTPUT" | tail -10 | sed 's/^/ /'
|
|
95
157
|
fi
|
|
96
|
-
else
|
|
97
|
-
# semgrep runtime error (timeout, config error, etc.)
|
|
98
|
-
echo " ⚠️ semgrep exited with code ${SEMGREP_EXIT} — skipping gate"
|
|
99
158
|
echo " ⏭️ SKIPPED - SAST (semgrep runtime error)"
|
|
100
159
|
GATE_9_STATUS="SKIP"
|
|
160
|
+
elif [ "$V_BLOCKING" -gt 0 ]; then
|
|
161
|
+
echo ""
|
|
162
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
163
|
+
echo " GATE 9: Semgrep Security Gate"
|
|
164
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
165
|
+
echo " CRITICAL/HIGH/ERROR: ${V_BLOCKING} ❌ BLOCKED"
|
|
166
|
+
echo " MEDIUM/LOW/WARNING/INFO: ${V_WARNINGS} ⚠️ warning"
|
|
167
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
168
|
+
echo " ❌ BLOCKED — blocking-severity vulnerability found"
|
|
169
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
170
|
+
echo "$FINDING_DETAILS"
|
|
171
|
+
echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
|
|
172
|
+
GATE_9_STATUS="FAIL"
|
|
173
|
+
exit 1
|
|
174
|
+
else
|
|
175
|
+
echo ""
|
|
176
|
+
echo " ✅ PASSED - No blocking vulnerabilities (files scanned: ${#SEMGREP_ARGS[@]})"
|
|
177
|
+
if [ "$V_WARNINGS" -gt 0 ]; then
|
|
178
|
+
echo " ⚠️ ${V_WARNINGS} non-blocking findings (warnings only)"
|
|
179
|
+
echo "$FINDING_DETAILS"
|
|
180
|
+
fi
|
|
181
|
+
GATE_9_STATUS="PASS"
|
|
101
182
|
fi
|
|
183
|
+
|
|
184
|
+
fi # node available
|
|
102
185
|
fi
|
|
103
186
|
fi
|
|
104
187
|
record_gate_audit "gate-9" "sast-security" "$GATE_9_STATUS" "0" "$GATE_9_START"
|