Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions templates/default/.ax/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ spec 은 ID(`2026-10-03-1b92`, 옛 순번 `014`)로 적어도 되고 디렉터
마스킹 행이 키 목록 조각을 공유해요. `${GITHUB_TOKEN}`·`<from env>` 같은 참조 표기와 `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 에
Expand Down
10 changes: 7 additions & 3 deletions templates/default/.ax/hooks/pre-commit/check-mistake-secrets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
45 changes: 32 additions & 13 deletions templates/default/.ax/hooks/pre-commit/critical-rule-grep.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <category> <file:line> <규칙>
# 위반마다 `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; }
Expand All @@ -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
Expand All @@ -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) 을 읽고,
Expand Down Expand Up @@ -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))
Expand All @@ -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

Expand Down
60 changes: 59 additions & 1 deletion tests/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down Expand Up @@ -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 금지
<!-- 검출 패턴: 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 "✨ 결과"
# ───────────────────────────────────────────────────────────
Expand Down
Loading