// ─── 보안 토폴로지 확장 모드 ──────────────────────────────────────────────── // ① 공격 시나리오 시뮬레이터 — 위협이 어느 계층에서 멈추는지 단계별 재생 // ② 내 회사 진단 — 보유 솔루션 체크 → 비어 있는 계층 시각화 + 리포트 // 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=>( ))}
{!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}}
); })}
{(()=>{ const b = atk.steps.filter(s=>s.block); return b.length ? (
{isKo?'이 시나리오를 막는 솔루션':(lang==='ja'?'この攻撃を止める製品':'Solutions that stop this')}
{b.map((s,i)=>( ))}
) : 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 ( ); })}
{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`}
}
)}
); }; 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 });