just-shield 0.2.0

Pre-execution supply-chain scanner for GitHub Actions workflows
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>just-shield 설계 결정 요약</title>
<style>
:root {
  --bg: #f7f8fa;
  --surface: #ffffff;
  --surface-2: #f1f3f5;
  --text: #1a1a1a;
  --text-muted: #5f6b7a;
  --text-faint: #8a95a5;
  --border: #e3e8ef;
  --border-strong: #cbd5e1;

  --primary: #2563eb;
  --primary-soft: #eff6ff;

  --priv: #dc2626;
  --priv-bg: #fef2f2;
  --priv-border: #fecaca;

  --pub: #059669;
  --pub-bg: #ecfdf5;
  --pub-border: #a7f3d0;

  --cert: #7c3aed;
  --cert-bg: #f5f3ff;
  --cert-border: #ddd6fe;

  --client: #0284c7;
  --client-bg: #f0f9ff;

  --server: #ea580c;
  --server-bg: #fff7ed;

  --warn: #d97706;
  --warn-bg: #fffbeb;
  --warn-border: #fde68a;

  --danger: #dc2626;
  --danger-bg: #fef2f2;
  --danger-border: #fecaca;

  --info: #2563eb;
  --info-bg: #eff6ff;
  --info-border: #bfdbfe;

  --tip: #0d9488;
  --tip-bg: #f0fdfa;
  --tip-border: #99f6e4;

  --code-bg: #f1f5f9;
  --code-text: #0f172a;
  --pre-bg: #0f172a;
  --pre-text: #e2e8f0;
  --pre-comment: #64748b;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-size: 16px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
}

.layout {
  display: grid;
  grid-template-columns: 1fr min(820px, 100%) 280px 1fr;
  gap: 0;
  min-height: 100vh;
}

/* grid-row pinned on all four .layout children — without it, sparse
   auto-placement drops main onto the row below .toc (a col 3 → col 2
   backward step forces a new row once the TOC is present, ≥1100px). */
main {
  grid-column: 2;
  grid-row: 2;
  padding: 3rem 2rem 6rem;
}

.toc {
  grid-column: 3;
  grid-row: 2;
  padding: 3rem 1.5rem;
  position: sticky;
  top: 0;
  align-self: start;
  max-height: 100vh;
  overflow-y: auto;
  font-size: 0.875rem;
}

.toc h2 {
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-faint);
  margin: 0 0 1rem;
}

.toc ol {
  list-style: none;
  padding: 0;
  margin: 0;
  border-left: 2px solid var(--border);
}

.toc li { margin: 0; }

.toc a {
  display: block;
  padding: 0.4rem 0.875rem;
  color: var(--text-muted);
  text-decoration: none;
  border-left: 2px solid transparent;
  margin-left: -2px;
  transition: all 0.15s;
}

.toc a:hover {
  color: var(--primary);
  background: var(--primary-soft);
}

.toc .sub { padding-left: 1.75rem; font-size: 0.8125rem; color: var(--text-faint); }

.page-header {
  grid-column: 2 / 4;
  grid-row: 1;
  padding: 3rem 2rem 1rem;
  border-bottom: 1px solid var(--border);
}

.page-header h1 {
  font-size: 2.25rem;
  margin: 0 0 0.5rem;
  letter-spacing: -0.02em;
}

.page-header .subtitle {
  color: var(--text-muted);
  margin: 0 0 1rem;
  font-size: 1.0625rem;
}

.page-header .nav-links {
  display: flex;
  gap: 0.5rem;
  margin-top: 1rem;
}

.page-header .nav-links a {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.4rem 0.875rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  text-decoration: none;
  font-size: 0.875rem;
  font-weight: 500;
}

.page-header .nav-links a:hover {
  border-color: var(--primary);
  color: var(--primary);
}

h2 {
  font-size: 1.625rem;
  margin: 3.5rem 0 1rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--border);
  letter-spacing: -0.01em;
  scroll-margin-top: 1rem;
}

h3 {
  font-size: 1.1875rem;
  margin: 2rem 0 0.75rem;
  scroll-margin-top: 1rem;
}

h4 { font-size: 1rem; margin: 1.25rem 0 0.5rem; }

p { margin: 0.75rem 0; }

a { color: var(--primary); }

