security: 보안 강제 시스템 구축 + 하드코딩 비밀번호 제거
보안 감사 결과 CRITICAL 2건, HIGH 5건 발견 → 수정 완료 + 자동화 구축. [보안 수정] - issue-view.js: 하드코딩 비밀번호 → crypto.getRandomValues() 랜덤 생성 - pushSubscriptionController.js: ntfy 비밀번호 → process.env.NTFY_SUB_PASSWORD - DEPLOY-GUIDE.md/PROGRESS.md/migration SQL: 평문 비밀번호 → placeholder - docker-compose.yml/.env.example: NTFY_SUB_PASSWORD 환경변수 추가 [보안 강제 시스템 - 신규] - scripts/security-scan.sh: 8개 규칙 (CRITICAL 2, HIGH 4, MEDIUM 2) 3모드(staged/all/diff), severity, .securityignore, MEDIUM 임계값 - .githooks/pre-commit: 로컬 빠른 피드백 - .githooks/pre-receive-server.sh: Gitea 서버 최종 차단 bypass 거버넌스([SECURITY-BYPASS: 사유] + 사용자 제한 + 로그) - SECURITY-CHECKLIST.md: 10개 카테고리 자동/수동 구분 - docs/SECURITY-GUIDE.md: 운영자 가이드 (워크플로우, bypass, FAQ) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@@ -74,7 +74,7 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# DB 접속 (NAS)
|
# DB 접속 (NAS)
|
||||||
ssh hyungi@100.71.132.52 "docker exec tk-mariadb mysql -uhyungi_user -p'hyung-ddfdf3-D341@' hyungi"
|
ssh hyungi@100.71.132.52 "docker exec tk-mariadb mysql -uhyungi_user -p\"\$MYSQL_PASSWORD\" hyungi"
|
||||||
|
|
||||||
# 로그 확인 (NAS)
|
# 로그 확인 (NAS)
|
||||||
ssh hyungi@100.71.132.52 "cd /volume1/docker/tk-factory-services && export PATH=\$PATH:/volume2/@appstore/ContainerManager/usr/bin && docker compose logs -f --tail=50 <서비스>"
|
ssh hyungi@100.71.132.52 "cd /volume1/docker/tk-factory-services && export PATH=\$PATH:/volume2/@appstore/ContainerManager/usr/bin && docker compose logs -f --tail=50 <서비스>"
|
||||||
|
|||||||
12
.env.example
@@ -99,4 +99,16 @@ OLLAMA_TIMEOUT=120
|
|||||||
# tkfb.technicalkorea.net → http://tk-gateway:80
|
# tkfb.technicalkorea.net → http://tk-gateway:80
|
||||||
# tkreport.technicalkorea.net → http://tk-system2-web:80
|
# tkreport.technicalkorea.net → http://tk-system2-web:80
|
||||||
# tkqc.technicalkorea.net → http://tk-system3-web:80
|
# tkqc.technicalkorea.net → http://tk-system3-web:80
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# ntfy 푸시 알림 서버
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
NTFY_BASE_URL=http://ntfy:80
|
||||||
|
NTFY_PUBLISH_TOKEN=change_this_ntfy_publish_token
|
||||||
|
NTFY_EXTERNAL_URL=https://ntfy.technicalkorea.net
|
||||||
|
NTFY_SUB_PASSWORD=change_this_ntfy_subscriber_password
|
||||||
|
TKFB_BASE_URL=https://tkfb.technicalkorea.net
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Cloudflare Tunnel
|
||||||
|
# -------------------------------------------------------------------
|
||||||
CLOUDFLARE_TUNNEL_TOKEN=your_tunnel_token_here
|
CLOUDFLARE_TUNNEL_TOKEN=your_tunnel_token_here
|
||||||
|
|||||||
5
.githooks/pre-commit
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# pre-commit hook — 로컬 빠른 피드백
|
||||||
|
# 역할: 커밋 전 보안 검사 (staged 파일만)
|
||||||
|
# 우회: git commit --no-verify (서버 pre-receive에서 최종 차단됨)
|
||||||
|
exec "$(git rev-parse --show-toplevel)/scripts/security-scan.sh" --staged
|
||||||
168
.githooks/pre-receive-server.sh
Executable file
@@ -0,0 +1,168 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# pre-receive-server.sh — Gitea 서버용 보안 게이트
|
||||||
|
# =============================================================================
|
||||||
|
# 설치: Gitea 웹 관리자 → 저장소 → Settings → Git Hooks → pre-receive
|
||||||
|
# 또는: cp pre-receive-server.sh $REPO_PATH/custom/hooks/pre-receive
|
||||||
|
#
|
||||||
|
# 동작: push 시 변경 내용을 regex 검사, 위반 시 push 차단
|
||||||
|
# bypass: 커밋 메시지에 [SECURITY-BYPASS: 사유] 포함 시 통과 + 로그
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
# --- 설정 ---
|
||||||
|
BYPASS_LOG="/data/gitea/security-bypass.log"
|
||||||
|
ALLOWED_BYPASS_EMAILS="ahn@hyungi.net hyungi@technicalkorea.net"
|
||||||
|
MEDIUM_THRESHOLD=5
|
||||||
|
ZERO_REV="0000000000000000000000000000000000000000"
|
||||||
|
|
||||||
|
# --- 검출 규칙 (security-scan.sh와 동일, 자체 내장) ---
|
||||||
|
RULES=(
|
||||||
|
'SECRET_HARDCODE|CRITICAL|비밀정보 하드코딩|(password|token|apiKey|secret|api_key)\s*[:=]\s*[\x27"][^${\x27"][^\x27"]{3,}'
|
||||||
|
'SECRET_KNOWN|CRITICAL|알려진 비밀번호 패턴|(fukdon-riwbaq|hyung-ddfdf3|djg3-jj34|tkfactory-sub)'
|
||||||
|
'LOCALSTORAGE_AUTH|HIGH|localStorage 인증정보|localStorage\.(setItem|getItem).*\b(token|password|auth|credential)'
|
||||||
|
'INNERHTML_XSS|HIGH|innerHTML XSS 위험|\.innerHTML\s*[+=]'
|
||||||
|
'CORS_WILDCARD|HIGH|CORS 와일드카드|origin:\s*[\x27"`]\*[\x27"`]'
|
||||||
|
'SQL_INTERPOLATION|HIGH|SQL 문자열 보간|query\(`.*\$\{'
|
||||||
|
'LOG_SECRET|MEDIUM|로그에 비밀정보|console\.(log|error|warn).*\b(password|token|secret|apiKey)'
|
||||||
|
'ENV_HARDCODE|MEDIUM|환경설정 하드코딩|NODE_ENV\s*[:=]\s*[\x27"]development[\x27"]'
|
||||||
|
)
|
||||||
|
|
||||||
|
EXCLUDE_PATTERNS="node_modules|\.git|__pycache__|package-lock\.json|\.min\.js|\.min\.css"
|
||||||
|
|
||||||
|
# --- 메인 ---
|
||||||
|
while read -r oldrev newrev refname; do
|
||||||
|
# 브랜치 삭제 시 스킵
|
||||||
|
if [[ "$newrev" == "$ZERO_REV" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 신규 브랜치
|
||||||
|
if [[ "$oldrev" == "$ZERO_REV" ]]; then
|
||||||
|
# 첫 push: 최근 커밋만 검사 (또는 스킵)
|
||||||
|
echo "[SECURITY] New branch detected — scanning latest commit only"
|
||||||
|
oldrev=$(git rev-parse "${newrev}~1" 2>/dev/null || echo "$ZERO_REV")
|
||||||
|
if [[ "$oldrev" == "$ZERO_REV" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- bypass 확인 ---
|
||||||
|
BYPASS_FOUND=false
|
||||||
|
BYPASS_REASON=""
|
||||||
|
while IFS= read -r msg; do
|
||||||
|
if echo "$msg" | grep -qP '\[SECURITY-BYPASS:\s*.+\]'; then
|
||||||
|
BYPASS_FOUND=true
|
||||||
|
BYPASS_REASON=$(echo "$msg" | grep -oP '\[SECURITY-BYPASS:\s*\K[^\]]+')
|
||||||
|
elif echo "$msg" | grep -q '\[SECURITY-BYPASS\]'; then
|
||||||
|
echo "[SECURITY] ERROR: Bypass requires reason: [SECURITY-BYPASS: hotfix 사유]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done < <(git log --format='%s' "$oldrev".."$newrev" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ "$BYPASS_FOUND" == "true" ]]; then
|
||||||
|
AUTHOR=$(git log -1 --format='%ae' "$newrev" 2>/dev/null || echo "unknown")
|
||||||
|
# 사용자 제한
|
||||||
|
ALLOWED=false
|
||||||
|
for email in $ALLOWED_BYPASS_EMAILS; do
|
||||||
|
if [[ "$AUTHOR" == "$email" ]]; then
|
||||||
|
ALLOWED=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$ALLOWED" != "true" ]]; then
|
||||||
|
echo "[SECURITY] Bypass not allowed for: $AUTHOR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# 로그 기록
|
||||||
|
echo "$(date -Iseconds) | user=$AUTHOR | ref=$refname | commits=$oldrev..$newrev | reason=$BYPASS_REASON | TODO=24h내 수정 필수" \
|
||||||
|
>> "$BYPASS_LOG" 2>/dev/null || true
|
||||||
|
echo "[SECURITY] ⚠ Bypass accepted — reason: $BYPASS_REASON (logged, 24h 내 수정 필수)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- diff 기반 보안 검사 ---
|
||||||
|
VIOLATIONS=0
|
||||||
|
MEDIUM_COUNT=0
|
||||||
|
OUTPUT=""
|
||||||
|
|
||||||
|
DIFF_OUTPUT=$(git diff -U0 --diff-filter=ACMRT "$oldrev" "$newrev" 2>/dev/null || true)
|
||||||
|
if [[ -z "$DIFF_OUTPUT" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
CURRENT_FILE=""
|
||||||
|
CURRENT_LINE=0
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^diff\ --git\ a/(.+)\ b/(.+)$ ]]; then
|
||||||
|
CURRENT_FILE="${BASH_REMATCH[2]}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "$line" =~ ^\+\+\+\ b/(.+)$ ]]; then
|
||||||
|
CURRENT_FILE="${BASH_REMATCH[1]}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "$line" =~ ^@@.*\+([0-9]+) ]]; then
|
||||||
|
CURRENT_LINE="${BASH_REMATCH[1]}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "$line" =~ ^\+ ]] && [[ ! "$line" =~ ^\+\+\+ ]]; then
|
||||||
|
local_content="${line:1}"
|
||||||
|
|
||||||
|
# 제외 패턴
|
||||||
|
if echo "$CURRENT_FILE" | grep -qEi "$EXCLUDE_PATTERNS" 2>/dev/null; then
|
||||||
|
CURRENT_LINE=$((CURRENT_LINE + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 인라인 ignore 체크 + 규칙 검사
|
||||||
|
for i in "${!RULES[@]}"; do
|
||||||
|
IFS='|' read -r r_name r_sev r_desc r_pat <<< "${RULES[$i]}"
|
||||||
|
if echo "$local_content" | grep -qP "$r_pat" 2>/dev/null; then
|
||||||
|
# 라인 단위 ignore
|
||||||
|
if echo "$local_content" | grep -qP "security-ignore:\s*$r_name" 2>/dev/null; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
RNUM=$((i + 1))
|
||||||
|
TRIMMED=$(echo "$local_content" | sed 's/^[[:space:]]*//' | head -c 100)
|
||||||
|
if [[ "$r_sev" == "CRITICAL" || "$r_sev" == "HIGH" ]]; then
|
||||||
|
OUTPUT+="$(printf "\n ✗ [%s] #%d %s — %s\n → %s:%d\n %s\n" \
|
||||||
|
"$r_sev" "$RNUM" "$r_name" "$r_desc" "$CURRENT_FILE" "$CURRENT_LINE" "$TRIMMED")"
|
||||||
|
VIOLATIONS=$((VIOLATIONS + 1))
|
||||||
|
else
|
||||||
|
OUTPUT+="$(printf "\n ⚠ [%s] #%d %s — %s\n → %s:%d\n %s\n" \
|
||||||
|
"$r_sev" "$RNUM" "$r_name" "$r_desc" "$CURRENT_FILE" "$CURRENT_LINE" "$TRIMMED")"
|
||||||
|
MEDIUM_COUNT=$((MEDIUM_COUNT + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
CURRENT_LINE=$((CURRENT_LINE + 1))
|
||||||
|
fi
|
||||||
|
done <<< "$DIFF_OUTPUT"
|
||||||
|
|
||||||
|
TOTAL=$((VIOLATIONS + MEDIUM_COUNT))
|
||||||
|
if [[ $TOTAL -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "[SECURITY] $TOTAL issue(s) found in push to $refname:"
|
||||||
|
echo "$OUTPUT"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ $MEDIUM_COUNT -gt $MEDIUM_THRESHOLD ]]; then
|
||||||
|
echo "[SECURITY] MEDIUM violations ($MEDIUM_COUNT) exceed threshold ($MEDIUM_THRESHOLD) — blocking"
|
||||||
|
VIOLATIONS=$((VIOLATIONS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $VIOLATIONS -gt 0 ]]; then
|
||||||
|
echo "Push rejected. Fix violations or use [SECURITY-BYPASS: 사유] in commit message."
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Warnings only ($MEDIUM_COUNT MEDIUM) — push allowed."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
exit 0
|
||||||
24
.securityignore
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# .securityignore — 보안 스캔 제외 목록
|
||||||
|
# =============================================================================
|
||||||
|
# 규칙:
|
||||||
|
# - 모든 항목에 사유 주석 필수 (없으면 경고)
|
||||||
|
# - 월 1회 정기 검토 → 불필요 항목 제거
|
||||||
|
# - 날짜 표기 권장
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# 스캔 스크립트 자체 (규칙 패턴 포함)
|
||||||
|
scripts/security-scan.sh # 규칙 정의 자체 (2026-04-10)
|
||||||
|
.githooks/pre-receive-server.sh # 규칙 정의 자체 (2026-04-10)
|
||||||
|
|
||||||
|
# 환경변수 템플릿 (placeholder만 포함)
|
||||||
|
.env.example # placeholder 값만 (2026-04-10)
|
||||||
|
|
||||||
|
# 보안 감사 보고서 (발견된 패턴 인용)
|
||||||
|
SECURITY-AUDIT-20260402.md # 감사 보고서 인용 (2026-04-10)
|
||||||
|
SECURITY-FINDINGS-SUMMARY.txt # 감사 요약 인용 (2026-04-10)
|
||||||
|
SECURITY-CODE-SNIPPETS.md # 코드 스니펫 인용 (2026-04-10)
|
||||||
|
|
||||||
|
# 보안 가이드/체크리스트 (규칙 예시 포함)
|
||||||
|
SECURITY-CHECKLIST.md # 규칙 참조 예시 (2026-04-10)
|
||||||
|
docs/SECURITY-GUIDE.md # 가이드 예시 코드 (2026-04-10)
|
||||||
@@ -95,7 +95,7 @@ cat "/volume1/Technicalkorea Document/tkfb-package/.env" | grep MYSQL
|
|||||||
cat /volume1/docker/tkqc/tkqc-package/.env | grep POSTGRES
|
cat /volume1/docker/tkqc/tkqc-package/.env | grep POSTGRES
|
||||||
|
|
||||||
# Cloudflare Tunnel 토큰
|
# Cloudflare Tunnel 토큰
|
||||||
echo 'fukdon-riwbaq-fiQfy2' | sudo -S /usr/local/bin/docker inspect tkfb_cloudflared | grep TUNNEL_TOKEN
|
echo "${NAS_SUDO_PASSWORD}" | sudo -S /usr/local/bin/docker inspect tkfb_cloudflared | grep TUNNEL_TOKEN
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -112,12 +112,12 @@ ssh hyungi@192.168.0.3
|
|||||||
mkdir -p /volume1/docker/backups/$(date +%Y%m%d)
|
mkdir -p /volume1/docker/backups/$(date +%Y%m%d)
|
||||||
|
|
||||||
# MariaDB 백업
|
# MariaDB 백업
|
||||||
echo 'fukdon-riwbaq-fiQfy2' | sudo -S /usr/local/bin/docker exec tkfb_db \
|
echo "${NAS_SUDO_PASSWORD}" | sudo -S /usr/local/bin/docker exec tkfb_db \
|
||||||
mysqldump -u root -p<ROOT_PASSWORD> --all-databases > \
|
mysqldump -u root -p<ROOT_PASSWORD> --all-databases > \
|
||||||
/volume1/docker/backups/$(date +%Y%m%d)/mariadb-all.sql
|
/volume1/docker/backups/$(date +%Y%m%d)/mariadb-all.sql
|
||||||
|
|
||||||
# PostgreSQL 백업
|
# PostgreSQL 백업
|
||||||
echo 'fukdon-riwbaq-fiQfy2' | sudo -S /usr/local/bin/docker exec tkqc-db \
|
echo "${NAS_SUDO_PASSWORD}" | sudo -S /usr/local/bin/docker exec tkqc-db \
|
||||||
pg_dumpall -U mproject > \
|
pg_dumpall -U mproject > \
|
||||||
/volume1/docker/backups/$(date +%Y%m%d)/postgres-all.sql
|
/volume1/docker/backups/$(date +%Y%m%d)/postgres-all.sql
|
||||||
|
|
||||||
@@ -167,11 +167,11 @@ rm -rf ../tk-factory-services.bak
|
|||||||
# NAS SSH
|
# NAS SSH
|
||||||
# TK-FB 중지
|
# TK-FB 중지
|
||||||
cd "/volume1/Technicalkorea Document/tkfb-package"
|
cd "/volume1/Technicalkorea Document/tkfb-package"
|
||||||
echo 'fukdon-riwbaq-fiQfy2' | sudo -S /usr/local/bin/docker compose down
|
echo "${NAS_SUDO_PASSWORD}" | sudo -S /usr/local/bin/docker compose down
|
||||||
|
|
||||||
# TKQC 중지
|
# TKQC 중지
|
||||||
cd /volume1/docker/tkqc/tkqc-package
|
cd /volume1/docker/tkqc/tkqc-package
|
||||||
echo 'fukdon-riwbaq-fiQfy2' | sudo -S /usr/local/bin/docker compose down
|
echo "${NAS_SUDO_PASSWORD}" | sudo -S /usr/local/bin/docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 4: 통합 서비스 기동
|
### Step 4: 통합 서비스 기동
|
||||||
@@ -180,10 +180,10 @@ echo 'fukdon-riwbaq-fiQfy2' | sudo -S /usr/local/bin/docker compose down
|
|||||||
cd /volume1/docker/tk-factory-services
|
cd /volume1/docker/tk-factory-services
|
||||||
|
|
||||||
# Docker 이미지 빌드 + 서비스 기동
|
# Docker 이미지 빌드 + 서비스 기동
|
||||||
echo 'fukdon-riwbaq-fiQfy2' | sudo -S /usr/local/bin/docker compose up --build -d
|
echo "${NAS_SUDO_PASSWORD}" | sudo -S /usr/local/bin/docker compose up --build -d
|
||||||
|
|
||||||
# 로그 확인
|
# 로그 확인
|
||||||
echo 'fukdon-riwbaq-fiQfy2' | sudo -S /usr/local/bin/docker compose logs -f --tail=50
|
echo "${NAS_SUDO_PASSWORD}" | sudo -S /usr/local/bin/docker compose logs -f --tail=50
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 5: DB 마이그레이션
|
### Step 5: DB 마이그레이션
|
||||||
@@ -196,7 +196,7 @@ echo 'fukdon-riwbaq-fiQfy2' | sudo -S /usr/local/bin/docker compose logs -f --ta
|
|||||||
# (system3-nonconformance/api/migrations/ → PostgreSQL init)
|
# (system3-nonconformance/api/migrations/ → PostgreSQL init)
|
||||||
|
|
||||||
# 헬스체크 확인
|
# 헬스체크 확인
|
||||||
echo 'fukdon-riwbaq-fiQfy2' | sudo -S /usr/local/bin/docker compose ps
|
echo "${NAS_SUDO_PASSWORD}" | sudo -S /usr/local/bin/docker compose ps
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 6: Cloudflare Tunnel 설정
|
### Step 6: Cloudflare Tunnel 설정
|
||||||
@@ -291,15 +291,15 @@ git log --oneline -10
|
|||||||
```bash
|
```bash
|
||||||
# 통합 서비스 중지
|
# 통합 서비스 중지
|
||||||
cd /volume1/docker_1/tk-factory-services
|
cd /volume1/docker_1/tk-factory-services
|
||||||
echo 'fukdon-riwbaq-fiQfy2' | sudo -S /usr/local/bin/docker compose down
|
echo "${NAS_SUDO_PASSWORD}" | sudo -S /usr/local/bin/docker compose down
|
||||||
|
|
||||||
# TK-FB 복원
|
# TK-FB 복원
|
||||||
cd "/volume1/Technicalkorea Document/tkfb-package"
|
cd "/volume1/Technicalkorea Document/tkfb-package"
|
||||||
echo 'fukdon-riwbaq-fiQfy2' | sudo -S /usr/local/bin/docker compose up -d
|
echo "${NAS_SUDO_PASSWORD}" | sudo -S /usr/local/bin/docker compose up -d
|
||||||
|
|
||||||
# TKQC 복원
|
# TKQC 복원
|
||||||
cd /volume1/docker/tkqc/tkqc-package
|
cd /volume1/docker/tkqc/tkqc-package
|
||||||
echo 'fukdon-riwbaq-fiQfy2' | sudo -S /usr/local/bin/docker compose up -d
|
echo "${NAS_SUDO_PASSWORD}" | sudo -S /usr/local/bin/docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ TK-FB-Project(공장관리+신고)와 M-Project(부적합관리)를 **3개 독
|
|||||||
### NAS (192.168.0.3)
|
### NAS (192.168.0.3)
|
||||||
- TK-FB 운영: `/volume1/Technicalkorea Document/tkfb-package/`
|
- TK-FB 운영: `/volume1/Technicalkorea Document/tkfb-package/`
|
||||||
- TKQC 운영: `/volume1/docker/tkqc/tkqc-package/`
|
- TKQC 운영: `/volume1/docker/tkqc/tkqc-package/`
|
||||||
- SSH: `hyungi` / `fukdon-riwbaq-fiQfy2`
|
- SSH: `hyungi` / `${SSH_PASSWORD}` (비밀번호는 비밀관리 시스템 참조)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
39
SECURITY-CHECKLIST.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 보안 PR 체크리스트 — TK Factory Services
|
||||||
|
|
||||||
|
> 공통 원칙: `claude-config/memory/feedback_security_pr_checklist.md`
|
||||||
|
> 자동 검증: `scripts/security-scan.sh` (pre-commit + pre-receive)
|
||||||
|
|
||||||
|
## 체크리스트
|
||||||
|
|
||||||
|
| # | 카테고리 | 검증 | 확인 항목 | 참조 파일 |
|
||||||
|
|---|---------|------|----------|----------|
|
||||||
|
| 1 | 비밀 정보 | **자동** #1,#2 | 코드/문서에 비밀번호·토큰·API키 하드코딩 없음 | `.env.example` |
|
||||||
|
| 2 | 인증 | 수동 | 모든 라우트에 `requireAuth` 적용 | `shared/middleware/auth.js` |
|
||||||
|
| 3 | 권한 RBAC | 수동 | 쓰기(POST/PUT/DELETE)에 `requirePage()` 또는 `requireRole()` | `shared/middleware/pagePermission.js` |
|
||||||
|
| 4 | 입력 검증 | 수동 | path traversal(`../`), 타입, 길이 검증 | `system1-factory/api/utils/validator.js` |
|
||||||
|
| 5 | 파일 업로드 | 수동 | magic number + 확장자 + MIME + 크기 제한 | `system1-factory/api/utils/fileUploadSecurity.js` |
|
||||||
|
| 6 | 네트워크 | **자동** #5 | CORS 와일드카드 없음, rate limiting 적용 | `system1-factory/api/config/cors.js` |
|
||||||
|
| 7 | DB 쿼리 | **자동** #6 | 파라미터화(`?`), `await`, `COALESCE` 패턴 | CLAUDE.md 주의사항 |
|
||||||
|
| 8 | 에러/로그 | **자동** #7 | 로그에 비밀정보 없음, 스택트레이스 prod 비노출 | `shared/utils/errors.js` |
|
||||||
|
| 9 | 보안 헤더 | 수동 | CSP, HSTS, X-Frame-Options | `system1-factory/api/config/security.js` |
|
||||||
|
| 10 | 자동 검증 | **자동** | pre-commit + pre-receive 통과 | `scripts/security-scan.sh` |
|
||||||
|
|
||||||
|
## 자동 검출 규칙
|
||||||
|
|
||||||
|
| 규칙# | 이름 | 심각도 | 동작 |
|
||||||
|
|-------|------|--------|------|
|
||||||
|
| 1 | SECRET_HARDCODE | CRITICAL | 차단 |
|
||||||
|
| 2 | SECRET_KNOWN | CRITICAL | 차단 |
|
||||||
|
| 3 | LOCALSTORAGE_AUTH | HIGH | 차단 |
|
||||||
|
| 4 | INNERHTML_XSS | HIGH | 차단 |
|
||||||
|
| 5 | CORS_WILDCARD | HIGH | 차단 |
|
||||||
|
| 6 | SQL_INTERPOLATION | HIGH | 차단 |
|
||||||
|
| 7 | LOG_SECRET | MEDIUM | 경고 (5개 초과 시 차단) |
|
||||||
|
| 8 | ENV_HARDCODE | MEDIUM | 경고 (5개 초과 시 차단) |
|
||||||
|
|
||||||
|
## 수동 확인 필요 항목 (자동화 한계)
|
||||||
|
|
||||||
|
- RBAC 설계 오류 / 인증 흐름
|
||||||
|
- 비즈니스 로직 / race condition
|
||||||
|
- third-party dependency 취약점 (`npm audit`)
|
||||||
|
- 환경변수 값 강도
|
||||||
@@ -309,6 +309,7 @@ services:
|
|||||||
- NTFY_BASE_URL=${NTFY_BASE_URL:-http://ntfy:80}
|
- NTFY_BASE_URL=${NTFY_BASE_URL:-http://ntfy:80}
|
||||||
- NTFY_PUBLISH_TOKEN=${NTFY_PUBLISH_TOKEN}
|
- NTFY_PUBLISH_TOKEN=${NTFY_PUBLISH_TOKEN}
|
||||||
- NTFY_EXTERNAL_URL=${NTFY_EXTERNAL_URL:-https://ntfy.technicalkorea.net}
|
- NTFY_EXTERNAL_URL=${NTFY_EXTERNAL_URL:-https://ntfy.technicalkorea.net}
|
||||||
|
- NTFY_SUB_PASSWORD=${NTFY_SUB_PASSWORD}
|
||||||
- TKFB_BASE_URL=${TKFB_BASE_URL:-https://tkfb.technicalkorea.net}
|
- TKFB_BASE_URL=${TKFB_BASE_URL:-https://tkfb.technicalkorea.net}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
|
||||||
|
|||||||
157
docs/SECURITY-GUIDE.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# 보안 시스템 운영 가이드
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
TK Factory Services에는 2계층 보안 검사 시스템이 적용되어 있습니다.
|
||||||
|
|
||||||
|
| 계층 | 위치 | 역할 | 우회 가능 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| pre-commit | 로컬 (개발자 PC) | 빠른 피드백 | `--no-verify` |
|
||||||
|
| pre-receive | Gitea 서버 | 최종 차단 | `[SECURITY-BYPASS: 사유]`만 |
|
||||||
|
|
||||||
|
## 개발 워크플로우
|
||||||
|
|
||||||
|
```
|
||||||
|
코드 작성 → git add → git commit
|
||||||
|
↓
|
||||||
|
pre-commit hook
|
||||||
|
(security-scan.sh --staged)
|
||||||
|
↓
|
||||||
|
위반 있으면 → 커밋 차단 + 상세 출력
|
||||||
|
위반 없으면 → 커밋 성공
|
||||||
|
↓
|
||||||
|
git push
|
||||||
|
↓
|
||||||
|
pre-receive hook (서버)
|
||||||
|
(diff 기반 검사)
|
||||||
|
↓
|
||||||
|
위반 있으면 → push 차단
|
||||||
|
위반 없으면 → push 성공
|
||||||
|
```
|
||||||
|
|
||||||
|
## 위반 발생 시 대처
|
||||||
|
|
||||||
|
### 에러 메시지 읽기
|
||||||
|
|
||||||
|
```
|
||||||
|
[SECURITY] 2 issue(s) found:
|
||||||
|
|
||||||
|
✗ [CRITICAL] #1 SECRET_HARDCODE — 비밀정보 하드코딩
|
||||||
|
→ src/controllers/auth.js:64
|
||||||
|
password: 'my-secret-123'
|
||||||
|
```
|
||||||
|
|
||||||
|
- `[CRITICAL]` / `[HIGH]` → 차단됨, 반드시 수정
|
||||||
|
- `[MEDIUM]` → 경고, 5개 초과 시 차단
|
||||||
|
- `→ 파일:라인번호` → 수정할 위치
|
||||||
|
- 아래 줄 → 문제가 된 코드
|
||||||
|
|
||||||
|
### 수정 방법 (규칙별)
|
||||||
|
|
||||||
|
| 규칙 | 수정 방법 |
|
||||||
|
|------|----------|
|
||||||
|
| SECRET_HARDCODE | `process.env.변수명`으로 이동, `.env`에 추가 |
|
||||||
|
| LOCALSTORAGE_AUTH | HttpOnly 쿠키 또는 Authorization 헤더 사용 |
|
||||||
|
| INNERHTML_XSS | `textContent` 사용 또는 DOMPurify 적용 |
|
||||||
|
| CORS_WILDCARD | 허용 도메인을 명시적으로 나열 |
|
||||||
|
| SQL_INTERPOLATION | 파라미터화 쿼리(`?` placeholder) 사용 |
|
||||||
|
| LOG_SECRET | 로그에서 비밀정보 제거 |
|
||||||
|
|
||||||
|
## bypass 사용법 (긴급 시)
|
||||||
|
|
||||||
|
### 형식
|
||||||
|
```
|
||||||
|
git commit -m "fix: 긴급 장애 대응 [SECURITY-BYPASS: prod 서비스 다운 긴급 핫픽스]"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 규칙
|
||||||
|
- **사유 필수**: `[SECURITY-BYPASS]`만으로는 거부됨
|
||||||
|
- **허용 사용자만**: 운영담당자(ahn@hyungi.net)만 bypass 가능
|
||||||
|
- **24시간 내 수정**: bypass 후 반드시 보안 이슈 수정 PR 제출
|
||||||
|
- **로그 기록**: 모든 bypass는 서버에 자동 기록됨
|
||||||
|
|
||||||
|
### bypass 후 조치
|
||||||
|
1. bypass한 코드의 보안 이슈 파악
|
||||||
|
2. 24시간 내 수정 커밋
|
||||||
|
3. `security-scan.sh --all`로 전체 검증
|
||||||
|
|
||||||
|
## 규칙 추가/수정 방법
|
||||||
|
|
||||||
|
### 새 규칙 추가
|
||||||
|
`scripts/security-scan.sh`의 RULES 배열에 추가:
|
||||||
|
```bash
|
||||||
|
'RULE_NAME|SEVERITY|설명|REGEX_PATTERN'
|
||||||
|
```
|
||||||
|
|
||||||
|
예시:
|
||||||
|
```bash
|
||||||
|
'EVAL_USAGE|HIGH|eval 사용 위험|eval\s*\('
|
||||||
|
```
|
||||||
|
|
||||||
|
### 같은 규칙을 서버에도 반영
|
||||||
|
`.githooks/pre-receive-server.sh`의 RULES 배열에도 동일하게 추가.
|
||||||
|
Gitea 서버의 hook 파일도 업데이트 필요.
|
||||||
|
|
||||||
|
## false positive 등록
|
||||||
|
|
||||||
|
### 파일 단위 제외
|
||||||
|
`.securityignore`에 추가 (주석 필수):
|
||||||
|
```
|
||||||
|
path/to/file.js # 사유 설명 (날짜)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 라인 단위 제외
|
||||||
|
소스 코드에 인라인 주석:
|
||||||
|
```javascript
|
||||||
|
const pattern = /password/; // security-ignore: SECRET_HARDCODE — regex 패턴 정의
|
||||||
|
```
|
||||||
|
|
||||||
|
### 주의
|
||||||
|
- 주석 없는 항목은 스캔 시 경고
|
||||||
|
- 월 1회 `.securityignore` 검토하여 불필요 항목 제거
|
||||||
|
|
||||||
|
## 수동 검사
|
||||||
|
|
||||||
|
### 전체 프로젝트 스캔
|
||||||
|
```bash
|
||||||
|
./scripts/security-scan.sh --all
|
||||||
|
```
|
||||||
|
|
||||||
|
### 엄격 모드 (MEDIUM도 차단)
|
||||||
|
```bash
|
||||||
|
./scripts/security-scan.sh --all --strict
|
||||||
|
```
|
||||||
|
|
||||||
|
### 두 커밋 간 비교
|
||||||
|
```bash
|
||||||
|
./scripts/security-scan.sh --diff HEAD~5 HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
## 초기 설정 (새 머신)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. git hooks 경로 설정
|
||||||
|
git config core.hooksPath .githooks
|
||||||
|
|
||||||
|
# 2. 전체 스캔 확인
|
||||||
|
./scripts/security-scan.sh --all
|
||||||
|
|
||||||
|
# 3. 테스트 (선택)
|
||||||
|
echo "password: 'test'" >> /tmp/test.js
|
||||||
|
git add /tmp/test.js
|
||||||
|
git commit -m "test" # → 차단되어야 함
|
||||||
|
```
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
**Q: pre-commit이 너무 느리다**
|
||||||
|
A: staged 파일만 검사하므로 보통 1초 이내. 파일이 많으면 `--no-verify`로 우회 후 push 시 서버에서 검사.
|
||||||
|
|
||||||
|
**Q: false positive가 계속 뜬다**
|
||||||
|
A: `.securityignore`에 등록하거나 라인에 `// security-ignore: RULE_NAME` 추가.
|
||||||
|
|
||||||
|
**Q: 규칙을 비활성화하고 싶다**
|
||||||
|
A: RULES 배열에서 해당 규칙을 주석 처리. 단, CRITICAL 규칙 비활성화는 비권장.
|
||||||
|
|
||||||
|
**Q: 새 서비스 추가 시**
|
||||||
|
A: 추가 설정 불필요. `.securityignore`에 제외할 파일이 있으면 등록.
|
||||||
355
scripts/security-scan.sh
Executable file
@@ -0,0 +1,355 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# security-scan.sh — TK Factory Services 보안 스캔 엔진
|
||||||
|
# =============================================================================
|
||||||
|
# 용도: pre-commit hook, pre-receive hook, 수동 전체 검사
|
||||||
|
# 모드:
|
||||||
|
# --staged staged 파일만 검사 (pre-commit 기본)
|
||||||
|
# --all 프로젝트 전체 파일 검사
|
||||||
|
# --diff OLD NEW 두 커밋 간 변경 검사 (pre-receive용)
|
||||||
|
# --strict MEDIUM도 차단
|
||||||
|
#
|
||||||
|
# 커버리지 한계 (PR 리뷰에서 수동):
|
||||||
|
# - RBAC 설계 오류 / 인증 흐름
|
||||||
|
# - 비즈니스 로직 / race condition
|
||||||
|
# - third-party dependency (npm audit 영역)
|
||||||
|
# - 환경변수 값 강도
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- 색상 ---
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# --- 설정 ---
|
||||||
|
MEDIUM_THRESHOLD=5
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
IGNORE_FILE="$PROJECT_ROOT/.securityignore"
|
||||||
|
|
||||||
|
# --- 검출 규칙: NAME|SEVERITY|설명|PATTERN ---
|
||||||
|
RULES=(
|
||||||
|
'SECRET_HARDCODE|CRITICAL|비밀정보 하드코딩|(password|token|apiKey|secret|api_key)\s*[:=]\s*[\x27"][^${\x27"][^\x27"]{3,}'
|
||||||
|
'SECRET_KNOWN|CRITICAL|알려진 비밀번호 패턴|(fukdon-riwbaq|hyung-ddfdf3|djg3-jj34|tkfactory-sub)'
|
||||||
|
'LOCALSTORAGE_AUTH|HIGH|localStorage 인증정보|localStorage\.(setItem|getItem).*\b(token|password|auth|credential)'
|
||||||
|
'INNERHTML_XSS|HIGH|innerHTML XSS 위험|\.innerHTML\s*[+=]'
|
||||||
|
'CORS_WILDCARD|HIGH|CORS 와일드카드|origin:\s*[\x27"`]\*[\x27"`]'
|
||||||
|
'SQL_INTERPOLATION|HIGH|SQL 문자열 보간|query\(`.*\$\{'
|
||||||
|
'LOG_SECRET|MEDIUM|로그에 비밀정보|console\.(log|error|warn).*\b(password|token|secret|apiKey)'
|
||||||
|
'ENV_HARDCODE|MEDIUM|환경설정 하드코딩|NODE_ENV\s*[:=]\s*[\x27"]development[\x27"]'
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- 제외 패턴 ---
|
||||||
|
EXCLUDE_DIRS="node_modules|\.git|__pycache__|\.next|dist|build|coverage"
|
||||||
|
EXCLUDE_FILES="package-lock\.json|yarn\.lock|\.min\.js|\.min\.css|\.map"
|
||||||
|
|
||||||
|
# --- 파싱 함수 ---
|
||||||
|
parse_rule() {
|
||||||
|
local rule="$1"
|
||||||
|
RULE_NAME=$(echo "$rule" | cut -d'|' -f1)
|
||||||
|
RULE_SEVERITY=$(echo "$rule" | cut -d'|' -f2)
|
||||||
|
RULE_DESC=$(echo "$rule" | cut -d'|' -f3)
|
||||||
|
RULE_PATTERN=$(echo "$rule" | cut -d'|' -f4-)
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- .securityignore 로드 ---
|
||||||
|
load_ignore_list() {
|
||||||
|
IGNORED_FILES=()
|
||||||
|
if [[ -f "$IGNORE_FILE" ]]; then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# 빈 줄, 순수 주석 스킵
|
||||||
|
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
|
||||||
|
# 파일명 추출 (주석 앞부분)
|
||||||
|
local filepath
|
||||||
|
filepath=$(echo "$line" | sed 's/#.*$//' | xargs)
|
||||||
|
[[ -z "$filepath" ]] && continue
|
||||||
|
# 주석 없는 항목 경고
|
||||||
|
if ! echo "$line" | grep -q '#'; then
|
||||||
|
echo -e "${YELLOW}[WARN] .securityignore: '$filepath' 에 사유 주석이 없습니다${NC}" >&2
|
||||||
|
fi
|
||||||
|
IGNORED_FILES+=("$filepath")
|
||||||
|
done < "$IGNORE_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_ignored_file() {
|
||||||
|
local file="$1"
|
||||||
|
for ignored in "${IGNORED_FILES[@]}"; do
|
||||||
|
[[ "$file" == "$ignored" || "$file" == *"/$ignored" ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
is_line_ignored() {
|
||||||
|
local line_content="$1"
|
||||||
|
local rule_name="$2"
|
||||||
|
echo "$line_content" | grep -qP "security-ignore:\s*$rule_name" && return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- diff 파싱 + 검사 ---
|
||||||
|
scan_diff() {
|
||||||
|
local diff_input="$1"
|
||||||
|
local violations=0
|
||||||
|
local medium_count=0
|
||||||
|
local current_file=""
|
||||||
|
local current_line=0
|
||||||
|
local results=""
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# 파일명 추출
|
||||||
|
if [[ "$line" =~ ^diff\ --git\ a/(.+)\ b/(.+)$ ]]; then
|
||||||
|
current_file="${BASH_REMATCH[2]}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# +++ b/filename
|
||||||
|
if [[ "$line" =~ ^\+\+\+\ b/(.+)$ ]]; then
|
||||||
|
current_file="${BASH_REMATCH[1]}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# hunk header → 라인 번호
|
||||||
|
if [[ "$line" =~ ^@@.*\+([0-9]+) ]]; then
|
||||||
|
current_line="${BASH_REMATCH[1]}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# 추가된 라인만 검사
|
||||||
|
if [[ "$line" =~ ^\+ ]] && [[ ! "$line" =~ ^\+\+\+ ]]; then
|
||||||
|
local content="${line:1}" # + 제거
|
||||||
|
current_line=$((current_line))
|
||||||
|
|
||||||
|
# 제외 디렉토리/파일 체크
|
||||||
|
if echo "$current_file" | grep -qEi "($EXCLUDE_DIRS)" 2>/dev/null; then
|
||||||
|
current_line=$((current_line + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if echo "$current_file" | grep -qEi "($EXCLUDE_FILES)" 2>/dev/null; then
|
||||||
|
current_line=$((current_line + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# .securityignore 체크
|
||||||
|
if is_ignored_file "$current_file"; then
|
||||||
|
current_line=$((current_line + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 각 규칙 검사
|
||||||
|
for i in "${!RULES[@]}"; do
|
||||||
|
parse_rule "${RULES[$i]}"
|
||||||
|
if echo "$content" | grep -qP "$RULE_PATTERN" 2>/dev/null; then
|
||||||
|
# 라인 단위 ignore 체크
|
||||||
|
if is_line_ignored "$content" "$RULE_NAME"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
local rule_num=$((i + 1))
|
||||||
|
local trimmed
|
||||||
|
trimmed=$(echo "$content" | sed 's/^[[:space:]]*//' | head -c 100)
|
||||||
|
if [[ "$RULE_SEVERITY" == "CRITICAL" || "$RULE_SEVERITY" == "HIGH" ]]; then
|
||||||
|
results+="$(printf "\n ${RED}✗ [%s] #%d %s${NC} — %s\n → %s:%d\n %s\n" \
|
||||||
|
"$RULE_SEVERITY" "$rule_num" "$RULE_NAME" "$RULE_DESC" \
|
||||||
|
"$current_file" "$current_line" "$trimmed")"
|
||||||
|
violations=$((violations + 1))
|
||||||
|
else
|
||||||
|
results+="$(printf "\n ${YELLOW}⚠ [%s] #%d %s${NC} — %s\n → %s:%d\n %s\n" \
|
||||||
|
"$RULE_SEVERITY" "$rule_num" "$RULE_NAME" "$RULE_DESC" \
|
||||||
|
"$current_file" "$current_line" "$trimmed")"
|
||||||
|
medium_count=$((medium_count + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
current_line=$((current_line + 1))
|
||||||
|
fi
|
||||||
|
done <<< "$diff_input"
|
||||||
|
|
||||||
|
# 결과 출력
|
||||||
|
local total=$((violations + medium_count))
|
||||||
|
if [[ $total -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}[SECURITY] ${total} issue(s) found:${NC}"
|
||||||
|
echo -e "$results"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# MEDIUM 임계값 체크
|
||||||
|
if [[ $medium_count -gt $MEDIUM_THRESHOLD ]]; then
|
||||||
|
echo -e "${RED}[SECURITY] MEDIUM violations ($medium_count) exceed threshold ($MEDIUM_THRESHOLD) — blocking${NC}"
|
||||||
|
violations=$((violations + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# strict 모드
|
||||||
|
if [[ "${STRICT_MODE:-false}" == "true" && $medium_count -gt 0 ]]; then
|
||||||
|
echo -e "${RED}[SECURITY] --strict mode: MEDIUM violations also block${NC}"
|
||||||
|
violations=$((violations + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $violations -gt 0 ]]; then
|
||||||
|
echo -e "${RED}Push/commit rejected. Fix violations or use [SECURITY-BYPASS: 사유] in commit message.${NC}"
|
||||||
|
echo ""
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}Warnings only — commit/push allowed.${NC}"
|
||||||
|
echo ""
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 전체 파일 검사 (--all 모드) ---
|
||||||
|
scan_all() {
|
||||||
|
local violations=0
|
||||||
|
local medium_count=0
|
||||||
|
local results=""
|
||||||
|
|
||||||
|
load_ignore_list
|
||||||
|
|
||||||
|
local files
|
||||||
|
files=$(find "$PROJECT_ROOT" -type f \
|
||||||
|
\( -name "*.js" -o -name "*.py" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" \
|
||||||
|
-o -name "*.md" -o -name "*.sql" -o -name "*.yml" -o -name "*.yaml" \
|
||||||
|
-o -name "*.json" -o -name "*.sh" -o -name "*.html" \) \
|
||||||
|
! -path "*/node_modules/*" \
|
||||||
|
! -path "*/.git/*" \
|
||||||
|
! -path "*/__pycache__/*" \
|
||||||
|
! -path "*/dist/*" \
|
||||||
|
! -path "*/build/*" \
|
||||||
|
! -path "*/coverage/*" \
|
||||||
|
! -path "*/.claude/worktrees/*" \
|
||||||
|
! -name "package-lock.json" \
|
||||||
|
! -name "*.min.js" \
|
||||||
|
! -name "*.min.css" \
|
||||||
|
2>/dev/null || true)
|
||||||
|
|
||||||
|
while IFS= read -r filepath; do
|
||||||
|
[[ -z "$filepath" ]] && continue
|
||||||
|
local relpath="${filepath#$PROJECT_ROOT/}"
|
||||||
|
|
||||||
|
# .securityignore 체크
|
||||||
|
if is_ignored_file "$relpath"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
for i in "${!RULES[@]}"; do
|
||||||
|
parse_rule "${RULES[$i]}"
|
||||||
|
|
||||||
|
local matches
|
||||||
|
matches=$(grep -nP "$RULE_PATTERN" "$filepath" 2>/dev/null || true)
|
||||||
|
[[ -z "$matches" ]] && continue
|
||||||
|
|
||||||
|
while IFS= read -r match; do
|
||||||
|
local linenum content
|
||||||
|
linenum=$(echo "$match" | cut -d: -f1)
|
||||||
|
content=$(echo "$match" | cut -d: -f2- | sed 's/^[[:space:]]*//' | head -c 100)
|
||||||
|
|
||||||
|
# 라인 단위 ignore
|
||||||
|
local full_line
|
||||||
|
full_line=$(sed -n "${linenum}p" "$filepath" 2>/dev/null || true)
|
||||||
|
if is_line_ignored "$full_line" "$RULE_NAME"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local rule_num=$((i + 1))
|
||||||
|
if [[ "$RULE_SEVERITY" == "CRITICAL" || "$RULE_SEVERITY" == "HIGH" ]]; then
|
||||||
|
results+="$(printf "\n ${RED}✗ [%s] #%d %s${NC} — %s\n → %s:%s\n %s\n" \
|
||||||
|
"$RULE_SEVERITY" "$rule_num" "$RULE_NAME" "$RULE_DESC" \
|
||||||
|
"$relpath" "$linenum" "$content")"
|
||||||
|
violations=$((violations + 1))
|
||||||
|
else
|
||||||
|
results+="$(printf "\n ${YELLOW}⚠ [%s] #%d %s${NC} — %s\n → %s:%s\n %s\n" \
|
||||||
|
"$RULE_SEVERITY" "$rule_num" "$RULE_NAME" "$RULE_DESC" \
|
||||||
|
"$relpath" "$linenum" "$content")"
|
||||||
|
medium_count=$((medium_count + 1))
|
||||||
|
fi
|
||||||
|
done <<< "$matches"
|
||||||
|
done
|
||||||
|
done <<< "$files"
|
||||||
|
|
||||||
|
# 결과 출력
|
||||||
|
local total=$((violations + medium_count))
|
||||||
|
if [[ $total -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}[SECURITY] Full scan: ${total} issue(s) found:${NC}"
|
||||||
|
echo -e "$results"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ $medium_count -gt $MEDIUM_THRESHOLD ]]; then
|
||||||
|
echo -e "${RED}[SECURITY] MEDIUM violations ($medium_count) exceed threshold ($MEDIUM_THRESHOLD)${NC}"
|
||||||
|
violations=$((violations + 1))
|
||||||
|
fi
|
||||||
|
if [[ "${STRICT_MODE:-false}" == "true" && $medium_count -gt 0 ]]; then
|
||||||
|
echo -e "${RED}[SECURITY] --strict mode: MEDIUM violations also count${NC}"
|
||||||
|
violations=$((violations + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $violations -gt 0 ]]; then
|
||||||
|
echo -e "${RED}${violations} blocking violation(s) found.${NC}"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}Warnings only (${medium_count} MEDIUM).${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}[SECURITY] Full scan: 0 violations found.${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 메인 ---
|
||||||
|
main() {
|
||||||
|
local mode="staged"
|
||||||
|
local old_rev="" new_rev=""
|
||||||
|
STRICT_MODE="false"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--staged) mode="staged"; shift ;;
|
||||||
|
--all) mode="all"; shift ;;
|
||||||
|
--diff) mode="diff"; old_rev="$2"; new_rev="$3"; shift 3 ;;
|
||||||
|
--strict) STRICT_MODE="true"; shift ;;
|
||||||
|
-h|--help)
|
||||||
|
echo "Usage: security-scan.sh [--staged|--all|--diff OLD NEW] [--strict]"
|
||||||
|
echo " --staged Check staged files (default, for pre-commit)"
|
||||||
|
echo " --all Scan entire project"
|
||||||
|
echo " --diff Check changes between two commits (for pre-receive)"
|
||||||
|
echo " --strict Block MEDIUM violations too"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) echo "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
load_ignore_list
|
||||||
|
|
||||||
|
case "$mode" in
|
||||||
|
staged)
|
||||||
|
local diff_output
|
||||||
|
diff_output=$(git diff --cached -U0 --diff-filter=ACMRT 2>/dev/null || true)
|
||||||
|
if [[ -z "$diff_output" ]]; then
|
||||||
|
echo -e "${GREEN}[SECURITY] No staged changes to scan.${NC}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
scan_diff "$diff_output"
|
||||||
|
;;
|
||||||
|
all)
|
||||||
|
scan_all
|
||||||
|
;;
|
||||||
|
diff)
|
||||||
|
if [[ -z "$old_rev" || -z "$new_rev" ]]; then
|
||||||
|
echo "Error: --diff requires OLD and NEW revisions"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
local diff_output
|
||||||
|
diff_output=$(git diff -U0 --diff-filter=ACMRT "$old_rev" "$new_rev" 2>/dev/null || true)
|
||||||
|
if [[ -z "$diff_output" ]]; then
|
||||||
|
echo -e "${GREEN}[SECURITY] No changes to scan.${NC}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
scan_diff "$diff_output"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
-- Push 구독 테이블 생성
|
-- Push 구독 테이블 생성
|
||||||
-- 실행: docker exec -i tk-mariadb mysql -uhyungi_user -p'hyung-ddfdf3-D341@' hyungi < db/migrations/20260313001000_create_push_subscriptions.sql
|
-- 실행: docker exec -i tk-mariadb mysql -uhyungi_user -p"$MYSQL_PASSWORD" hyungi < db/migrations/20260313001000_create_push_subscriptions.sql
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 369 KiB |