@boyingliu01/xp-gate 0.20.1 → 0.21.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/java.sh +373 -27
- package/adapters/python.sh +73 -6
- package/gate-10.sh +107 -64
- package/gate-3.sh +51 -3
- package/gate-4.sh +32 -1
- package/hooks/gate-10.sh +107 -64
- package/hooks/gate-3.sh +51 -3
- package/hooks/gate-4.sh +32 -1
- package/hooks/pre-commit +319 -57
- package/lib/__tests__/doctor-spawn-capability.test.js +116 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +49 -10
- package/lib/doctor-tui.js +21 -3
- package/lib/doctor.js +111 -8
- package/lib/gate-audit.ts +3 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/admin-template-guidelines/SKILL.md +1 -0
- package/plugins/claude-code/skills/batch-grill-me/SKILL.md +1 -0
- package/plugins/claude-code/skills/clipboard-vision/SKILL.md +1 -0
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/domain-modeling/SKILL.md +1 -0
- package/plugins/claude-code/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/claude-code/skills/grilling/SKILL.md +1 -0
- package/plugins/claude-code/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/plugins/claude-code/skills/ralph-loop/SKILL.md +1 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/test-driven-development/SKILL.md +1 -0
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +1 -0
- package/plugins/claude-code/skills/to-issues/SKILL.md +1 -0
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/admin-template-guidelines/SKILL.md +1 -0
- package/plugins/dsh/skills/batch-grill-me/SKILL.md +1 -0
- package/plugins/dsh/skills/clipboard-vision/SKILL.md +1 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/domain-modeling/SKILL.md +1 -0
- package/plugins/dsh/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/dsh/skills/grilling/SKILL.md +1 -0
- package/plugins/dsh/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/plugins/dsh/skills/ralph-loop/SKILL.md +1 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/test-driven-development/SKILL.md +1 -0
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/skills/test-specification-alignment/SKILL.md +1 -0
- package/plugins/dsh/skills/to-issues/SKILL.md +1 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/admin-template-guidelines/SKILL.md +1 -0
- package/plugins/opencode/skills/batch-grill-me/SKILL.md +1 -0
- package/plugins/opencode/skills/clipboard-vision/SKILL.md +1 -0
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/domain-modeling/SKILL.md +1 -0
- package/plugins/opencode/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/opencode/skills/grilling/SKILL.md +1 -0
- package/plugins/opencode/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/plugins/opencode/skills/ralph-loop/SKILL.md +1 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/test-driven-development/SKILL.md +1 -0
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/opencode/skills/test-specification-alignment/SKILL.md +1 -0
- package/plugins/opencode/skills/to-issues/SKILL.md +1 -0
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +4 -4
- package/principles/adapters/__tests__/java.test.ts +317 -0
- package/principles/adapters/java.ts +325 -13
- package/skills/admin-template-guidelines/SKILL.md +1 -0
- package/skills/batch-grill-me/SKILL.md +1 -0
- package/skills/clipboard-vision/SKILL.md +1 -0
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/domain-modeling/SKILL.md +1 -0
- package/skills/grill-with-docs/SKILL.md +1 -0
- package/skills/grilling/SKILL.md +1 -0
- package/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/skills/ralph-loop/SKILL.md +1 -0
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/test-driven-development/SKILL.md +1 -0
- package/skills/test-specification-alignment/AGENTS.md +4 -4
- package/skills/test-specification-alignment/SKILL.md +1 -0
- package/skills/to-issues/SKILL.md +1 -0
package/gate-10.sh
CHANGED
|
@@ -57,82 +57,125 @@ else
|
|
|
57
57
|
$SEMGREP_FILES
|
|
58
58
|
EOF
|
|
59
59
|
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
# The verdict is parsed from the JSON with node (see below), so node is a
|
|
61
|
+
# hard dependency of this gate — check it BEFORE spending a semgrep run
|
|
62
|
+
# (#518 R2): without it we would silently SKIP every finding as
|
|
63
|
+
# "unparseable" and lose the whole security net on minimal CI images.
|
|
64
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
65
|
+
echo " ⚠️ WARN - node is not installed — Gate 10 needs it to parse semgrep's JSON verdict"
|
|
66
|
+
echo " Install node (https://nodejs.org); without it SAST findings cannot be classified"
|
|
67
|
+
echo " ⏭️ SKIPPED - SAST (node not installed)"
|
|
68
|
+
GATE_10_STATUS="SKIP"
|
|
69
|
+
else
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
# Run semgrep with JSON output. stdout and stderr are captured SEPARATELY:
|
|
72
|
+
# --json writes findings to stdout, progress/summary lines to stderr, and the
|
|
73
|
+
# old `2>&1` merge poisoned the very JSON the verdict now depends on (#518).
|
|
74
|
+
# SEMGREP_OUTPUT keeps its name and role — what the tool said, printed on the
|
|
75
|
+
# SKIP path (AC-475-04).
|
|
76
|
+
#
|
|
77
|
+
# #518: the exit code does NOT carry the verdict. semgrep exits 0 even with
|
|
78
|
+
# ERROR-severity findings, and its exit-code semantics have drifted across
|
|
79
|
+
# versions and platforms (upstream currently reports findings WITH --error
|
|
80
|
+
# as exit 1 and failures as 2, but that mapping is not something a gate
|
|
81
|
+
# should bet on). The JSON carries what the exit code never could —
|
|
82
|
+
# per-severity counts — so the verdict is read from the JSON and the exit
|
|
83
|
+
# code is demoted to "did the tool itself run". --error stays OFF: it would
|
|
84
|
+
# add nothing the JSON does not already provide, and keeping it off leaves
|
|
85
|
+
# failure exit codes unambiguous for the #475 diagnostics path.
|
|
86
|
+
SEMGREP_STDOUT_FILE=$(mktemp)
|
|
87
|
+
SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check "${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
|
|
88
|
+
SEMGREP_EXIT=$?
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
# One node pass over the JSON: first line = verdict counts, remaining lines =
|
|
91
|
+
# finding details. The severity vocabulary is the UNION of the two scales
|
|
92
|
+
# (#518): semgrep's own ERROR is its most severe tier (it maps to the old
|
|
93
|
+
# CRITICAL/HIGH → BLOCK intent), its WARNING/INFO align with the old
|
|
94
|
+
# MEDIUM/LOW → warnings-only tier. Anything OUTSIDE both lists (missing,
|
|
95
|
+
# empty, or a future/new tier) counts as BLOCKING — a security gate must
|
|
96
|
+
# fail closed on severities it does not recognize (#518 R2). Parsing
|
|
97
|
+
# failure degrades to PARSE=FAIL — a fail-closed SKIP below, never a PASS.
|
|
98
|
+
SEMGREP_VERDICT=$(node -e '
|
|
99
|
+
let d="";process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>{
|
|
100
|
+
let data;try{data=JSON.parse(d);}catch(e){console.log("PARSE=FAIL RESULTS=0 ERRORS=0 BLOCKING=0 WARNINGS=0");return;}
|
|
101
|
+
const results=data.results||[];const errors=data.errors||[];
|
|
102
|
+
const sev=r=>((r.extra&&r.extra.severity)||"").toUpperCase();
|
|
103
|
+
const WARNV=["WARNING","INFO","MEDIUM","LOW"];
|
|
104
|
+
let b=0,w=0;results.forEach(r=>{if(WARNV.indexOf(sev(r))>=0)w++;else b++;});
|
|
105
|
+
console.log("PARSE=OK RESULTS="+results.length+" ERRORS="+errors.length+" BLOCKING="+b+" WARNINGS="+w);
|
|
106
|
+
results.slice(0,5).forEach(r=>{const extra=r.extra||{};const s=sev(r)||"UNKNOWN";
|
|
107
|
+
console.log(" ["+s+"] "+(r.check_id||"unknown"));
|
|
108
|
+
console.log(" "+(r.path||"unknown")+":"+((r.start&&r.start.line)||"?")+" → "+(extra.message||"").slice(0,80));
|
|
109
|
+
console.log("");});
|
|
110
|
+
});' < "$SEMGREP_STDOUT_FILE") || SEMGREP_VERDICT="PARSE=FAIL RESULTS=0 ERRORS=0 BLOCKING=0 WARNINGS=0"
|
|
85
111
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
112
|
+
PARSE_LINE=$(printf '%s\n' "$SEMGREP_VERDICT" | sed -n '1p')
|
|
113
|
+
FINDING_DETAILS=$(printf '%s\n' "$SEMGREP_VERDICT" | sed -n '2,$p')
|
|
114
|
+
V_PARSE=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/^PARSE=\([A-Z]*\) .*/\1/p')
|
|
115
|
+
V_RESULTS=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* RESULTS=\([0-9]*\) .*/\1/p')
|
|
116
|
+
V_ERRORS=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* ERRORS=\([0-9]*\) .*/\1/p')
|
|
117
|
+
V_BLOCKING=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* BLOCKING=\([0-9]*\) .*/\1/p')
|
|
118
|
+
V_WARNINGS=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* WARNINGS=\([0-9]*\)$/\1/p')
|
|
119
|
+
# Keep the raw stdout tail for the unparseable branch's diagnostics BEFORE
|
|
120
|
+
# the temp file goes away (#518 R2: stderr alone often says nothing there).
|
|
121
|
+
SEMGREP_STDOUT_TAIL=$(tail -5 "$SEMGREP_STDOUT_FILE" 2>/dev/null)
|
|
122
|
+
rm -f "$SEMGREP_STDOUT_FILE"
|
|
97
123
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
echo "
|
|
107
|
-
|
|
108
|
-
echo "$FINDING_DETAILS"
|
|
109
|
-
echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
|
|
110
|
-
GATE_10_STATUS="FAIL"
|
|
111
|
-
exit 1
|
|
112
|
-
else
|
|
113
|
-
echo ""
|
|
114
|
-
echo " ✅ PASSED - No critical/high vulnerabilities"
|
|
115
|
-
if [ "$MEDIUM_LOW" -gt 0 ]; then
|
|
116
|
-
echo " ⚠️ ${MEDIUM_LOW} medium/low findings (warnings only)"
|
|
117
|
-
echo "$FINDING_DETAILS"
|
|
118
|
-
fi
|
|
119
|
-
GATE_10_STATUS="PASS"
|
|
124
|
+
# Fail closed: an unparseable payload OR an OK parse with any missing count
|
|
125
|
+
# field (verdict-line format drift) must SKIP, never silently degrade to
|
|
126
|
+
# PASS (#518).
|
|
127
|
+
if [ "$V_PARSE" != "OK" ] || [ -z "$V_RESULTS" ] || [ -z "$V_ERRORS" ] || [ -z "$V_BLOCKING" ] || [ -z "$V_WARNINGS" ]; then
|
|
128
|
+
# Unparseable output: nothing the exit code claims can be trusted (#518).
|
|
129
|
+
# Fail closed — SKIP with diagnostics, never a PASS.
|
|
130
|
+
echo " ⚠️ semgrep output unparseable or verdict fields missing (exit code ${SEMGREP_EXIT}) — cannot determine verdict"
|
|
131
|
+
if [ -n "$SEMGREP_OUTPUT" ]; then
|
|
132
|
+
echo " ── what semgrep reported on stderr ──"
|
|
133
|
+
printf '%s\n' "$SEMGREP_OUTPUT" | tail -10 | sed 's/^/ /'
|
|
120
134
|
fi
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
if [ -n "$SEMGREP_STDOUT_TAIL" ]; then
|
|
136
|
+
echo " ── what semgrep wrote to stdout (tail) ──"
|
|
137
|
+
printf '%s\n' "$SEMGREP_STDOUT_TAIL" | sed 's/^/ /'
|
|
138
|
+
fi
|
|
139
|
+
echo " ⏭️ SKIPPED - SAST (semgrep output unparseable)"
|
|
140
|
+
GATE_10_STATUS="SKIP"
|
|
141
|
+
elif [ "$V_RESULTS" -eq 0 ] && { [ "$V_ERRORS" -gt 0 ] || [ "$SEMGREP_EXIT" -ne 0 ]; }; then
|
|
142
|
+
# Nothing was scanned to a verdict: semgrep either failed outright
|
|
143
|
+
# (nonzero exit) or completed but reported scan errors for every input
|
|
144
|
+
# (#518 R2: exit 0 + results:[] + errors:[{...}] used to fall through to
|
|
145
|
+
# PASS). An operational failure must neither block nor claim a pass
|
|
146
|
+
# (#475 kept).
|
|
147
|
+
echo " ⚠️ semgrep exit=${SEMGREP_EXIT}, scan errors=${V_ERRORS}, findings=${V_RESULTS} — nothing to judge"
|
|
129
148
|
if [ -n "$SEMGREP_OUTPUT" ]; then
|
|
130
149
|
echo " ── what semgrep reported ──"
|
|
131
150
|
printf '%s\n' "$SEMGREP_OUTPUT" | tail -10 | sed 's/^/ /'
|
|
132
151
|
fi
|
|
133
152
|
echo " ⏭️ SKIPPED - SAST (semgrep runtime error)"
|
|
134
153
|
GATE_10_STATUS="SKIP"
|
|
154
|
+
elif [ "$V_BLOCKING" -gt 0 ]; then
|
|
155
|
+
echo ""
|
|
156
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
157
|
+
echo " GATE 10: Semgrep Security Gate"
|
|
158
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
159
|
+
echo " CRITICAL/HIGH/ERROR: ${V_BLOCKING} ❌ BLOCKED"
|
|
160
|
+
echo " MEDIUM/LOW/WARNING/INFO: ${V_WARNINGS} ⚠️ warning"
|
|
161
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
162
|
+
echo " ❌ BLOCKED — blocking-severity vulnerability found"
|
|
163
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
164
|
+
echo "$FINDING_DETAILS"
|
|
165
|
+
echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
|
|
166
|
+
GATE_10_STATUS="FAIL"
|
|
167
|
+
exit 1
|
|
168
|
+
else
|
|
169
|
+
echo ""
|
|
170
|
+
echo " ✅ PASSED - No blocking vulnerabilities (files scanned: ${#SEMGREP_ARGS[@]})"
|
|
171
|
+
if [ "$V_WARNINGS" -gt 0 ]; then
|
|
172
|
+
echo " ⚠️ ${V_WARNINGS} non-blocking findings (warnings only)"
|
|
173
|
+
echo "$FINDING_DETAILS"
|
|
174
|
+
fi
|
|
175
|
+
GATE_10_STATUS="PASS"
|
|
135
176
|
fi
|
|
177
|
+
|
|
178
|
+
fi # node available
|
|
136
179
|
fi
|
|
137
180
|
fi
|
|
138
181
|
record_gate_audit "gate-10" "sast-security" "$GATE_10_STATUS" "0" "$GATE_10_START"
|
package/gate-3.sh
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
1
2
|
# ============================================================================
|
|
2
3
|
# GATE 3: Cyclomatic Complexity Check
|
|
3
4
|
# Uses lizard - checks cyclomatic complexity of changed source files
|
|
@@ -17,7 +18,45 @@ elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
|
17
18
|
GATE_3_STATUS="SKIP"
|
|
18
19
|
|
|
19
20
|
else
|
|
20
|
-
|
|
21
|
+
# S2 (#507): three-level CCN threshold override --
|
|
22
|
+
# .xp-gate/ccn-threshold (positive integer) > XP_GATE_CCN_THRESHOLD > 5.
|
|
23
|
+
# Invalid values WARN on stderr and fall back to the default; the resolved
|
|
24
|
+
# value reaches the gate-3 audit record via the --detail argument below.
|
|
25
|
+
# Implemented with bash builtins only (no tr/grep spawns): every process
|
|
26
|
+
# spawn is expensive on Windows under AV filter drivers.
|
|
27
|
+
resolve_ccn_threshold() {
|
|
28
|
+
local default=5 val
|
|
29
|
+
if [ -f ".xp-gate/ccn-threshold" ]; then
|
|
30
|
+
val=$(<".xp-gate/ccn-threshold")
|
|
31
|
+
val="${val//[[:space:]]/}"
|
|
32
|
+
case "$val" in
|
|
33
|
+
''|*[!0-9]*|0)
|
|
34
|
+
echo "WARN - invalid .xp-gate/ccn-threshold value '${val}' (expected a positive integer), falling back to ${default}" >&2
|
|
35
|
+
;;
|
|
36
|
+
*)
|
|
37
|
+
echo "$val"
|
|
38
|
+
return 0
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
echo "$default"
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
val=${XP_GATE_CCN_THRESHOLD:-}
|
|
45
|
+
if [ -n "$val" ]; then
|
|
46
|
+
case "$val" in
|
|
47
|
+
''|*[!0-9]*|0)
|
|
48
|
+
echo "WARN - invalid XP_GATE_CCN_THRESHOLD value '${val}' (expected a positive integer), falling back to ${default}" >&2
|
|
49
|
+
;;
|
|
50
|
+
*)
|
|
51
|
+
echo "$val"
|
|
52
|
+
return 0
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
fi
|
|
56
|
+
echo "$default"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
CCN_THRESHOLD=$(resolve_ccn_threshold)
|
|
21
60
|
|
|
22
61
|
# Check lizard availability
|
|
23
62
|
LIZARD_CMD=""
|
|
@@ -37,7 +76,16 @@ else
|
|
|
37
76
|
echo "Checking complexity for source files..."
|
|
38
77
|
|
|
39
78
|
# Run lizard with CCN threshold
|
|
40
|
-
|
|
79
|
+
# One path per line, into an array. Unquoted $CC_FILES relied on word
|
|
80
|
+
# splitting, which also splits any path containing a space -- handing
|
|
81
|
+
# lizard two bogus paths and checking neither (#457 defect class, same
|
|
82
|
+
# fix as gate-4's PRINCIPLES_ARGS). Quoting the scalar would instead
|
|
83
|
+
# collapse the list into one argument. An array does both.
|
|
84
|
+
CC_FILES_ARGS=()
|
|
85
|
+
while IFS= read -r _cc_file; do
|
|
86
|
+
[ -n "$_cc_file" ] && CC_FILES_ARGS+=("$_cc_file")
|
|
87
|
+
done <<< "$CC_FILES"
|
|
88
|
+
CC_OUTPUT=$("$LIZARD_PATH" -C "$CCN_THRESHOLD" ${CC_FILES_ARGS[@]+"${CC_FILES_ARGS[@]}"} 2>&1 || true)
|
|
41
89
|
|
|
42
90
|
# Parse warning count from the summary table: "Warning cnt 8"
|
|
43
91
|
# Use anchored grep to avoid matching lizard table headers (e.g. "Rt" column)
|
|
@@ -65,4 +113,4 @@ else
|
|
|
65
113
|
GATE_3_STATUS="WARN"
|
|
66
114
|
fi
|
|
67
115
|
fi
|
|
68
|
-
record_gate_audit "gate-3" "complexity" "$GATE_3_STATUS" "${CC_WARNINGS:-0}" "$GATE_3_START"
|
|
116
|
+
record_gate_audit "gate-3" "complexity" "$GATE_3_STATUS" "${CC_WARNINGS:-0}" "$GATE_3_START" "ccn_threshold=${CCN_THRESHOLD:-5}"
|
package/gate-4.sh
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
1
2
|
# ============================================================================
|
|
2
3
|
# GATE 4: Principles Checker (Clean Code + SOLID)
|
|
3
4
|
# Reuses existing principles checker logic
|
|
@@ -23,6 +24,31 @@ if ! declare -F run_tsx >/dev/null 2>&1; then
|
|
|
23
24
|
}
|
|
24
25
|
fi
|
|
25
26
|
|
|
27
|
+
# Java rule coverage annotation (#507 S4, AC-507-04-02). The normative matrix
|
|
28
|
+
# lives in docs/java-principles-coverage.md; these constants mirror it. Before
|
|
29
|
+
# #507 the Java adapter fed rules only {name,type,line,code}, so the 12 rules
|
|
30
|
+
# reading structured metrics silently saw `undefined` and a Java commit got a
|
|
31
|
+
# green "checked" with zero effective coverage. 12/15 now fire; the three
|
|
32
|
+
# inert ones are rule-side gaps documented in the coverage doc -- they must
|
|
33
|
+
# NOT be announced as covered.
|
|
34
|
+
JAVA_COVERAGE_EFFECTIVE=12
|
|
35
|
+
JAVA_COVERAGE_TOTAL=15
|
|
36
|
+
JAVA_COVERAGE_INERT="lsp, many-exports, code-duplication"
|
|
37
|
+
|
|
38
|
+
# Print the coverage annotation when (and only when) the staged file list
|
|
39
|
+
# contains Java sources. Non-Java commits keep the original output byte for
|
|
40
|
+
# byte. Pure shell builtins: no subprocess per file.
|
|
41
|
+
java_coverage_note() {
|
|
42
|
+
local _f _has_java=""
|
|
43
|
+
for _f in "$@"; do
|
|
44
|
+
case "$_f" in
|
|
45
|
+
*.java|*.Java|*.JAVA) _has_java=1; break ;;
|
|
46
|
+
esac
|
|
47
|
+
done
|
|
48
|
+
[ -n "$_has_java" ] || return 0
|
|
49
|
+
echo "ℹ️ Java rule coverage: $JAVA_COVERAGE_EFFECTIVE/$JAVA_COVERAGE_TOTAL rules effective (inert on Java: $JAVA_COVERAGE_INERT -- see docs/java-principles-coverage.md)"
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
2>&1 echo ""
|
|
27
53
|
2>&1 echo "→ Gate 4: Principles checker (Clean Code + SOLID)..."
|
|
28
54
|
GATE_4_START=$(gate_start_ms)
|
|
@@ -72,7 +98,12 @@ else
|
|
|
72
98
|
|
|
73
99
|
if [ -n "$PRINCIPLES_DIR" ]; then
|
|
74
100
|
echo "Checking Clean Code + SOLID principles..."
|
|
75
|
-
|
|
101
|
+
|
|
102
|
+
# AC-507-04-02: say what a Java commit is actually covered for, instead
|
|
103
|
+
# of implying a full 15-rule check (#507). Printed before the run so it
|
|
104
|
+
# appears on both the PASS and the FAIL path.
|
|
105
|
+
java_coverage_note ${PRINCIPLES_ARGS[@]+"${PRINCIPLES_ARGS[@]}"}
|
|
106
|
+
|
|
76
107
|
if command -v npx > /dev/null 2>&1 || [ -f "$PRINCIPLES_BASE/node_modules/tsx/dist/cli.mjs" ]; then
|
|
77
108
|
# Run principles checker and store results.
|
|
78
109
|
#
|
package/hooks/gate-10.sh
CHANGED
|
@@ -57,82 +57,125 @@ else
|
|
|
57
57
|
$SEMGREP_FILES
|
|
58
58
|
EOF
|
|
59
59
|
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
# The verdict is parsed from the JSON with node (see below), so node is a
|
|
61
|
+
# hard dependency of this gate — check it BEFORE spending a semgrep run
|
|
62
|
+
# (#518 R2): without it we would silently SKIP every finding as
|
|
63
|
+
# "unparseable" and lose the whole security net on minimal CI images.
|
|
64
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
65
|
+
echo " ⚠️ WARN - node is not installed — Gate 10 needs it to parse semgrep's JSON verdict"
|
|
66
|
+
echo " Install node (https://nodejs.org); without it SAST findings cannot be classified"
|
|
67
|
+
echo " ⏭️ SKIPPED - SAST (node not installed)"
|
|
68
|
+
GATE_10_STATUS="SKIP"
|
|
69
|
+
else
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
# Run semgrep with JSON output. stdout and stderr are captured SEPARATELY:
|
|
72
|
+
# --json writes findings to stdout, progress/summary lines to stderr, and the
|
|
73
|
+
# old `2>&1` merge poisoned the very JSON the verdict now depends on (#518).
|
|
74
|
+
# SEMGREP_OUTPUT keeps its name and role — what the tool said, printed on the
|
|
75
|
+
# SKIP path (AC-475-04).
|
|
76
|
+
#
|
|
77
|
+
# #518: the exit code does NOT carry the verdict. semgrep exits 0 even with
|
|
78
|
+
# ERROR-severity findings, and its exit-code semantics have drifted across
|
|
79
|
+
# versions and platforms (upstream currently reports findings WITH --error
|
|
80
|
+
# as exit 1 and failures as 2, but that mapping is not something a gate
|
|
81
|
+
# should bet on). The JSON carries what the exit code never could —
|
|
82
|
+
# per-severity counts — so the verdict is read from the JSON and the exit
|
|
83
|
+
# code is demoted to "did the tool itself run". --error stays OFF: it would
|
|
84
|
+
# add nothing the JSON does not already provide, and keeping it off leaves
|
|
85
|
+
# failure exit codes unambiguous for the #475 diagnostics path.
|
|
86
|
+
SEMGREP_STDOUT_FILE=$(mktemp)
|
|
87
|
+
SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check "${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
|
|
88
|
+
SEMGREP_EXIT=$?
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
# One node pass over the JSON: first line = verdict counts, remaining lines =
|
|
91
|
+
# finding details. The severity vocabulary is the UNION of the two scales
|
|
92
|
+
# (#518): semgrep's own ERROR is its most severe tier (it maps to the old
|
|
93
|
+
# CRITICAL/HIGH → BLOCK intent), its WARNING/INFO align with the old
|
|
94
|
+
# MEDIUM/LOW → warnings-only tier. Anything OUTSIDE both lists (missing,
|
|
95
|
+
# empty, or a future/new tier) counts as BLOCKING — a security gate must
|
|
96
|
+
# fail closed on severities it does not recognize (#518 R2). Parsing
|
|
97
|
+
# failure degrades to PARSE=FAIL — a fail-closed SKIP below, never a PASS.
|
|
98
|
+
SEMGREP_VERDICT=$(node -e '
|
|
99
|
+
let d="";process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>{
|
|
100
|
+
let data;try{data=JSON.parse(d);}catch(e){console.log("PARSE=FAIL RESULTS=0 ERRORS=0 BLOCKING=0 WARNINGS=0");return;}
|
|
101
|
+
const results=data.results||[];const errors=data.errors||[];
|
|
102
|
+
const sev=r=>((r.extra&&r.extra.severity)||"").toUpperCase();
|
|
103
|
+
const WARNV=["WARNING","INFO","MEDIUM","LOW"];
|
|
104
|
+
let b=0,w=0;results.forEach(r=>{if(WARNV.indexOf(sev(r))>=0)w++;else b++;});
|
|
105
|
+
console.log("PARSE=OK RESULTS="+results.length+" ERRORS="+errors.length+" BLOCKING="+b+" WARNINGS="+w);
|
|
106
|
+
results.slice(0,5).forEach(r=>{const extra=r.extra||{};const s=sev(r)||"UNKNOWN";
|
|
107
|
+
console.log(" ["+s+"] "+(r.check_id||"unknown"));
|
|
108
|
+
console.log(" "+(r.path||"unknown")+":"+((r.start&&r.start.line)||"?")+" → "+(extra.message||"").slice(0,80));
|
|
109
|
+
console.log("");});
|
|
110
|
+
});' < "$SEMGREP_STDOUT_FILE") || SEMGREP_VERDICT="PARSE=FAIL RESULTS=0 ERRORS=0 BLOCKING=0 WARNINGS=0"
|
|
85
111
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
112
|
+
PARSE_LINE=$(printf '%s\n' "$SEMGREP_VERDICT" | sed -n '1p')
|
|
113
|
+
FINDING_DETAILS=$(printf '%s\n' "$SEMGREP_VERDICT" | sed -n '2,$p')
|
|
114
|
+
V_PARSE=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/^PARSE=\([A-Z]*\) .*/\1/p')
|
|
115
|
+
V_RESULTS=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* RESULTS=\([0-9]*\) .*/\1/p')
|
|
116
|
+
V_ERRORS=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* ERRORS=\([0-9]*\) .*/\1/p')
|
|
117
|
+
V_BLOCKING=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* BLOCKING=\([0-9]*\) .*/\1/p')
|
|
118
|
+
V_WARNINGS=$(printf '%s\n' "$PARSE_LINE" | sed -n 's/.* WARNINGS=\([0-9]*\)$/\1/p')
|
|
119
|
+
# Keep the raw stdout tail for the unparseable branch's diagnostics BEFORE
|
|
120
|
+
# the temp file goes away (#518 R2: stderr alone often says nothing there).
|
|
121
|
+
SEMGREP_STDOUT_TAIL=$(tail -5 "$SEMGREP_STDOUT_FILE" 2>/dev/null)
|
|
122
|
+
rm -f "$SEMGREP_STDOUT_FILE"
|
|
97
123
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
echo "
|
|
107
|
-
|
|
108
|
-
echo "$FINDING_DETAILS"
|
|
109
|
-
echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
|
|
110
|
-
GATE_10_STATUS="FAIL"
|
|
111
|
-
exit 1
|
|
112
|
-
else
|
|
113
|
-
echo ""
|
|
114
|
-
echo " ✅ PASSED - No critical/high vulnerabilities"
|
|
115
|
-
if [ "$MEDIUM_LOW" -gt 0 ]; then
|
|
116
|
-
echo " ⚠️ ${MEDIUM_LOW} medium/low findings (warnings only)"
|
|
117
|
-
echo "$FINDING_DETAILS"
|
|
118
|
-
fi
|
|
119
|
-
GATE_10_STATUS="PASS"
|
|
124
|
+
# Fail closed: an unparseable payload OR an OK parse with any missing count
|
|
125
|
+
# field (verdict-line format drift) must SKIP, never silently degrade to
|
|
126
|
+
# PASS (#518).
|
|
127
|
+
if [ "$V_PARSE" != "OK" ] || [ -z "$V_RESULTS" ] || [ -z "$V_ERRORS" ] || [ -z "$V_BLOCKING" ] || [ -z "$V_WARNINGS" ]; then
|
|
128
|
+
# Unparseable output: nothing the exit code claims can be trusted (#518).
|
|
129
|
+
# Fail closed — SKIP with diagnostics, never a PASS.
|
|
130
|
+
echo " ⚠️ semgrep output unparseable or verdict fields missing (exit code ${SEMGREP_EXIT}) — cannot determine verdict"
|
|
131
|
+
if [ -n "$SEMGREP_OUTPUT" ]; then
|
|
132
|
+
echo " ── what semgrep reported on stderr ──"
|
|
133
|
+
printf '%s\n' "$SEMGREP_OUTPUT" | tail -10 | sed 's/^/ /'
|
|
120
134
|
fi
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
if [ -n "$SEMGREP_STDOUT_TAIL" ]; then
|
|
136
|
+
echo " ── what semgrep wrote to stdout (tail) ──"
|
|
137
|
+
printf '%s\n' "$SEMGREP_STDOUT_TAIL" | sed 's/^/ /'
|
|
138
|
+
fi
|
|
139
|
+
echo " ⏭️ SKIPPED - SAST (semgrep output unparseable)"
|
|
140
|
+
GATE_10_STATUS="SKIP"
|
|
141
|
+
elif [ "$V_RESULTS" -eq 0 ] && { [ "$V_ERRORS" -gt 0 ] || [ "$SEMGREP_EXIT" -ne 0 ]; }; then
|
|
142
|
+
# Nothing was scanned to a verdict: semgrep either failed outright
|
|
143
|
+
# (nonzero exit) or completed but reported scan errors for every input
|
|
144
|
+
# (#518 R2: exit 0 + results:[] + errors:[{...}] used to fall through to
|
|
145
|
+
# PASS). An operational failure must neither block nor claim a pass
|
|
146
|
+
# (#475 kept).
|
|
147
|
+
echo " ⚠️ semgrep exit=${SEMGREP_EXIT}, scan errors=${V_ERRORS}, findings=${V_RESULTS} — nothing to judge"
|
|
129
148
|
if [ -n "$SEMGREP_OUTPUT" ]; then
|
|
130
149
|
echo " ── what semgrep reported ──"
|
|
131
150
|
printf '%s\n' "$SEMGREP_OUTPUT" | tail -10 | sed 's/^/ /'
|
|
132
151
|
fi
|
|
133
152
|
echo " ⏭️ SKIPPED - SAST (semgrep runtime error)"
|
|
134
153
|
GATE_10_STATUS="SKIP"
|
|
154
|
+
elif [ "$V_BLOCKING" -gt 0 ]; then
|
|
155
|
+
echo ""
|
|
156
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
157
|
+
echo " GATE 10: Semgrep Security Gate"
|
|
158
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
159
|
+
echo " CRITICAL/HIGH/ERROR: ${V_BLOCKING} ❌ BLOCKED"
|
|
160
|
+
echo " MEDIUM/LOW/WARNING/INFO: ${V_WARNINGS} ⚠️ warning"
|
|
161
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
162
|
+
echo " ❌ BLOCKED — blocking-severity vulnerability found"
|
|
163
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
164
|
+
echo "$FINDING_DETAILS"
|
|
165
|
+
echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
|
|
166
|
+
GATE_10_STATUS="FAIL"
|
|
167
|
+
exit 1
|
|
168
|
+
else
|
|
169
|
+
echo ""
|
|
170
|
+
echo " ✅ PASSED - No blocking vulnerabilities (files scanned: ${#SEMGREP_ARGS[@]})"
|
|
171
|
+
if [ "$V_WARNINGS" -gt 0 ]; then
|
|
172
|
+
echo " ⚠️ ${V_WARNINGS} non-blocking findings (warnings only)"
|
|
173
|
+
echo "$FINDING_DETAILS"
|
|
174
|
+
fi
|
|
175
|
+
GATE_10_STATUS="PASS"
|
|
135
176
|
fi
|
|
177
|
+
|
|
178
|
+
fi # node available
|
|
136
179
|
fi
|
|
137
180
|
fi
|
|
138
181
|
record_gate_audit "gate-10" "sast-security" "$GATE_10_STATUS" "0" "$GATE_10_START"
|
package/hooks/gate-3.sh
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
1
2
|
# ============================================================================
|
|
2
3
|
# GATE 3: Cyclomatic Complexity Check
|
|
3
4
|
# Uses lizard - checks cyclomatic complexity of changed source files
|
|
@@ -17,7 +18,45 @@ elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
|
17
18
|
GATE_3_STATUS="SKIP"
|
|
18
19
|
|
|
19
20
|
else
|
|
20
|
-
|
|
21
|
+
# S2 (#507): three-level CCN threshold override --
|
|
22
|
+
# .xp-gate/ccn-threshold (positive integer) > XP_GATE_CCN_THRESHOLD > 5.
|
|
23
|
+
# Invalid values WARN on stderr and fall back to the default; the resolved
|
|
24
|
+
# value reaches the gate-3 audit record via the --detail argument below.
|
|
25
|
+
# Implemented with bash builtins only (no tr/grep spawns): every process
|
|
26
|
+
# spawn is expensive on Windows under AV filter drivers.
|
|
27
|
+
resolve_ccn_threshold() {
|
|
28
|
+
local default=5 val
|
|
29
|
+
if [ -f ".xp-gate/ccn-threshold" ]; then
|
|
30
|
+
val=$(<".xp-gate/ccn-threshold")
|
|
31
|
+
val="${val//[[:space:]]/}"
|
|
32
|
+
case "$val" in
|
|
33
|
+
''|*[!0-9]*|0)
|
|
34
|
+
echo "WARN - invalid .xp-gate/ccn-threshold value '${val}' (expected a positive integer), falling back to ${default}" >&2
|
|
35
|
+
;;
|
|
36
|
+
*)
|
|
37
|
+
echo "$val"
|
|
38
|
+
return 0
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
echo "$default"
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
val=${XP_GATE_CCN_THRESHOLD:-}
|
|
45
|
+
if [ -n "$val" ]; then
|
|
46
|
+
case "$val" in
|
|
47
|
+
''|*[!0-9]*|0)
|
|
48
|
+
echo "WARN - invalid XP_GATE_CCN_THRESHOLD value '${val}' (expected a positive integer), falling back to ${default}" >&2
|
|
49
|
+
;;
|
|
50
|
+
*)
|
|
51
|
+
echo "$val"
|
|
52
|
+
return 0
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
fi
|
|
56
|
+
echo "$default"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
CCN_THRESHOLD=$(resolve_ccn_threshold)
|
|
21
60
|
|
|
22
61
|
# Check lizard availability
|
|
23
62
|
LIZARD_CMD=""
|
|
@@ -37,7 +76,16 @@ else
|
|
|
37
76
|
echo "Checking complexity for source files..."
|
|
38
77
|
|
|
39
78
|
# Run lizard with CCN threshold
|
|
40
|
-
|
|
79
|
+
# One path per line, into an array. Unquoted $CC_FILES relied on word
|
|
80
|
+
# splitting, which also splits any path containing a space -- handing
|
|
81
|
+
# lizard two bogus paths and checking neither (#457 defect class, same
|
|
82
|
+
# fix as gate-4's PRINCIPLES_ARGS). Quoting the scalar would instead
|
|
83
|
+
# collapse the list into one argument. An array does both.
|
|
84
|
+
CC_FILES_ARGS=()
|
|
85
|
+
while IFS= read -r _cc_file; do
|
|
86
|
+
[ -n "$_cc_file" ] && CC_FILES_ARGS+=("$_cc_file")
|
|
87
|
+
done <<< "$CC_FILES"
|
|
88
|
+
CC_OUTPUT=$("$LIZARD_PATH" -C "$CCN_THRESHOLD" ${CC_FILES_ARGS[@]+"${CC_FILES_ARGS[@]}"} 2>&1 || true)
|
|
41
89
|
|
|
42
90
|
# Parse warning count from the summary table: "Warning cnt 8"
|
|
43
91
|
# Use anchored grep to avoid matching lizard table headers (e.g. "Rt" column)
|
|
@@ -65,4 +113,4 @@ else
|
|
|
65
113
|
GATE_3_STATUS="WARN"
|
|
66
114
|
fi
|
|
67
115
|
fi
|
|
68
|
-
record_gate_audit "gate-3" "complexity" "$GATE_3_STATUS" "${CC_WARNINGS:-0}" "$GATE_3_START"
|
|
116
|
+
record_gate_audit "gate-3" "complexity" "$GATE_3_STATUS" "${CC_WARNINGS:-0}" "$GATE_3_START" "ccn_threshold=${CCN_THRESHOLD:-5}"
|