@boyingliu01/xp-gate 0.30.1 → 0.30.3
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/bin/xp-gate.js +8 -0
- package/gate-10.sh +32 -2
- package/gate-3.sh +47 -6
- package/gate-4.sh +12 -0
- package/gate-7.sh +4 -0
- package/hooks/gate-10.sh +32 -2
- package/hooks/gate-3.sh +47 -6
- 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 +97 -6
- package/hooks/sprint-gate.sh +81 -2
- package/lib/__tests__/crap-cli.test.js +76 -0
- package/lib/__tests__/crap.test.js +137 -0
- package/lib/__tests__/hook-health.test.js +85 -0
- package/lib/__tests__/retro-gate-m.test.js +144 -0
- package/lib/coverage-total.sh +92 -0
- package/lib/crap-cli.js +191 -0
- package/lib/crap.js +196 -0
- package/lib/doctor.js +44 -0
- package/lib/hook-health.js +121 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/retro.js +139 -2
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- 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/bin/xp-gate.js
CHANGED
|
@@ -350,6 +350,14 @@ const COMMANDS = {
|
|
|
350
350
|
},
|
|
351
351
|
usage: 'xp-gate retro [--days N] [--all] [--json] [--sprint <id>]'
|
|
352
352
|
},
|
|
353
|
+
'crap': {
|
|
354
|
+
description: 'Rank functions by CRAP score (complexity x weak coverage) — advisory, never blocks (#404)',
|
|
355
|
+
run: subargs => {
|
|
356
|
+
const { handleCrap } = require('../lib/crap-cli.js');
|
|
357
|
+
handleCrap(subargs).then(code => process.exit(code));
|
|
358
|
+
},
|
|
359
|
+
usage: 'xp-gate crap [--top N] [--threshold N] [--json] [--paths <p...>] [--include-mirrors]'
|
|
360
|
+
},
|
|
353
361
|
'sprint-init': {
|
|
354
362
|
description: 'Initialize sprint state (single entry point for sprint creation, resolves #366)',
|
|
355
363
|
run: subargs => {
|
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
|
|
@@ -91,6 +111,8 @@ EOF
|
|
|
91
111
|
echo " Install node (https://nodejs.org); without it SAST findings cannot be classified"
|
|
92
112
|
echo " ⏭️ SKIPPED - SAST (node not installed)"
|
|
93
113
|
GATE_10_STATUS="SKIP"
|
|
114
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
115
|
+
# shellcheck disable=SC2034
|
|
94
116
|
GATE_10_REASON="tool_not_installed"
|
|
95
117
|
else
|
|
96
118
|
|
|
@@ -164,6 +186,8 @@ console.log("");});
|
|
|
164
186
|
fi
|
|
165
187
|
echo " ⏭️ SKIPPED - SAST (semgrep output unparseable)"
|
|
166
188
|
GATE_10_STATUS="SKIP"
|
|
189
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
190
|
+
# shellcheck disable=SC2034
|
|
167
191
|
GATE_10_REASON="output_unparseable"
|
|
168
192
|
elif [ "$V_RESULTS" -eq 0 ] && { [ "$V_ERRORS" -gt 0 ] || [ "$SEMGREP_EXIT" -ne 0 ]; }; then
|
|
169
193
|
# Nothing was scanned to a verdict: semgrep either failed outright
|
|
@@ -178,6 +202,8 @@ console.log("");});
|
|
|
178
202
|
fi
|
|
179
203
|
echo " ⏭️ SKIPPED - SAST (semgrep runtime error)"
|
|
180
204
|
GATE_10_STATUS="SKIP"
|
|
205
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
206
|
+
# shellcheck disable=SC2034
|
|
181
207
|
GATE_10_REASON="semgrep_runtime_error"
|
|
182
208
|
elif [ "$V_BLOCKING" -gt 0 ]; then
|
|
183
209
|
echo ""
|
|
@@ -192,9 +218,13 @@ console.log("");});
|
|
|
192
218
|
echo "$FINDING_DETAILS"
|
|
193
219
|
echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
|
|
194
220
|
GATE_10_STATUS="FAIL"
|
|
221
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
222
|
+
# shellcheck disable=SC2034
|
|
195
223
|
GATE_10_SCANNED_FILES=$SEMGREP_FILE_COUNT
|
|
196
224
|
exit 1
|
|
197
225
|
else
|
|
226
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
227
|
+
# shellcheck disable=SC2034
|
|
198
228
|
GATE_10_SCANNED_FILES=$SEMGREP_FILE_COUNT
|
|
199
229
|
echo ""
|
|
200
230
|
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)
|
|
@@ -179,16 +188,44 @@ else
|
|
|
179
188
|
GATE_3_STATUS="PASS"
|
|
180
189
|
fi
|
|
181
190
|
else
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
191
|
+
# #543: count from lizard's machine-readable CSV, never its tables.
|
|
192
|
+
#
|
|
193
|
+
# The previous parser grepped '^Warning cnt' for a summary row. Two things
|
|
194
|
+
# made that never match: "Warning cnt" is a column header in the MIDDLE of
|
|
195
|
+
# a line ("Total nloc Avg.NLOC ... Warning cnt Fun Rt nloc Rt"), and
|
|
196
|
+
# lizard omits the summary block entirely when given explicit file
|
|
197
|
+
# arguments -- which is how this gate always invokes it. CC_WARNINGS was
|
|
198
|
+
# therefore always 0 and the flat-threshold path could not block anything.
|
|
199
|
+
#
|
|
200
|
+
# --csv has a stable column order (NLOC,CCN,token,PARAM,length,location,
|
|
201
|
+
# file,name,signature,...), so the verdict comes from the same numbers
|
|
202
|
+
# lizard uses, independent of version and table formatting. --csv here is
|
|
203
|
+
# the flat-threshold run; the ratchet branch above builds its own.
|
|
204
|
+
CC_CSV_FLAT=$("$LIZARD_PATH" --csv -C "$CCN_THRESHOLD" ${CC_FILES_ARGS[@]+"${CC_FILES_ARGS[@]}"} 2>/dev/null || true)
|
|
205
|
+
CC_WARNINGS=$(printf '%s\n' "$CC_CSV_FLAT" | awk -F, -v t="$CCN_THRESHOLD" '
|
|
206
|
+
NF > 1 && $2 + 0 > t + 0 { c++ }
|
|
207
|
+
END { print c + 0 }
|
|
208
|
+
')
|
|
209
|
+
# A lizard that produced no parsable row must not read as "clean". Without
|
|
210
|
+
# this, a crashing or mis-invoked lizard yields 0 warnings and the gate
|
|
211
|
+
# reports PASSED on zero analysed functions -- the same silent-success
|
|
212
|
+
# failure this issue is about.
|
|
213
|
+
if ! printf '%s\n' "$CC_CSV_FLAT" | awk -F, 'NF > 1 && $2 ~ /^[0-9]+$/ { found = 1; exit }
|
|
214
|
+
END { exit(found ? 0 : 1) }'; then
|
|
215
|
+
echo "⚠️ WARN - lizard produced no parsable results; complexity not verified"
|
|
216
|
+
GATE_3_STATUS="WARN"
|
|
217
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
218
|
+
# shellcheck disable=SC2034
|
|
219
|
+
GATE_3_REASON="lizard_output_unparsable"
|
|
220
|
+
elif [ "$CC_WARNINGS" -gt 0 ]; then
|
|
188
221
|
echo "$CC_OUTPUT"
|
|
189
222
|
echo ""
|
|
190
223
|
echo "❌ BLOCKED - $CC_WARNINGS functions with CCN > $CCN_THRESHOLD found."
|
|
191
224
|
echo "Refactor high-complexity functions to keep below $CCN_THRESHOLD complexity."
|
|
225
|
+
GATE_3_STATUS="BLOCK"
|
|
226
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
227
|
+
# shellcheck disable=SC2034
|
|
228
|
+
GATE_3_REASON="ccn_over_threshold"
|
|
192
229
|
exit 1
|
|
193
230
|
else
|
|
194
231
|
echo "✅ PASSED - All functions within complexity threshold ($CCN_THRESHOLD)."
|
|
@@ -198,6 +235,8 @@ else
|
|
|
198
235
|
else
|
|
199
236
|
echo "⏭️ SKIPPED - Complexity (no source files to check)."
|
|
200
237
|
GATE_3_STATUS="SKIP"
|
|
238
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
239
|
+
# shellcheck disable=SC2034
|
|
201
240
|
GATE_3_REASON="no_source_files_changed"
|
|
202
241
|
fi
|
|
203
242
|
else
|
|
@@ -205,6 +244,8 @@ else
|
|
|
205
244
|
echo " Install with: pip install --user lizard"
|
|
206
245
|
echo " Gate 3: Complexity check (WARN, tool not available)"
|
|
207
246
|
GATE_3_STATUS="WARN"
|
|
247
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
248
|
+
# shellcheck disable=SC2034
|
|
208
249
|
GATE_3_REASON="tool_not_installed"
|
|
209
250
|
fi
|
|
210
251
|
fi
|
package/gate-4.sh
CHANGED
|
@@ -56,11 +56,15 @@ GATE_4_START=$(gate_start_ms)
|
|
|
56
56
|
GATE_4_STATUS=""
|
|
57
57
|
# #537: every non-run records WHY (no files / tool missing / unsupported lang)
|
|
58
58
|
# so the quality report can distinguish "not applicable" from "not covered".
|
|
59
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
60
|
+
# shellcheck disable=SC2034
|
|
59
61
|
GATE_4_REASON=""
|
|
60
62
|
|
|
61
63
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
62
64
|
echo "⏭️ SKIPPED - Principles check (documentation project)."
|
|
63
65
|
GATE_4_STATUS="SKIP"
|
|
66
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
67
|
+
# shellcheck disable=SC2034
|
|
64
68
|
GATE_4_REASON="documentation_project"
|
|
65
69
|
|
|
66
70
|
else
|
|
@@ -156,6 +160,8 @@ else
|
|
|
156
160
|
echo "⚠️ Warning: Principles checker execution failed"
|
|
157
161
|
echo "⏭️ SKIPPED - Principles check (execution issue)"
|
|
158
162
|
GATE_4_STATUS="SKIP"
|
|
163
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
164
|
+
# shellcheck disable=SC2034
|
|
159
165
|
GATE_4_REASON="checker_execution_error"
|
|
160
166
|
rm -f "$PRINCIPLES_JSON"
|
|
161
167
|
else
|
|
@@ -191,17 +197,23 @@ else
|
|
|
191
197
|
echo "ℹ️ npx/tsx not available - skipping principles check"
|
|
192
198
|
echo "⏭️ SKIPPED - Principles check (no Node.js/tsx)"
|
|
193
199
|
GATE_4_STATUS="SKIP"
|
|
200
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
201
|
+
# shellcheck disable=SC2034
|
|
194
202
|
GATE_4_REASON="tool_not_installed"
|
|
195
203
|
fi
|
|
196
204
|
else
|
|
197
205
|
echo "ℹ️ Principles checker not found in project - skipping"
|
|
198
206
|
echo "⏭️ SKIPPED - Principles check (checker not in project)"
|
|
199
207
|
GATE_4_STATUS="SKIP"
|
|
208
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
209
|
+
# shellcheck disable=SC2034
|
|
200
210
|
GATE_4_REASON="checker_not_found"
|
|
201
211
|
fi
|
|
202
212
|
else
|
|
203
213
|
echo "⏭️ SKIPPED - Principles check (no matching source files changed)"
|
|
204
214
|
GATE_4_STATUS="SKIP"
|
|
215
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
216
|
+
# shellcheck disable=SC2034
|
|
205
217
|
GATE_4_REASON="no_matching_files_changed"
|
|
206
218
|
fi
|
|
207
219
|
fi
|
package/gate-7.sh
CHANGED
|
@@ -12,11 +12,15 @@ GATE_7_START=$(gate_start_ms)
|
|
|
12
12
|
IAC_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E "\.(tf|yaml|yml)$|Dockerfile" || true)
|
|
13
13
|
# #536: scanned_files makes a trivial PASS (nothing to scan) auditable in the
|
|
14
14
|
# quality report instead of reading like a covered security check.
|
|
15
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
16
|
+
# shellcheck disable=SC2034
|
|
15
17
|
GATE_7_SCANNED_FILES=0
|
|
16
18
|
if [ -z "$IAC_FILES" ]; then
|
|
17
19
|
echo "✅ PASSED - No IaC files detected in changes."
|
|
18
20
|
GATE_7_STATUS="PASS"
|
|
19
21
|
else
|
|
22
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
23
|
+
# shellcheck disable=SC2034
|
|
20
24
|
GATE_7_SCANNED_FILES=$(printf '%s\n' "$IAC_FILES" | sed -n '/./p' | wc -l | tr -d '[:space:]')
|
|
21
25
|
# Run IaC adapter. Resolution follows the documented 3-tier convention
|
|
22
26
|
# (adapter-common.sh): global ~/.config/xp-gate/adapters -> project
|