Compare commits
11 Commits
de6d918d42
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
312369d9ac | ||
|
|
9d2179e47a | ||
|
|
ba9ef32808 | ||
|
|
bbffa47a9d | ||
|
|
bf0d7fd87a | ||
|
|
56f626911a | ||
|
|
178155df6b | ||
|
|
d49aa01bd5 | ||
|
|
f28922a3ae | ||
|
|
7db072ed14 | ||
|
|
0de9d5bb48 |
@@ -74,7 +74,7 @@
|
||||
|
||||
```bash
|
||||
# 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)
|
||||
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
|
||||
# tkreport.technicalkorea.net → http://tk-system2-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
|
||||
|
||||
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
|
||||
|
||||
# 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)
|
||||
|
||||
# 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 > \
|
||||
/volume1/docker/backups/$(date +%Y%m%d)/mariadb-all.sql
|
||||
|
||||
# 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 > \
|
||||
/volume1/docker/backups/$(date +%Y%m%d)/postgres-all.sql
|
||||
|
||||
@@ -167,11 +167,11 @@ rm -rf ../tk-factory-services.bak
|
||||
# NAS SSH
|
||||
# TK-FB 중지
|
||||
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 중지
|
||||
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: 통합 서비스 기동
|
||||
@@ -180,10 +180,10 @@ echo 'fukdon-riwbaq-fiQfy2' | sudo -S /usr/local/bin/docker compose down
|
||||
cd /volume1/docker/tk-factory-services
|
||||
|
||||
# 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 마이그레이션
|
||||
@@ -196,7 +196,7 @@ echo 'fukdon-riwbaq-fiQfy2' | sudo -S /usr/local/bin/docker compose logs -f --ta
|
||||
# (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 설정
|
||||
@@ -291,15 +291,15 @@ git log --oneline -10
|
||||
```bash
|
||||
# 통합 서비스 중지
|
||||
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 복원
|
||||
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 복원
|
||||
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)
|
||||
- TK-FB 운영: `/volume1/Technicalkorea Document/tkfb-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`)
|
||||
- 환경변수 값 강도
|
||||
@@ -136,7 +136,7 @@ services:
|
||||
ports:
|
||||
- "30080:80"
|
||||
volumes:
|
||||
- ./system1-factory/web:/usr/share/nginx/html:ro
|
||||
- ./system1-factory/web/public:/usr/share/nginx/html:ro
|
||||
depends_on:
|
||||
system1-api:
|
||||
condition: service_healthy
|
||||
@@ -309,6 +309,7 @@ services:
|
||||
- NTFY_BASE_URL=${NTFY_BASE_URL:-http://ntfy:80}
|
||||
- NTFY_PUBLISH_TOKEN=${NTFY_PUBLISH_TOKEN}
|
||||
- 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}
|
||||
healthcheck:
|
||||
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`에 제외할 파일이 있으면 등록.
|
||||
@@ -489,7 +489,7 @@
|
||||
|
||||
// ===== Card Definitions =====
|
||||
var SYSTEM_CARDS = [
|
||||
{ id: 'factory', name: '공장관리', desc: '작업장 현황, TBM, 설비관리', icon: '\uD83C\uDFED', subdomain: 'tkfb', path: '/pages/dashboard.html', pageKey: 'dashboard', color: '#1a56db' },
|
||||
{ id: 'factory', name: '공장관리', desc: '작업장 현황, TBM, 설비관리', icon: '\uD83C\uDFED', subdomain: 'tkfb', path: '/pages/dashboard-new.html', pageKey: 'dashboard', color: '#1a56db' },
|
||||
{ id: 'report_sys', name: '신고', desc: '사건·사고 신고 접수', icon: '\uD83D\uDEA8', subdomain: 'tkreport', accessKey: 'system2', color: '#dc2626' },
|
||||
{ id: 'quality', name: '부적합관리', desc: '부적합 이슈 추적·처리', icon: '\uD83D\uDCCA', subdomain: 'tkqc', accessKey: 'system3', color: '#059669' },
|
||||
{ id: 'purchase', name: '구매관리', desc: '자재 구매, 일용직 관리', icon: '\uD83D\uDED2', subdomain: 'tkpurchase', color: '#d97706' },
|
||||
@@ -781,7 +781,8 @@
|
||||
|
||||
var redirect = new URLSearchParams(location.search).get('redirect');
|
||||
if (redirect && isSafeRedirect(redirect)) {
|
||||
window.location.href = redirect;
|
||||
var sep = redirect.indexOf('#') === -1 ? '#' : '&';
|
||||
window.location.href = redirect + sep + '_sso=' + encodeURIComponent(data.access_token);
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
@@ -840,7 +841,8 @@
|
||||
// Already logged in + redirect param
|
||||
var redirect = params.get('redirect');
|
||||
if (redirect && isSafeRedirect(redirect)) {
|
||||
window.location.href = redirect;
|
||||
var sep = redirect.indexOf('#') === -1 ? '#' : '&';
|
||||
window.location.href = redirect + sep + '_sso=' + encodeURIComponent(token);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
94
scripts/check-webroot-security.sh
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# 웹루트 보안 검증 스크립트
|
||||
# 배포 후 실행: docker 이미지 내 /usr/share/nginx/html에 허용된 파일만 있는지 확인
|
||||
# 화이트리스트 방식 — 허용되지 않은 파일이 있으면 FAIL
|
||||
# =============================================================================
|
||||
|
||||
SERVICES=("system1-web" "system2-web" "system3-web")
|
||||
|
||||
# 허용 목록 (줄바꿈 구분 — 공백 파일명 안전)
|
||||
ALLOWED_system1_web="index.html
|
||||
manifest.json
|
||||
sw.js
|
||||
logo.png
|
||||
components
|
||||
css
|
||||
img
|
||||
js
|
||||
pages
|
||||
static"
|
||||
|
||||
ALLOWED_system2_web="push-sw.js
|
||||
css
|
||||
img
|
||||
js
|
||||
pages"
|
||||
|
||||
ALLOWED_system3_web="ai-assistant.html
|
||||
app.html
|
||||
favicon.ico
|
||||
issue-view.html
|
||||
issues-archive.html
|
||||
issues-dashboard.html
|
||||
issues-inbox.html
|
||||
issues-management.html
|
||||
m
|
||||
push-sw.js
|
||||
reports-daily.html
|
||||
reports-monthly.html
|
||||
reports-weekly.html
|
||||
reports.html
|
||||
static
|
||||
sw.js
|
||||
uploads"
|
||||
|
||||
FAIL=0
|
||||
|
||||
for service in "${SERVICES[@]}"; do
|
||||
varname="ALLOWED_${service//-/_}"
|
||||
allowed="${!varname}"
|
||||
|
||||
echo "Checking $service..."
|
||||
|
||||
# 컨테이너 생성만 (실행 안 함) → exec으로 검사 → 제거
|
||||
docker compose create --no-deps "$service" >/dev/null 2>&1
|
||||
container=$(docker compose ps -q "$service" | head -n1)
|
||||
if [ -z "$container" ]; then
|
||||
echo " FAIL: container not found for $service"
|
||||
FAIL=1; continue
|
||||
fi
|
||||
|
||||
entries=$(docker exec "$container" \
|
||||
find /usr/share/nginx/html -maxdepth 1 -mindepth 1 -printf '%f\n' 2>/dev/null || true)
|
||||
docker compose rm -f "$service" >/dev/null 2>&1
|
||||
|
||||
# 빈 webroot 체크 (COPY public/ 실패 감지)
|
||||
if [ -z "$entries" ]; then
|
||||
echo " FAIL: $service webroot is empty"
|
||||
FAIL=1; continue
|
||||
fi
|
||||
|
||||
while IFS= read -r f; do
|
||||
[ -z "$f" ] && continue
|
||||
# -xF: 정확히 일치하는 줄만 (substring 매칭 방지)
|
||||
if ! echo "$allowed" | grep -qxF "$f"; then
|
||||
echo " FAIL: unexpected file in webroot → $f"
|
||||
FAIL=1
|
||||
fi
|
||||
done <<< "$entries"
|
||||
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
echo " OK"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
echo "✓ All web roots clean"
|
||||
else
|
||||
echo "✗ Security check FAILED — fix before deploying"
|
||||
fi
|
||||
exit $FAIL
|
||||
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 "$@"
|
||||
39
shared/frontend/sso-relay.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* SSO Token Relay — 인앱 브라우저(카카오톡 등) 서브도메인 쿠키 미공유 대응
|
||||
*
|
||||
* Canonical source: shared/frontend/sso-relay.js
|
||||
* 전 서비스 동일 코드 — 수정 시 아래 파일 <20><><EFBFBD>체 갱신 필요:
|
||||
* system1-factory/web/js/sso-relay.js
|
||||
* system2-report/web/js/sso-relay.js
|
||||
* system3-nonconformance/web/static/js/sso-relay.js
|
||||
* user-management/web/static/js/sso-relay.js
|
||||
* tkpurchase/web/static/js/sso-relay.js
|
||||
* tksafety/web/static/js/sso-relay.js
|
||||
* tksupport/web/static/js/sso-relay.js
|
||||
*
|
||||
* 동작: URL hash에 _sso= 파라미터가 있으면 토큰을 로컬 쿠키+localStorage에 설정하고 hash를 제거.
|
||||
* gateway/dashboard.html에서 로그인 성공 후 redirect URL에 #_sso=<token>을 붙여 전달.
|
||||
*/
|
||||
(function() {
|
||||
var hash = location.hash;
|
||||
if (!hash || hash.indexOf('_sso=') === -1) return;
|
||||
|
||||
var match = hash.match(/[#&]_sso=([^&]*)/);
|
||||
if (!match) return;
|
||||
|
||||
var token = decodeURIComponent(match[1]);
|
||||
if (!token) return;
|
||||
|
||||
// 로컬(1st-party) 쿠키 설정
|
||||
var cookie = 'sso_token=' + encodeURIComponent(token) + '; path=/; max-age=604800';
|
||||
if (location.hostname.indexOf('technicalkorea.net') !== -1) {
|
||||
cookie += '; domain=.technicalkorea.net; secure; samesite=lax';
|
||||
}
|
||||
document.cookie = cookie;
|
||||
|
||||
// localStorage 폴백
|
||||
try { localStorage.setItem('sso_token', token); } catch (e) {}
|
||||
|
||||
// URL에서 hash 제거
|
||||
history.replaceState(null, '', location.pathname + location.search);
|
||||
})();
|
||||
@@ -1,5 +1,5 @@
|
||||
-- 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 (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
# 정적 파일 복사
|
||||
COPY . /usr/share/nginx/html/
|
||||
|
||||
# 디렉토리 권한 보정 (macOS에서 복사 시 700이 되는 문제 방지)
|
||||
RUN find /usr/share/nginx/html -type d -exec chmod 755 {} +
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
COPY public/ /usr/share/nginx/html/
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
Before Width: | Height: | Size: 15 KiB |
@@ -12,6 +12,22 @@ server {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# 민감 파일 차단 — exact match (^~ 우회 불가)
|
||||
location = /Dockerfile { return 404; }
|
||||
location = /docker-compose.yml { return 404; }
|
||||
location = /nginx.conf { return 404; }
|
||||
location = /.env { return 404; }
|
||||
location = /.gitignore { return 404; }
|
||||
|
||||
# .git 디렉토리 전체 차단
|
||||
location ^~ /.git/ { return 404; }
|
||||
location = /.git { return 404; }
|
||||
|
||||
# 민감 파일 차단 — regex (하위 경로 + 변형 대비)
|
||||
location ~* (Dockerfile|docker-compose|\.env|nginx\.conf) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# HTML 캐시 비활성화
|
||||
location ~* \.html$ {
|
||||
expires -1;
|
||||
@@ -100,12 +116,19 @@ server {
|
||||
proxy_send_timeout 180s;
|
||||
}
|
||||
|
||||
# 레거시 /login, /dashboard → gateway 리다이렉트
|
||||
# /login → 로그인 페이지 (gateway 대시보드)
|
||||
# tkfb.technicalkorea.net이 system1-web을 직접 가리키므로
|
||||
# 외부 리다이렉트 대신 gateway 내부 프록시로 처리
|
||||
location = /login {
|
||||
return 302 $scheme://tkfb.technicalkorea.net/dashboard$is_args$args;
|
||||
set $gw http://gateway:80;
|
||||
rewrite ^/login$ /dashboard break;
|
||||
proxy_pass $gw;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
location = /dashboard {
|
||||
return 301 $scheme://tkfb.technicalkorea.net/dashboard;
|
||||
set $gw http://gateway:80;
|
||||
proxy_pass $gw;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# Health check
|
||||
|
||||
|
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 |
@@ -10,6 +10,7 @@
|
||||
if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then(function(r){r.forEach(function(reg){reg.unregister()});})}
|
||||
if('caches' in window){caches.keys().then(function(k){k.forEach(function(key){caches.delete(key)})})}
|
||||
</script>
|
||||
<script src="/js/sso-relay.js?v=20260401"></script>
|
||||
<script src="/js/api-base.js?v=2026031401"></script>
|
||||
<script>
|
||||
// SSO 토큰 확인
|
||||