@boyingliu01/xp-gate 0.30.2 → 0.30.4
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/iac.sh +45 -4
- package/adapters/java.sh +82 -16
- package/adapters/powershell.sh +70 -9
- package/gate-10.sh +110 -13
- package/gate-3.sh +17 -0
- package/gate-4.sh +12 -0
- package/gate-7.sh +4 -0
- package/hooks/gate-10.sh +110 -13
- package/hooks/gate-3.sh +17 -0
- package/hooks/gate-4.sh +12 -0
- package/hooks/gate-7.sh +4 -0
- package/hooks/hook-module-resolution.sh +14 -0
- package/hooks/lib/coverage-total.sh +1 -1
- package/hooks/pre-commit +138 -17
- package/hooks/pre-push +150 -9
- package/hooks/sprint-gate.sh +81 -2
- package/lib/__tests__/hook-health.test.js +85 -0
- package/lib/coverage-total.sh +92 -0
- package/lib/doctor.js +44 -0
- package/lib/hook-health.js +121 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/mutation/__tests__/gate-m-survivors.test.ts +133 -0
- package/mutation/gate-m.ts +114 -11
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +2 -2
- package/sprint-gate.sh +81 -2
package/adapters/iac.sh
CHANGED
|
@@ -64,10 +64,29 @@ run_static_analysis() {
|
|
|
64
64
|
if command -v checkov >/dev/null 2>&1; then
|
|
65
65
|
echo "Running checkov IaC security scan..."
|
|
66
66
|
|
|
67
|
-
# checkov 3.x
|
|
68
|
-
#
|
|
67
|
+
# checkov 3.x exits 0 on "found failures" (we parse the output for those),
|
|
68
|
+
# so the status cannot be the only signal. It is still NOT discardable:
|
|
69
|
+
# exit 2 means checkov itself crashed -- e.g. AttributeError from a broken
|
|
70
|
+
# detect_secrets dependency -- and a crashed scanner has not judged
|
|
71
|
+
# anything. Counting a traceback as findings invents security issues; a
|
|
72
|
+
# traceback that happens to lack the marker reports a clean pass. Both are
|
|
73
|
+
# wrong, so a crash is tracked separately and answered as "unusable" (#552).
|
|
69
74
|
local checkov_log=$(mktemp)
|
|
70
75
|
local checkov_args="--compact"
|
|
76
|
+
local CHECKOV_CRASHED=0
|
|
77
|
+
local checkov_scan_count=0
|
|
78
|
+
|
|
79
|
+
# Record a checkov run's exit status and flag a crash. Codes 0 and 1 are
|
|
80
|
+
# normal (1 = findings); anything else, or any traceback in the log, is the
|
|
81
|
+
# tool failing rather than reporting.
|
|
82
|
+
record_checkov_run() {
|
|
83
|
+
local status=$1 file=$2
|
|
84
|
+
checkov_scan_count=$((checkov_scan_count + 1))
|
|
85
|
+
if [ "$status" -ne 0 ] && [ "$status" -ne 1 ]; then
|
|
86
|
+
CHECKOV_CRASHED=1
|
|
87
|
+
echo " ⚠️ checkov exited $status on $file — the scanner failed, this is not a finding"
|
|
88
|
+
fi
|
|
89
|
+
}
|
|
71
90
|
|
|
72
91
|
# Run checkov on all detected IaC files
|
|
73
92
|
local tf_files=$(echo "$iac_files" | grep "TERRAFORM:" | sed 's/TERRAFORM://')
|
|
@@ -79,6 +98,7 @@ run_static_analysis() {
|
|
|
79
98
|
for file in $tf_files; do
|
|
80
99
|
if [ -f "$file" ]; then
|
|
81
100
|
checkov --file "$file" $checkov_args >> "$checkov_log" 2>&1
|
|
101
|
+
record_checkov_run $? "$file"
|
|
82
102
|
fi
|
|
83
103
|
done
|
|
84
104
|
fi
|
|
@@ -99,6 +119,7 @@ run_static_analysis() {
|
|
|
99
119
|
for file in $k8s_only; do
|
|
100
120
|
if [ -f "$file" ]; then
|
|
101
121
|
checkov --file "$file" $checkov_args --framework kubernetes >> "$checkov_log" 2>&1
|
|
122
|
+
record_checkov_run $? "$file"
|
|
102
123
|
fi
|
|
103
124
|
done
|
|
104
125
|
fi
|
|
@@ -108,6 +129,7 @@ run_static_analysis() {
|
|
|
108
129
|
for file in $gh_files; do
|
|
109
130
|
if [ -f "$file" ]; then
|
|
110
131
|
checkov --file "$file" $checkov_args --framework github_actions >> "$checkov_log" 2>&1
|
|
132
|
+
record_checkov_run $? "$file"
|
|
111
133
|
fi
|
|
112
134
|
done
|
|
113
135
|
fi
|
|
@@ -118,20 +140,39 @@ run_static_analysis() {
|
|
|
118
140
|
for file in $docker_files; do
|
|
119
141
|
if [ -f "$file" ]; then
|
|
120
142
|
checkov --file "$file" $checkov_args >> "$checkov_log" 2>&1
|
|
143
|
+
record_checkov_run $? "$file"
|
|
121
144
|
fi
|
|
122
145
|
done
|
|
123
146
|
fi
|
|
124
147
|
|
|
125
148
|
# Print captured output then check for failures
|
|
126
149
|
cat "$checkov_log"
|
|
150
|
+
|
|
151
|
+
# A crash is answered before any parsing. The traceback is not evidence of
|
|
152
|
+
# anything: its text decides the verdict if we let grep see it.
|
|
153
|
+
if [ "$CHECKOV_CRASHED" -eq 1 ]; then
|
|
154
|
+
rm -f "$checkov_log"
|
|
155
|
+
echo ""
|
|
156
|
+
echo "❌ BLOCKED - checkov could not run (the scanner crashed, so nothing was scanned)."
|
|
157
|
+
echo " This is NOT a security finding. Fix the environment, usually:"
|
|
158
|
+
echo " pip install -U checkov detect-secrets"
|
|
159
|
+
echo " (A common cause is a detect_secrets version whose FileType enum lacks JSON.)"
|
|
160
|
+
echo " To accept the risk for this commit: XP_GATE_IAC_SOFT=1"
|
|
161
|
+
if [ "${XP_GATE_IAC_SOFT:-}" = "1" ]; then
|
|
162
|
+
echo "⚠️ XP_GATE_IAC_SOFT=1: unusable scanner downgraded to WARN"
|
|
163
|
+
return 3
|
|
164
|
+
fi
|
|
165
|
+
return 2
|
|
166
|
+
fi
|
|
167
|
+
|
|
127
168
|
local failed_count=$(grep -c "FAILED for resource" "$checkov_log" 2>/dev/null || true)
|
|
128
169
|
rm -f "$checkov_log"
|
|
129
170
|
|
|
130
171
|
if [ "$failed_count" -gt 0 ]; then
|
|
131
|
-
echo "❌ $failed_count security issue(s) found by checkov. Blocking commit."
|
|
172
|
+
echo "❌ $failed_count security issue(s) found by checkov across $checkov_scan_count file(s). Blocking commit."
|
|
132
173
|
exit_code=1
|
|
133
174
|
fi
|
|
134
|
-
echo "checkov scan completed."
|
|
175
|
+
echo "checkov scan completed ($checkov_scan_count file(s) scanned)."
|
|
135
176
|
return $exit_code
|
|
136
177
|
fi
|
|
137
178
|
|
package/adapters/java.sh
CHANGED
|
@@ -106,10 +106,23 @@ java_staged_files() {
|
|
|
106
106
|
# counts from the object count -> unusable -> BLOCK;
|
|
107
107
|
# - a truncated response (no closing bracket) is unusable -> BLOCK.
|
|
108
108
|
# Prints the violation (object) count on success; returns 1 if unusable.
|
|
109
|
+
#
|
|
110
|
+
# On rejection, a one-line reason goes to STDERR naming the invariant that
|
|
111
|
+
# failed plus the measured counts (#545). Before this, every rejection looked
|
|
112
|
+
# identical to the operator as "not a usable violations JSON", which is why
|
|
113
|
+
# #545 was filed against a code path that was never involved. The number the
|
|
114
|
+
# callers capture comes from stdout, so diagnostics on stderr change nothing
|
|
115
|
+
# for `count=$(wc_java_json_violation_count ...)`.
|
|
116
|
+
wc_java_diag() {
|
|
117
|
+
echo " wc-java JSON rejected: $*" >&2
|
|
118
|
+
}
|
|
109
119
|
# ---------------------------------------------------------------------------
|
|
110
120
|
wc_java_json_violation_count() {
|
|
111
121
|
local json_file="$1"
|
|
112
|
-
[ -s "$json_file" ]
|
|
122
|
+
if [ ! -s "$json_file" ]; then
|
|
123
|
+
wc_java_diag "empty or missing response file ($json_file)"
|
|
124
|
+
return 1
|
|
125
|
+
fi
|
|
113
126
|
|
|
114
127
|
# Two shapes are accepted, each closed by its own bracket:
|
|
115
128
|
# - bare array `[ ... ]` — the originally pinned contract ([] = clean);
|
|
@@ -122,9 +135,18 @@ wc_java_json_violation_count() {
|
|
|
122
135
|
head1=$(head -c 1 "$json_file")
|
|
123
136
|
tail=$(tr -d '[:space:]' < "$json_file" | tail -c 1)
|
|
124
137
|
case "$head1" in
|
|
125
|
-
"[") shape=array
|
|
126
|
-
|
|
127
|
-
|
|
138
|
+
"[") shape=array
|
|
139
|
+
if [ "$tail" != "]" ]; then
|
|
140
|
+
wc_java_diag "array response is unterminated (first char '[', tail '$tail' not ']') — truncated output?"
|
|
141
|
+
return 1
|
|
142
|
+
fi ;;
|
|
143
|
+
"{") shape=object
|
|
144
|
+
if [ "$tail" != "}" ]; then
|
|
145
|
+
wc_java_diag "object response is unterminated (first char '{', tail '$tail' not '}') — truncated output?"
|
|
146
|
+
return 1
|
|
147
|
+
fi ;;
|
|
148
|
+
*) wc_java_diag "response does not start with a JSON bracket (entry char '$head1') — a banner/log line on stdout would cause this"
|
|
149
|
+
return 1 ;;
|
|
128
150
|
esac
|
|
129
151
|
|
|
130
152
|
local structural obj_count file_count rule_count sev_count
|
|
@@ -155,15 +177,36 @@ wc_java_json_violation_count() {
|
|
|
155
177
|
file_count=$(grep -o '"file"' <<< "$structural" | wc -l | tr -d '[:space:]')
|
|
156
178
|
rule_count=$(grep -o '"rule"' <<< "$structural" | wc -l | tr -d '[:space:]')
|
|
157
179
|
sev_count=$(grep -o '"severity"' <<< "$structural" | wc -l | tr -d '[:space:]')
|
|
158
|
-
[ "$file_count" -
|
|
159
|
-
|
|
180
|
+
if [ "$file_count" -ne "$rule_count" ] || [ "$file_count" -ne "$sev_count" ]; then
|
|
181
|
+
wc_java_diag "issue keys are out of sync (file=$file_count rule=$rule_count severity=$sev_count) — an issue record is missing a required key"
|
|
182
|
+
return 1
|
|
183
|
+
fi
|
|
160
184
|
# Everything from the "issues" member onward (the tool emits it last; if
|
|
161
185
|
# a future format moves it, keys before it fall outside record parsing ->
|
|
162
186
|
# sum mismatch -> BLOCK, fail closed). Flatten to one line first: real
|
|
163
187
|
# output is pretty-printed across lines.
|
|
164
188
|
flattened=$(tr '\n' ' ' <<< "$structural")
|
|
165
189
|
tail_from_issues=$(sed -n 's/.*"issues"[[:space:]]*:[[:space:]]*\[/\n/p' <<< "$flattened")
|
|
166
|
-
[ -
|
|
190
|
+
if [ -z "$tail_from_issues" ]; then
|
|
191
|
+
# `"issues": null` is a legitimate zero-violation report, not a broken one
|
|
192
|
+
# (#545). The Go producer's nil slice marshals to null, and it does so
|
|
193
|
+
# whenever the scan hid at least one info-level finding -- so a file whose
|
|
194
|
+
# only problems are info-level gets null while a fully clean file gets [].
|
|
195
|
+
# Whichever branch produced it, null means "no visible violations".
|
|
196
|
+
if sed -n 's/.*"issues"[[:space:]]*:[[:space:]]*null.*/found/p' <<< "$flattened" | grep -q found; then
|
|
197
|
+
# null is only trustworthy when the summary agrees there is nothing to
|
|
198
|
+
# report. A nonzero total_issues with null is the lying-tool shape.
|
|
199
|
+
null_total=$(sed -n 's/.*"total_issues"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' <<< "$flattened")
|
|
200
|
+
if [ -n "$null_total" ] && [ "$null_total" -gt 0 ]; then
|
|
201
|
+
wc_java_diag "issues is null but total_issues=$null_total — the tool claims violations it did not report"
|
|
202
|
+
return 1
|
|
203
|
+
fi
|
|
204
|
+
echo 0
|
|
205
|
+
return 0
|
|
206
|
+
fi
|
|
207
|
+
wc_java_diag 'no "issues" array found after the summary object — the response is not the {summary, issues} contract'
|
|
208
|
+
return 1
|
|
209
|
+
fi
|
|
167
210
|
records=$(awk '
|
|
168
211
|
{ rest = $0
|
|
169
212
|
depth = 0; rec = ""
|
|
@@ -182,15 +225,17 @@ wc_java_json_violation_count() {
|
|
|
182
225
|
cf=$(grep -o '"file"' <<< "$_rec" | wc -l | tr -d '[:space:]')
|
|
183
226
|
cr=$(grep -o '"rule"' <<< "$_rec" | wc -l | tr -d '[:space:]')
|
|
184
227
|
cs=$(grep -o '"severity"' <<< "$_rec" | wc -l | tr -d '[:space:]')
|
|
185
|
-
[ "$cf" -
|
|
186
|
-
|
|
187
|
-
|
|
228
|
+
if [ "$cf" -ne 1 ] || [ "$cr" -ne 1 ] || [ "$cs" -ne 1 ]; then
|
|
229
|
+
wc_java_diag "issue record ${rec_count} has the wrong key multiplicity (file=$cf rule=$cr severity=$cs, each must be 1)"
|
|
230
|
+
return 1
|
|
231
|
+
fi
|
|
188
232
|
rf=$((rf + cf)); rr=$((rr + cr)); rs=$((rs + cs))
|
|
189
233
|
rec_count=$((rec_count + 1))
|
|
190
234
|
done <<< "$records"
|
|
191
|
-
[ "$rf" -
|
|
192
|
-
|
|
193
|
-
|
|
235
|
+
if [ "$rf" -ne "$file_count" ] || [ "$rr" -ne "$rule_count" ] || [ "$rs" -ne "$sev_count" ]; then
|
|
236
|
+
wc_java_diag "per-record key totals do not match the document (records rf=$rf rr=$rr rs=$rs vs document file=$file_count rule=$rule_count severity=$sev_count) — a required key appears outside an issue record"
|
|
237
|
+
return 1
|
|
238
|
+
fi
|
|
194
239
|
echo "$rec_count"
|
|
195
240
|
return 0
|
|
196
241
|
fi
|
|
@@ -199,9 +244,10 @@ wc_java_json_violation_count() {
|
|
|
199
244
|
file_count=$(grep -o '"file"' <<< "$structural" | wc -l | tr -d '[:space:]')
|
|
200
245
|
rule_count=$(grep -o '"rule"' <<< "$structural" | wc -l | tr -d '[:space:]')
|
|
201
246
|
sev_count=$(grep -o '"severity"' <<< "$structural" | wc -l | tr -d '[:space:]')
|
|
202
|
-
[ "$obj_count" -
|
|
203
|
-
|
|
204
|
-
|
|
247
|
+
if [ "$obj_count" -ne "$file_count" ] || [ "$obj_count" -ne "$rule_count" ] || [ "$obj_count" -ne "$sev_count" ]; then
|
|
248
|
+
wc_java_diag "array elements and keys are out of sync (objects=$obj_count file=$file_count rule=$rule_count severity=$sev_count)"
|
|
249
|
+
return 1
|
|
250
|
+
fi
|
|
205
251
|
|
|
206
252
|
echo "$obj_count"
|
|
207
253
|
}
|
|
@@ -223,6 +269,26 @@ run_wc_java_lint() {
|
|
|
223
269
|
return 9 # not installed -> caller falls back to the legacy chain
|
|
224
270
|
fi
|
|
225
271
|
|
|
272
|
+
# #550: --staged means "only the Java files in the index", and wc-java-lint
|
|
273
|
+
# 0.4.1 honours that for checkstyle, PMD, lizard and simian -- but spotbugs
|
|
274
|
+
# scans the whole target/classes tree regardless. Measured with only
|
|
275
|
+
# docs/_tmp_probe.md staged: three tools reported "skipped: no Java files"
|
|
276
|
+
# while spotbugs reported "checked 1 .class file, found 3 issues" from a class
|
|
277
|
+
# compiled earlier. The verdict then blocks a docs commit over a warning that
|
|
278
|
+
# predates it and that its author never touched.
|
|
279
|
+
#
|
|
280
|
+
# The adapter cannot fix the tool, but it can refuse to relay a verdict the
|
|
281
|
+
# tool had no basis to produce. With no staged Java file there is nothing this
|
|
282
|
+
# gate is entitled to say, so it says nothing -- out loud, not silently.
|
|
283
|
+
local staged_java
|
|
284
|
+
staged_java=$(java_staged_files 2>/dev/null | grep -c '.' || true)
|
|
285
|
+
if [ "${staged_java:-0}" -eq 0 ]; then
|
|
286
|
+
echo "⏭️ SKIPPED - Java static analysis: no staged .java files (wc-java-lint --staged)"
|
|
287
|
+
echo " (spotbugs scans all of target/classes regardless of --staged, so its"
|
|
288
|
+
echo " verdict is withheld here - upstream defect, see xp-gate #550)"
|
|
289
|
+
return 0
|
|
290
|
+
fi
|
|
291
|
+
|
|
226
292
|
timeout_s=$(resolve_wc_java_lint_timeout)
|
|
227
293
|
out_file=$(mktemp "${TMPDIR:-/tmp}/wc-java-lint.XXXXXX")
|
|
228
294
|
err_file="${out_file}.err"
|
package/adapters/powershell.sh
CHANGED
|
@@ -17,16 +17,46 @@
|
|
|
17
17
|
# SKIP for now.
|
|
18
18
|
|
|
19
19
|
# Detect PowerShell executable (prefer pwsh 7+, fallback to powershell.exe 5.1)
|
|
20
|
+
#
|
|
21
|
+
# #547 finding 4: `command -v pwsh` alone is not enough. Under git-bash with a
|
|
22
|
+
# stripped PATH, the standard Windows installs are invisible even though the
|
|
23
|
+
# user can run pwsh from any terminal: pwsh 7 installs to
|
|
24
|
+
# %LOCALAPPDATA%\Microsoft\WindowsApps (a PATH alias that git-bash does not
|
|
25
|
+
# always inherit) and PS 5.1 lives at System32\WindowsPowerShell\v1.0. Probing
|
|
26
|
+
# those absolute paths converts a silent degrade into a working gate.
|
|
27
|
+
#
|
|
28
|
+
# The order is unchanged: whatever is on PATH wins, absolute paths are a
|
|
29
|
+
# fallback, and an empty result still means "not available" -- this widens
|
|
30
|
+
# detection, it does not invent an interpreter.
|
|
20
31
|
_detect_pwsh() {
|
|
21
32
|
if command -v pwsh &>/dev/null 2>&1; then
|
|
22
33
|
echo "pwsh"
|
|
23
|
-
|
|
34
|
+
return 0
|
|
35
|
+
fi
|
|
36
|
+
if command -v powershell.exe &>/dev/null 2>&1; then
|
|
24
37
|
echo "powershell.exe"
|
|
25
|
-
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
40
|
+
if command -v powershell &>/dev/null 2>&1; then
|
|
26
41
|
echo "powershell"
|
|
27
|
-
|
|
28
|
-
echo ""
|
|
42
|
+
return 0
|
|
29
43
|
fi
|
|
44
|
+
# Windows absolute-path fallbacks.
|
|
45
|
+
local candidate
|
|
46
|
+
for candidate in \
|
|
47
|
+
"${LOCALAPPDATA:-$HOME/AppData/Local}/Microsoft/WindowsApps/pwsh.exe" \
|
|
48
|
+
"/c/Program Files/PowerShell/7/pwsh.exe" \
|
|
49
|
+
"/c/Program Files/PowerShell/6/pwsh.exe" \
|
|
50
|
+
"/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe" \
|
|
51
|
+
"/mnt/c/Program Files/PowerShell/7/pwsh.exe" \
|
|
52
|
+
"/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"; do
|
|
53
|
+
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
|
54
|
+
echo "$candidate"
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
done
|
|
58
|
+
echo ""
|
|
59
|
+
return 1
|
|
30
60
|
}
|
|
31
61
|
|
|
32
62
|
_find_powershell_files() {
|
|
@@ -87,18 +117,49 @@ run_static_analysis() {
|
|
|
87
117
|
echo "Running PSScriptAnalyzer static analysis on PowerShell scripts..."
|
|
88
118
|
# Recursively analyze all .ps1 files from repo root
|
|
89
119
|
# Exit with non-zero if Error or Warning severity issues found
|
|
90
|
-
|
|
120
|
+
local PSSA_RC=0
|
|
121
|
+
local PSSA_OUT
|
|
122
|
+
PSSA_OUT=$("$PWSH" "${POWERSHELL_ARGS[@]}" "
|
|
123
|
+
if (-not (Get-Module -ListAvailable -Name PSScriptAnalyzer)) {
|
|
124
|
+
Write-Output 'PSSA_MODULE_MISSING'
|
|
125
|
+
exit 0
|
|
126
|
+
}
|
|
91
127
|
\$results = Invoke-ScriptAnalyzer -Path . -Recurse -Severity Error,Warning
|
|
92
128
|
if (\$results) {
|
|
93
129
|
\$results | Format-Table -AutoSize
|
|
94
130
|
exit 1
|
|
95
131
|
}
|
|
96
132
|
exit 0
|
|
97
|
-
"
|
|
98
|
-
|
|
133
|
+
" 2>&1) || PSSA_RC=$?
|
|
134
|
+
printf '%s\n' "$PSSA_OUT"
|
|
135
|
+
case "$PSSA_RC" in
|
|
136
|
+
0)
|
|
137
|
+
if printf '%s\n' "$PSSA_OUT" | grep -q 'PSSA_MODULE_MISSING'; then
|
|
138
|
+
# #547 finding 2: the engine exists but its module does not. That is
|
|
139
|
+
# NOT a pass -- nothing was analysed. The XP_G1_REASON protocol (the
|
|
140
|
+
# one the java branch uses) turns this into a WARN that stays in the
|
|
141
|
+
# scoring denominator instead of vanishing.
|
|
142
|
+
echo "XP_G1_REASON: PSScriptAnalyzer module not installed (Install-Module PSScriptAnalyzer)"
|
|
143
|
+
return 3
|
|
144
|
+
fi
|
|
145
|
+
return 0
|
|
146
|
+
;;
|
|
147
|
+
1)
|
|
148
|
+
return 1 # real findings -> BLOCK upstream
|
|
149
|
+
;;
|
|
150
|
+
*)
|
|
151
|
+
echo "XP_G1_REASON: PSScriptAnalyzer run failed (exit $PSSA_RC)"
|
|
152
|
+
return 2 # unusable -> fail closed upstream
|
|
153
|
+
;;
|
|
154
|
+
esac
|
|
99
155
|
else
|
|
100
|
-
|
|
101
|
-
|
|
156
|
+
# #547 finding 4: previously `return 0`, i.e. "no PowerShell available"
|
|
157
|
+
# was indistinguishable from "PowerShell is clean" -- a silent false pass on
|
|
158
|
+
# a PowerShell commit. Return the not-available verdict so the caller can
|
|
159
|
+
# WARN and record a reason.
|
|
160
|
+
echo "PowerShell executable not found (probe: pwsh, powershell.exe, and the standard Windows install paths)"
|
|
161
|
+
echo "XP_G1_REASON: pwsh not available (install PowerShell 7: https://aka.ms/powershell)"
|
|
162
|
+
return 3
|
|
102
163
|
fi
|
|
103
164
|
}
|
|
104
165
|
|
package/gate-10.sh
CHANGED
|
@@ -12,7 +12,20 @@ GATE_10_START=$(gate_start_ms)
|
|
|
12
12
|
GATE_10_STATUS=""
|
|
13
13
|
# #536: audit trail for "did this commit actually get scanned?". The report
|
|
14
14
|
# surfaces reason + scanned_files so a non-scan never reads as a clean PASS.
|
|
15
|
+
#
|
|
16
|
+
# GATE_10_REASON and GATE_10_SCANNED_FILES are consumed by the sourcing parent
|
|
17
|
+
# (pre-commit builds its report JSON from them after `source gate-10.sh`), not
|
|
18
|
+
# inside this file. The linter checks one file at a time and cannot see the
|
|
19
|
+
# consumer, so each assignment is flagged SC2034 "appears unused" -- which
|
|
20
|
+
# blocks Gate 1 for any repo that vendors this adapter, because that repo's own
|
|
21
|
+
# lint pass runs over the vendored copy (#548). Each assignment carries a
|
|
22
|
+
# scoped directive with that reason stated at the site.
|
|
23
|
+
#
|
|
24
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
25
|
+
# shellcheck disable=SC2034
|
|
15
26
|
GATE_10_REASON=""
|
|
27
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
28
|
+
# shellcheck disable=SC2034
|
|
16
29
|
GATE_10_SCANNED_FILES=0
|
|
17
30
|
|
|
18
31
|
# Semgrep availability check
|
|
@@ -29,14 +42,21 @@ if [ -z "$SEMGREP_CMD" ]; then
|
|
|
29
42
|
echo " Pre-cache rules: semgrep --config=p/security-audit"
|
|
30
43
|
echo " Gate 10: SAST Security (WARN, semgrep not installed)"
|
|
31
44
|
GATE_10_STATUS="WARN"
|
|
45
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
46
|
+
# shellcheck disable=SC2034
|
|
32
47
|
GATE_10_REASON="tool_not_installed"
|
|
33
48
|
else
|
|
34
|
-
# Get staged files filtered to Semgrep-supported languages
|
|
35
|
-
|
|
49
|
+
# Get staged files filtered to Semgrep-supported languages.
|
|
50
|
+
# #547 finding 3: ps1|psm1 were missing, so a PowerShell-only commit never
|
|
51
|
+
# reached semgrep even when Gate 10 ran -- the SAST gate was simply absent
|
|
52
|
+
# for that language.
|
|
53
|
+
SEMGREP_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.(ts|tsx|js|jsx|py|go|java|c|cpp|cs|rb|php|scala|swift|ps1|psm1)$' || true)
|
|
36
54
|
|
|
37
55
|
if [ -z "$SEMGREP_FILES" ]; then
|
|
38
56
|
echo " ⏭️ SKIPPED - SAST (no supported language files changed)"
|
|
39
57
|
GATE_10_STATUS="SKIP"
|
|
58
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
59
|
+
# shellcheck disable=SC2034
|
|
40
60
|
GATE_10_REASON="no_supported_files"
|
|
41
61
|
else
|
|
42
62
|
# One path per line into an array. Quoting the newline-joined list would hand
|
|
@@ -71,11 +91,27 @@ else
|
|
|
71
91
|
[ -n "$_semgrep_file" ] || continue
|
|
72
92
|
SEMGREP_FILE_COUNT=$((SEMGREP_FILE_COUNT + 1))
|
|
73
93
|
SEMGREP_ARGS+=("--include=${_semgrep_file##*/}")
|
|
74
|
-
|
|
75
|
-
#
|
|
76
|
-
|
|
94
|
+
# A repo-root file has no slash at all, so "%/*" returns it unchanged and
|
|
95
|
+
# the old comment's assumption (`"./x.ts" strips to "./"`) never held:
|
|
96
|
+
# the target became the FILE path dressed as a directory ("./x.ts").
|
|
97
|
+
# Strip only when a slash is present; otherwise the target is the repo
|
|
98
|
+
# root, spelled ".". Scanning "." widens the scan to the whole tree, which
|
|
99
|
+
# is the safe side (see the note above) and is what a root-level staged
|
|
100
|
+
# file requires in order to be analysed at all.
|
|
101
|
+
case "$_semgrep_file" in
|
|
102
|
+
*/*) _semgrep_dir="./${_semgrep_file%/*}" ;;
|
|
103
|
+
*) _semgrep_dir="." ;;
|
|
104
|
+
esac
|
|
105
|
+
# #558: dedup must match WHOLE lines. The previous `case "$SEEN" in
|
|
106
|
+
# *"$_semgrep_dir"*)` was an unanchored substring test: once
|
|
107
|
+
# ./fastdl/tests was recorded, the parent ./fastdl matched it and was
|
|
108
|
+
# never added as a target, so a staged fastdl/a.py contributed an
|
|
109
|
+
# --include= with no directory to scan it in. Nothing errored and nothing
|
|
110
|
+
# was reported -- the file was simply not analysed, which for a security
|
|
111
|
+
# gate is the worst outcome: a silent net loss that looks like a pass.
|
|
112
|
+
# Sibling prefixes (src/api vs src/app) collapsed the same way.
|
|
77
113
|
case "$SEMGREP_DIRS_SEEN" in
|
|
78
|
-
|
|
114
|
+
*$'\n'"$_semgrep_dir"$'\n'*) : ;;
|
|
79
115
|
*) SEMGREP_ARGS+=("$_semgrep_dir"); SEMGREP_DIRS_SEEN="$SEMGREP_DIRS_SEEN$_semgrep_dir"$'\n' ;;
|
|
80
116
|
esac
|
|
81
117
|
done <<EOF
|
|
@@ -91,6 +127,8 @@ EOF
|
|
|
91
127
|
echo " Install node (https://nodejs.org); without it SAST findings cannot be classified"
|
|
92
128
|
echo " ⏭️ SKIPPED - SAST (node not installed)"
|
|
93
129
|
GATE_10_STATUS="SKIP"
|
|
130
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
131
|
+
# shellcheck disable=SC2034
|
|
94
132
|
GATE_10_REASON="tool_not_installed"
|
|
95
133
|
else
|
|
96
134
|
|
|
@@ -109,9 +147,41 @@ EOF
|
|
|
109
147
|
# code is demoted to "did the tool itself run". --error stays OFF: it would
|
|
110
148
|
# add nothing the JSON does not already provide, and keeping it off leaves
|
|
111
149
|
# failure exit codes unambiguous for the #475 diagnostics path.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
150
|
+
# #559: bound the run. Without a timeout, a wedged semgrep blocks the
|
|
151
|
+
# invocation for as long as its process lives; the fail-closed verdict logic
|
|
152
|
+
# below is correct but unreachable, so the hook hangs instead of judging.
|
|
153
|
+
# A gate that can wedge an interactive `git commit` gets bypassed with
|
|
154
|
+
# --no-verify, which is worse than any verdict it could have produced.
|
|
155
|
+
# `-k` after the primary signal: pysemgrep spinning in a syscall ignores
|
|
156
|
+
# SIGTERM, and the shipped 0.29 code used the same `-k 10` shape.
|
|
157
|
+
SEMGREP_TIMEOUT_S="${XP_GATE_SEMGREP_TIMEOUT:-180}"
|
|
158
|
+
SEMGREP_TIMED_OUT=false
|
|
159
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
160
|
+
SEMGREP_STDOUT_FILE=$(mktemp)
|
|
161
|
+
SEMGREP_OUTPUT=$(timeout -k 10 "$SEMGREP_TIMEOUT_S" \
|
|
162
|
+
"$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check \
|
|
163
|
+
"${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
|
|
164
|
+
SEMGREP_EXIT=$?
|
|
165
|
+
# 124 = timeout's own "I gave up" code. 137 = 128+SIGKILL, which is what
|
|
166
|
+
# `timeout -k` returns when the child ignored SIGTERM and had to be
|
|
167
|
+
# killed -- measured with a semgrep stand-in that traps TERM. Both mean
|
|
168
|
+
# "bounded, no verdict", so both must be reported as a timeout; checking
|
|
169
|
+
# only 124 told the user their semgrep was unparseable when in fact the
|
|
170
|
+
# scan had been cut off.
|
|
171
|
+
case "$SEMGREP_EXIT" in
|
|
172
|
+
124|137) SEMGREP_TIMED_OUT=true ;;
|
|
173
|
+
esac
|
|
174
|
+
else
|
|
175
|
+
# No `timeout` on this platform (stock macOS). Say so: an unbounded run
|
|
176
|
+
# nobody mentioned is exactly how #559 stayed invisible for a whole
|
|
177
|
+
# release. The scan still runs -- refusing to scan would be a worse
|
|
178
|
+
# trade -- but the unbounded window is now stated.
|
|
179
|
+
echo " ⚠️ 'timeout' command not available — semgrep runs UNBOUNDED (install coreutils or set up gtimeout)"
|
|
180
|
+
SEMGREP_STDOUT_FILE=$(mktemp)
|
|
181
|
+
SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check \
|
|
182
|
+
"${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
|
|
183
|
+
SEMGREP_EXIT=$?
|
|
184
|
+
fi
|
|
115
185
|
|
|
116
186
|
# One node pass over the JSON: first line = verdict counts, remaining lines =
|
|
117
187
|
# finding details. The severity vocabulary is the UNION of the two scales
|
|
@@ -153,7 +223,18 @@ console.log("");});
|
|
|
153
223
|
if [ "$V_PARSE" != "OK" ] || [ -z "$V_RESULTS" ] || [ -z "$V_ERRORS" ] || [ -z "$V_BLOCKING" ] || [ -z "$V_WARNINGS" ]; then
|
|
154
224
|
# Unparseable output: nothing the exit code claims can be trusted (#518).
|
|
155
225
|
# Fail closed — SKIP with diagnostics, never a PASS.
|
|
156
|
-
|
|
226
|
+
#
|
|
227
|
+
# #559: name a timeout as a timeout. The generic "unparseable" message
|
|
228
|
+
# would be true but useless here -- it points at the JSON parser when the
|
|
229
|
+
# real problem is that the scan was cut off. Same verdict (SKIP), but a
|
|
230
|
+
# diagnosis the reader can act on.
|
|
231
|
+
if [ "$SEMGREP_TIMED_OUT" = true ]; then
|
|
232
|
+
echo " ⏱️ semgrep exceeded ${SEMGREP_TIMEOUT_S}s and was killed — no verdict is possible"
|
|
233
|
+
echo " This is NOT a finding and NOT a clean scan. Raise XP_GATE_SEMGREP_TIMEOUT"
|
|
234
|
+
echo " if the repo legitimately takes longer; investigate if semgrep itself is wedged."
|
|
235
|
+
else
|
|
236
|
+
echo " ⚠️ semgrep output unparseable or verdict fields missing (exit code ${SEMGREP_EXIT}) — cannot determine verdict"
|
|
237
|
+
fi
|
|
157
238
|
if [ -n "$SEMGREP_OUTPUT" ]; then
|
|
158
239
|
echo " ── what semgrep reported on stderr ──"
|
|
159
240
|
printf '%s\n' "$SEMGREP_OUTPUT" | tail -10 | sed 's/^/ /'
|
|
@@ -162,9 +243,19 @@ console.log("");});
|
|
|
162
243
|
echo " ── what semgrep wrote to stdout (tail) ──"
|
|
163
244
|
printf '%s\n' "$SEMGREP_STDOUT_TAIL" | sed 's/^/ /'
|
|
164
245
|
fi
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
246
|
+
if [ "$SEMGREP_TIMED_OUT" = true ]; then
|
|
247
|
+
echo " ⏭️ SKIPPED - SAST (semgrep timed out after ${SEMGREP_TIMEOUT_S}s)"
|
|
248
|
+
GATE_10_STATUS="SKIP"
|
|
249
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
250
|
+
# shellcheck disable=SC2034
|
|
251
|
+
GATE_10_REASON="semgrep_timeout"
|
|
252
|
+
else
|
|
253
|
+
echo " ⏭️ SKIPPED - SAST (semgrep output unparseable)"
|
|
254
|
+
GATE_10_STATUS="SKIP"
|
|
255
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
256
|
+
# shellcheck disable=SC2034
|
|
257
|
+
GATE_10_REASON="output_unparseable"
|
|
258
|
+
fi
|
|
168
259
|
elif [ "$V_RESULTS" -eq 0 ] && { [ "$V_ERRORS" -gt 0 ] || [ "$SEMGREP_EXIT" -ne 0 ]; }; then
|
|
169
260
|
# Nothing was scanned to a verdict: semgrep either failed outright
|
|
170
261
|
# (nonzero exit) or completed but reported scan errors for every input
|
|
@@ -178,6 +269,8 @@ console.log("");});
|
|
|
178
269
|
fi
|
|
179
270
|
echo " ⏭️ SKIPPED - SAST (semgrep runtime error)"
|
|
180
271
|
GATE_10_STATUS="SKIP"
|
|
272
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
273
|
+
# shellcheck disable=SC2034
|
|
181
274
|
GATE_10_REASON="semgrep_runtime_error"
|
|
182
275
|
elif [ "$V_BLOCKING" -gt 0 ]; then
|
|
183
276
|
echo ""
|
|
@@ -192,9 +285,13 @@ console.log("");});
|
|
|
192
285
|
echo "$FINDING_DETAILS"
|
|
193
286
|
echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
|
|
194
287
|
GATE_10_STATUS="FAIL"
|
|
288
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
289
|
+
# shellcheck disable=SC2034
|
|
195
290
|
GATE_10_SCANNED_FILES=$SEMGREP_FILE_COUNT
|
|
196
291
|
exit 1
|
|
197
292
|
else
|
|
293
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
294
|
+
# shellcheck disable=SC2034
|
|
198
295
|
GATE_10_SCANNED_FILES=$SEMGREP_FILE_COUNT
|
|
199
296
|
echo ""
|
|
200
297
|
echo " ✅ PASSED - No blocking vulnerabilities (files scanned: $SEMGREP_FILE_COUNT)"
|
package/gate-3.sh
CHANGED
|
@@ -8,11 +8,15 @@
|
|
|
8
8
|
2>&1 echo "→ Gate 3: Cyclomatic complexity..."
|
|
9
9
|
GATE_3_START=$(gate_start_ms)
|
|
10
10
|
# #537: every non-run records WHY so the report can say why a gate is SKIP.
|
|
11
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
12
|
+
# shellcheck disable=SC2034
|
|
11
13
|
GATE_3_REASON=""
|
|
12
14
|
|
|
13
15
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
14
16
|
echo "⏭️ SKIPPED - Complexity (documentation project)."
|
|
15
17
|
GATE_3_STATUS="SKIP"
|
|
18
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
19
|
+
# shellcheck disable=SC2034
|
|
16
20
|
GATE_3_REASON="documentation_project"
|
|
17
21
|
|
|
18
22
|
elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
@@ -21,6 +25,8 @@ elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
|
21
25
|
GATE_3_STATUS="SKIP"
|
|
22
26
|
# #537: a PowerShell-primary repo was invisible to this dimension while the
|
|
23
27
|
# score implied full coverage — say exactly why nothing ran.
|
|
28
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
29
|
+
# shellcheck disable=SC2034
|
|
24
30
|
GATE_3_REASON="no_powershell_tool"
|
|
25
31
|
|
|
26
32
|
else
|
|
@@ -159,7 +165,10 @@ else
|
|
|
159
165
|
# empty stdin — the verdict comes back empty and Gate 3 passes on zero
|
|
160
166
|
# rows (measured while wiring #544: same input passed or blocked
|
|
161
167
|
# depending only on where the prefix sat).
|
|
168
|
+
# Consumed by evaluate_ccn_ratchet in the sourced lib, not here.
|
|
169
|
+
# shellcheck disable=SC2034
|
|
162
170
|
CCN_WARN_THRESHOLD="$CCN_THRESHOLD"
|
|
171
|
+
# shellcheck disable=SC2034
|
|
163
172
|
CCN_BLOCK_THRESHOLD=$((CCN_THRESHOLD * 2))
|
|
164
173
|
RATCHET_VERDICT=$(printf '%s\n' "$CC_CSV" | evaluate_ccn_ratchet ".ccn-baseline.json")
|
|
165
174
|
CC_VIOLATIONS=$(printf '%s\n' "$RATCHET_VERDICT" | grep -c '^BLOCK ' || true)
|
|
@@ -205,6 +214,8 @@ else
|
|
|
205
214
|
END { exit(found ? 0 : 1) }'; then
|
|
206
215
|
echo "⚠️ WARN - lizard produced no parsable results; complexity not verified"
|
|
207
216
|
GATE_3_STATUS="WARN"
|
|
217
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
218
|
+
# shellcheck disable=SC2034
|
|
208
219
|
GATE_3_REASON="lizard_output_unparsable"
|
|
209
220
|
elif [ "$CC_WARNINGS" -gt 0 ]; then
|
|
210
221
|
echo "$CC_OUTPUT"
|
|
@@ -212,6 +223,8 @@ else
|
|
|
212
223
|
echo "❌ BLOCKED - $CC_WARNINGS functions with CCN > $CCN_THRESHOLD found."
|
|
213
224
|
echo "Refactor high-complexity functions to keep below $CCN_THRESHOLD complexity."
|
|
214
225
|
GATE_3_STATUS="BLOCK"
|
|
226
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
227
|
+
# shellcheck disable=SC2034
|
|
215
228
|
GATE_3_REASON="ccn_over_threshold"
|
|
216
229
|
exit 1
|
|
217
230
|
else
|
|
@@ -222,6 +235,8 @@ else
|
|
|
222
235
|
else
|
|
223
236
|
echo "⏭️ SKIPPED - Complexity (no source files to check)."
|
|
224
237
|
GATE_3_STATUS="SKIP"
|
|
238
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
239
|
+
# shellcheck disable=SC2034
|
|
225
240
|
GATE_3_REASON="no_source_files_changed"
|
|
226
241
|
fi
|
|
227
242
|
else
|
|
@@ -229,6 +244,8 @@ else
|
|
|
229
244
|
echo " Install with: pip install --user lizard"
|
|
230
245
|
echo " Gate 3: Complexity check (WARN, tool not available)"
|
|
231
246
|
GATE_3_STATUS="WARN"
|
|
247
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
248
|
+
# shellcheck disable=SC2034
|
|
232
249
|
GATE_3_REASON="tool_not_installed"
|
|
233
250
|
fi
|
|
234
251
|
fi
|