sentix 2.0.1 → 2.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,206 @@
1
+ name: deploy
2
+ # ── 공급망 보안 정책 ──────────────────────────────────────────
3
+ # 1. 모든 GitHub Action은 커밋 SHA로 고정 (태그 오염 공격 방어)
4
+ # 2. ${{ }} 표현식은 env로 분리 (expression injection 방어)
5
+
6
+ on:
7
+ push:
8
+ branches: [master]
9
+ repository_dispatch:
10
+ types: [interface-updated]
11
+
12
+ jobs:
13
+ # ── 배포 필요 여부 판단 ──────────────────────────────────
14
+ check-deploy:
15
+ runs-on: ubuntu-latest
16
+ outputs:
17
+ needs_deploy: ${{ steps.check.outputs.needs_deploy }}
18
+ access_method: ${{ steps.profile.outputs.access_method }}
19
+ steps:
20
+ - name: Checkout
21
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
22
+
23
+ - name: Check DEPLOY_FLAG from recent ticket
24
+ id: check
25
+ run: |
26
+ # 최신 티켓에서 DEPLOY_FLAG 확인
27
+ LATEST_TICKET=$(ls -t tasks/tickets/dev-*.md 2>/dev/null | head -1)
28
+ if [[ -n "$LATEST_TICKET" ]]; then
29
+ DEPLOY_FLAG=$(grep -i "DEPLOY_FLAG:" "$LATEST_TICKET" | head -1 | awk '{print $2}')
30
+ echo "Ticket: $LATEST_TICKET → DEPLOY_FLAG: $DEPLOY_FLAG"
31
+ fi
32
+
33
+ # diff 기반 자동 감지 (티켓 없거나 플래그 없을 때 폴백)
34
+ if [[ -z "$DEPLOY_FLAG" ]]; then
35
+ CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "")
36
+ if echo "$CHANGED" | grep -qE "^(app/api/|prisma/|Dockerfile|docker-compose|package\.json)"; then
37
+ DEPLOY_FLAG="true"
38
+ echo "Auto-detected: runtime-affecting changes found"
39
+ else
40
+ DEPLOY_FLAG="false"
41
+ echo "Auto-detected: no deploy-requiring changes"
42
+ fi
43
+ fi
44
+
45
+ echo "needs_deploy=$DEPLOY_FLAG" >> $GITHUB_OUTPUT
46
+
47
+ - name: Read environment profile
48
+ id: profile
49
+ run: |
50
+ PROFILE="env-profiles/active.toml"
51
+ if [[ -f "$PROFILE" ]]; then
52
+ METHOD=$(grep "^method" "$PROFILE" | head -1 | sed 's/.*=\s*//; s/"//g')
53
+ echo "access_method=$METHOD" >> $GITHUB_OUTPUT
54
+ echo "Profile loaded: $(grep '^name' "$PROFILE" | head -1)"
55
+ else
56
+ echo "access_method=manual" >> $GITHUB_OUTPUT
57
+ echo "No active profile — defaulting to manual"
58
+ fi
59
+
60
+ # ── 배포 실행 (필요할 때만) ──────────────────────────────
61
+ deploy:
62
+ needs: check-deploy
63
+ if: needs.check-deploy.outputs.needs_deploy == 'true'
64
+ runs-on: ubuntu-latest
65
+ timeout-minutes: 30
66
+
67
+ steps:
68
+ - name: Checkout
69
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
70
+
71
+ # ── CI 자동 배포 (ssm/ssh) ─────────────────────────
72
+ - name: Configure AWS (SSM mode)
73
+ if: needs.check-deploy.outputs.access_method == 'ssm'
74
+ uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0
75
+ with:
76
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
77
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
78
+ aws-region: ap-northeast-2
79
+
80
+ - name: Execute deploy script
81
+ if: needs.check-deploy.outputs.access_method == 'ssm' || needs.check-deploy.outputs.access_method == 'ssh'
82
+ id: deploy-auto
83
+ run: |
84
+ chmod +x scripts/deploy.sh
85
+ bash scripts/deploy.sh 2>&1 | tee /tmp/deploy-output.txt
86
+ # STATUS 추출
87
+ if grep -q "\[STATUS\] PASSED" /tmp/deploy-output.txt; then
88
+ echo "deploy_status=success" >> $GITHUB_OUTPUT
89
+ else
90
+ echo "deploy_status=failed" >> $GITHUB_OUTPUT
91
+ fi
92
+
93
+ # ── 수동 배포 (manual mode) ────────────────────────
94
+ - name: Generate deploy script (manual mode)
95
+ if: needs.check-deploy.outputs.access_method == 'manual'
96
+ id: deploy-manual
97
+ run: |
98
+ chmod +x scripts/deploy.sh
99
+ bash scripts/deploy.sh --generate-only
100
+ echo "deploy_status=manual_pending" >> $GITHUB_OUTPUT
101
+ echo "::warning::Manual deployment required — check tasks/deploy-output.md"
102
+
103
+ - name: Commit deploy output
104
+ if: always()
105
+ run: |
106
+ if [[ -f tasks/deploy-output.md ]]; then
107
+ git config user.name "devops-agent"
108
+ git config user.email "devops@noreply"
109
+ git add tasks/deploy-output.md
110
+ git diff --staged --quiet || git commit -m "chore: deploy output $(date +%Y-%m-%d)"
111
+ git push origin master || true
112
+ fi
113
+
114
+ # ── security 에이전트 트리거 ───────────────────────
115
+ - name: Dispatch security scan (auto deploy succeeded)
116
+ if: steps.deploy-auto.outputs.deploy_status == 'success'
117
+ env:
118
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
119
+ REPO: ${{ github.repository }}
120
+ SHA: ${{ github.sha }}
121
+ run: |
122
+ curl -X POST \
123
+ -H "Authorization: token ${GH_TOKEN}" \
124
+ -H "Content-Type: application/json" \
125
+ "https://api.github.com/repos/${REPO}/dispatches" \
126
+ -d "$(jq -n --arg sha "$SHA" '{
127
+ event_type: "security-scan",
128
+ client_payload: { sha: $sha }
129
+ }')"
130
+
131
+ # ── 실패 시 dev-fix 트리거 ─────────────────────────
132
+ - name: Trigger dev-fix on failure
133
+ if: failure()
134
+ env:
135
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
136
+ REPO: ${{ github.repository }}
137
+ SHA: ${{ github.sha }}
138
+ RUN_ID: ${{ github.run_id }}
139
+ run: |
140
+ curl -X POST \
141
+ -H "Authorization: token ${GH_TOKEN}" \
142
+ -H "Content-Type: application/json" \
143
+ "https://api.github.com/repos/${REPO}/dispatches" \
144
+ -d "$(jq -n --arg sha "$SHA" --arg run_id "$RUN_ID" '{
145
+ event_type: "devops-failed",
146
+ client_payload: {
147
+ sha: $sha,
148
+ run_id: $run_id,
149
+ issue: "Deploy failed — check deploy output"
150
+ }
151
+ }')"
152
+
153
+ # ── 배포 건너뜀 → security 직행 ─────────────────────────
154
+ skip-deploy:
155
+ needs: check-deploy
156
+ if: needs.check-deploy.outputs.needs_deploy == 'false'
157
+ runs-on: ubuntu-latest
158
+ steps:
159
+ - name: Skip deploy — trigger security directly
160
+ env:
161
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
162
+ REPO: ${{ github.repository }}
163
+ SHA: ${{ github.sha }}
164
+ run: |
165
+ echo "Deploy skipped (DEPLOY_FLAG: false)"
166
+ curl -X POST \
167
+ -H "Authorization: token ${GH_TOKEN}" \
168
+ -H "Content-Type: application/json" \
169
+ "https://api.github.com/repos/${REPO}/dispatches" \
170
+ -d "$(jq -n --arg sha "$SHA" '{
171
+ event_type: "security-scan",
172
+ client_payload: { sha: $sha, deploy_skipped: true }
173
+ }')"
174
+
175
+ # ── 멀티 프로젝트 cascade ──────────────────────────────
176
+ cascade:
177
+ needs: [check-deploy]
178
+ if: contains(github.event.commits.*.modified, 'INTERFACE.md') || github.event_name == 'repository_dispatch'
179
+ runs-on: ubuntu-latest
180
+ steps:
181
+ - name: Checkout
182
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
183
+
184
+ - name: Cascade to dependent projects
185
+ env:
186
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
187
+ SOURCE_REPO: ${{ github.event.repository.name }}
188
+ SHA: ${{ github.sha }}
189
+ run: |
190
+ if [[ ! -f registry.md ]]; then
191
+ echo "No registry.md — skipping cascade"
192
+ exit 0
193
+ fi
194
+ DEPENDENTS=$(grep '^\|' registry.md | grep -v 'header\|---\|프로젝트' | awk -F'|' '{print $3}' | tr -d ' ')
195
+ for repo in $DEPENDENTS; do
196
+ PROJECT=$(basename "$repo")
197
+ echo "Dispatching to $PROJECT..."
198
+ curl -s -X POST \
199
+ -H "Authorization: token ${GH_TOKEN}" \
200
+ -H "Content-Type: application/json" \
201
+ "https://api.github.com/repos/kgg1226/${PROJECT}/dispatches" \
202
+ -d "$(jq -n --arg source "$SOURCE_REPO" --arg sha "$SHA" '{
203
+ event_type: "interface-updated",
204
+ client_payload: { source: $source, sha: $sha }
205
+ }')"
206
+ done
@@ -0,0 +1,362 @@
1
+ name: security-scan
2
+ # ── 공급망 보안 정책 ──────────────────────────────────────────
3
+ # 1. 모든 GitHub Action은 커밋 SHA로 고정 (태그 오염 공격 방어)
4
+ # 2. 외부 바이너리는 SHA-256 체크섬 검증 필수
5
+ # 3. ${{ }} 표현식은 env로 분리 (expression injection 방어)
6
+ # ref: https://m.boannews.com/html/detail.html?tab_type=1&idx=142760
7
+
8
+ on:
9
+ repository_dispatch:
10
+ types: [security-scan]
11
+ workflow_dispatch:
12
+
13
+ jobs:
14
+ scan:
15
+ runs-on: ubuntu-latest
16
+ timeout-minutes: 15
17
+
18
+ steps:
19
+ - name: Checkout
20
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
21
+ with:
22
+ ref: ${{ github.event.client_payload.sha || github.sha }}
23
+
24
+ # ── Trivy CLI 설치 (공급망 안전) ────────────────────────
25
+ # 주의: aquasecurity/trivy-action 은 공급망 공격으로 오염됨
26
+ # GitHub Action 대신 CLI 바이너리를 직접 설치하고 체크섬 검증
27
+ - name: Install Trivy CLI (supply-chain safe)
28
+ run: |
29
+ TRIVY_VERSION="0.69.3"
30
+ TRIVY_DEB="trivy_${TRIVY_VERSION}_Linux-64bit.deb"
31
+ TRIVY_EXPECTED_SHA256="a484057aafde31089cf2558ca0f79a4bc835125a5ee6834183a5bcf0735af358"
32
+
33
+ curl -sfL -o "/tmp/${TRIVY_DEB}" \
34
+ "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/${TRIVY_DEB}"
35
+
36
+ ACTUAL_SHA256=$(sha256sum "/tmp/${TRIVY_DEB}" | awk '{print $1}')
37
+ if [ "$ACTUAL_SHA256" != "$TRIVY_EXPECTED_SHA256" ]; then
38
+ echo "::error::Trivy checksum mismatch! Expected: ${TRIVY_EXPECTED_SHA256}, Got: ${ACTUAL_SHA256}"
39
+ echo "::error::Possible supply chain compromise — aborting installation"
40
+ exit 1
41
+ fi
42
+ echo "Trivy checksum verified: ${ACTUAL_SHA256}"
43
+
44
+ sudo dpkg -i "/tmp/${TRIVY_DEB}"
45
+ trivy --version
46
+
47
+ # ── 정적 분석 ─────────────────────────────────────────
48
+ - name: Setup Node
49
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
50
+ with:
51
+ node-version: 20
52
+
53
+ - name: Install dependencies
54
+ run: npm ci
55
+
56
+ - name: Run audit
57
+ id: audit
58
+ run: |
59
+ npm audit --audit-level=high --json > /tmp/audit.json || true
60
+ CRITICAL=$(jq '.metadata.vulnerabilities.critical // 0' /tmp/audit.json)
61
+ HIGH=$(jq '.metadata.vulnerabilities.high // 0' /tmp/audit.json)
62
+ echo "critical=$CRITICAL" >> $GITHUB_OUTPUT
63
+ echo "high=$HIGH" >> $GITHUB_OUTPUT
64
+ echo "--- npm audit summary ---"
65
+ jq '.metadata.vulnerabilities' /tmp/audit.json
66
+
67
+ # ── Trivy 스캔 (3종) ─────────────────────────────────
68
+ - name: Trivy filesystem vulnerability scan
69
+ id: trivy-fs
70
+ run: |
71
+ trivy fs --format json --output /tmp/trivy-fs.json \
72
+ --severity CRITICAL,HIGH \
73
+ --scanners vuln \
74
+ --skip-dirs node_modules \
75
+ . || true
76
+
77
+ FS_CRITICAL=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' /tmp/trivy-fs.json 2>/dev/null)
78
+ FS_HIGH=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="HIGH")] | length' /tmp/trivy-fs.json 2>/dev/null)
79
+ echo "fs_critical=${FS_CRITICAL:-0}" >> $GITHUB_OUTPUT
80
+ echo "fs_high=${FS_HIGH:-0}" >> $GITHUB_OUTPUT
81
+ echo "--- Trivy FS scan: critical=${FS_CRITICAL:-0}, high=${FS_HIGH:-0} ---"
82
+
83
+ - name: Trivy secrets scan
84
+ id: trivy-secrets
85
+ run: |
86
+ trivy fs --format json --output /tmp/trivy-secrets.json \
87
+ --scanners secret \
88
+ --skip-dirs node_modules,.git \
89
+ . || true
90
+
91
+ SECRETS_COUNT=$(jq '[.Results[]?.Secrets[]?] | length' /tmp/trivy-secrets.json 2>/dev/null)
92
+ echo "trivy_secrets=${SECRETS_COUNT:-0}" >> $GITHUB_OUTPUT
93
+
94
+ if [ "${SECRETS_COUNT:-0}" -gt 0 ]; then
95
+ echo "--- Trivy secrets findings ---"
96
+ jq '.Results[]? | select(.Secrets) | .Secrets[] | {RuleID, Title, Match: .Match[0:80]}' /tmp/trivy-secrets.json
97
+ else
98
+ echo "--- Trivy secrets scan: clean ---"
99
+ fi
100
+
101
+ - name: Trivy misconfiguration scan
102
+ id: trivy-misconfig
103
+ run: |
104
+ trivy fs --format json --output /tmp/trivy-misconfig.json \
105
+ --scanners misconfig \
106
+ --skip-dirs node_modules,.git \
107
+ . || true
108
+
109
+ MISCONFIG_CRITICAL=$(jq '[.Results[]?.Misconfigurations[]? | select(.Severity=="CRITICAL")] | length' /tmp/trivy-misconfig.json 2>/dev/null)
110
+ MISCONFIG_HIGH=$(jq '[.Results[]?.Misconfigurations[]? | select(.Severity=="HIGH")] | length' /tmp/trivy-misconfig.json 2>/dev/null)
111
+ echo "misconfig_critical=${MISCONFIG_CRITICAL:-0}" >> $GITHUB_OUTPUT
112
+ echo "misconfig_high=${MISCONFIG_HIGH:-0}" >> $GITHUB_OUTPUT
113
+ echo "--- Trivy misconfig scan: critical=${MISCONFIG_CRITICAL:-0}, high=${MISCONFIG_HIGH:-0} ---"
114
+
115
+ # ── 환경변수 노출 체크 ────────────────────────────────
116
+ - name: Check for hardcoded secrets
117
+ id: secrets-check
118
+ run: |
119
+ FOUND=$(grep -rn \
120
+ -e "ANTHROPIC_API_KEY\s*=\s*['\"]sk-" \
121
+ -e "password\s*=\s*['\"][^$]" \
122
+ -e "AWS_SECRET_ACCESS_KEY\s*=\s*['\"]" \
123
+ -e "PRIVATE_KEY\s*=\s*['\"]" \
124
+ --include="*.ts" --include="*.js" --include="*.tsx" --include="*.env" \
125
+ app/ lib/ || true)
126
+ if [ -n "$FOUND" ]; then
127
+ echo "HARDCODED_SECRETS=true" >> $GITHUB_OUTPUT
128
+ echo "$FOUND"
129
+ else
130
+ echo "HARDCODED_SECRETS=false" >> $GITHUB_OUTPUT
131
+ fi
132
+
133
+ # ── 인증 미들웨어 커버리지 체크 ──────────────────────
134
+ - name: Check auth coverage on API routes
135
+ id: auth-check
136
+ run: |
137
+ ROUTES=$(find app/api -name "route.ts" 2>/dev/null | wc -l)
138
+ AUTH=$(grep -rl "getCurrentUser\|requireAdmin\|getSession" app/api/ 2>/dev/null | wc -l)
139
+ echo "total_routes=$ROUTES" >> $GITHUB_OUTPUT
140
+ echo "auth_covered=$AUTH" >> $GITHUB_OUTPUT
141
+ echo "Routes: $ROUTES / Auth covered: $AUTH"
142
+ if [ "$AUTH" -lt "$ROUTES" ]; then
143
+ MISSING=$(comm -23 \
144
+ <(find app/api -name "route.ts" | sort) \
145
+ <(grep -rl "getCurrentUser\|requireAdmin" app/api/ | sort) 2>/dev/null)
146
+ echo "unprotected=$MISSING" >> $GITHUB_OUTPUT
147
+ echo "Unprotected routes: $MISSING"
148
+ fi
149
+
150
+ # ── 리포트 생성 (severity 기반) ──────────────────────
151
+ # env로 분리하여 expression injection 방어
152
+ - name: Generate security report
153
+ id: report
154
+ env:
155
+ AUDIT_CRITICAL: ${{ steps.audit.outputs.critical }}
156
+ AUDIT_HIGH: ${{ steps.audit.outputs.high }}
157
+ GREP_SECRETS: ${{ steps.secrets-check.outputs.HARDCODED_SECRETS }}
158
+ TOTAL_ROUTES: ${{ steps.auth-check.outputs.total_routes }}
159
+ AUTH_COVERED: ${{ steps.auth-check.outputs.auth_covered }}
160
+ AUTH_UNPROTECTED: ${{ steps.auth-check.outputs.unprotected }}
161
+ TRIVY_FS_CRITICAL: ${{ steps.trivy-fs.outputs.fs_critical }}
162
+ TRIVY_FS_HIGH: ${{ steps.trivy-fs.outputs.fs_high }}
163
+ TRIVY_SECRETS: ${{ steps.trivy-secrets.outputs.trivy_secrets }}
164
+ TRIVY_MISCONFIG_CRITICAL: ${{ steps.trivy-misconfig.outputs.misconfig_critical }}
165
+ TRIVY_MISCONFIG_HIGH: ${{ steps.trivy-misconfig.outputs.misconfig_high }}
166
+ DEPLOY_SKIPPED: ${{ github.event.client_payload.deploy_skipped }}
167
+ run: |
168
+ # severity 분류
169
+ FINDINGS=""
170
+ if [ "$AUDIT_CRITICAL" -gt 0 ]; then
171
+ FINDINGS+="[critical] npm audit: $AUDIT_CRITICAL critical vulnerabilities\n"
172
+ fi
173
+ if [ "$GREP_SECRETS" = "true" ]; then
174
+ FINDINGS+="[critical] Hardcoded secrets detected in source code\n"
175
+ fi
176
+ if [ "$AUDIT_HIGH" -gt 0 ]; then
177
+ FINDINGS+="[warning] npm audit: $AUDIT_HIGH high vulnerabilities\n"
178
+ fi
179
+ if [ "$AUTH_COVERED" -lt "$TOTAL_ROUTES" ]; then
180
+ FINDINGS+="[warning] Unprotected API routes: $AUTH_UNPROTECTED\n"
181
+ fi
182
+
183
+ # Trivy 결과 반영
184
+ if [ "$TRIVY_FS_CRITICAL" -gt 0 ]; then
185
+ FINDINGS+="[critical] Trivy: $TRIVY_FS_CRITICAL critical filesystem vulnerabilities\n"
186
+ fi
187
+ if [ "$TRIVY_FS_HIGH" -gt 0 ]; then
188
+ FINDINGS+="[warning] Trivy: $TRIVY_FS_HIGH high filesystem vulnerabilities\n"
189
+ fi
190
+ if [ "$TRIVY_SECRETS" -gt 0 ]; then
191
+ FINDINGS+="[critical] Trivy: $TRIVY_SECRETS secrets detected in codebase\n"
192
+ fi
193
+ if [ "$TRIVY_MISCONFIG_CRITICAL" -gt 0 ]; then
194
+ FINDINGS+="[critical] Trivy: $TRIVY_MISCONFIG_CRITICAL critical misconfigurations\n"
195
+ fi
196
+ if [ "$TRIVY_MISCONFIG_HIGH" -gt 0 ]; then
197
+ FINDINGS+="[warning] Trivy: $TRIVY_MISCONFIG_HIGH high misconfigurations\n"
198
+ fi
199
+
200
+ # STATUS/VERDICT 결정
201
+ HAS_CRITICAL=false
202
+ HAS_WARNING=false
203
+
204
+ if [ "$AUDIT_CRITICAL" -gt 0 ] || [ "$GREP_SECRETS" = "true" ] || \
205
+ [ "$TRIVY_FS_CRITICAL" -gt 0 ] || [ "$TRIVY_SECRETS" -gt 0 ] || \
206
+ [ "$TRIVY_MISCONFIG_CRITICAL" -gt 0 ]; then
207
+ HAS_CRITICAL=true
208
+ fi
209
+
210
+ if [ "$AUDIT_HIGH" -gt 0 ] || [ "$AUTH_COVERED" -lt "$TOTAL_ROUTES" ] || \
211
+ [ "$TRIVY_FS_HIGH" -gt 0 ] || [ "$TRIVY_MISCONFIG_HIGH" -gt 0 ]; then
212
+ HAS_WARNING=true
213
+ fi
214
+
215
+ if [ "$HAS_CRITICAL" = "true" ]; then
216
+ STATUS="FAILED"
217
+ VERDICT="CRITICAL"
218
+ elif [ "$HAS_WARNING" = "true" ]; then
219
+ STATUS="NEEDS_FIX"
220
+ VERDICT="MEDIUM"
221
+ else
222
+ STATUS="PASSED"
223
+ VERDICT="LOW"
224
+ FINDINGS+="[suggestion] Consider adding rate limiting to public API routes\n"
225
+ FINDINGS+="[suggestion] Review dependency update schedule\n"
226
+ fi
227
+
228
+ echo "status=$STATUS" >> $GITHUB_OUTPUT
229
+ echo "verdict=$VERDICT" >> $GITHUB_OUTPUT
230
+
231
+ cat > tasks/security-report.md << EOF
232
+ # Security Report — $(date +%Y-%m-%d)
233
+
234
+ ## Summary
235
+ - npm audit critical: $AUDIT_CRITICAL
236
+ - npm audit high: $AUDIT_HIGH
237
+ - Hardcoded secrets: $GREP_SECRETS
238
+ - API routes: $TOTAL_ROUTES / Auth covered: $AUTH_COVERED
239
+ - Trivy FS critical: $TRIVY_FS_CRITICAL
240
+ - Trivy FS high: $TRIVY_FS_HIGH
241
+ - Trivy secrets: $TRIVY_SECRETS
242
+ - Trivy misconfig critical: $TRIVY_MISCONFIG_CRITICAL
243
+ - Trivy misconfig high: $TRIVY_MISCONFIG_HIGH
244
+ - Deploy skipped: ${DEPLOY_SKIPPED:-false}
245
+
246
+ ## Findings
247
+ $(echo -e "$FINDINGS")
248
+
249
+ ## VERDICT: $VERDICT
250
+ ## [STATUS] $STATUS
251
+ EOF
252
+
253
+ cat tasks/security-report.md
254
+
255
+ - name: Commit security report
256
+ if: steps.report.outputs.status != ''
257
+ run: |
258
+ git config user.name "security-agent"
259
+ git config user.email "security@noreply"
260
+ git add tasks/security-report.md
261
+ git diff --staged --quiet || git commit -m "chore: security report $(date +%Y-%m-%d)"
262
+ git push origin master || true
263
+
264
+ # ── severity 기반 분기 ───────────────────────────────
265
+ # env로 분리하여 expression injection 방어
266
+ # CRITICAL → dev-fix (최대 3회 후 에스컬레이션)
267
+ - name: Trigger dev-fix (critical)
268
+ if: steps.report.outputs.status == 'FAILED'
269
+ env:
270
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
271
+ REPO: ${{ github.repository }}
272
+ SHA: ${{ github.sha }}
273
+ VERDICT: ${{ steps.report.outputs.verdict }}
274
+ AUDIT_CRITICAL: ${{ steps.audit.outputs.critical }}
275
+ AUDIT_HIGH: ${{ steps.audit.outputs.high }}
276
+ GREP_SECRETS: ${{ steps.secrets-check.outputs.HARDCODED_SECRETS }}
277
+ TRIVY_FS_CRITICAL: ${{ steps.trivy-fs.outputs.fs_critical }}
278
+ TRIVY_SECRETS: ${{ steps.trivy-secrets.outputs.trivy_secrets }}
279
+ TRIVY_MISCONFIG_CRITICAL: ${{ steps.trivy-misconfig.outputs.misconfig_critical }}
280
+ run: |
281
+ curl -X POST \
282
+ -H "Authorization: token ${GH_TOKEN}" \
283
+ -H "Content-Type: application/json" \
284
+ "https://api.github.com/repos/${REPO}/dispatches" \
285
+ -d "$(jq -n \
286
+ --arg sha "$SHA" \
287
+ --arg verdict "$VERDICT" \
288
+ --argjson critical "$AUDIT_CRITICAL" \
289
+ --argjson high "$AUDIT_HIGH" \
290
+ --argjson trivy_fs "$TRIVY_FS_CRITICAL" \
291
+ --argjson trivy_secrets "$TRIVY_SECRETS" \
292
+ --argjson trivy_misconfig "$TRIVY_MISCONFIG_CRITICAL" \
293
+ --arg secrets "$GREP_SECRETS" \
294
+ '{
295
+ event_type: "security-failed",
296
+ client_payload: {
297
+ sha: $sha,
298
+ severity: "critical",
299
+ verdict: $verdict,
300
+ critical: $critical,
301
+ high: $high,
302
+ trivy_fs_critical: $trivy_fs,
303
+ trivy_secrets: $trivy_secrets,
304
+ trivy_misconfig_critical: $trivy_misconfig,
305
+ issue: "Security scan CRITICAL: audit=\($critical), secrets=\($secrets), trivy_fs=\($trivy_fs), trivy_secrets=\($trivy_secrets)"
306
+ }
307
+ }')"
308
+
309
+ # WARNING → dev-fix (최대 10회)
310
+ - name: Trigger dev-fix (warning)
311
+ if: steps.report.outputs.status == 'NEEDS_FIX'
312
+ env:
313
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
314
+ REPO: ${{ github.repository }}
315
+ SHA: ${{ github.sha }}
316
+ VERDICT: ${{ steps.report.outputs.verdict }}
317
+ AUDIT_CRITICAL: ${{ steps.audit.outputs.critical }}
318
+ AUDIT_HIGH: ${{ steps.audit.outputs.high }}
319
+ TRIVY_FS_HIGH: ${{ steps.trivy-fs.outputs.fs_high }}
320
+ TRIVY_MISCONFIG_HIGH: ${{ steps.trivy-misconfig.outputs.misconfig_high }}
321
+ run: |
322
+ curl -X POST \
323
+ -H "Authorization: token ${GH_TOKEN}" \
324
+ -H "Content-Type: application/json" \
325
+ "https://api.github.com/repos/${REPO}/dispatches" \
326
+ -d "$(jq -n \
327
+ --arg sha "$SHA" \
328
+ --arg verdict "$VERDICT" \
329
+ --argjson critical "$AUDIT_CRITICAL" \
330
+ --argjson high "$AUDIT_HIGH" \
331
+ --argjson trivy_fs_high "$TRIVY_FS_HIGH" \
332
+ --argjson trivy_misconfig_high "$TRIVY_MISCONFIG_HIGH" \
333
+ '{
334
+ event_type: "security-failed",
335
+ client_payload: {
336
+ sha: $sha,
337
+ severity: "warning",
338
+ verdict: $verdict,
339
+ critical: $critical,
340
+ high: $high,
341
+ trivy_fs_high: $trivy_fs_high,
342
+ trivy_misconfig_high: $trivy_misconfig_high,
343
+ issue: "Security scan MEDIUM: audit_high=\($high), trivy_fs_high=\($trivy_fs_high), trivy_misconfig_high=\($trivy_misconfig_high)"
344
+ }
345
+ }')"
346
+
347
+ # PASSED → roadmap
348
+ - name: Dispatch roadmap
349
+ if: steps.report.outputs.status == 'PASSED'
350
+ env:
351
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
352
+ REPO: ${{ github.repository }}
353
+ SHA: ${{ github.sha }}
354
+ run: |
355
+ curl -X POST \
356
+ -H "Authorization: token ${GH_TOKEN}" \
357
+ -H "Content-Type: application/json" \
358
+ "https://api.github.com/repos/${REPO}/dispatches" \
359
+ -d "$(jq -n --arg sha "$SHA" '{
360
+ event_type: "generate-roadmap",
361
+ client_payload: { sha: $sha }
362
+ }')"
@@ -0,0 +1,64 @@
1
+ # 파괴 방지 하드 룰 6개 (HARD RULE — Governor도 우회 불가)
2
+
3
+ > 이 파일은 .sentix/rules/에 별도 격리된 불변 규칙이다.
4
+ > 동일한 내용이 FRAMEWORK.md와 CLAUDE.md에도 존재한다.
5
+ > 세 곳 모두 동일해야 한다. 어느 한 곳이라도 다르면 가장 엄격한 버전을 따른다.
6
+
7
+ ---
8
+
9
+ ## 1. 작업 전 테스트 스냅샷 필수
10
+
11
+ ```
12
+ npm run test -- --json > tasks/.pre-fix-test-results.json
13
+ ```
14
+
15
+ 코드를 수정하기 전에 반드시 현재 테스트 상태를 기록한다.
16
+ 이 스냅샷은 pr-review가 회귀 검증에 사용한다.
17
+
18
+ ## 2. 티켓 SCOPE 밖 파일 수정 금지
19
+
20
+ planner가 정의한 SCOPE 내 파일만 수정할 수 있다.
21
+ 별도 개선이 필요하면 → Governor에게 "SCOPE 확장 필요" 반환.
22
+
23
+ ## 3. 기존 export/API 삭제 금지
24
+
25
+ 다른 모듈이 의존하는 export나 API 엔드포인트를 삭제하지 않는다.
26
+ 시그니처 변경이 불가피하면 → Governor에게 "planner 재소환 필요" 반환.
27
+
28
+ ## 4. 기존 테스트 삭제/약화 금지
29
+
30
+ 테스트가 실패하면 코드를 고친다. 테스트를 고치지 않는다.
31
+ 테스트 케이스를 삭제하거나, assert 조건을 완화하거나, skip하지 않는다.
32
+
33
+ ## 5. 순삭제 50줄 제한
34
+
35
+ 한 번의 작업에서 순수하게 삭제되는 줄 수가 50줄을 초과하면 안 된다.
36
+ 초과 시 → Governor에게 "리팩터링 분리 필요" 반환.
37
+
38
+ ## 6. 기존 기능/핸들러 삭제 금지 (가장 중요)
39
+
40
+ **"버그가 있는 기능은 고치는 것이지, 없애는 것이 아니다."**
41
+
42
+ 여기서 "기능"이란:
43
+ - UI 요소 (버튼, 입력폼, 토글, 탭 등)
44
+ - 이벤트 핸들러 (onClick, onChange, onSubmit 등)
45
+ - API 라우트 또는 엔드포인트
46
+ - 비즈니스 로직 함수 (내부 함수 포함, export 여부 무관)
47
+ - 설정 옵션, 파라미터
48
+ - 사용자가 인지할 수 있는 모든 동작
49
+
50
+ 올바른 버그 수정:
51
+ - ✅ 기능을 유지한 채 로직을 수정한다
52
+ - ✅ 조건 분기를 추가한다
53
+ - ✅ 예외 처리를 보강한다
54
+ - ✅ 입력 검증을 강화한다
55
+
56
+ 잘못된 버그 수정:
57
+ - ❌ 해당 기능을 삭제해서 버그를 "해결"한다
58
+ - ❌ 핸들러를 제거해서 에러를 "없앤다"
59
+ - ❌ 라우트를 비활성화해서 문제를 "회피한다"
60
+ - ❌ UI 요소를 숨겨서 사용자가 버그를 "못 만나게" 한다
61
+
62
+ 기능 삭제가 진짜 필요한 경우 (deprecated 등):
63
+ → Governor에게 "기능 삭제 필요 — planner 경유 요청" 반환
64
+ → planner가 별도 티켓으로 분리 (삭제 영향 범위 사전 분석)