From 888736387d681c15c70c78139f980ae2ae6c6c6f Mon Sep 17 00:00:00 2001 From: bluecheat Date: Sat, 3 Oct 2026 20:00:19 +0900 Subject: [PATCH] =?UTF-8?q?fix(hooks):=20=EC=9C=84=EB=B0=98=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=A0=EB=A7=88=EB=8B=A4=20file:line=20=EA=B3=BC=20=EA=B1=B8?= =?UTF-8?q?=EB=A6=B0=20=EA=B7=9C=EC=B9=99=EC=9D=84,=20=EC=9A=94=EC=95=BD?= =?UTF-8?q?=20=EC=A4=84=EC=97=90=EB=8F=84=20=EC=9C=84=EC=B9=98=EB=A5=BC=20?= =?UTF-8?q?=EC=8B=A4=EC=96=B4=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 실측(one-tenth): 한 커밋은 파일 이름이 나왔고, 다른 커밋은 "⚠ CRITICAL 위반 1건 — 경고만 (mode=warning)" 한 줄만 보여서 위치를 diff 에서 손으로 찾았어요. 시크릿은 줄 없이 파일 이름만 찍었고, 요약 줄에는 위치가 없어서 출력이 잘리거나 다른 훅 출력과 섞이면 위치가 사라졌어요. - critical-rule-grep: `report <규칙>` — 위반마다 ` ⚠ file:line — 규칙` 한 줄. 시크릿도 걸린 줄마다 `file:line — secrets:<라벨>` (줄 내용은 여전히 안 찍어요). 예전 2인자 호출(프로젝트가 채운 스캐폴드)도 그대로 찍혀요 - 요약 줄 `CRITICAL 위반 N건 — …` 에 앞 3곳 + "외 N건" 을 실어요 — 모드(warning·fail) 무관 - check-mistake-secrets: 파일 목록에 줄 번호(`file:3,7`)를, 요약 줄에 위치를 smoke §60: warning·fail 두 모드에서 위반 줄과 요약 줄 양쪽에 file:line·규칙, 시크릿 값 비노출, mistake 파일 줄 번호, 요약 축약(앞 3곳 + 외 2건). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01C8rZ6V3FW3nwYA9T7iygPd --- templates/default/.ax/hooks/README.md | 5 +- .../hooks/pre-commit/check-mistake-secrets.sh | 10 +++- .../hooks/pre-commit/critical-rule-grep.sh | 45 ++++++++++---- tests/smoke.sh | 60 ++++++++++++++++++- 4 files changed, 101 insertions(+), 19 deletions(-) diff --git a/templates/default/.ax/hooks/README.md b/templates/default/.ax/hooks/README.md index 0503a60..8b811e8 100644 --- a/templates/default/.ax/hooks/README.md +++ b/templates/default/.ax/hooks/README.md @@ -92,8 +92,9 @@ spec 은 ID(`2026-10-03-1b92`, 옛 순번 `014`)로 적어도 되고 디렉터 마스킹 행이 키 목록 조각을 공유해요. `${GITHUB_TOKEN}`·`` 같은 참조 표기와 `secret: null`· `token_count = 0` 은 값 첫 글자와 최소 길이로 걸러요. TS·Kotlin·Swift 타입 자리(`token: string,` · `apiKey?: Foo` · `(token: string, …)`)는 `key=value` 를 보기 전에 줄에서 지워요 — 같은 줄의 값 대입은 그대로 -남아 걸려요. 검출되면 **파일 이름만** 찍어요 — 매칭된 줄을 -stderr 로 흘리면 검출한 의미가 없어요. +남아 걸려요. 검출되면 **`file:line — secrets:<라벨>` 만** 찍어요 — 매칭된 +줄 내용을 stderr 로 흘리면 검출한 의미가 없어요. 위반 요약 줄(`CRITICAL 위반 N건 …`)도 앞 3곳의 위치를 담아요 — +출력이 잘려 요약 한 줄만 보여도 어디를 고칠지 알 수 있게요. `common.sh` 가 없으면 검출할 패턴 자체가 없어요. 그때는 조용히 통과하지 않고 `[goax] common.sh 없음 — secrets 안전망 비활성 (.ax/scripts/bash/common.sh 복구 필요)` 를 stderr 에 diff --git a/templates/default/.ax/hooks/pre-commit/check-mistake-secrets.sh b/templates/default/.ax/hooks/pre-commit/check-mistake-secrets.sh index 9dae348..68bcf7f 100755 --- a/templates/default/.ax/hooks/pre-commit/check-mistake-secrets.sh +++ b/templates/default/.ax/hooks/pre-commit/check-mistake-secrets.sh @@ -51,10 +51,14 @@ while IFS= read -r rel; do ORIG=$(cat "$f") RED=$(printf '%s' "$ORIG" | redact_secrets) if [ "$ORIG" != "$RED" ]; then - LEAK_FILES="${LEAK_FILES}${rel}\n" + # 어느 줄인지까지 — redact_secrets 는 sed 라 줄 수가 같아요. 원본과 다른 줄 번호만 뽑아요 (내용은 안 찍어요) + LINES=$(redact_secrets < "$f" | awk 'NR==FNR { r[FNR] = $0; next } r[FNR] != $0 { printf "%s%d", (n++ ? "," : ""), FNR }' - "$f") + LEAK_FILES="${LEAK_FILES}${rel}${LINES:+:$LINES}\n" LEAK_COUNT=$((LEAK_COUNT + 1)) fi done <<< "$STAGED" +# 요약 줄에도 위치를 실어요 — 출력이 잘려 요약 한 줄만 보여도 어디인지 알 수 있게 (앞 3곳 + 외 N건) +LEAK_TAIL=$(printf '%b' "$LEAK_FILES" | awk 'NF { if (++n <= 3) printf "%s%s", (n > 1 ? ", " : " — "), $0 } END { if (n > 3) printf " 외 %d건", n - 3 }') if [ "$LEAK_COUNT" -gt 0 ]; then printf '\033[31m[goax pre-commit]\033[0m mistake 파일에 시크릿 추정 패턴이 남아있어요 (%d건):\n' "$LEAK_COUNT" >&2 @@ -64,10 +68,10 @@ if [ "$LEAK_COUNT" -gt 0 ]; then printf ' (탐지 패턴: %skey=value≥12자)\n' \ "$(goax_secret_rules | awk -F'\t' '($1=="both"||$1=="mask") && $2!="kv-mask" && !seen[$2]++ {printf "%s·", $2}')" >&2 if [ "$SENSOR_MODE" = "fail" ]; then - printf '[goax] ✗ mistake secrets %d건 — 차단 (mode=fail)\n' "$LEAK_COUNT" >&2 + printf '[goax] ✗ mistake secrets %d건 — 차단 (mode=fail)%s\n' "$LEAK_COUNT" "$LEAK_TAIL" >&2 exit 2 fi - printf '[goax] ⚠ mistake secrets %d건 — 경고만 (mode=%s)\n' "$LEAK_COUNT" "$SENSOR_MODE" >&2 + printf '[goax] ⚠ mistake secrets %d건 — 경고만 (mode=%s)%s\n' "$LEAK_COUNT" "$SENSOR_MODE" "$LEAK_TAIL" >&2 exit 0 fi diff --git a/templates/default/.ax/hooks/pre-commit/critical-rule-grep.sh b/templates/default/.ax/hooks/pre-commit/critical-rule-grep.sh index 93d340d..8e7f530 100755 --- a/templates/default/.ax/hooks/pre-commit/critical-rule-grep.sh +++ b/templates/default/.ax/hooks/pre-commit/critical-rule-grep.sh @@ -42,14 +42,33 @@ SENSOR_MODE=$(goax_mode 2>/dev/null || echo warning) [ "$SENSOR_MODE" = "off" ] && exit 0 VIOLATIONS=0 +LOCS=() # 요약 줄에 다시 싣는 위치 — `file:line (규칙)` # 위반 보고 (counter 증가만 — mistake 기록은 사용자 명시 mistake skill 로) +# Usage: report <규칙> +# 위반마다 `file:line — 규칙` 한 줄이에요. 위치 없는 경고는 실측에서 diff 를 손으로 뒤지게 했어요. +# 예전 2인자 형식(`report cat "메시지"`)도 그대로 찍혀요 — 프로젝트가 아래 스캐폴드를 채웠을 수 있어요. report() { - local category="$1"; local message="$2"; local detail="${3:-}" - printf ' ⚠ %s\n' "$message" >&2 + local category="$1" loc="$2" rule="${3:-}" + if [ -n "$rule" ]; then + printf ' ⚠ %s — %s\n' "$loc" "$rule" >&2 + LOCS+=("$loc (${rule%% (*})") + else + printf ' ⚠ %s\n' "$loc" >&2 + LOCS+=("$loc") + fi VIOLATIONS=$((VIOLATIONS + 1)) } +# 요약 줄 꼬리 — 앞 3곳 + 외 N건. 출력이 잘리거나 다른 훅 출력과 섞여 요약 한 줄만 보여도 위치가 남아요. +locs_tail() { + local n=${#LOCS[@]} i out="" + [ "$n" -gt 0 ] || return 0 + for ((i = 0; i < n && i < 3; i++)); do out="${out}${out:+, }${LOCS[$i]}"; done + [ "$n" -gt 3 ] && out="${out} 외 $((n - 3))건" + printf ' — %s' "$out" +} + # core.quotePath=false — 기본값이면 한글 등 비ASCII 파일명이 "\355\225\234…" 로 인용돼 `-f` 검사에서 빠져요 STAGED=$(git -c core.quotePath=false diff --cached --name-only 2>/dev/null || true) [ -z "$STAGED" ] && { echo "[goax] staged 파일 없음" >&2; exit 0; } @@ -69,7 +88,8 @@ echo "[goax] CRITICAL 룰 검사 (mode=$SENSOR_MODE) — 대상 $(echo "$STAGED" # *test/context/*) continue ;; # 제외 경로 — 패턴 마커로는 못 해요 # *.kt) # if grep -qE '^import[[:space:]]+org\.junit\.jupiter\.' "$f" 2>/dev/null; then -# report "testing" "$f: JUnit5 import 금지 — SP-TEST-001" +# ln=$(grep -nE '^import[[:space:]]+org\.junit\.jupiter\.' "$f" | head -1 | cut -d: -f1) +# report "testing" "$f:$ln" "SP-TEST-001 JUnit5 import 금지" # fi # ;; # esac @@ -92,16 +112,15 @@ echo "[goax] CRITICAL 룰 검사 (mode=$SENSOR_MODE) — 대상 $(echo "$STAGED" # TS·Kotlin·Swift 타입 자리(`token: string,`)는 kv-detect 전에 지워요 — `goax_secret_scan_file` 이 해요 # # 파일을 읽는 건 `goax_secret_scan_file` 하나예요 (줄번호 + 라벨만 내고 내용은 안 내요). -SECRET_FILES="" +# 걸린 줄마다 `file:line — secrets:<라벨>` 이에요. 매칭된 줄 **내용**은 안 찍어요 — 시크릿을 +# stderr·로그로 다시 흘리면 검출한 의미가 없어요. while IFS= read -r f; do [ -z "$f" ] || [ ! -f "$f" ] && continue - # 매칭된 줄은 안 찍어요 — 시크릿을 stderr·로그로 다시 흘리면 검출한 의미가 없어요 - [ -n "$(goax_secret_scan_file "$f")" ] && SECRET_FILES="${SECRET_FILES}${f}"$'\n' + while IFS=$'\t' read -r ln label; do + [ -n "$ln" ] || continue + report "secrets" "$f:$ln" "secrets:$label (값은 안 찍어요)" + done < <(goax_secret_scan_file "$f") done <<< "$STAGED" -if [ -n "$SECRET_FILES" ]; then - SECRET_N=$(printf '%s' "$SECRET_FILES" | grep -c . || true) - report "secrets" "Secrets 추정 패턴 — ${SECRET_N}개 파일: $(printf '%s' "$SECRET_FILES" | tr '\n' ' ')" -fi # ─── ① 룰 패턴 — `` 를 실제로 돌려요 ───────────────────── # 룰 파일마다: severity(파일) · paths(파일) · 패턴(룰 단위, goax_rule_patterns) 을 읽고, @@ -162,7 +181,7 @@ if [ -n "$STAGED_SRC" ] && type goax_rule_patterns >/dev/null 2>&1; then [ -z "$ln" ] && continue hit="$tf:$ln" if [ "$SEV" = "critical" ]; then - report "rule-pattern" "$hit — $token (critical · $RF_REL)" + report "rule-pattern" "$hit" "$token (critical · $RF_REL)" else printf ' · %s — %s (%s · %s — 차단 안 함)\n' "$hit" "$token" "$SEV" "$RF_REL" >&2 WARNINGS=$((WARNINGS + 1)) @@ -175,10 +194,10 @@ fi if [ "$VIOLATIONS" -gt 0 ]; then if [ "$SENSOR_MODE" = "fail" ]; then - echo "[goax] ✗ CRITICAL 위반 ${VIOLATIONS}건 — 차단 (mode=fail)" >&2 + echo "[goax] ✗ CRITICAL 위반 ${VIOLATIONS}건 — 차단 (mode=fail)$(locs_tail)" >&2 exit 2 fi - echo "[goax] ⚠ CRITICAL 위반 ${VIOLATIONS}건 — 경고만 (mode=$SENSOR_MODE). 기록 원하면 mistake skill 호출" >&2 + echo "[goax] ⚠ CRITICAL 위반 ${VIOLATIONS}건 — 경고만 (mode=$SENSOR_MODE)$(locs_tail). 기록 원하면 mistake skill 호출" >&2 exit 0 fi diff --git a/tests/smoke.sh b/tests/smoke.sh index 8ded8f1..bcd0bcf 100644 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -3322,7 +3322,7 @@ else SEC_ERR=$(CLAUDE_PROJECT_DIR="$SEC" bash .ax/hooks/pre-commit/critical-rule-grep.sh 2>&1 >/dev/null) printf '%s' "$SEC_ERR" | grep -q 'AKIAIOSFODNN7EXAMPLE' \ && fail "critical-rule-grep — 매칭 줄을 그대로 출력 (시크릿이 컨텍스트·로그로 새요)" \ - || pass "critical-rule-grep — 파일 이름만 보고, 매칭 줄은 안 찍음" + || pass "critical-rule-grep — 위치(file:line)만 보고, 매칭 줄은 안 찍음" # kv-detect 는 키 이름 뒤 `:` 를 값의 시작으로 읽어서 TS 타입 자리까지 시크릿으로 잡았어요. # 실측(one-tenth): `token: string,` · `messageFor?(target: UserTarget, token: string, platform: Platform)` @@ -4943,6 +4943,64 @@ mt --task "T009, T008,T010,T008" --json | jq -e '.result.changed==true and .resu || fail "mark-task 다중 ID: $(tr '\n' '|' < "$MTF")" rm -rf "$MT" +# ─────────────────────────────────────────────────────────── +section "60. 위반 위치 — 어느 모드든 경고마다 file:line 과 걸린 규칙, 요약 줄에도" +# ─────────────────────────────────────────────────────────── +# 실측(one-tenth): 한 커밋은 파일 이름이 나왔고, 다른 커밋은 "⚠ CRITICAL 위반 1건 — 경고만 (mode=warning)" +# 한 줄만 보여서 위치를 diff 에서 손으로 찾았어요. 시크릿은 줄 없이 파일 이름만 찍었고, 요약 줄엔 위치가 +# 없어서 출력이 잘리거나 섞이면 위치가 사라졌어요. 이제 위반마다 `file:line — 규칙` 이고, 요약 줄도 위치를 담아요. +VL=$(mktemp -d) +pushd "$VL" >/dev/null || fail "VL pushd 실패" +git init -q . >/dev/null 2>&1 +mkdir -p .ax/scripts/bash .ax/hooks/pre-commit .ax/spirit/rules .ax/mistakes src +cp "$REPO/templates/default/.ax/scripts/bash/common.sh" .ax/scripts/bash/ +cp "$REPO/templates/default/.ax/hooks/pre-commit/critical-rule-grep.sh" .ax/hooks/pre-commit/ +cp "$REPO/templates/default/.ax/hooks/pre-commit/check-mistake-secrets.sh" .ax/hooks/pre-commit/ +printf '# C\n' > CLAUDE.md +cat > .ax/spirit/rules/vl.md <<'EOF' +--- +category: vl +severity: critical +paths: + - "src/**" +--- +## SP-VL-001: console.log 금지 + +EOF +printf 'const a = 1;\nconst b = 2;\nconsole.log(a);\n' > src/log.ts +printf 'export const x = 1;\nexport const password = "%s";\n' hunter2xyz99 > src/cfg.ts +printf 'line one\nline two\npg_key=%s\n' abcdefgh12345678 > .ax/mistakes/2026-10-01-secrets.md +git add -f src/log.ts src/cfg.ts .ax/mistakes/2026-10-01-secrets.md >/dev/null 2>&1 +for vm in warning fail; do + printf 'sensors:\n mode: %s\n' "$vm" > .ax/config.yml + VL_ERR=$(CLAUDE_PROJECT_DIR="$VL" bash .ax/hooks/pre-commit/critical-rule-grep.sh 2>&1 >/dev/null) + VL_LAST=$(printf '%s\n' "$VL_ERR" | tail -1) + { printf '%s' "$VL_ERR" | grep -q 'src/cfg.ts:2 — secrets:kv-detect' \ + && printf '%s' "$VL_ERR" | grep -q 'src/log.ts:3 — SP-VL-001'; } \ + && pass "critical-rule-grep (mode=$vm) — 위반마다 file:line — 규칙 (시크릿도 줄까지, 내용은 안 찍음)" \ + || fail "critical-rule-grep (mode=$vm) — 위반 줄에 file:line·규칙 없음: $VL_ERR" + { printf '%s' "$VL_LAST" | grep -q 'src/cfg.ts:2' && printf '%s' "$VL_LAST" | grep -q 'src/log.ts:3'; } \ + && pass "critical-rule-grep (mode=$vm) — 요약 줄 하나만 보여도 위치가 있어요" \ + || fail "critical-rule-grep (mode=$vm) — 요약 줄에 위치 없음: $VL_LAST" + printf '%s' "$VL_ERR" | grep -q 'hunter2xyz99' \ + && fail "critical-rule-grep (mode=$vm) — 시크릿 값이 출력에 샘" || true + VM_ERR=$(CLAUDE_PROJECT_DIR="$VL" bash .ax/hooks/pre-commit/check-mistake-secrets.sh 2>&1 >/dev/null) + { printf '%s' "$VM_ERR" | grep -q '\.ax/mistakes/2026-10-01-secrets\.md:3' \ + && printf '%s\n' "$VM_ERR" | tail -1 | grep -q '2026-10-01-secrets\.md:3'; } \ + && pass "check-mistake-secrets (mode=$vm) — file:line 을 목록과 요약 줄에" \ + || fail "check-mistake-secrets (mode=$vm) — 줄 번호 없음: $VM_ERR" +done +# 위치가 많으면 요약 줄은 앞 3개 + "외 N건" 으로 줄여요 (요약이 화면을 덮지 않게) +printf 'console.log(1);\nconsole.log(2);\nconsole.log(3);\nconsole.log(4);\nconsole.log(5);\n' > src/many.ts +git reset -q >/dev/null 2>&1; git add -f src/many.ts >/dev/null 2>&1 +VL_MANY=$(CLAUDE_PROJECT_DIR="$VL" bash .ax/hooks/pre-commit/critical-rule-grep.sh 2>&1 >/dev/null | tail -1) +{ printf '%s' "$VL_MANY" | grep -q 'src/many.ts:3' && printf '%s' "$VL_MANY" | grep -q '외 2건' \ + && ! printf '%s' "$VL_MANY" | grep -q 'src/many.ts:4'; } \ + && pass "critical-rule-grep — 요약 줄은 앞 3곳 + 외 N건" \ + || fail "critical-rule-grep — 요약 줄 축약 실패: $VL_MANY" +popd >/dev/null || true +rm -rf "$VL" + # ─────────────────────────────────────────────────────────── section "✨ 결과" # ───────────────────────────────────────────────────────────