Skip to content

Commit 597f52a

Browse files
author
Secure TDD Framework
committed
fix(git): Unignore .agents/lib and track all hook engine scripts on GitHub
1 parent e88b249 commit 597f52a

5 files changed

Lines changed: 639 additions & 6 deletions

File tree

‎.agents/lib/engine_codemender.sh‎

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
#!/bin/bash
2+
# CodeMender Scanner Engine Module (Stage 2: Semantic Scan & Remediation)
3+
# Sourced by security_gate_hook.sh
4+
5+
run_codemender_gate() {
6+
local in_pipeline="${1:-false}"
7+
_init_pipeline_dir
8+
9+
if ! command -v cm >/dev/null 2>&1; then
10+
if [ "$in_pipeline" = "true" ]; then
11+
# If in pipeline and semgrep had unresolved findings, deny; otherwise allow
12+
if [ -f "$SECURITY_GATE_PIPELINE_DIR/imported_findings.jsonl" ] && [ -s "$SECURITY_GATE_PIPELINE_DIR/imported_findings.jsonl" ]; then
13+
local IMPORTED_COUNT
14+
IMPORTED_COUNT=$(wc -l < "$SECURITY_GATE_PIPELINE_DIR/imported_findings.jsonl" | tr -d ' ')
15+
deny "Stage 1 found $IMPORTED_COUNT unresolved finding(s) and CodeMender is not installed to perform Stage 2 remediation."
16+
fi
17+
allow
18+
else
19+
handle_scan_error "codemender" "the 'cm' CLI is not on PATH" true
20+
fi
21+
fi
22+
23+
# 1. Discover modified files
24+
local MODIFIED_FILES
25+
MODIFIED_FILES=$(git diff --name-only origin/main 2>/dev/null || git diff --name-only HEAD~1 2>/dev/null || true)
26+
if [ -z "$MODIFIED_FILES" ]; then
27+
allow
28+
fi
29+
local MODIFIED_FILES_ARR
30+
read_lines_into_array MODIFIED_FILES_ARR "$MODIFIED_FILES"
31+
32+
# 2. Run CodeMender scan on modified files
33+
echo "Stage 2: Running Semantic Analysis & Verification (CodeMender)..." >&2
34+
for file in "${MODIFIED_FILES_ARR[@]}"; do
35+
cm find "$file" -y --bypass-warning >/dev/null 2>&1 || true
36+
done
37+
38+
local REPORT_ERR_FILE
39+
REPORT_ERR_FILE=$(mktemp)
40+
local REPORT_RAW REPORT_EXIT
41+
if REPORT_RAW=$(cm report --status OPEN --format json 2>"$REPORT_ERR_FILE"); then
42+
REPORT_EXIT=0
43+
else
44+
REPORT_EXIT=$?
45+
fi
46+
if [ "$REPORT_EXIT" -ne 0 ] || ! echo "$REPORT_RAW" | jq -e . >/dev/null 2>&1; then
47+
local ERR_DETAIL
48+
ERR_DETAIL=$(tail -c 500 "$REPORT_ERR_FILE" 2>/dev/null)
49+
rm -f "$REPORT_ERR_FILE"
50+
handle_scan_error "codemender" "'cm report' failed (exit $REPORT_EXIT): ${ERR_DETAIL:-no output}" true
51+
fi
52+
rm -f "$REPORT_ERR_FILE"
53+
54+
# Filter findings to modified files
55+
local SCAN_RESULT
56+
SCAN_RESULT=$(echo "$REPORT_RAW" | jq --arg files "$MODIFIED_FILES" '
57+
($files | split("\n")) as $mod_files |
58+
[ .[] | select((.FilePath | gsub("\\\\"; "/")) as $fp | any($mod_files[]; . as $mf | $mf != "" and ($fp | endswith($mf)))) ]
59+
' 2>/dev/null || echo '[]')
60+
61+
# 3. Ingest imported findings from Stage 1 if present
62+
local WORK_DIR
63+
WORK_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'cm_work')
64+
echo "$SCAN_RESULT" | jq -c '.[]' > "$WORK_DIR/all.jsonl"
65+
66+
if [ -f "$SECURITY_GATE_PIPELINE_DIR/imported_findings.jsonl" ]; then
67+
while IFS= read -r imp; do
68+
[ -z "$imp" ] && continue
69+
local IMP_ID IMP_FILE IMP_SEV
70+
IMP_ID=$(echo "$imp" | jq -r '.check_id // .FindingID // "imported_issue"')
71+
IMP_FILE=$(echo "$imp" | jq -r '.path // .FilePath // "unknown"')
72+
IMP_SEV=$(echo "$imp" | jq -r '.extra.severity // .Severity // "HIGH"')
73+
jq -n --arg id "$IMP_ID" --arg file "$IMP_FILE" --arg sev "$IMP_SEV" \
74+
'{FindingID:$id, FilePath:$file, Severity:$sev, Source:"stage1_imported"}' >> "$WORK_DIR/all.jsonl"
75+
done < "$SECURITY_GATE_PIPELINE_DIR/imported_findings.jsonl"
76+
fi
77+
78+
: > "$WORK_DIR/blocking.jsonl"
79+
: > "$WORK_DIR/advisory.jsonl"
80+
81+
while IFS= read -r finding; do
82+
[ -z "$finding" ] && continue
83+
local SEV FILE FID
84+
SEV=$(echo "$finding" | jq -r '.Severity // .severity // "UNKNOWN"')
85+
FILE=$(echo "$finding" | jq -r '.FilePath // .path // ""')
86+
FID=$(echo "$finding" | jq -r '.FindingID // .check_id // ""')
87+
88+
if is_blocking_severity "$SEV" && matches_threat_model "$FID" "$FILE" "$FID"; then
89+
echo "$finding" >> "$WORK_DIR/blocking.jsonl"
90+
else
91+
echo "$finding" >> "$WORK_DIR/advisory.jsonl"
92+
fi
93+
done < "$WORK_DIR/all.jsonl"
94+
95+
local ADV_COUNT
96+
ADV_COUNT=$(wc -l < "$WORK_DIR/advisory.jsonl" | tr -d ' ')
97+
if [ "$ADV_COUNT" -gt 0 ]; then
98+
local ADV_JSON
99+
ADV_JSON=$(jq -s '.' "$WORK_DIR/advisory.jsonl")
100+
log_event "ADVISORY" "codemender" "$(jq -n --argjson f "$ADV_JSON" '{findings:$f}')"
101+
notify "ADVISORY" "$ADV_COUNT finding(s) treated as advisory." \
102+
"$(jq -n --argjson f "$ADV_JSON" '{findings:$f}')"
103+
fi
104+
105+
local BLOCK_COUNT
106+
BLOCK_COUNT=$(wc -l < "$WORK_DIR/blocking.jsonl" | tr -d ' ')
107+
if [ "$BLOCK_COUNT" -eq 0 ]; then
108+
rm -rf "$WORK_DIR"
109+
echo "No blocking vulnerabilities detected. Code verified clean." >&2
110+
allow
111+
fi
112+
113+
echo "Detected $BLOCK_COUNT blocking vulnerabilit(y/ies). Entering 3-attempt TDD remediation loop..." >&2
114+
115+
# 4. 3-Attempt TDD Remediation Loop (RED-GREEN)
116+
while IFS= read -r finding <&3; do
117+
[ -z "$finding" ] && continue
118+
local FINDING_ID FILE SEV
119+
FINDING_ID=$(echo "$finding" | jq -r '.FindingID // .check_id')
120+
FILE=$(echo "$finding" | jq -r '.FilePath // .path')
121+
SEV=$(echo "$finding" | jq -r '.Severity // .severity // "UNKNOWN"')
122+
123+
echo "Vulnerability in scope: $FINDING_ID ($SEV) in $FILE" >&2
124+
125+
local RETRY_COUNT=0
126+
local RESOLVED=false
127+
local LARGE_DIFF=false
128+
129+
while [ "$RETRY_COUNT" -lt "$SECURITY_GATE_MAX_RETRIES" ]; do
130+
RETRY_COUNT=$((RETRY_COUNT+1))
131+
echo "Attempting cm fix for: $FINDING_ID (Attempt $RETRY_COUNT/$SECURITY_GATE_MAX_RETRIES)..." >&2
132+
if ! cm fix "$FINDING_ID" -y --bypass-warning; then
133+
echo "cm fix returned non-zero. Reverting..." >&2
134+
git checkout -- . 2>/dev/null || true
135+
continue
136+
fi
137+
138+
if [ -n "$SECURITY_GATE_TEST_CMD" ]; then
139+
if ! sh -c "$SECURITY_GATE_TEST_CMD" >/dev/null 2>&1; then
140+
echo "Fix failed unit/regression test assertions. Reverting..." >&2
141+
git checkout -- . 2>/dev/null || true
142+
continue
143+
fi
144+
fi
145+
146+
cm find "$FILE" -y --bypass-warning >/dev/null 2>&1 || true
147+
local STILL_OPEN
148+
STILL_OPEN=$(cm report --status OPEN --format json 2>/dev/null \
149+
| jq --arg id "$FINDING_ID" '[.[] | select(.FindingID == $id)] | length' 2>/dev/null || echo 0)
150+
if [ "$STILL_OPEN" != "0" ]; then
151+
echo "Tests passed, but $FINDING_ID is still reported open after rescan." >&2
152+
git checkout -- . 2>/dev/null || true
153+
continue
154+
fi
155+
156+
local DIFF_STAT INS DEL CHANGED_LINES
157+
DIFF_STAT=$(git diff --shortstat 2>/dev/null || echo "")
158+
INS=$(echo "$DIFF_STAT" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)
159+
DEL=$(echo "$DIFF_STAT" | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo 0)
160+
CHANGED_LINES=$((INS + DEL))
161+
if [ "$CHANGED_LINES" -gt "$SECURITY_GATE_LARGE_FIX_LINES" ]; then
162+
echo "Fix diff is large ($CHANGED_LINES lines > $SECURITY_GATE_LARGE_FIX_LINES threshold) - escalating to human review." >&2
163+
LARGE_DIFF=true
164+
git checkout -- . 2>/dev/null || true
165+
break
166+
fi
167+
168+
commit_fix "$FINDING_ID" "$FILE" "$SEV"
169+
evolve_context_and_skills "$FINDING_ID" "$FILE" "$SEV"
170+
log_event "FIXED" "codemender" "$(jq -n --argjson f "$finding" '{findings:[$f]}')"
171+
RESOLVED=true
172+
break
173+
done
174+
175+
# 5. Verification Step if fix could not pass tests within 3 attempts
176+
if [ "$RESOLVED" != "true" ] && [ "$LARGE_DIFF" != "true" ]; then
177+
git checkout -- . 2>/dev/null || true
178+
echo "Fix failed tests past 3rd attempt. Running 'cm verify' to evaluate exploitability..." >&2
179+
180+
local VERIFY_OUT VERIFY_EXIT
181+
VERIFY_OUT=$(mktemp)
182+
if cm verify "$FINDING_ID" -y --bypass-warning >"$VERIFY_OUT" 2>&1; then
183+
VERIFY_EXIT=0
184+
else
185+
VERIFY_EXIT=$?
186+
fi
187+
rm -f "$VERIFY_OUT"
188+
189+
# If cm verify returns non-zero (1), the issue is conclusively not exploitable / false positive
190+
if [ "$VERIFY_EXIT" -eq 1 ]; then
191+
echo "cm verify clarified $FINDING_ID is not exploitable in this context. Recording as advisory." >&2
192+
log_event "ADVISORY" "codemender" "$(jq -n --argjson f "$finding" '{findings:[$f], note:"cm verify clarified non-exploitable after 3 test retries"}')"
193+
notify "ADVISORY" "Finding $FINDING_ID remained after 3 failed fix attempts, but cm verify confirmed non-exploitable in context." \
194+
"$(jq -n --argjson f "$finding" '{findings:[$f]}')"
195+
RESOLVED=true
196+
else
197+
# cm verify returned 0 (confirmed exploitable) or crashed (exit > 1)
198+
echo "cm verify confirmed finding $FINDING_ID is exploitable or verify failed (exit $VERIFY_EXIT)." >&2
199+
fi
200+
fi
201+
202+
# 6. Escalation to HITL
203+
if [ "$RESOLVED" != "true" ]; then
204+
local REASON_HINT="auto-fix could not satisfy test assertions after $SECURITY_GATE_MAX_RETRIES attempts"
205+
[ "$LARGE_DIFF" = true ] && REASON_HINT="fix diff exceeds safe auto-commit threshold"
206+
207+
echo "Escalating $FINDING_ID to human review ($REASON_HINT)." >&2
208+
209+
# If interactive TTY is present, offer interactive prompt
210+
if [ -t 0 ] || [ -t 1 ]; then
211+
echo "Select action for finding $FINDING_ID:" >&2
212+
echo "1) Defer with audited justification" >&2
213+
echo "2) Abort and fix manually (blocks push)" >&2
214+
local CHOICE
215+
read -p "Enter choice [1-2]: " CHOICE
216+
case "$CHOICE" in
217+
1)
218+
local JUSTIFICATION
219+
read -p "Enter deferral justification: " JUSTIFICATION
220+
log_event "ADVISORY" "codemender" "$(jq -n --argjson f "$finding" --arg j "$JUSTIFICATION" '{findings:[$f], justification:$j}')"
221+
notify "ADVISORY" "Finding $FINDING_ID deferred: $JUSTIFICATION" "$(jq -n --argjson f "$finding" '{findings:[$f]}')"
222+
RESOLVED=true
223+
;;
224+
*)
225+
log_event "BLOCKED" "codemender" "$(jq -n --argjson f "$finding" '{findings:[$f], reason:"manual resolution required"}')"
226+
rm -rf "$WORK_DIR"
227+
deny "Unresolved finding $FINDING_ID ($SEV). Push blocked for manual fix."
228+
;;
229+
esac
230+
else
231+
# Non-interactive / headless environment fails closed
232+
log_event "BLOCKED" "codemender" "$(jq -n --argjson f "$finding" --arg r "$REASON_HINT" '{findings:[$f], reason:$r}')"
233+
rm -rf "$WORK_DIR"
234+
deny "Finding $FINDING_ID ($SEV) blocked: $REASON_HINT. See $SECURITY_GATE_LOG for details."
235+
fi
236+
fi
237+
done 3< "$WORK_DIR/blocking.jsonl"
238+
239+
rm -rf "$WORK_DIR"
240+
allow
241+
}

0 commit comments

Comments
 (0)