<!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;
}
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); }
.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); }
.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-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;
}
.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-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; }
.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>