refactor: overhaul incident simulator component for enhanced user experience and functionality

- Replaced the existing HTML structure with a new layout focusing on a Cyber-Defense SOC Dashboard.
- Streamlined the simulation configuration panel, including operation mode selection and target input.
- Updated the simulation execution logic to utilize a more reactive approach with signals.
- Removed deprecated logging and simulation stages, simplifying the overall codebase.
- Enhanced error handling and loading states for better user feedback during simulations.
- Introduced new metrics and forensic evidence display for improved simulation results.
This commit is contained in:
bryancito1090 2026-09-16 16:54:37 -05:00
parent 7c2582b209
commit 3676637de3
4 changed files with 433 additions and 627 deletions

View File

@ -6,6 +6,8 @@ import { RetroScorePort } from './domain/ports/retro-score.port';
import { LocalStorageRetroScoreAdapter } from './infrastructure/adapters/local-storage-retro-score.adapter'; import { LocalStorageRetroScoreAdapter } from './infrastructure/adapters/local-storage-retro-score.adapter';
import { ChatPort } from './domain/ports/chat.port'; import { ChatPort } from './domain/ports/chat.port';
import { N8nChatAdapter } from './infrastructure/adapters/n8n-chat.adapter'; import { N8nChatAdapter } from './infrastructure/adapters/n8n-chat.adapter';
import { SecurityAgentPort } from './domain/ports/security-agent.port';
import { SecurityAgentService } from './services/security-agent.service';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
@ -13,6 +15,7 @@ export const appConfig: ApplicationConfig = {
provideRouter(routes, withComponentInputBinding()), provideRouter(routes, withComponentInputBinding()),
provideHttpClient(), provideHttpClient(),
{ provide: RetroScorePort, useClass: LocalStorageRetroScoreAdapter }, { provide: RetroScorePort, useClass: LocalStorageRetroScoreAdapter },
{ provide: ChatPort, useClass: N8nChatAdapter } { provide: ChatPort, useClass: N8nChatAdapter },
{ provide: SecurityAgentPort, useClass: SecurityAgentService }
] ]
}; };

View File

