// ─── 보안 토폴로지 확장 모드 ────────────────────────────────────────────────
// ① 공격 시나리오 시뮬레이터 — 위협이 어느 계층에서 멈추는지 단계별 재생
// ② 내 회사 진단 — 보유 솔루션 체크 → 비어 있는 계층 시각화 + 리포트
// topology.jsx 의 TopologyPage 에서 mode 로 전환해 사용한다.
const TOPO_ATTACKS = [
{ id:'ransom', c:'#F87171',
ko:'랜섬웨어 메일', en:'Ransomware email', ja:'ランサムメール',
story:{ ko:'공격자가 거래처를 사칭해 매크로가 포함된 견적서를 보냅니다.',
en:'An attacker impersonates a supplier and sends a macro-laden quote file.',
ja:'取引先を装いマクロ付きの見積書を送信します。' },
steps:[
{ n:'internet', ko:'외부에서 악성 첨부 메일 발송', en:'Malicious attachment sent from outside' },
{ n:'fw', ko:'방화벽 통과 — 정상 SMTP 트래픽으로 보임', en:'Passes the firewall as ordinary SMTP' },
{ n:'spamout', ko:'SpamOut 이 첨부 매크로·발신자 사칭을 탐지해 격리', en:'SpamOut detects the macro and spoofed sender, quarantines it', block:true,
by:'SpamOut', byPage:'sol_spamout' },
{ n:'mserver', ko:'(미차단 시) 메일함으로 배달', en:'(If unblocked) delivered to the mailbox' },
{ n:'pc', ko:'(미차단 시) 사용자 실행 → 랜섬웨어 암호화 시작', en:'(If unblocked) user opens it → encryption begins' },
{ n:'edr', ko:'Insights E 가 암호화 행위를 탐지해 단말 격리', en:'Insights E detects the behavior and isolates the endpoint', block:true,
by:'Genian Insights E', byPage:'sol_edr' },
] },
{ id:'takeover', c:'#FBBF24',
ko:'계정 탈취 · BEC', en:'Account takeover / BEC', ja:'アカウント乗っ取り',
story:{ ko:'탈취한 메일 계정으로 결제 계좌 변경 메일을 발송합니다.',
en:'A hijacked mailbox is used to send a bank-account change request.',
ja:'乗っ取ったメールで振込先変更を依頼します。' },
steps:[
{ n:'internet', ko:'유출 비밀번호로 외부에서 로그인 시도', en:'Login attempt with leaked credentials' },
{ n:'vpn', ko:'VPN·ZTNA 에서 비정상 위치 접속 탐지', en:'VPN / ZTNA flags an anomalous location', block:true, by:'VPN · ZTNA', byPage:'sol_network' },
{ n:'mserver', ko:'(미차단 시) 메일함 접근 — 규칙 생성 후 잠복', en:'(If unblocked) mailbox access, hidden rules created' },
{ n:'checkout', ko:'CheckOut 이 계좌정보 변경 메일을 발송 전 승인 대상으로 보류', en:'CheckOut holds the account-change mail for approval', block:true,
by:'CheckOut', byPage:'sol_checkout' },
{ n:'arch5', ko:'Arch5 에 원본이 남아 사후 추적·소명 가능', en:'Arch5 retains originals for investigation' },
] },
{ id:'insider', c:'#A78BFA',
ko:'내부자 정보 유출', en:'Insider data leak', ja:'内部者による漏えい',
story:{ ko:'퇴직 예정자가 고객 DB를 내려받아 외부로 반출하려 합니다.',
en:'A departing employee tries to export the customer database.',
ja:'退職予定者が顧客DBを外部へ持ち出そうとします。' },
steps:[
{ n:'pc', ko:'사내 단말에서 대량 조회 시도', en:'Bulk query attempt from an internal PC' },
{ n:'dbac', ko:'DBSAFER 가 대량 SELECT 를 차단하고 결재를 요구', en:'DBSAFER blocks the bulk SELECT and requires approval', block:true,
by:'DBSAFER DB', byPage:'sol_dbsafer' },
{ n:'catchsecu',ko:'CatchSecu 가 개인정보 포함 여부를 판별해 기록', en:'CatchSecu identifies personal data and logs it' },
{ n:'checkout', ko:'외부 메일 반출 시도 → CheckOut 이 발송 차단', en:'Outbound mail attempt → CheckOut blocks it', block:true,
by:'CheckOut', byPage:'sol_checkout' },
{ n:'audit', ko:'전 과정이 감사 로그로 남아 소명 자료가 됩니다', en:'The full trail remains as audit evidence' },
] },
{ id:'ddos', c:'#38BDF8',
ko:'디도스 공격', en:'DDoS attack', ja:'DDoS攻撃',
story:{ ko:'대량 트래픽으로 대외 서비스를 마비시키려 합니다.',
en:'A traffic flood aims to take public services offline.',
ja:'大量トラフィックで対外サービスを停止させます。' },
steps:[
{ n:'internet', ko:'봇넷에서 대량 트래픽 유입', en:'Botnet floods the link' },
{ n:'ddos', ko:'SNIPER ONE-d 가 회선 앞단에서 공격 트래픽만 정제', en:'SNIPER ONE-d scrubs attack traffic at the edge', block:true,
by:'SNIPER ONE-d', byPage:'sol_sniper' },
{ n:'fw', ko:'(미차단 시) 방화벽 세션 테이블 고갈', en:'(If unblocked) firewall session table exhausts' },
{ n:'websrv', ko:'(미차단 시) 대외 서비스 중단', en:'(If unblocked) public service goes down' },
{ n:'ndr', ko:'NetWitness 가 공격 패턴을 기록해 사후 분석 지원', en:'NetWitness records the pattern for post-incident analysis' },
] },
];
// ── 진단 체크리스트 (보유 여부 → 계층 커버리지) ───────────────────────────────
const TOPO_ASSESS = [
{ id:'mailin', nodes:['spamout'], ko:'수신 메일 보안 (안티스팸·피싱 차단)', en:'Inbound email security', page:'sol_spamout', prod:'SpamOut' },
{ id:'mailout', nodes:['checkout'], ko:'발신 메일 통제 (유출·오발송 방지)', en:'Outbound email control', page:'sol_checkout', prod:'CheckOut' },
{ id:'archive', nodes:['arch5'], ko:'메일 원본 보존 (감사·소송 대응)', en:'Email archiving', page:'sol_arch5', prod:'Arch5' },
{ id:'nac', nodes:['nac'], ko:'단말 접근제어 (비인가 기기 차단)', en:'Network access control', page:'sol_genian', prod:'Genian NAC' },
{ id:'edr', nodes:['edr','eset'], ko:'엔드포인트 탐지·대응 (EDR)', en:'Endpoint detection & response', page:'sol_edr', prod:'Genian Insights E' },
{ id:'db', nodes:['dbac','db'], ko:'DB 접근제어·감사', en:'Database access control', page:'sol_dbsafer', prod:'DBSAFER DB' },
{ id:'privacy', nodes:['catchsecu'], ko:'개인정보 수집·파기 자동화', en:'Privacy lifecycle automation', page:'sol_catchsecu', prod:'CatchSecu' },
{ id:'vuln', nodes:['cce','cve'], ko:'취약점 상시 진단 (CCE·CVE)', en:'Continuous vulnerability assessment', page:'sol_cce', prod:'SolidStep' },
{ id:'ddos', nodes:['ddos'], ko:'디도스 방어', en:'DDoS protection', page:'sol_sniper', prod:'SNIPER ONE-d' },
{ id:'soc', nodes:['mdr','soc'], ko:'24/7 관제 (자체 또는 위탁)', en:'24/7 monitoring', page:'sol_mdr', prod:'Genian MDR' },
];
// ── 패널: 공격 시뮬레이터 ────────────────────────────────────────────────────
const AttackPanel = ({ lang, atk, step, playing, onPick, onStep, onToggle, setPage }) => {
const isKo = lang === 'ko';
const L = (o, k) => (o && (o[k+'_'+lang] || o[lang] || o.ko)) || '';
const t = (o) => (o && (o[lang] || o.ko)) || '';
return (
{isKo?'공격 시나리오':(lang==='ja'?'攻撃シナリオ':'Attack scenario')}
{TOPO_ATTACKS.map(a=>(
onPick(a.id)}>
{t(a)}
))}
{!atk ? (
{isKo?'시나리오를 선택하면 위협이 토폴로지를 따라 이동하며, 어느 제품에서 멈추는지 단계별로 보여줍니다.'
:lang==='ja'?'シナリオを選ぶと、脅威がどの製品で止まるかを段階的に表示します。'
:'Pick a scenario to watch the threat travel the topology and see exactly where it stops.'}
) : (
<>
{t(atk.story)}
{atk.steps.map((s,i)=>{
const state = i < step ? 'done' : i === step ? 'now' : 'next';
return (
onStep(i)}>
{s.block ? '✓' : i+1}
{t(s)}
{s.block && {isKo?'여기서 차단됩니다':(lang==='ja'?'ここで遮断':'Blocked here')} · {s.by} }
);
})}
{playing?'⏸ '+(isKo?'정지':'Pause'):'▶ '+(isKo?'재생':'Play')}
onStep(0)}>↺ {isKo?'처음부터':(lang==='ja'?'最初から':'Restart')}
{(()=>{ const b = atk.steps.filter(s=>s.block); return b.length ? (
{isKo?'이 시나리오를 막는 솔루션':(lang==='ja'?'この攻撃を止める製品':'Solutions that stop this')}
{b.map((s,i)=>(
setPage(s.byPage)}>{s.by} →
))}
) : null; })()}
>
)}
);
};
// ── 패널: 내 회사 진단 ───────────────────────────────────────────────────────
const AssessPanel = ({ lang, have, onToggle, onReset, setPage, openQuote }) => {
const isKo = lang === 'ko';
const t = o => (o && (o[lang] || o.ko)) || '';
const gaps = TOPO_ASSESS.filter(a=>!have.includes(a.id));
const score = Math.round(((TOPO_ASSESS.length - gaps.length) / TOPO_ASSESS.length) * 100);
const grade = score >= 80 ? (isKo?'양호':'Strong') : score >= 50 ? (isKo?'보통':'Moderate') : (isKo?'취약':'At risk');
const gc = score >= 80 ? '#34D399' : score >= 50 ? '#FBBF24' : '#F87171';
return (
{isKo?'내 회사 보안 진단':(lang==='ja'?'自社セキュリティ診断':'Assess your stack')}
{isKo?'이미 운영 중인 항목을 체크하세요. 비어 있는 계층이 토폴로지에 표시됩니다.'
:lang==='ja'?'導入済みの項目をチェックしてください。不足層が図に表示されます。'
:'Check what you already run — the gaps light up on the diagram.'}
{score}% {isKo?'커버리지':(lang==='ja'?'カバレッジ':'Coverage')}
{grade}
{TOPO_ASSESS.map(a=>{
const on = have.includes(a.id);
return (
onToggle(a.id)}/>
{on?'✓':''}
{t(a)}
);
})}
{gaps.length > 0 && (
{isKo?`비어 있는 계층 ${gaps.length}개`:(lang==='ja'?`不足している層 ${gaps.length}件`:`${gaps.length} gaps found`)}
{gaps.slice(0,4).map(g=>(
setPage(g.page)}>
{t(g)} {g.prod} →
))}
{gaps.length > 4 &&
{isKo?`외 ${gaps.length-4}개`:`+${gaps.length-4} more`}
}
openQuote(gaps[0].prod)}>
{isKo?'빈 계층 보완 견적 요청':(lang==='ja'?'不足分の見積を依頼':'Get a quote for the gaps')}
)}
{isKo?'초기화':(lang==='ja'?'リセット':'Reset')}
);
};
const topoModeStyles = `
.tp-modes{display:inline-flex;gap:3px;padding:3px;border-radius:100px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1)}
.tp-modes button{padding:7px 16px;border-radius:100px;border:none;background:transparent;color:rgba(255,255,255,.58);
font-family:inherit;font-size:12.5px;font-weight:600;cursor:pointer;transition:all .2s ease;white-space:nowrap}
.tp-modes button:hover{color:#fff}
.tp-modes button.on{background:rgba(255,255,255,.94);color:#0B1730}
.tp-atk-picks{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px}
.tp-atk-pick{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:100px;cursor:pointer;font-family:inherit;
font-size:12px;font-weight:600;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);color:rgba(255,255,255,.62);transition:all .18s}
.tp-atk-pick:hover{color:#fff;background:rgba(255,255,255,.08)}
.tp-atk-pick s{width:7px;height:7px;border-radius:50%;display:block;text-decoration:none}
.tp-atk-story{font-size:13px;color:rgba(255,255,255,.66);line-height:1.68;margin:0 0 14px;padding-bottom:13px;border-bottom:1px solid rgba(255,255,255,.08)}
.tp-atk-steps{display:flex;flex-direction:column;gap:2px}
.tp-atk-step{display:flex;gap:10px;padding:9px 8px;border-radius:9px;cursor:pointer;transition:background .18s;align-items:flex-start}
.tp-atk-step:hover{background:rgba(255,255,255,.04)}
.tp-atk-step.next{opacity:.38}
.tp-atk-step.now{background:rgba(103,232,249,.09)}
.tp-atk-no{flex:none;width:20px;height:20px;border-radius:50%;border:1px solid rgba(255,255,255,.22);display:flex;align-items:center;justify-content:center;
font-family:'DM Sans',sans-serif;font-size:10.5px;font-weight:700;color:rgba(255,255,255,.7);margin-top:1px}
.tp-atk-tx{font-size:12.5px;line-height:1.6;color:rgba(255,255,255,.76)}
.tp-atk-ctl{display:flex;gap:6px;margin-top:12px}
.tp-atk-ctl button{flex:1;padding:8px 0;border-radius:9px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.05);
color:rgba(255,255,255,.75);font-family:inherit;font-size:12px;font-weight:600;cursor:pointer;transition:all .18s}
.tp-atk-ctl button:hover{background:rgba(255,255,255,.1);color:#fff}
.tp-atk-sum{margin-top:16px;padding-top:14px;border-top:1px solid rgba(255,255,255,.08)}
.tp-atk-sum-k{font-family:'DM Sans',sans-serif;font-size:10px;font-weight:700;letter-spacing:1.5px;color:rgba(255,255,255,.4);text-transform:uppercase;margin-bottom:4px}
.tp-as-score{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-radius:12px;border:1px solid;margin-bottom:14px}
.tp-as-score b{font-family:'DM Sans',sans-serif;font-size:26px;font-weight:700;display:block;line-height:1}
.tp-as-score span{font-size:11px;color:rgba(255,255,255,.45);display:block;margin-top:3px}
.tp-as-grade{font-size:13px;font-weight:700}
.tp-as-list{display:flex;flex-direction:column;gap:2px}
.tp-as-item{display:flex;align-items:center;gap:10px;padding:8px;border-radius:9px;cursor:pointer;transition:background .18s}
.tp-as-item:hover{background:rgba(255,255,255,.04)}
.tp-as-item input{position:absolute;opacity:0;width:0;height:0}
.tp-as-box{flex:none;width:18px;height:18px;border-radius:5px;border:1px solid rgba(255,255,255,.24);display:flex;align-items:center;
justify-content:center;font-size:11px;font-weight:700;color:#04121f;transition:all .18s}
.tp-as-item.on .tp-as-box{background:#34D399;border-color:#34D399}
.tp-as-tx{font-size:12.5px;line-height:1.5;color:rgba(255,255,255,.72)}
.tp-as-item.on .tp-as-tx{color:#fff}
.tp-as-gaps{margin-top:16px;padding-top:14px;border-top:1px solid rgba(255,255,255,.08)}
.tp-as-gap{display:flex;align-items:center;gap:8px;padding:8px 0;cursor:pointer;font-size:12.5px;color:rgba(255,255,255,.72);transition:color .18s}
.tp-as-gap:hover{color:#fff}
.tp-as-gap s{width:6px;height:6px;border-radius:50%;background:#F87171;display:block;text-decoration:none;flex:none}
.tp-as-gap span{flex:1;min-width:0}
.tp-as-gap b{font-size:11.5px;color:#67E8F9;font-weight:600;white-space:nowrap}
.tp-as-more{font-size:11.5px;color:rgba(255,255,255,.4);padding-top:4px}
/* 노드 상태 */
.tp-node.st-ok circle.ring{stroke:#34D399!important;stroke-width:2.4!important;fill:rgba(52,211,153,.14)!important}
.tp-node.st-gap circle.ring{stroke:#F87171!important;stroke-width:2.2!important;stroke-dasharray:4 4;fill:rgba(248,113,113,.1)!important}
.tp-node.st-hit circle.ring{stroke:#F87171!important;stroke-width:2.6!important;fill:rgba(248,113,113,.2)!important}
.tp-node.st-hit{filter:drop-shadow(0 0 16px rgba(248,113,113,.6))}
.tp-node.st-block circle.ring{stroke:#34D399!important;stroke-width:3!important;fill:rgba(52,211,153,.24)!important}
.tp-node.st-block{filter:drop-shadow(0 0 20px rgba(52,211,153,.7))}
@media(max-width:1120px){.tp-modes{width:100%;justify-content:center}}
`;
Object.assign(window, { TOPO_ATTACKS, TOPO_ASSESS, AttackPanel, AssessPanel, topoModeStyles });