strong { font-weight: 600; }

code {
  font-family: 'JetBrains Mono', 'D2Coding', 'Cascadia Mono', Consolas, monospace;
  font-size: 0.875em;
  background: var(--code-bg);
  color: var(--code-text);
  padding: 0.125rem 0.375rem;
  border-radius: 4px;
}

pre {
  background: var(--pre-bg);
  color: var(--pre-text);
  padding: 1rem 1.25rem;
  border-radius: 8px;
  overflow-x: auto;
  font-family: 'JetBrains Mono', 'D2Coding', 'Cascadia Mono', Consolas, monospace;
  font-size: 0.875rem;
  line-height: 1.6;
  margin: 1rem 0;
  position: relative;
}

pre code {
  background: transparent;
  color: inherit;
  padding: 0;
  font-size: inherit;
}

pre .comment { color: var(--pre-comment); }
pre .key { color: #93c5fd; }
pre .val { color: #fcd34d; }
pre .str { color: #86efac; }

table {
  border-collapse: collapse;
  width: 100%;
  margin: 1rem 0;
  font-size: 0.9375rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}

th {
  background: var(--surface-2);
  text-align: left;
  padding: 0.625rem 0.875rem;
  font-weight: 600;
  border-bottom: 1px solid var(--border);
  font-size: 0.875rem;
}

td {
  padding: 0.625rem 0.875rem;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}

tr:last-child td { border-bottom: none; }

tr:hover td { background: var(--surface-2); }

/* Callouts */
.callout {
  border-left: 4px solid;
  border-radius: 6px;
  padding: 0.875rem 1.125rem;
  margin: 1rem 0;
  font-size: 0.9375rem;
}

.callout-title {
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 0.375rem;
  margin-bottom: 0.25rem;
}

.callout p:first-child { margin-top: 0; }
.callout p:last-child { margin-bottom: 0; }
.callout pre { margin: 0.5rem 0; }

.callout.info { background: var(--info-bg); border-color: var(--info); }
.callout.tip { background: var(--tip-bg); border-color: var(--tip); }
.callout.warn { background: var(--warn-bg); border-color: var(--warn); }
.callout.danger { background: var(--danger-bg); border-color: var(--danger); }

/* ──────────────── SYSTEM OVERVIEW DIAGRAM ──────────────── */
.overview {
  display: grid;
  grid-template-columns: 1fr 80px 1fr;
  gap: 0.5rem;
  align-items: stretch;
  margin: 2rem 0;
}

.overview-card {
  background: var(--surface);
  border: 2px solid var(--border-strong);
  border-radius: 12px;
  padding: 1.25rem;
  position: relative;
}

.overview-card.client { border-color: var(--client); background: var(--client-bg); }
.overview-card.server { border-color: var(--server); background: var(--server-bg); }

.overview-card-title {
  font-size: 0.8125rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  margin-bottom: 0.875rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.overview-card.client .overview-card-title { color: var(--client); }
.overview-card.server .overview-card-title { color: var(--server); }

.file-list { display: flex; flex-direction: column; gap: 0.375rem; }

.file-item {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 0.5rem;
  align-items: center;
  padding: 0.5rem 0.75rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 0.875rem;
}

.file-item .file-name {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.8125rem;
  font-weight: 500;
}

.file-item .file-tag {
  font-size: 0.6875rem;
  padding: 0.125rem 0.5rem;
  border-radius: 99px;
  font-weight: 600;
  white-space: nowrap;
}

.file-item.priv { border-color: var(--priv-border); background: var(--priv-bg); }
.file-item.priv .file-tag { background: var(--priv); color: white; }

.file-item.pub { border-color: var(--pub-border); background: var(--pub-bg); }
.file-item.pub .file-tag { background: var(--pub); color: white; }

.file-item.cert { border-color: var(--cert-border); background: var(--cert-bg); }
.file-item.cert .file-tag { background: var(--cert); color: white; }

.file-item.config { border-color: var(--border); background: var(--surface-2); }
.file-item.config .file-tag { background: var(--text-muted); color: white; }

.overview-flow {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
}

.overview-flow-label {
  font-size: 0.6875rem;
  color: var(--text-muted);
  text-align: center;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.overview-flow svg { display: block; }

.overview-caption {
  margin-top: 1.25rem;
  padding: 1rem 1.25rem;
  background: var(--primary-soft);
  border-radius: 8px;
  color: var(--text);
  font-size: 0.9375rem;
}

/* ──────────────── STEP NUMBERS ──────────────── */
.step-block {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1.25rem 1.5rem;
  margin: 1.25rem 0;
}

.step-block-title {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 0 0 0.75rem;
  font-size: 1.0625rem;
  font-weight: 600;
}

.step-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  background: var(--primary);
  color: white;
  border-radius: 50%;
  font-size: 0.8125rem;
  font-weight: 700;
  flex-shrink: 0;
}

/* ──────────────── KEYWORD BADGES ──────────────── */
.badge {
  display: inline-flex;
  align-items: center;
  padding: 0.0625rem 0.5rem;
  border-radius: 99px;
  font-size: 0.75rem;
  font-weight: 600;
  margin: 0 0.125rem;
}

.badge.priv { background: var(--priv-bg); color: var(--priv); border: 1px solid var(--priv-border); }
.badge.pub { background: var(--pub-bg); color: var(--pub); border: 1px solid var(--pub-border); }
.badge.cert { background: var(--cert-bg); color: var(--cert); border: 1px solid var(--cert-border); }

/* ──────────────── TWO COLUMN ROW ──────────────── */
.two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin: 1rem 0;
}

.two-col > div {
  padding: 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
}

.two-col h4 { margin-top: 0; }

/* ──────────────── FOOTER ──────────────── */
.page-footer {
  grid-column: 2 / 4;
  grid-row: 3;
  margin: 4rem 2rem 2rem;
  padding-top: 2rem;
  border-top: 1px solid var(--border);
  color: var(--text-faint);
  font-size: 0.875rem;
  text-align: center;
}

@media (max-width: 1100px) {
  .layout { grid-template-columns: 1fr min(820px, calc(100% - 2rem)) 1fr; }
  .toc { display: none; }
  .page-header { grid-column: 2; }
  .page-footer { grid-column: 2; }
}

@media (max-width: 640px) {
  main { padding: 1.5rem 1rem 4rem; }
  .page-header { padding: 1.5rem 1rem 1rem; }
  .page-header h1 { font-size: 1.75rem; }
  .overview { grid-template-columns: 1fr; }
  .overview-flow { transform: rotate(90deg); padding: 0.5rem 0; }
  .two-col { grid-template-columns: 1fr; }
}
</style>
</head>
<body>

<div class="layout">

<header class="page-header">
  <h1>just-shield 설계 결정 요약</h1>
  <p class="subtitle">지금까지 인터뷰에서 정한 것들을 쉬운 말로 — 무엇을, 왜 그렇게 정했는지</p>
</header>

<aside class="toc">
  <h2>목차</h2>
  <ol>
    <li><a href="#sec-1">1. 이 프로젝트가 뭔가요?</a></li>
    <li><a href="#sec-2">2. 적을 알기: TeamPCP 사건</a></li>
    <li><a href="#sec-3">3. 지금까지 내린 결정 7개</a></li>
    <li><a href="#sec-4">4. 확정된 검사 규칙 9개</a></li>
    <li><a href="#sec-5">5. 아직 정하지 않은 것</a></li>
    <li><a href="#sec-6">6. 용어 미니 사전</a></li>
  </ol>
</aside>

<main>

<section id="sec-1">
<h2>1. 이 프로젝트가 뭔가요?</h2>

<p><strong>just-shield</strong>는 한 문장으로 이렇습니다:</p>

<div class="callout tip">
  <div class="callout-title">💡 한 문장 정의</div>
  <p>우리 저장소의 CI 설정 파일(<code>.github/workflows</code>)을 읽고, <strong>"바깥에서 받아 쓰는 부품(GitHub Action) 중에 오염됐거나 오염될 수 있는 게 있는지"</strong>를 실행 전에 검사해 주는 명령줄 도구.</p>
</div>

<div class="overview">
  <div class="overview-card client">
    <div class="overview-card-title">바깥 세상 (서드파티 공급망)</div>
    <div class="file-list">
      <div class="file-item priv">
        <span class="file-name">aquasecurity/trivy-action</span>
        <span></span>
        <span class="file-tag">오염된 적 있음</span>
      </div>
      <div class="file-item priv">
        <span class="file-name">checkmarx/kics-…-action</span>
        <span></span>
        <span class="file-tag">오염된 적 있음</span>
      </div>
      <div class="file-item pub">
        <span class="file-name">actions/checkout</span>
        <span></span>
        <span class="file-tag">GitHub 공식</span>
      </div>
    </div>
  </div>

  <div class="overview-flow">
    <span class="overview-flow-label">uses: 로<br>받아서 실행</span>
    <svg width="60" height="40" viewBox="0 0 60 40">
      <defs>
        <marker id="arrowR" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
          <polygon points="0 0, 10 3, 0 6" fill="#2563eb"/>
        </marker>
      </defs>
      <line x1="2" y1="20" x2="52" y2="20" stroke="#2563eb" stroke-width="2.5" marker-end="url(#arrowR)"/>
    </svg>
  </div>

  <div class="overview-card server">
    <div class="overview-card-title">우리 CI (탈취 표적)</div>
    <div class="file-list">
      <div class="file-item cert">
        <span class="file-name">GITHUB_TOKEN</span>
        <span></span>
        <span class="file-tag">저장소 권한</span>
      </div>
      <div class="file-item priv">
        <span class="file-name">AWS / 클라우드 키</span>
        <span></span>
        <span class="file-tag">시크릿</span>
      </div>
      <div class="file-item config">
        <span class="file-name">배포용 자격증명</span>
        <span></span>
        <span class="file-tag">시크릿</span>
      </div>
    </div>
  </div>
</div>

<div class="overview-caption">
  <strong>just-shield가 서는 자리</strong> — 가운데 화살표(받아서 실행하는 순간) 직전입니다. 바깥 부품이 우리 시크릿 옆에서 실행되기 전에 "이거 진짜 맞아?"를 검사합니다.
</div>
</section>

<section id="sec-2">
<h2>2. 적을 알기: TeamPCP 사건</h2>

<p>TeamPCP는 실제로 존재하는 해커 그룹입니다 (Google 추적명 UNC6780). 이들의 수법은 단순하지만 치명적입니다:</p>

<ol>
  <li><strong>표적을 직접 안 때립니다.</strong> 대신 모두가 믿고 쓰는 보안 도구(Trivy, KICS, LiteLLM)의 배포 계정을 훔칩니다.</li>
  <li><strong>버전 이름표를 바꿔치기합니다.</strong> 예를 들어 Trivy의 버전 태그 77개 중 76개를 악성 코드가 든 커밋으로 옮겨 꽂았습니다. 사용자는 어제와 똑같이 <code>@v3</code>을 받았는데 내용물만 바뀐 겁니다.</li>
  <li><strong>CI 안의 열쇠를 쓸어 담습니다.</strong> 악성 코드는 CI에서 실행되며 클라우드 키, 토큰 등을 외부 서버로 보냈습니다. 약 50만 개 자격증명이 털린 것으로 추정됩니다.</li>
  <li><strong>훔친 열쇠로 다음 피해자를 만듭니다.</strong> 훔친 토큰으로 47개 패키지를 60초 만에 추가 감염시켰습니다 — 웜(전염병)처럼 번집니다.</li>
</ol>

<div class="callout info">
  <div class="callout-title">ℹ 우리 프로젝트에서 TeamPCP의 역할</div>
  <p>TeamPCP "전용 백신"을 만드는 게 아닙니다. <strong>일반적인 공급망 방어 도구</strong>를 만들되, "TeamPCP의 실제 공격을 우리 도구가 막았을까?"를 채점 기준(시험 문제)으로 쓰는 겁니다. (결정 ①)</p>
</div>
</section>

<section id="sec-3">
<h2>3. 지금까지 내린 결정 7개</h2>

<p>인터뷰에서 큰 결정부터 차례로 내려왔습니다. 각 결정은 다음 결정의 전제가 됩니다.</p>

<div class="step-block">
  <h3 class="step-block-title"><span class="step-num">1</span>범위: 특정 그룹 전용이 아닌 일반 방어</h3>
  <p><strong>정한 것:</strong> TeamPCP 전용 탐지기가 아니라, 공급망 공격이라는 일반 문제를 푼다. TeamPCP 사건은 검증 시나리오로 쓴다.</p>
  <p><strong>왜:</strong> 특정 그룹의 흔적(악성 도메인, 파일 해시)만 쫓는 도구는 그룹이 수법을 바꾸면 쓸모없어집니다.</p>
</div>

<div class="step-block">
  <h3 class="step-block-title"><span class="step-num">2</span>방어 지점: 받기 전에 검사한다</h3>
  <p><strong>정한 것:</strong> 공격 체인(① 업스트림 오염 → ② 오염된 것 섭취 → ③ 열쇠 수집 → ④ 유출) 중 <strong>②를 끊는다</strong>. 실행 중 감시(③④ 차단)는 v2 후보로 미룸.</p>
  <p><strong>왜:</strong> ②에서 막으면 ③④는 아예 일어나지 않습니다. 그리고 ②는 파일만 읽으면 검사할 수 있어서 만들기가 현실적입니다. 비유: 택배를 <em>열기 전에</em> 송장과 봉인을 확인하는 것.</p>
</div>

<div class="step-block">
  <h3 class="step-block-title"><span class="step-num">3</span>검사 대상: GitHub Actions부터</h3>
  <p><strong>정한 것:</strong> v1은 <code>.github/workflows</code>의 워크플로 파일이 참조하는 GitHub Action만 검사. npm/PyPI 패키지(락파일)는 v2 후보.</p>
  <p><strong>왜:</strong> 액션은 시크릿 바로 옆에서 실행되는 가장 위험한 부품이고, 검사 대상이 YAML 몇 개로 좁아서 v1으로 완성 가능한 크기입니다.</p>
</div>

<div class="step-block">
  <h3 class="step-block-title"><span class="step-num">4</span>형태: CLI 엔진 먼저, 포장은 나중에</h3>
  <p><strong>정한 것:</strong> 검사 로직(엔진)을 명령줄 도구로 먼저 완성. GitHub Action 래퍼(CI에서 자동 실행)는 v1.5에 얹는다.</p>
  <p><strong>왜:</strong> GitHub Action·VS Code 확장·AI 에이전트 연동은 전부 같은 엔진을 호출하는 "포장"입니다. 엔진 없이 포장부터 만들 수는 없습니다. CI에서는 래퍼 없이도 <code>run: just-shield scan</code> 한 줄로 쓸 수 있습니다.</p>
</div>

<div class="step-block">
  <h3 class="step-block-title"><span class="step-num">5</span>규칙 범위: 두 계층, 9개로 고정 (ADR-0001)</h3>
  <p><strong>정한 것:</strong> "받는 게 진짜인가"(섭취 검증 6개) + "털려도 덜 털리게"(피해 반경 3개) = 9개. 그 외 일반 CI 보안 검사(스크립트 인젝션 등)는 <strong>일부러 안 한다</strong> — 그 영역은 기존 도구 zizmor가 이미 잘함.</p>
  <p><strong>왜:</strong> 다 하려다 보면 기존 도구의 열화 복제품이 됩니다. 좁고 깊은 정체성이 생존 전략입니다.</p>
</div>

<div class="step-block">
  <h3 class="step-block-title"><span class="step-num">6</span>신뢰 경계: 누구를 의심할 것인가</h3>
  <p><strong>정한 것:</strong> 내 저장소·내 조직의 액션 = <span class="badge pub">퍼스트파티 (검사 제외)</span>, GitHub 공식 <code>actions/*</code> = <span class="badge cert">서드파티지만 완화된 경고</span>, 그 외 전부 = <span class="badge priv">예외 없이 엄격</span>.</p>
  <p><strong>왜:</strong> TeamPCP에게 털린 곳이 바로 "믿을 만한 보안 회사들"이었습니다. 평판은 신뢰 근거가 아닙니다. 단, 경고를 남발하면 사용자가 경고 자체를 무시하게 되므로 등급을 나눕니다.</p>
</div>

<div class="step-block">
  <h3 class="step-block-title"><span class="step-num">7</span>한계 인정: 무엇을 보증하지 않는가</h3>
  <p><strong>정한 것:</strong> SHA 핀 고정은 "내용물이 안 바뀐다"(불변성)를 보증할 뿐 "내용물이 착하다"(무해성)는 보증하지 않는다. 아무도 모르는 오염(제로데이)은 v1 보증 범위 밖이며, 이를 ADR에 명시했다. 대신 <strong>이미 알려진 감염 버전</strong>을 잡는 규칙 R9를 추가했다.</p>
  <p><strong>왜:</strong> "다 막아준다"고 암시하는 보안 도구는 통과 마크가 거짓 안심이 되어 오히려 위험합니다.</p>
</div>
</section>

<section id="sec-4">
<h2>4. 확정된 검사 규칙 9개</h2>

<h3 id="sec-4-1">4-1. 섭취 검증 — "받는 물건이 진짜인가" <span class="badge cert">Tier 1</span></h3>

<table>
<thead><tr><th>규칙</th><th>무엇을 잡나</th><th>쉬운 비유</th></tr></thead>
<tbody>
<tr><td><strong>R1</strong> 가변 참조</td><td>액션을 <code>@v3</code> 같은 태그로 받는 것. 태그는 공격자가 옮겨 꽂을 수 있음 → 커밋 SHA(지문)로 받아야 함</td><td>"이름표"가 아니라 "지문"으로 신원 확인</td></tr>
<tr><td><strong>R2</strong> 타이포스쿼팅</td><td><code>aquasecurtiy</code>처럼 한 글자 바꾼 짝퉁 이름의 액션</td><td>짝퉁 상표 감별</td></tr>
<tr><td><strong>R3</strong> 미검증 설치</td><td><code>curl | sh</code>처럼 검증 없이 인터넷에서 받아 바로 실행하는 스텝</td><td>출처 미상 설치파일 더블클릭</td></tr>
<tr><td><strong>R4</strong> 이미지 미고정</td><td><code>image: foo:latest</code>처럼 내용물이 바뀔 수 있는 컨테이너 참조</td><td>내용물 바뀌는 택배 상자</td></tr>
<tr><td><strong>R5</strong> 임포스터 커밋 <span class="badge priv">온라인</span></td><td>SHA로 핀했어도 그 커밋이 해당 저장소 족보(히스토리)에 없는 경우 — 강한 오염 신호</td><td>족보에 없는 가짜 호적</td></tr>
<tr><td><strong>R9</strong> 알려진 감염 버전 <span class="badge priv">DB 동봉</span></td><td>보안 커뮤니티가 이미 "악성"으로 공표한 버전을 아직 쓰고 있는 경우</td><td>리콜 목록과 대조</td></tr>
</tbody>
</table>

<h3 id="sec-4-2">4-2. 피해 반경 축소 — "털려도 덜 털리게" <span class="badge pub">Tier 2</span></h3>

<table>
<thead><tr><th>규칙</th><th>무엇을 잡나</th><th>쉬운 비유</th></tr></thead>
<tbody>
<tr><td><strong>R6</strong> 시크릿 노출</td><td>서드파티 액션이 시크릿에 접근 가능한 잡에서 실행되는 것</td><td>금고 열어둔 방에 외부 수리기사 들이기</td></tr>
<tr><td><strong>R7</strong> 권한 과잉</td><td><code>permissions</code> 미선언 — 기본 토큰이 필요 이상으로 강력해짐</td><td>알바생에게 마스터키 지급</td></tr>
<tr><td><strong>R8</strong> 위험 트리거</td><td><code>pull_request_target</code> + 외부 PR 코드 체크아웃 조합 — 외부인이 시크릿 있는 환경에서 코드 실행 가능</td><td>낯선 사람을 금고 방에 초대</td></tr>
</tbody>
</table>

<div class="callout warn">
  <div class="callout-title">⚠ 동작 원칙</div>
  <p>기본은 <strong>완전 오프라인</strong>(저장소 파일만 읽음). 네트워크가 필요한 검사(R5, R9 최신화)는 <code>--online</code> 옵션을 켰을 때만 실행. 이유: 방어 도구 자신이 "아무 데도 접속 안 한다"고 말할 수 있어야 신뢰받습니다.</p>
</div>
</section>

<section id="sec-5">
<h2>5. 아직 정하지 않은 것</h2>

<div class="callout danger">
  <div class="callout-title">⏸ 멈춰 있는 질문 (질문 8): 제로데이 대응 — 쿨다운 규칙 R10</div>
  <p>아무도 모르는 오염은 탐지가 불가능하지만, <strong>"나온 지 7일 안 된 버전은 일단 쓰지 말라"</strong>는 규칙(쿨다운)으로 위험 기간을 피해갈 수 있습니다. 오염은 보통 며칠 안에 발각되므로, 남들이 먼저 밟게 하는 전략입니다.</p>
  <p><strong>현재 추천:</strong> R10으로 추가 (온라인 규칙, 기본 7일). 트레이드오프: 길수록 안전하지만 버그 패치도 그만큼 늦게 받습니다.</p>
</div>

<p>그 뒤에 남아 있는 결정들 (난이도 순으로 정렬, 위가 쉬움):</p>

<table>
<thead><tr><th>결정</th><th>질문 내용</th><th>전문성 필요도</th></tr></thead>
<tbody>
<tr><td>리포트 형식</td><td>검사 결과를 어떻게 보여줄까? (표 형태, 심각도 색깔, JSON 출력 등)</td><td>낮음 — 취향 문제</td></tr>
<tr><td>실패 기준</td><td>어떤 심각도부터 CI를 빨갛게(빌드 실패) 만들까?</td><td>낮음</td></tr>
<tr><td>예외 처리</td><td>"이 경고는 알고 쓰는 거야"라고 무시 표시하는 방법</td><td>중간</td></tr>
<tr><td>구현 언어</td><td>Python, Go, Rust 중 무엇으로 만들까?</td><td>중간 — 팀이 아는 언어가 우선</td></tr>
<tr><td>테스트 전략</td><td>TeamPCP 시나리오를 재현한 가짜 워크플로로 채점하는 방법</td><td>중간</td></tr>
</tbody>
</table>

<div class="callout tip">
  <div class="callout-title">💡 길을 잃었을 때 기억할 것</div>
  <p>어려운 결정(위협 모델, 규칙 범위, 신뢰 경계)은 <strong>이미 다 끝났습니다.</strong> 남은 것은 대부분 "도구를 어떻게 편하게 쓸까"에 대한 가벼운 결정이고, 모르겠으면 추천안을 그대로 받아들여도 안전합니다.</p>
</div>
</section>

<section id="sec-6">
<h2>6. 용어 미니 사전</h2>

<table>
<thead><tr><th>용어</th><th>쉬운 뜻</th></tr></thead>
<tbody>
<tr><td><strong>공급망 공격</strong></td><td>나를 직접 해킹하는 대신, 내가 믿고 받아 쓰는 부품을 오염시켜서 나까지 감염시키는 공격</td></tr>
<tr><td><strong>CI/CD</strong></td><td>코드를 올리면 자동으로 빌드·테스트·배포해 주는 시스템. 배포 권한(열쇠)을 잔뜩 들고 있어서 해커의 표적</td></tr>
<tr><td><strong>GitHub Action</strong></td><td>CI에서 받아 쓰는 남이 만든 부품 (예: 코드 체크아웃, 스캔 도구)</td></tr>
<tr><td><strong>태그 / SHA</strong></td><td>태그(<code>@v3</code>)는 옮겨 붙일 수 있는 이름표, SHA는 바꿀 수 없는 지문</td></tr>
<tr><td><strong>태그 하이재킹</strong></td><td>공격자가 이름표를 악성 코드에 옮겨 붙이는 것</td></tr>
<tr><td><strong>임포스터 커밋</strong></td><td>저장소의 정식 기록에 없는 가짜 커밋</td></tr>
<tr><td><strong>섭취 전 검증</strong></td><td>부품을 실행하기 전에 진짜인지 확인하는 것 — just-shield의 본업</td></tr>
<tr><td><strong>피해 반경</strong></td><td>뚫렸을 때 얼마나 털리는가의 범위. 권한을 줄이면 반경이 줄어듦</td></tr>
<tr><td><strong>제로데이</strong></td><td>아직 아무도 모르는 (그래서 목록 대조로 못 잡는) 오염</td></tr>
<tr><td><strong>ADR</strong></td><td>"왜 이렇게 결정했는지" 기록한 문서. <code>docs/adr/</code>에 있음</td></tr>
</tbody>
</table>
</section>

</main>

<footer class="page-footer">
  Generated from CONTEXT.md · docs/adr/0001 · 설계 인터뷰 (2026-06-11)
</footer>

</div>

</body>
</html>