@ -1,126 +1,179 @@
:host{display:block;width:100%} :host {
.zen-sim-container{max-width:1540px;margin:0 auto;padding:0 24px 40px;display:flex;flex-direction:column;gap:16px;width:100%;box-sizing:border-box} display: block;
.sim-section-header,.report-header,.console-titlebar,.arch-metrics-bar{display:flex;align-items:center;justify-content:space-between} width: 100%;
.sim-section-header{width:100%;padding-bottom:8px;border-bottom:1px solid #1e2533;margin-bottom:4px;flex-wrap:wrap;gap:12px} color: var(--zen-text-primary, #f0f3f8);
.header-left,.report-header-left,.email-input-wrapper,.story-step-card,.arch-node,.node-info,.secops-console-frame,.forensic-report-card,.report-metric-box,.metric-card{display:flex;flex-direction:column} font-family: var(--font-sans, 'Plus Jakarta Sans', -apple-system, sans-serif);
.header-left{gap:4px} }
.title-with-badge,.email-label,.action-buttons,.sim-trigger-btn,.mode-btn,.console-cursor-row,.download-report-btn{display:flex;align-items:center}
.title-with-badge{gap:12px} .soc-dashboard {
.section-tag,.step-badge,.node-badge-tag,.report-badge,.box-label,.metric-label,.console-title,.console-pill,.log-row,.status-pill,.step-meta,.node-detail,.node-meta-ip,.node-meta-svc,.email-field,.box-val,.metric-val,.console-prompt{font-family:var(--font-mono,monospace)} max-width: 1100px;
.section-tag{font-size:.6875rem;color:#5e697d;letter-spacing:.08em} margin: 0 auto;
.pulse-indicator{width:10px;height:10px;border-radius:50%;background:#c59b27;box-shadow:0 0 12px #c59b27;flex-shrink:0} padding: 1.5rem 1rem;
.section-title{margin:0;font-size:clamp(1.6rem,3vw,2.25rem);font-weight:800;color:#f0f3f8;line-height:1.15;text-transform:uppercase} }
.header-controls{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.view-mode-toggle{display:inline-flex;background:#0d1117;border:1px solid #1e2533;border-radius:4px;padding:2px;gap:2px} .soc-header {
.mode-btn{gap:6px;padding:5px 10px;background:0 0;border:none;border-radius:2px;color:#9aa5b8;font-size:.75rem;font-weight:600;cursor:pointer} display: flex;
.mode-btn.active{background:#131720;color:#c59b27;box-shadow:0 1px 4px rgba(0,0,0,.4)} justify-content: space-between;
.mode-icon{width:13px;height:13px} align-items: center;
.status-pill{gap:8px;background:rgba(30,41,59,.6);border:1px solid rgba(255,255,255,.1);padding:6px 14px;border-radius:999px;font-size:.75rem;font-weight:600;color:#9aa5b8} border-bottom: 1px solid var(--zen-border-subtle, #1e293b);
.status-beacon{width:8px;height:8px;border-radius:50%;background:#38bdf8;box-shadow:0 0 8px #38bdf8;animation:pulseBeacon 2s infinite ease-in-out} padding-bottom: 1.25rem;
.status-pill.danger .status-beacon,.dot.red{background:#ef4444} margin-bottom: 1.5rem;
.status-pill.active .status-beacon,.dot.yellow{background:#eab308} gap: 1rem;
.status-pill.contained .status-beacon,.dot.green{background:#22c55e} flex-wrap: wrap;
.zen-sim-board{background:#0a0d14;border:1px solid #1e2533;border-radius:6px;padding:24px;display:flex;flex-direction:column;gap:20px} }
.email-control-deck{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;background:#10141d;border:1px solid #1c2331;border-radius:4px;padding:14px 18px;flex-wrap:wrap}
.email-input-wrapper{gap:6px;flex:1;min-width:260px} .logo-area { display: flex; align-items: center; gap: 0.85rem; }
.email-label{gap:6px;font-size:.78rem;font-weight:600;color:#9aa5b8} .shield-badge {
.email-icon{width:14px;height:14px;color:#c59b27} display: flex; align-items: center; justify-content: center;
.email-field{background:#07090e;border:1px solid #1e2533;border-radius:3px;color:#f0f3f8;font-size:.8125rem;padding:8px 12px;outline:0} width: 44px; height: 44px;
.email-field:focus{border-color:#c59b27;box-shadow:0 0 6px rgba(197,155,39,.25)} background: rgba(56, 189, 248, 0.12);
.action-buttons{gap:10px} border: 1px solid rgba(56, 189, 248, 0.3);
.sim-trigger-btn{gap:8px;background:#c59b27;color:#07080b;border:1px solid #e2b94a;border-radius:3px;padding:9px 18px;font-size:.84rem;font-weight:700;cursor:pointer} border-radius: var(--zen-radius-sm, 4px);
.sim-trigger-btn:hover:not(:disabled){background:#d4af37;box-shadow:0 0 12px rgba(197,155,39,.4)} color: #38bdf8;
.sim-trigger-btn:disabled{opacity:.6;cursor:not-allowed} flex-shrink: 0;
.sim-reset-btn{background:0 0;color:#9aa5b8;border:1px solid #2d3748;border-radius:3px;padding:8px 14px;font-size:.8125rem;font-weight:600;cursor:pointer} }
.story-grid-flow{display:flex;align-items:stretch;justify-content:space-between;gap:8px;width:100%} .shield-icon { width: 22px; height: 22px; }
.story-step-card{flex:1;background:#11151f;border:1px solid #1e293b;border-radius:4px;padding:16px 14px;gap:8px;transition:all .3s} .soc-title { font-size: 1.35rem; font-weight: 700; color: #38bdf8; margin: 0; }
.step-badge,.node-badge-tag{font-size:.625rem;font-weight:700;letter-spacing:.08em} .soc-subtitle { font-size: 0.78rem; color: var(--zen-text-muted, #94a3b8); margin: 0.2rem 0 0; font-family: var(--font-mono, monospace); }
.step-badge{color:#64748b} .engine-badge {
.step-icon-wrap{width:36px;height:36px;border-radius:4px;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.04);color:#9aa5b8} background: #111827; color: #94a3b8; padding: 0.35rem 0.75rem;
.step-icon-wrap svg{width:20px;height:20px} border-radius: var(--zen-radius-xs, 4px); font-size: 0.78rem; font-family: var(--font-mono, monospace);
.node-badge-tag.threat,.step-icon-wrap.danger{color:#f87171;background:rgba(239,68,68,.1)} border: 1px solid #1e293b;
.node-badge-tag.detection,.step-icon-wrap.warning{color:#38bdf8;background:rgba(56,189,248,.1)} }
.node-badge-tag.remediation,.step-icon-wrap.action{color:#4ade80;background:rgba(34,197,94,.1)}
.node-badge-tag.notification,.step-icon-wrap.success{color:#e2b94a;background:rgba(197,155,39,.1)} .control-panel, .results-dashboard {
.step-title,.node-title{font-size:.8125rem;font-weight:700;color:#f0f3f8;margin:0} background: #111827; border: 1px solid #1f2937;
.step-title{font-size:.875rem} border-radius: var(--zen-radius-sm, 6px); padding: 1.5rem;
.step-desc{font-size:.75rem;color:#9aa5b8;line-height:1.45;margin:0;flex-grow:1} margin-bottom: 1.5rem; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
.step-meta{font-size:.6875rem;color:#64748b;border-top:1px solid #1c2331;padding-top:6px} }
.story-step-card.active{border-color:#c59b27;background:#171b26;box-shadow:0 0 16px rgba(197,155,39,.2);transform:translateY(-2px)}
.story-step-card.completed,.arch-node.past{border-color:rgba(34,197,94,.4)} .panel-tag { display: inline-block; font-family: var(--font-mono, monospace); font-size: 0.72rem; color: #e2b94a; margin-bottom: 0.3rem; }
.story-arrow{display:flex;align-items:center;justify-content:center;color:#2d3748;font-size:1.1rem;font-weight:700;padding:0 4px} .panel-title { font-size: 1.15rem; font-weight: 600; color: #f8fafc; margin: 0 0 1.25rem; }
.story-arrow.active{color:#c59b27}
.diagram-canvas{display:grid;grid-template-columns:1fr auto 1fr auto 1fr auto 1fr;align-items:center;gap:8px;width:100%} .form-grid {
.arch-node{background:#11151f;border:1px solid #1e293b;border-radius:4px;padding:12px 10px;gap:6px;position:relative;min-height:125px;box-sizing:border-box} display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
.node-badge-tag{padding:2px 6px;border-radius:2px;width:fit-content} gap: 1.25rem; margin-bottom: 1.25rem;
.node-icon-frame{width:28px;height:28px;display:flex;align-items:center;justify-content:center;color:#9aa5b8;flex-shrink:0} }
.node-svg{width:22px;height:22px;max-width:22px;max-height:22px;display:block} .form-group label { display: block; font-size: 0.8rem; color: #94a3b8; margin-bottom: 0.4rem; font-weight: 500; }
.node-info{gap:2px} .form-group input[type="text"], .form-group input[type="email"], .form-group select {
.node-detail,.node-meta-ip,.node-meta-svc{font-size:.6875rem;color:#9aa5b8} width: 100%; padding: 0.7rem 0.85rem; background: #0f172a; border: 1px solid #334155;
.node-meta-ip,.node-meta-svc{font-size:.625rem;color:#64748b} color: #f8fafc; border-radius: var(--zen-radius-xs, 4px); box-sizing: border-box;
.node-status-indicator{position:absolute;top:8px;right:8px;width:6px;height:6px;border-radius:50%;background:#334155} font-family: var(--font-mono, monospace); font-size: 0.88rem; transition: border-color 0.15s;
.threat-node.active{border-color:#ef4444;background:#1c1316} }
.threat-node.active .node-icon-frame,.threat-node.active .node-status-indicator.ping{color:#ef4444;background:#ef4444} .form-group input:focus, .form-group select:focus { outline: none; border-color: #38bdf8; }
.detection-node.active{border-color:#38bdf8;background:#0f1c29}
.detection-node.active .node-icon-frame,.detection-node.active .node-status-indicator.ping{color:#38bdf8;background:#38bdf8} .target-presets { display: flex; align-items: center; gap: 0.4rem; margin-top: 0.45rem; flex-wrap: wrap; }
.remediation-node.active{border-color:#22c55e;background:#0f2316} .preset-label { font-size: 0.72rem; color: #64748b; font-family: var(--font-mono, monospace); }
.remediation-node.active .node-icon-frame,.remediation-node.active .node-status-indicator.ping{color:#22c55e;background:#22c55e} .preset-btn {
.notification-node.active{border-color:#c59b27;background:#231c0e} background: #1e293b; border: 1px solid #334155; color: #93c5fd; font-size: 0.7rem;
.notification-node.active .node-icon-frame,.notification-node.active .node-status-indicator.ping{color:#c59b27;background:#c59b27} font-family: var(--font-mono, monospace); padding: 0.15rem 0.45rem; border-radius: 3px; cursor: pointer;
.arch-node.past .node-status-indicator{background:#22c55e} }
.flow-connector{display:flex;align-items:center;justify-content:center;position:relative;height:20px;color:#334155} .preset-btn:hover { background: #334155; color: #ffffff; }
.beam-line{position:absolute;top:50%;left:0;right:0;height:2px;background:#1e293b;transform:translateY(-50%)}
.flow-arrow{position:relative;z-index:2;font-size:.625rem;background:#0a0d14;padding:0 2px} .checkbox-group { display: flex; align-items: center; }
.flow-connector.flowing{color:#38bdf8} .checkbox-label { display: flex; align-items: center; gap: 0.6rem; cursor: pointer; font-size: 0.85rem; color: #cbd5e1; }
.flow-connector.flowing .beam-line{background:linear-gradient(90deg,#ef4444,#38bdf8,#22c55e)} .checkbox-label input[type="checkbox"] { width: 18px; height: 18px; accent-color: #0284c7; cursor: pointer; }
.secops-console-frame{background:#06080c;border:1px solid #1e2533;border-radius:4px;overflow:hidden}
.console-titlebar{background:#0f131a;padding:6px 10px;border-bottom:1px solid #1e2533} .launch-btn {
.console-dots{display:flex;gap:5px} width: 100%; padding: 0.85rem 1.25rem;
.dot{width:7px;height:7px;border-radius:50%} background: linear-gradient(135deg, #0284c7 0%, #2563eb 100%);
.console-title{font-size:.6875rem;color:#94a3b8} color: #ffffff; border: none; border-radius: var(--zen-radius-sm, 6px);
.console-pill{font-size:.5625rem;color:#38bdf8;background:rgba(56,189,248,.12);padding:2px 5px;border-radius:2px} font-size: 0.95rem; font-weight: 600; cursor: pointer;
.console-body{padding:8px 10px;min-height:100px;max-height:160px;overflow-y:auto;display:flex;flex-direction:column;gap:4px;font-size:.75rem;line-height:1.4} display: flex; align-items: center; justify-content: center; gap: 0.5rem;
.log-row{display:flex;gap:8px;color:#94a3b8} transition: opacity 0.2s;
.log-time{color:#475569;flex-shrink:0} }
.log-tag{font-weight:700;flex-shrink:0} .launch-btn:hover:not(:disabled) { opacity: 0.92; }
.log-row.warn .log-msg,.log-row.warn .log-tag{color:#f87171} .launch-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.log-row.action .log-msg,.log-row.action .log-tag{color:#fde047} .btn-icon { width: 18px; height: 18px; }
.log-row.success .log-msg,.log-row.success .log-tag{color:#4ade80} .spinner-dot {
.console-cursor-row{gap:6px;margin-top:3px} width: 10px; height: 10px; border-radius: 50%; background: #38bdf8;
.console-prompt{color:#34d399;font-size:.6875rem} animation: pulse-spin 1s infinite alternate;
.cursor-block{width:6px;height:11px;background:#38bdf8} }
.forensic-report-card{background:linear-gradient(180deg,#10151f 0%,#0c1017 100%);border:1px solid rgba(197,155,39,.4);border-radius:6px;padding:20px;gap:16px;box-shadow:0 4px 20px rgba(0,0,0,.4);animation:fadeInReport .4s ease-out} @keyframes pulse-spin { 0% { transform: scale(0.8); opacity: 0.5; } 100% { transform: scale(1.3); opacity: 1; } }
@keyframes fadeInReport{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
.report-header{flex-wrap:wrap;gap:12px;border-bottom:1px solid #1e293b;padding-bottom:12px} .alert-box.error {
.report-header-left{gap:4px} background: #450a0a; color: #fca5a5; border: 1px solid #7f1d1d;
.report-badge{font-size:.625rem;color:#c59b27;letter-spacing:.1em;font-weight:700} padding: 1rem; border-radius: var(--zen-radius-sm, 4px); margin-bottom: 1.5rem;
.report-title{margin:0;font-size:1.05rem;font-weight:800;color:#f0f3f8} display: flex; align-items: center; gap: 0.75rem;
.report-status-pill{background:rgba(34,197,94,.15);border:1px solid rgba(34,197,94,.4);color:#4ade80;font-size:.6875rem;font-weight:700;padding:4px 10px;border-radius:999px} }
.report-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px} .alert-icon { width: 20px; height: 20px; flex-shrink: 0; }
.report-metric-box{background:#080b10;border:1px solid #1a2230;border-radius:4px;padding:10px 12px;gap:4px}
.box-label{font-size:.625rem;color:#64748b;text-transform:uppercase} .results-header {
.box-val{font-size:.8125rem;font-weight:700;color:#f0f3f8} display: flex; justify-content: space-between; align-items: center;
.box-val.highlight-mail{color:#e2b94a} border-bottom: 1px solid #1e293b; padding-bottom: 1rem; margin-bottom: 1.25rem; flex-wrap: wrap; gap: 0.75rem;
.box-val.highlight-time{color:#38bdf8} }
.box-val.success{color:#4ade80} .results-title-group { display: flex; align-items: center; gap: 0.6rem; }
.report-footer{display:flex;justify-content:flex-end} .live-dot { width: 8px; height: 8px; border-radius: 50%; background: #4ade80; box-shadow: 0 0 8px #4ade80; }
.download-report-btn{gap:8px;background:0 0;border:1px solid rgba(197,155,39,.5);color:#c59b27;padding:8px 16px;border-radius:3px;font-size:.8125rem;font-weight:600;cursor:pointer} .results-header h2 { font-size: 1.15rem; font-weight: 600; color: #f8fafc; margin: 0; }
.download-report-btn:hover{background:rgba(197,155,39,.1);border-color:#c59b27} .header-badges { display: flex; align-items: center; gap: 0.6rem; }
.dl-icon{width:15px;height:15px} .status-pill.status-completed {
.arch-metrics-bar{padding:16px 12px 6px;border-top:1px solid #1a2230;flex-wrap:nowrap;gap:16px;width:100%;box-sizing:border-box} background: rgba(34, 197, 94, 0.15); color: #4ade80; border: 1px solid rgba(34, 197, 94, 0.4);
.metric-card{align-items:center;justify-content:center;gap:4px;text-align:center;flex:1} padding: 0.3rem 0.65rem; border-radius: var(--zen-radius-xs, 4px); font-size: 0.75rem;
.metric-label{font-size:.6875rem;color:#5e697d;text-transform:uppercase;letter-spacing:.04em} font-family: var(--font-mono, monospace); font-weight: 600;
.metric-val{font-size:.875rem;font-weight:700;color:#f0f3f8} }
.metric-val.highlight{color:#38bdf8} .reset-btn {
.metric-val.success{color:#4ade80} background: #1e293b; color: #94a3b8; border: 1px solid #334155; padding: 0.3rem 0.65rem;
.metric-divider{width:1px;height:28px;background:#1e293b;flex-shrink:0} border-radius: var(--zen-radius-xs, 4px); font-size: 0.75rem; cursor: pointer;
@keyframes pulseBeacon{0%,100%{opacity:.6;transform:scale(1)}50%{opacity:1;transform:scale(1.3)}} }
@media (max-width:1024px){ .reset-btn:hover { background: #334155; color: #f8fafc; }
.story-grid-flow{flex-direction:column}
.story-arrow{transform:rotate(90deg);margin:-4px 0} .metrics-grid {
.report-grid{grid-template-columns:repeat(2,1fr)} display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
.diagram-canvas{grid-template-columns:1fr;gap:10px} gap: 1rem; margin-bottom: 1.5rem;
.flow-connector{height:16px;width:100%;transform:rotate(90deg)} }
.metric-divider{display:none} .metric-card {
.arch-metrics-bar{flex-direction:column;gap:12px} background: #1e293b; border-radius: var(--zen-radius-xs, 4px); padding: 1.1rem;
} text-align: center; border: 1px solid #2d3748;
}
.metric-title { display: block; font-size: 0.75rem; color: #94a3b8; margin-bottom: 0.5rem; text-transform: uppercase; }
.verdict-tag {
display: inline-block; padding: 0.35rem 0.85rem; border-radius: var(--zen-radius-xs, 4px);
font-weight: 700; font-size: 0.85rem; font-family: var(--font-mono, monospace);
}
.verdict-tag.critical_threat { background: #991b1b; color: #fee2e2; border: 1px solid #b91c1c; }
.verdict-tag.attention_required { background: #854d0e; color: #fef08a; border: 1px solid #a16207; }
.verdict-tag.safe { background: #166534; color: #dcfce7; border: 1px solid #15803d; }
.risk-gauge { font-size: 1.6rem; font-weight: 800; font-family: var(--font-mono, monospace); }
.risk-gauge.high-risk { color: #f87171; }
.risk-gauge.med-risk { color: #facc15; }
.risk-gauge.safe-risk { color: #4ade80; }
.gauge-max { font-size: 0.9rem; color: #64748b; font-weight: 400; }
.metric-value.highlight { font-size: 1.6rem; font-weight: 700; color: #38bdf8; font-family: var(--font-mono, monospace); }
.containment-value { font-size: 0.88rem; font-weight: 600; color: #94a3b8; }
.containment-value.contained { color: #4ade80; }
.mitre-section { margin-bottom: 1.5rem; }
.mitre-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.6rem; }
.mitre-label { font-size: 0.85rem; color: #cbd5e1; font-weight: 600; }
.mitre-count { font-size: 0.75rem; color: #94a3b8; font-family: var(--font-mono, monospace); }
.mitre-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.chip {
background: #1e293b; border: 1px solid #334155; color: #cbd5e1;
padding: 0.35rem 0.75rem; border-radius: var(--zen-radius-xs, 4px);
font-family: var(--font-mono, monospace); font-size: 0.82rem; display: flex; align-items: center; gap: 0.35rem;
}
.chip-dot { width: 6px; height: 6px; border-radius: 50%; background: #38bdf8; }
.forensic-box {
background: #090d16; border: 1px dashed #38bdf8; border-radius: var(--zen-radius-xs, 4px);
padding: 1.25rem; margin-top: 1rem;
}
.forensic-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.85rem; flex-wrap: wrap; gap: 0.5rem; }
.forensic-title { font-size: 0.88rem; font-weight: 600; color: #38bdf8; }
.timestamp-tag { font-size: 0.72rem; color: #64748b; font-family: var(--font-mono, monospace); }
.forensic-body p { margin: 0.4rem 0; font-size: 0.82rem; color: #cbd5e1; }
.forensic-body code {
font-family: var(--font-mono, monospace); font-size: 0.82rem;
background: rgba(15, 23, 42, 0.8); padding: 0.15rem 0.4rem; border-radius: 3px; color: #93c5fd;
}
.hash-code { color: #4ade80 !important; word-break: break-all; }
.action-buttons { margin-top: 1rem; }
.report-btn {
display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.65rem 1.25rem;
background: #0284c7; color: #ffffff; text-decoration: none; border-radius: var(--zen-radius-xs, 4px);
font-size: 0.85rem; font-weight: 500; transition: background 0.15s;
}
.report-btn:hover { background: #0369a1; }
.report-icon { width: 16px; height: 16px; }
.external-arrow { font-size: 0.95rem; }

View File

@ -1,367 +1,222 @@
<section class="zen-sim-container" aria-label="Simulador de Respuesta a Incidentes Cloud"> <div class="soc-dashboard" aria-label="Autonomous Cyber-Defense SOC Dashboard">
<!-- Cabecera de Sección con Selector de Modo y Status --> <!-- Encabezado Táctico del SOC -->
<header class="sim-section-header"> <header class="soc-header">
<div class="header-left"> <div class="logo-area">
<div class="title-with-badge"> <div class="shield-badge">
<span class="pulse-indicator" aria-hidden="true"></span> <svg class="shield-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<h2 class="section-title">{{ i18n.t().navSimulator }}</h2> <path d="M12 2L3 7v6c0 5.5 3.8 10.7 9 12 5.2-1.3 9-6.5 9-12V7l-9-5z"/>
<path d="M12 8v4m0 4h.01" stroke-linecap="round"/>
</svg>
</div> </div>
<span class="section-tag">{{ i18n.t().simTag }}</span> <div>
</div> <h1 class="soc-title">Autonomous Cyber-Defense &amp; Purple Teaming</h1>
<p class="soc-subtitle">Triage Cognitivo DeepSeek // Orquestador n8n // Evidencia Criptográfica Gotenberg</p>
<div class="header-controls">
<!-- Selector de Modo de Vista (Humana vs Técnica) -->
<div class="view-mode-toggle" role="group" aria-label="Modo de visualización">
<button
type="button"
class="mode-btn"
[class.active]="viewMode() === 'story'"
(click)="setViewMode('story')"
>
<svg class="mode-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
</svg>
<span>{{ i18n.t().simModeStory }}</span>
</button>
<button
type="button"
class="mode-btn"
[class.active]="viewMode() === 'tech'"
(click)="setViewMode('tech')"
>
<svg class="mode-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"/>
<line x1="12" y1="19" x2="20" y2="19"/>
</svg>
<span>{{ i18n.t().simModeTech }}</span>
</button>
</div>
<!-- Status Pill -->
<div class="status-pill" [class.danger]="stage() === 'breach' || stage() === 'detection'" [class.contained]="isContained()" [class.active]="stage() === 'remediation' || stage() === 'notification'">
<span class="status-beacon"></span>
<span class="status-text">
@if (stage() === 'idle') {
{{ i18n.t().simStatusIdle }}
} @else if (isContained()) {
{{ i18n.t().simStatusContained }}
} @else {
{{ i18n.t().simStatusActive }}
}
</span>
</div> </div>
</div> </div>
<span class="engine-badge">Engine: n8n + DeepSeek + Gotenberg</span>
</header> </header>
<!-- Panel Principal del Simulador --> <!-- Panel de Configuración de la Simulación -->
<div class="zen-sim-board"> <section class="control-panel">
<!-- Barra Interactiva de Captura de Correo y Ejecución --> <div class="panel-header">
<div class="email-control-deck"> <span class="panel-tag">SECOPS // OPERATIONS DISPATCHER</span>
<div class="email-input-wrapper"> <h2 class="panel-title">Lanzador de Operaciones de Ciberdefensa</h2>
<label for="sim-email-input" class="email-label"> </div>
<svg class="email-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/> <div class="form-grid">
<polyline points="22,6 12,13 2,6"/> <!-- Modo de Operación -->
</svg> <div class="form-group">
<span>{{ i18n.t().simEmailLabel }}</span> <label for="operation-mode">Modo de Operación:</label>
</label> <select
id="operation-mode"
[ngModel]="mode()"
(ngModelChange)="mode.set($event)"
[disabled]="isLoading()"
>
<option value="PURPLE_TEAM">PURPLE TEAM (Adversary Emulation + Defense)</option>
<option value="EASM">EASM (Superficie de Ataque Externa)</option>
<option value="SOAR">SOAR (Enriquecimiento e Inteligencia)</option>
</select>
</div>
<!-- Objetivo -->
<div class="form-group">
<label for="operation-target">Objetivo (Target / FQDN / IP):</label>
<input <input
id="sim-email-input" id="operation-target"
type="text"
[ngModel]="target()"
(ngModelChange)="target.set($event)"
placeholder="local_sandbox o IP/Dominio"
[disabled]="isLoading()"
/>
<div class="target-presets">
<span class="preset-label">Presets:</span>
<button type="button" class="preset-btn" (click)="setTargetPreset('local_sandbox')">local_sandbox</button>
<button type="button" class="preset-btn" (click)="setTargetPreset('gateway.perimeter.internal')">gateway.perimeter</button>
<button type="button" class="preset-btn" (click)="setTargetPreset('api.bryan-bano.com')">api.bryan-bano.com</button>
</div>
</div>
<!-- Auditor / Solicitante -->
<div class="form-group">
<label for="operation-auditor">Auditor / Solicitante:</label>
<input
id="operation-auditor"
type="email" type="email"
class="email-field" [ngModel]="requestedBy()"
[value]="userEmail()" (ngModelChange)="requestedBy.set($event)"
(input)="onEmailInput($event)" placeholder="auditor@empresa.com"
[placeholder]="i18n.t().simEmailPlaceholder" [disabled]="isLoading()"
[disabled]="isSimulating()"
/> />
</div> </div>
<div class="action-buttons"> <!-- Aislamiento Zero-Trust -->
<button <div class="form-group checkbox-group">
type="button" <label class="checkbox-label" for="operation-isolate">
class="sim-trigger-btn" <input
[disabled]="isSimulating()" id="operation-isolate"
(click)="startSimulation()" type="checkbox"
[attr.aria-label]="i18n.t().simBtnSimulate" [ngModel]="autoIsolate()"
> (ngModelChange)="autoIsolate.set($event)"
<span class="btn-glow"></span> [disabled]="isLoading()"
<span class="btn-icon">⚡</span> />
<span class="btn-text"> <span class="checkbox-text">Aislamiento y Contención Automática (Zero-Trust)</span>
@if (isSimulating()) { </label>
{{ i18n.t().simBtnSimulating }}
} @else {
{{ i18n.t().simBtnSimulate }}
}
</span>
</button>
<button
type="button"
class="sim-reset-btn"
[disabled]="isSimulating()"
(click)="resetSimulation()"
[attr.aria-label]="i18n.t().simBtnReset"
>
<span>↺ {{ i18n.t().simBtnReset }}</span>
</button>
</div> </div>
</div> </div>
<!-- VISTA 1: MODO STORY / EXPLICADO PARA TODO PÚBLICO --> <!-- Botón de Lanzamiento -->
@if (viewMode() === 'story') { <button
<div class="story-grid-flow" aria-live="polite"> type="button"
<!-- PASO 1 --> class="launch-btn"
<div class="story-step-card" [class.active]="stage() === 'breach'" [class.completed]="stage() !== 'idle' && stage() !== 'breach'"> (click)="executeSimulation()"
<div class="step-badge">01 // ATAQUE</div> [disabled]="isLoading()"
<div class="step-icon-wrap danger"> >
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"> @if (!isLoading()) {
<path d="M12 2L3 7v6c0 5.5 3.8 10.7 9 12 5.2-1.3 9-6.5 9-12V7l-9-5z"/> <svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 8v4m0 4h.01" stroke-linecap="round"/> <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
</svg> </svg>
<span>Iniciar Simulación de Ciberdefensa</span>
} @else {
<span class="spinner-dot"></span>
<span>Ejecutando Ciclo Cerrado (Triage Cognitivo DeepSeek)...</span>
}
</button>
</section>
<!-- Estado de Alerta de Error -->
@if (errorMessage(); as error) {
<div class="alert-box error" role="alert">
<svg class="alert-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
<div class="alert-content">
<strong>Error en Operación:</strong> {{ error }}
</div>
</div>
}
<!-- Dashboard de Resultados en Tiempo Real -->
@if (result(); as res) {
<section class="results-dashboard" aria-live="polite">
<div class="results-header">
<div class="results-title-group">
<span class="live-dot"></span>
<h2>Telemetría Forense Consolidada</h2>
</div>
<div class="header-badges">
<span class="status-pill status-completed">COMPLETADO (200 OK)</span>
<button type="button" class="reset-btn" (click)="resetSimulation()" title="Nueva Simulación">
Nueva Consulta
</button>
</div>
</div>
<div class="metrics-grid">
<!-- Veredicto Cognitivo -->
<div class="metric-card">
<span class="metric-title">Veredicto DeepSeek</span>
<div class="verdict-tag" [ngClass]="res.summary.verdict.toLowerCase()">
{{ res.summary.verdict }}
</div> </div>
<h3 class="step-title">{{ i18n.t().simStep1Title }}</h3>
<p class="step-desc">{{ i18n.t().simStep1Desc }}</p>
<div class="step-meta">IP: {{ threatIp() }}</div>
</div> </div>
<div class="story-arrow" [class.active]="stage() !== 'idle'">→</div> <!-- Risk Score -->
<div class="metric-card">
<!-- PASO 2 --> <span class="metric-title">Risk Score</span>
<div class="story-step-card" [class.active]="stage() === 'detection'" [class.completed]="stage() === 'remediation' || stage() === 'notification' || isContained()"> <div
<div class="step-badge">02 // DETECCIÓN</div> class="risk-gauge"
<div class="step-icon-wrap warning"> [ngClass]="res.summary.risk_score > 70 ? 'high-risk' : (res.summary.risk_score > 30 ? 'med-risk' : 'safe-risk')"
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"> >
<circle cx="12" cy="12" r="10"/> {{ res.summary.risk_score }} <span class="gauge-max">/ 100</span>
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
</div> </div>
<h3 class="step-title">{{ i18n.t().simStep2Title }}</h3>
<p class="step-desc">{{ i18n.t().simStep2Desc }}</p>
<div class="step-meta">&lt; 50ms // Monitoreo Activo</div>
</div> </div>
<div class="story-arrow" [class.active]="stage() === 'detection' || stage() === 'remediation' || stage() === 'notification' || isContained()">→</div> <!-- Latencia de Mitigación -->
<div class="metric-card">
<!-- PASO 3 --> <span class="metric-title">Latencia de Mitigación (&Delta;t)</span>
<div class="story-step-card" [class.active]="stage() === 'remediation'" [class.completed]="stage() === 'notification' || isContained()"> <div class="metric-value highlight">{{ res.summary.mitigation_time_ms }} ms</div>
<div class="step-badge">03 // BLOQUEO</div>
<div class="step-icon-wrap action">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
</div>
<h3 class="step-title">{{ i18n.t().simStep3Title }}</h3>
<p class="step-desc">{{ i18n.t().simStep3Desc }}</p>
<div class="step-meta">Revocación Inmediata STS</div>
</div> </div>
<div class="story-arrow" [class.active]="stage() === 'remediation' || stage() === 'notification' || isContained()">→</div> <!-- Contención Docker / Red -->
<div class="metric-card">
<span class="metric-title">Contención de Red</span>
<div class="metric-value containment-value" [class.contained]="res.summary.containment_executed">
{{ res.summary.containment_executed ? 'Aislado (Host Bridge Detached)' : 'Monitoreo Activo' }}
</div>
</div>
</div>
<!-- PASO 4 --> <!-- Técnicas MITRE ATT&CK -->
<div class="story-step-card" [class.active]="stage() === 'notification'" [class.completed]="isContained()"> <div class="mitre-section">
<div class="step-badge">04 // INFORME</div> <div class="mitre-header">
<div class="step-icon-wrap success"> <span class="mitre-label">Técnicas MITRE ATT&CK v14 Correlacionadas</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"> <span class="mitre-count">{{ res.summary.mitre_techniques.length }} detectadas</span>
</div>
<div class="mitre-chips">
@for (tech of res.summary.mitre_techniques; track tech) {
<span class="chip">
<span class="chip-dot"></span>
{{ tech }}
</span>
}
</div>
</div>
<!-- Evidencia Forense & Hash Criptográfico -->
<div class="forensic-box">
<div class="forensic-header">
<span class="forensic-title">Evidencia Forense Criptográfica (Principio de No Repudio)</span>
<span class="timestamp-tag">{{ res.timestamp }}</span>
</div>
<div class="forensic-body">
<p><strong>Simulation UUID:</strong> <code>{{ res.simulation_id }}</code></p>
<p><strong>Execution ID:</strong> <code>{{ res.execution_id }}</code></p>
<p><strong>Firma SHA-256:</strong> <code class="hash-code">{{ res.evidence.sha256_hash }}</code></p>
</div>
<div class="action-buttons">
<a
[href]="res.evidence.report_url"
target="_blank"
rel="noopener noreferrer"
class="report-btn"
>
<svg class="report-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/> <polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/> <line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/> <line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/> <polyline points="10 9 9 9 8 9"/>
</svg> </svg>
</div> <span>Ver Reporte Forense en Navegador</span>
<h3 class="step-title">{{ i18n.t().simStep4Title }}</h3> <span class="external-arrow">↗</span>
<p class="step-desc">{{ i18n.t().simStep4Desc }}</p> </a>
<div class="step-meta">{{ targetRecipient() }}</div>
</div> </div>
</div> </div>
} </section>
}
<!-- VISTA 2: MODO TÉCNICO SECOPS / AWS ARQUITECTURA --> </div>
@if (viewMode() === 'tech') {
<div class="diagram-canvas">
<!-- NODO 1: Vector de Amenaza -->
<div class="arch-node threat-node" [class.active]="stage() === 'breach'" [class.past]="stage() !== 'idle' && stage() !== 'breach'">
<div class="node-badge-tag threat">VECTOR // 01</div>
<div class="node-icon-frame">
<svg class="node-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M12 2L3 7v6c0 5.5 3.8 10.7 9 12 5.2-1.3 9-6.5 9-12V7l-9-5z"/>
<path d="M12 8v4m0 4h.01" stroke-linecap="round"/>
</svg>
</div>
<div class="node-info">
<h3 class="node-title">{{ i18n.t().simThreatNodeTitle }}</h3>
<span class="node-detail">{{ i18n.t().simThreatNodeSub }}</span>
<span class="node-meta-ip">IP: {{ threatIp() }}</span>
</div>
<div class="node-status-indicator" [class.ping]="stage() === 'breach'"></div>
</div>
<!-- CONECTOR 1 -> 2 -->
<div class="flow-connector" [class.flowing]="stage() !== 'idle'">
<div class="beam-line"></div>
<span class="flow-arrow">►</span>
</div>
<!-- NODO 2: AWS CloudTrail & EventBridge -->
<div class="arch-node detection-node" [class.active]="stage() === 'detection'" [class.past]="stage() === 'remediation' || stage() === 'notification' || isContained()">
<div class="node-badge-tag detection">AWS // 02</div>
<div class="node-icon-frame">
<svg class="node-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
</svg>
</div>
<div class="node-info">
<h3 class="node-title">{{ i18n.t().simCloudTrailNodeTitle }}</h3>
<span class="node-detail">{{ i18n.t().simCloudTrailNodeSub }}</span>
<span class="node-meta-svc">us-east-1 // EventBridge Rule</span>
</div>
<div class="node-status-indicator" [class.ping]="stage() === 'detection'"></div>
</div>
<!-- CONECTOR 2 -> 3 -->
<div class="flow-connector" [class.flowing]="stage() === 'detection' || stage() === 'remediation' || stage() === 'notification' || isContained()">
<div class="beam-line"></div>
<span class="flow-arrow">►</span>
</div>
<!-- NODO 3: AWS Lambda -->
<div class="arch-node remediation-node" [class.active]="stage() === 'remediation'" [class.past]="stage() === 'notification' || isContained()">
<div class="node-badge-tag remediation">SERVERLESS // 03</div>
<div class="node-icon-frame">
<svg class="node-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
</svg>
</div>
<div class="node-info">
<h3 class="node-title">{{ i18n.t().simLambdaNodeTitle }}</h3>
<span class="node-detail">{{ i18n.t().simLambdaNodeSub }}</span>
<span class="node-meta-svc">STS Invalidation & DenyPolicy</span>
</div>
<div class="node-status-indicator" [class.ping]="stage() === 'remediation'"></div>
</div>
<!-- CONECTOR 3 -> 4 -->
<div class="flow-connector" [class.flowing]="stage() === 'remediation' || stage() === 'notification' || isContained()">
<div class="beam-line"></div>
<span class="flow-arrow">►</span>
</div>
<!-- NODO 4: Amazon SES -->
<div class="arch-node notification-node" [class.active]="stage() === 'notification'" [class.past]="isContained()">
<div class="node-badge-tag notification">SECOPS // 04</div>
<div class="node-icon-frame">
<svg class="node-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
</div>
<div class="node-info">
<h3 class="node-title">{{ i18n.t().simSesNodeTitle }}</h3>
<span class="node-detail">{{ i18n.t().simSesNodeSub }}</span>
<span class="node-meta-svc">{{ targetRecipient() }}</span>
</div>
<div class="node-status-indicator" [class.ping]="stage() === 'notification'"></div>
</div>
</div>
<!-- Consola de Telemetría SecOps -->
<div class="secops-console-frame">
<header class="console-titlebar">
<div class="console-dots">
<span class="dot red"></span>
<span class="dot yellow"></span>
<span class="dot green"></span>
</div>
<span class="console-title">{{ i18n.t().simConsoleTitle }}</span>
<span class="console-pill">JSON LOGSTREAM</span>
</header>
<div class="console-body" role="log" aria-live="polite">
@for (log of logs(); track $index) {
<div class="log-row" [class.warn]="log.level === 'WARN'" [class.action]="log.level === 'ACTION'" [class.success]="log.level === 'SUCCESS'">
<span class="log-time">[{{ log.timestamp }}]</span>
<span class="log-tag">[{{ log.tag }}]</span>
<span class="log-msg">{{ log.message }}</span>
</div>
}
<div class="console-cursor-row">
<span class="console-prompt">secops@aws-perimeter:~$</span>
<span class="cursor-block"></span>
</div>
</div>
</div>
}
<!-- CERTIFICADO DE AUDITORÍA FORENSE (Visible al contener la amenaza) -->
@if (showReport() || isContained()) {
<div class="forensic-report-card">
<header class="report-header">
<div class="report-header-left">
<span class="report-badge">{{ i18n.t().simReportDemoBadge }}</span>
<h3 class="report-title">{{ i18n.t().simReportTitle }}</h3>
</div>
<span class="report-status-pill">{{ i18n.t().simReportStatusSecure }}</span>
</header>
<div class="report-grid">
<div class="report-metric-box">
<span class="box-label">{{ i18n.t().simReportTargetEmail }}</span>
<span class="box-val highlight-mail">{{ targetRecipient() }}</span>
</div>
<div class="report-metric-box">
<span class="box-label">{{ i18n.t().simMetricLatency }}</span>
<span class="box-val highlight-time">{{ i18n.t().simReportTimeVsAvg }}</span>
</div>
<div class="report-metric-box">
<span class="box-label">Integridad de Credenciales</span>
<span class="box-val">{{ i18n.t().simReportCredentialsCompromised }}</span>
</div>
<div class="report-metric-box">
<span class="box-label">Impacto Financiero Evitado</span>
<span class="box-val success">{{ i18n.t().simReportFinancialLoss }}</span>
</div>
</div>
<footer class="report-footer">
<button type="button" class="download-report-btn" (click)="simulateDownload()">
<svg class="dl-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
@if (downloadFeedback()) {
<span>✓ Reporte Forense Simulado con Éxito</span>
} @else {
<span>{{ i18n.t().simReportDownloadBtn }}</span>
}
</button>
</footer>
</div>
}
<!-- Métricas de Arquitectura Serverless -->
<footer class="arch-metrics-bar">
<div class="metric-card">
<span class="metric-label">{{ i18n.t().simMetricLatency }}</span>
<span class="metric-val highlight">&lt; {{ executionTimeMs() }} ms</span>
</div>
<div class="metric-divider"></div>
<div class="metric-card">
<span class="metric-label">{{ i18n.t().simMetricIntervention }}</span>
<span class="metric-val success">0.0 s (100% Auto)</span>
</div>
<div class="metric-divider"></div>
<div class="metric-card">
<span class="metric-label">{{ i18n.t().simMetricArchitecture }}</span>
<span class="metric-val">EventBridge + Lambda + SES</span>
</div>
</footer>
</div>
</section>

View File

@ -1,188 +1,83 @@
import { Component, ChangeDetectionStrategy, signal, computed, inject, OnDestroy, OnInit } from '@angular/core'; import { Component, ChangeDetectionStrategy, signal, inject } from '@angular/core';
import { I18nService } from '../../../application/services/i18n.service'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { SecurityAgentService } from '../../../services/security-agent.service';
import { RetroAudioService } from '../../../application/services/retro-audio.service'; import { RetroAudioService } from '../../../application/services/retro-audio.service';
import {
export type SimulationStage = 'idle' | 'breach' | 'detection' | 'remediation' | 'notification' | 'contained'; SimulationMode,
export type SimulatorViewMode = 'story' | 'tech'; SecurityAgentRequest,
SecurityAgentResponse
export interface SecOpsLog { } from '../../../domain/models/security.model';
readonly timestamp: string;
readonly level: 'WARN' | 'INFO' | 'ACTION' | 'SUCCESS';
readonly tag: string;
readonly message: string;
}
@Component({ @Component({
selector: 'app-incident-simulator', selector: 'app-incident-simulator, app-cyber-defense',
standalone: true, standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './incident-simulator.component.html', templateUrl: './incident-simulator.component.html',
styleUrl: './incident-simulator.component.css', styleUrl: './incident-simulator.component.css',
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class IncidentSimulatorComponent implements OnInit, OnDestroy { export class IncidentSimulatorComponent {
protected readonly i18n = inject(I18nService); private readonly securityService = inject(SecurityAgentService);
private readonly audio = inject(RetroAudioService); private readonly audio = inject(RetroAudioService);
protected readonly stage = signal<SimulationStage>('idle'); // Parámetros reactivos de configuración
protected readonly viewMode = signal<SimulatorViewMode>('story'); readonly mode = signal<SimulationMode>('PURPLE_TEAM');
protected readonly userEmail = signal<string>(''); readonly target = signal<string>('local_sandbox');
protected readonly logs = signal<SecOpsLog[]>([]); readonly requestedBy = signal<string>('auditor@bryan-bano.com');
protected readonly threatIp = signal<string>('198.51.100.42'); readonly autoIsolate = signal<boolean>(true);
protected readonly executionTimeMs = signal<number>(184);
protected readonly showReport = signal<boolean>(false);
protected readonly downloadFeedback = signal<boolean>(false);
protected readonly isSimulating = computed<boolean>(() => { // Estados de ejecución y telemetría
const s = this.stage(); readonly isLoading = signal<boolean>(false);
return s === 'breach' || s === 'detection' || s === 'remediation' || s === 'notification'; readonly errorMessage = signal<string | null>(null);
}); readonly result = signal<SecurityAgentResponse | null>(null);
protected readonly isContained = computed<boolean>(() => this.stage() === 'contained'); setTargetPreset(value: string): void {
protected readonly targetRecipient = computed<string>(() => {
const email = this.userEmail().trim();
return email.length > 3 && email.includes('@') ? email : 'secops-team@bryan.dev';
});
private timeoutIds: ReturnType<typeof setTimeout>[] = [];
ngOnInit(): void {
this.initDefaultLogs();
}
setViewMode(mode: SimulatorViewMode): void {
if (this.viewMode() === mode) return;
this.audio.playClick(); this.audio.playClick();
this.viewMode.set(mode); this.target.set(value);
} }
onEmailInput(event: Event): void { executeSimulation(): void {
const val = (event.target as HTMLInputElement).value; if (this.isLoading()) return;
this.userEmail.set(val);
}
startSimulation(): void {
if (this.isSimulating()) return;
this.clearTimeouts();
this.audio.playClick(); this.audio.playClick();
this.logs.set([]); this.isLoading.set(true);
this.showReport.set(false); this.errorMessage.set(null);
this.downloadFeedback.set(false); this.result.set(null);
// Paso 1: Intento de Intrusión (Vector de Amenaza) const payload: SecurityAgentRequest = {
this.stage.set('breach'); mode: this.mode(),
this.appendLog({ target: this.target().trim() || 'local_sandbox',
level: 'WARN', requested_by: this.requestedBy().trim() || 'auditor@bryan-bano.com',
tag: 'IAM-ALERT', parameters: {
message: `Anomalous API request: iam:CreateAccessKey from untrusted IP ${this.threatIp()} for principal "svc-deployer".` auto_isolate: this.autoIsolate(),
scan_depth: 'full'
}
};
this.securityService.triggerSimulation(payload).subscribe({
next: (response) => {
this.result.set(response);
this.isLoading.set(false);
this.audio.playVictory();
},
error: (err: Error) => {
this.errorMessage.set(err.message || 'Error al conectar con el orquestador n8n.');
this.isLoading.set(false);
this.audio.playGameOver();
}
}); });
// Paso 2: Detección Inmediata en EventBridge & CloudTrail (después de 700ms)
const t1 = setTimeout(() => {
this.stage.set('detection');
this.audio.playCellReveal();
this.appendLog({
level: 'INFO',
tag: 'CLOUDTRAIL',
message: 'Management event ingested in us-east-1. EventBridge rule "SecOps-AutoRemediate" triggered synchronously.'
});
}, 700);
this.timeoutIds.push(t1);
// Paso 3: Remediación Automatizada Serverless (después de 1500ms)
const t2 = setTimeout(() => {
this.stage.set('remediation');
this.audio.playFlagToggle();
this.appendLog({
level: 'ACTION',
tag: 'LAMBDA',
message: 'Executing fn-iam-incident-response: Revoking active STS sessions & attaching inline DenyAllSecurityPolicy.'
});
}, 1500);
this.timeoutIds.push(t2);
// Paso 4: Despacho de Reporte Forense (después de 2300ms)
const t3 = setTimeout(() => {
this.stage.set('notification');
this.audio.playCellReveal();
this.appendLog({
level: 'INFO',
tag: 'SES-ALERT',
message: `Dispatching cryptographic audit report to ${this.targetRecipient()} via Amazon SES.`
});
}, 2300);
this.timeoutIds.push(t3);
// Paso 5: Amenaza Contenida & Generación de Reporte (después de 3100ms)
const t4 = setTimeout(() => {
this.stage.set('contained');
this.audio.playVictory();
this.showReport.set(true);
this.appendLog({
level: 'SUCCESS',
tag: 'CONTAINED',
message: `Threat neutralized successfully in ${this.executionTimeMs()}ms. Zero credentials compromised. State: SECURE.`
});
}, 3100);
this.timeoutIds.push(t4);
} }
resetSimulation(): void { resetSimulation(): void {
this.clearTimeouts();
this.audio.playClick(); this.audio.playClick();
this.stage.set('idle'); this.errorMessage.set(null);
this.showReport.set(false); this.result.set(null);
this.downloadFeedback.set(false); this.isLoading.set(false);
this.initDefaultLogs();
}
simulateDownload(): void {
this.audio.playClick();
this.downloadFeedback.set(true);
setTimeout(() => {
this.downloadFeedback.set(false);
}, 3000);
}
private initDefaultLogs(): void {
const time = this.getCurrentTimestamp();
this.logs.set([
{
timestamp: time,
level: 'INFO',
tag: 'GUARDDUTY',
message: 'Cloud perimeter telemetry nominal. 0 active security findings in us-east-1.'
},
{
timestamp: time,
level: 'INFO',
tag: 'EVENTBRIDGE',
message: 'Active rule pattern matched: Rule-SecOps-IAM-AutoRemediation [ENABLED].'
}
]);
}
private appendLog(entry: Omit<SecOpsLog, 'timestamp'>): void {
const timestamp = this.getCurrentTimestamp();
this.logs.update(prev => [...prev, { ...entry, timestamp }]);
}
private getCurrentTimestamp(): string {
const now = new Date();
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
const ms = String(now.getMilliseconds()).padStart(3, '0');
return `${h}:${m}:${s}.${ms}`;
}
private clearTimeouts(): void {
this.timeoutIds.forEach(id => clearTimeout(id));
this.timeoutIds = [];
}
ngOnDestroy(): void {
this.clearTimeouts();
} }
} }
export {
IncidentSimulatorComponent as CyberDefenseComponent,
IncidentSimulatorComponent as CyberDefenseDashboardComponent
};