sentix 2.0.1 → 2.0.2
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.
- package/.github/workflows/deploy.yml +206 -0
- package/.github/workflows/security-scan.yml +362 -0
- package/.sentix/rules/hard-rules.md +64 -0
- package/FRAMEWORK.md +721 -0
- package/README.md +14 -0
- package/docs/agent-scopes.md +28 -0
- package/docs/architecture.md +128 -0
- package/docs/article-anthropic-academy-to-sentix.md +220 -0
- package/docs/governor-sop.md +113 -0
- package/docs/severity.md +33 -0
- package/package.json +9 -2
- package/scripts/pre-commit.js +57 -0
- package/scripts/update-downstream.sh +169 -0
|
@@ -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가 별도 티켓으로 분리 (삭제 영향 범위 사전 분석)
|