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 { ChatPort } from './domain/ports/chat.port';
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 = {
providers: [
@ -13,6 +15,7 @@ export const appConfig: ApplicationConfig = {
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(),
{ 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%}
.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}
.sim-section-header,.report-header,.console-titlebar,.arch-metrics-bar{display:flex;align-items:center;justify-content:space-between}
.sim-section-header{width:100%;padding-bottom:8px;border-bottom:1px solid #1e2533;margin-bottom:4px;flex-wrap:wrap;gap:12px}
.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}
.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}
.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)}
.section-tag{font-size:.6875rem;color:#5e697d;letter-spacing:.08em}
.pulse-indicator{width:10px;height:10px;border-radius:50%;background:#c59b27;box-shadow:0 0 12px #c59b27;flex-shrink:0}
.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}
.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}
.mode-btn.active{background:#131720;color:#c59b27;box-shadow:0 1px 4px rgba(0,0,0,.4)}
.mode-icon{width:13px;height:13px}
.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}
.status-beacon{width:8px;height:8px;border-radius:50%;background:#38bdf8;box-shadow:0 0 8px #38bdf8;animation:pulseBeacon 2s infinite ease-in-out}
.status-pill.danger .status-beacon,.dot.red{background:#ef4444}
.status-pill.active .status-beacon,.dot.yellow{background:#eab308}
.status-pill.contained .status-beacon,.dot.green{background:#22c55e}
.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}
.email-label{gap:6px;font-size:.78rem;font-weight:600;color:#9aa5b8}
.email-icon{width:14px;height:14px;color:#c59b27}
.email-field{background:#07090e;border:1px solid #1e2533;border-radius:3px;color:#f0f3f8;font-size:.8125rem;padding:8px 12px;outline:0}
.email-field:focus{border-color:#c59b27;box-shadow:0 0 6px rgba(197,155,39,.25)}
.action-buttons{gap:10px}
.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}
.sim-trigger-btn:hover:not(:disabled){background:#d4af37;box-shadow:0 0 12px rgba(197,155,39,.4)}
.sim-trigger-btn:disabled{opacity:.6;cursor:not-allowed}
.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%}
.story-step-card{flex:1;background:#11151f;border:1px solid #1e293b;border-radius:4px;padding:16px 14px;gap:8px;transition:all .3s}
.step-badge,.node-badge-tag{font-size:.625rem;font-weight:700;letter-spacing:.08em}
.step-badge{color:#64748b}
.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}
.step-icon-wrap svg{width:20px;height:20px}
.node-badge-tag.threat,.step-icon-wrap.danger{color:#f87171;background:rgba(239,68,68,.1)}
.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)}
.step-title,.node-title{font-size:.8125rem;font-weight:700;color:#f0f3f8;margin:0}
.step-title{font-size:.875rem}
.step-desc{font-size:.75rem;color:#9aa5b8;line-height:1.45;margin:0;flex-grow:1}
.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)}
.story-arrow{display:flex;align-items:center;justify-content:center;color:#2d3748;font-size:1.1rem;font-weight:700;padding:0 4px}
.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%}
.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}
.node-badge-tag{padding:2px 6px;border-radius:2px;width:fit-content}
.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}
.node-info{gap:2px}
.node-detail,.node-meta-ip,.node-meta-svc{font-size:.6875rem;color:#9aa5b8}
.node-meta-ip,.node-meta-svc{font-size:.625rem;color:#64748b}
.node-status-indicator{position:absolute;top:8px;right:8px;width:6px;height:6px;border-radius:50%;background:#334155}
.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}
.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}
.remediation-node.active{border-color:#22c55e;background:#0f2316}
.remediation-node.active .node-icon-frame,.remediation-node.active .node-status-indicator.ping{color:#22c55e;background:#22c55e}
.notification-node.active{border-color:#c59b27;background:#231c0e}
.notification-node.active .node-icon-frame,.notification-node.active .node-status-indicator.ping{color:#c59b27;background:#c59b27}
.arch-node.past .node-status-indicator{background:#22c55e}
.flow-connector{display:flex;align-items:center;justify-content:center;position:relative;height:20px;color:#334155}
.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}
.flow-connector.flowing{color:#38bdf8}
.flow-connector.flowing .beam-line{background:linear-gradient(90deg,#ef4444,#38bdf8,#22c55e)}
.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}
.console-dots{display:flex;gap:5px}
.dot{width:7px;height:7px;border-radius:50%}
.console-title{font-size:.6875rem;color:#94a3b8}
.console-pill{font-size:.5625rem;color:#38bdf8;background:rgba(56,189,248,.12);padding:2px 5px;border-radius:2px}
.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}
.log-row{display:flex;gap:8px;color:#94a3b8}
.log-time{color:#475569;flex-shrink:0}
.log-tag{font-weight:700;flex-shrink:0}
.log-row.warn .log-msg,.log-row.warn .log-tag{color:#f87171}
.log-row.action .log-msg,.log-row.action .log-tag{color:#fde047}
.log-row.success .log-msg,.log-row.success .log-tag{color:#4ade80}
.console-cursor-row{gap:6px;margin-top:3px}
.console-prompt{color:#34d399;font-size:.6875rem}
.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 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}
.report-header-left{gap:4px}
.report-badge{font-size:.625rem;color:#c59b27;letter-spacing:.1em;font-weight:700}
.report-title{margin:0;font-size:1.05rem;font-weight:800;color:#f0f3f8}
.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}
.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}
.box-val{font-size:.8125rem;font-weight:700;color:#f0f3f8}
.box-val.highlight-mail{color:#e2b94a}
.box-val.highlight-time{color:#38bdf8}
.box-val.success{color:#4ade80}
.report-footer{display:flex;justify-content:flex-end}
.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}
.download-report-btn:hover{background:rgba(197,155,39,.1);border-color:#c59b27}
.dl-icon{width:15px;height:15px}
.arch-metrics-bar{padding:16px 12px 6px;border-top:1px solid #1a2230;flex-wrap:nowrap;gap:16px;width:100%;box-sizing:border-box}
.metric-card{align-items:center;justify-content:center;gap:4px;text-align:center;flex:1}
.metric-label{font-size:.6875rem;color:#5e697d;text-transform:uppercase;letter-spacing:.04em}
.metric-val{font-size:.875rem;font-weight:700;color:#f0f3f8}
.metric-val.highlight{color:#38bdf8}
.metric-val.success{color:#4ade80}
.metric-divider{width:1px;height:28px;background:#1e293b;flex-shrink:0}
@keyframes pulseBeacon{0%,100%{opacity:.6;transform:scale(1)}50%{opacity:1;transform:scale(1.3)}}
@media (max-width:1024px){
.story-grid-flow{flex-direction:column}
.story-arrow{transform:rotate(90deg);margin:-4px 0}
.report-grid{grid-template-columns:repeat(2,1fr)}
.diagram-canvas{grid-template-columns:1fr;gap:10px}
.flow-connector{height:16px;width:100%;transform:rotate(90deg)}
.metric-divider{display:none}
.arch-metrics-bar{flex-direction:column;gap:12px}
}
:host {
display: block;
width: 100%;
color: var(--zen-text-primary, #f0f3f8);
font-family: var(--font-sans, 'Plus Jakarta Sans', -apple-system, sans-serif);
}
.soc-dashboard {
max-width: 1100px;
margin: 0 auto;
padding: 1.5rem 1rem;
}
.soc-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--zen-border-subtle, #1e293b);
padding-bottom: 1.25rem;
margin-bottom: 1.5rem;
gap: 1rem;
flex-wrap: wrap;
}
.logo-area { display: flex; align-items: center; gap: 0.85rem; }
.shield-badge {
display: flex; align-items: center; justify-content: center;
width: 44px; height: 44px;
background: rgba(56, 189, 248, 0.12);
border: 1px solid rgba(56, 189, 248, 0.3);
border-radius: var(--zen-radius-sm, 4px);
color: #38bdf8;
flex-shrink: 0;
}
.shield-icon { width: 22px; height: 22px; }
.soc-title { font-size: 1.35rem; font-weight: 700; color: #38bdf8; margin: 0; }
.soc-subtitle { font-size: 0.78rem; color: var(--zen-text-muted, #94a3b8); margin: 0.2rem 0 0; font-family: var(--font-mono, monospace); }
.engine-badge {
background: #111827; color: #94a3b8; padding: 0.35rem 0.75rem;
border-radius: var(--zen-radius-xs, 4px); font-size: 0.78rem; font-family: var(--font-mono, monospace);
border: 1px solid #1e293b;
}
.control-panel, .results-dashboard {
background: #111827; border: 1px solid #1f2937;
border-radius: var(--zen-radius-sm, 6px); padding: 1.5rem;
margin-bottom: 1.5rem; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
.panel-tag { display: inline-block; font-family: var(--font-mono, monospace); font-size: 0.72rem; color: #e2b94a; margin-bottom: 0.3rem; }
.panel-title { font-size: 1.15rem; font-weight: 600; color: #f8fafc; margin: 0 0 1.25rem; }
.form-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1.25rem; margin-bottom: 1.25rem;
}
.form-group label { display: block; font-size: 0.8rem; color: #94a3b8; margin-bottom: 0.4rem; font-weight: 500; }
.form-group input[type="text"], .form-group input[type="email"], .form-group select {
width: 100%; padding: 0.7rem 0.85rem; background: #0f172a; border: 1px solid #334155;
color: #f8fafc; border-radius: var(--zen-radius-xs, 4px); box-sizing: border-box;
font-family: var(--font-mono, monospace); font-size: 0.88rem; transition: border-color 0.15s;
}
.form-group input:focus, .form-group select:focus { outline: none; border-color: #38bdf8; }
.target-presets { display: flex; align-items: center; gap: 0.4rem; margin-top: 0.45rem; flex-wrap: wrap; }
.preset-label { font-size: 0.72rem; color: #64748b; font-family: var(--font-mono, monospace); }
.preset-btn {
background: #1e293b; border: 1px solid #334155; color: #93c5fd; font-size: 0.7rem;
font-family: var(--font-mono, monospace); padding: 0.15rem 0.45rem; border-radius: 3px; cursor: pointer;
}
.preset-btn:hover { background: #334155; color: #ffffff; }
.checkbox-group { display: flex; align-items: center; }
.checkbox-label { display: flex; align-items: center; gap: 0.6rem; cursor: pointer; font-size: 0.85rem; color: #cbd5e1; }
.checkbox-label input[type="checkbox"] { width: 18px; height: 18px; accent-color: #0284c7; cursor: pointer; }
.launch-btn {
width: 100%; padding: 0.85rem 1.25rem;
background: linear-gradient(135deg, #0284c7 0%, #2563eb 100%);
color: #ffffff; border: none; border-radius: var(--zen-radius-sm, 6px);
font-size: 0.95rem; font-weight: 600; cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 0.5rem;
transition: opacity 0.2s;
}
.launch-btn:hover:not(:disabled) { opacity: 0.92; }
.launch-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.btn-icon { width: 18px; height: 18px; }
.spinner-dot {
width: 10px; height: 10px; border-radius: 50%; background: #38bdf8;
animation: pulse-spin 1s infinite alternate;
}
@keyframes pulse-spin { 0% { transform: scale(0.8); opacity: 0.5; } 100% { transform: scale(1.3); opacity: 1; } }
.alert-box.error {
background: #450a0a; color: #fca5a5; border: 1px solid #7f1d1d;
padding: 1rem; border-radius: var(--zen-radius-sm, 4px); margin-bottom: 1.5rem;
display: flex; align-items: center; gap: 0.75rem;
}
.alert-icon { width: 20px; height: 20px; flex-shrink: 0; }
.results-header {
display: flex; justify-content: space-between; align-items: center;
border-bottom: 1px solid #1e293b; padding-bottom: 1rem; margin-bottom: 1.25rem; flex-wrap: wrap; gap: 0.75rem;
}
.results-title-group { display: flex; align-items: center; gap: 0.6rem; }
.live-dot { width: 8px; height: 8px; border-radius: 50%; background: #4ade80; box-shadow: 0 0 8px #4ade80; }
.results-header h2 { font-size: 1.15rem; font-weight: 600; color: #f8fafc; margin: 0; }
.header-badges { display: flex; align-items: center; gap: 0.6rem; }
.status-pill.status-completed {
background: rgba(34, 197, 94, 0.15); color: #4ade80; border: 1px solid rgba(34, 197, 94, 0.4);
padding: 0.3rem 0.65rem; border-radius: var(--zen-radius-xs, 4px); font-size: 0.75rem;
font-family: var(--font-mono, monospace); font-weight: 600;
}
.reset-btn {
background: #1e293b; color: #94a3b8; border: 1px solid #334155; padding: 0.3rem 0.65rem;
border-radius: var(--zen-radius-xs, 4px); font-size: 0.75rem; cursor: pointer;
}
.reset-btn:hover { background: #334155; color: #f8fafc; }
.metrics-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem; margin-bottom: 1.5rem;
}
.metric-card {
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">
<!-- Cabecera de Sección con Selector de Modo y Status -->
<header class="sim-section-header">
<div class="header-left">
<div class="title-with-badge">
<span class="pulse-indicator" aria-hidden="true"></span>
<h2 class="section-title">{{ i18n.t().navSimulator }}</h2>
<div class="soc-dashboard" aria-label="Autonomous Cyber-Defense SOC Dashboard">
<!-- Encabezado Táctico del SOC -->
<header class="soc-header">
<div class="logo-area">
<div class="shield-badge">
<svg class="shield-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<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>
<span class="section-tag">{{ i18n.t().simTag }}</span>
</div>
<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>
<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>
</div>
<span class="engine-badge">Engine: n8n + DeepSeek + Gotenberg</span>
</header>
<!-- Panel Principal del Simulador -->
<div class="zen-sim-board">
<!-- Barra Interactiva de Captura de Correo y Ejecución -->
<div class="email-control-deck">
<div class="email-input-wrapper">
<label for="sim-email-input" class="email-label">
<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"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
<span>{{ i18n.t().simEmailLabel }}</span>
</label>
<!-- Panel de Configuración de la Simulación -->
<section class="control-panel">
<div class="panel-header">
<span class="panel-tag">SECOPS // OPERATIONS DISPATCHER</span>
<h2 class="panel-title">Lanzador de Operaciones de Ciberdefensa</h2>
</div>
<div class="form-grid">
<!-- Modo de Operación -->
<div class="form-group">
<label for="operation-mode">Modo de Operación:</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
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"
class="email-field"
[value]="userEmail()"
(input)="onEmailInput($event)"
[placeholder]="i18n.t().simEmailPlaceholder"
[disabled]="isSimulating()"
[ngModel]="requestedBy()"
(ngModelChange)="requestedBy.set($event)"
placeholder="auditor@empresa.com"
[disabled]="isLoading()"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="sim-trigger-btn"
[disabled]="isSimulating()"
(click)="startSimulation()"
[attr.aria-label]="i18n.t().simBtnSimulate"
>
<span class="btn-glow"></span>
<span class="btn-icon">⚡</span>
<span class="btn-text">
@if (isSimulating()) {
{{ 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>
<!-- Aislamiento Zero-Trust -->
<div class="form-group checkbox-group">
<label class="checkbox-label" for="operation-isolate">
<input
id="operation-isolate"
type="checkbox"
[ngModel]="autoIsolate()"
(ngModelChange)="autoIsolate.set($event)"
[disabled]="isLoading()"
/>
<span class="checkbox-text">Aislamiento y Contención Automática (Zero-Trust)</span>
</label>
</div>
</div>
<!-- VISTA 1: MODO STORY / EXPLICADO PARA TODO PÚBLICO -->
@if (viewMode() === 'story') {
<div class="story-grid-flow" aria-live="polite">
<!-- PASO 1 -->
<div class="story-step-card" [class.active]="stage() === 'breach'" [class.completed]="stage() !== 'idle' && stage() !== 'breach'">
<div class="step-badge">01 // ATAQUE</div>
<div class="step-icon-wrap danger">
<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>
<!-- Botón de Lanzamiento -->
<button
type="button"
class="launch-btn"
(click)="executeSimulation()"
[disabled]="isLoading()"
>
@if (!isLoading()) {
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
</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>
<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 class="story-arrow" [class.active]="stage() !== 'idle'">→</div>
<!-- PASO 2 -->
<div class="story-step-card" [class.active]="stage() === 'detection'" [class.completed]="stage() === 'remediation' || stage() === 'notification' || isContained()">
<div class="step-badge">02 // DETECCIÓN</div>
<div class="step-icon-wrap warning">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<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>
<!-- Risk Score -->
<div class="metric-card">
<span class="metric-title">Risk Score</span>
<div
class="risk-gauge"
[ngClass]="res.summary.risk_score > 70 ? 'high-risk' : (res.summary.risk_score > 30 ? 'med-risk' : 'safe-risk')"
>
{{ res.summary.risk_score }} <span class="gauge-max">/ 100</span>
</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 class="story-arrow" [class.active]="stage() === 'detection' || stage() === 'remediation' || stage() === 'notification' || isContained()">→</div>
<!-- PASO 3 -->
<div class="story-step-card" [class.active]="stage() === 'remediation'" [class.completed]="stage() === 'notification' || isContained()">
<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>
<!-- Latencia de Mitigación -->
<div class="metric-card">
<span class="metric-title">Latencia de Mitigación (&Delta;t)</span>
<div class="metric-value highlight">{{ res.summary.mitigation_time_ms }} ms</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 -->
<div class="story-step-card" [class.active]="stage() === 'notification'" [class.completed]="isContained()">
<div class="step-badge">04 // INFORME</div>
<div class="step-icon-wrap success">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<!-- Técnicas MITRE ATT&CK -->
<div class="mitre-section">
<div class="mitre-header">
<span class="mitre-label">Técnicas MITRE ATT&CK v14 Correlacionadas</span>
<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"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
</div>
<h3 class="step-title">{{ i18n.t().simStep4Title }}</h3>
<p class="step-desc">{{ i18n.t().simStep4Desc }}</p>
<div class="step-meta">{{ targetRecipient() }}</div>
<span>Ver Reporte Forense en Navegador</span>
<span class="external-arrow">↗</span>
</a>
</div>
</div>
}
<!-- VISTA 2: MODO TÉCNICO SECOPS / AWS ARQUITECTURA -->
@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>
</section>
}
</div>

View File

@ -1,188 +1,83 @@
import { Component, ChangeDetectionStrategy, signal, computed, inject, OnDestroy, OnInit } from '@angular/core';
import { I18nService } from '../../../application/services/i18n.service';
import { Component, ChangeDetectionStrategy, signal, inject } from '@angular/core';
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';
export type SimulationStage = 'idle' | 'breach' | 'detection' | 'remediation' | 'notification' | 'contained';
export type SimulatorViewMode = 'story' | 'tech';
export interface SecOpsLog {
readonly timestamp: string;
readonly level: 'WARN' | 'INFO' | 'ACTION' | 'SUCCESS';
readonly tag: string;
readonly message: string;
}
import {
SimulationMode,
SecurityAgentRequest,
SecurityAgentResponse
} from '../../../domain/models/security.model';
@Component({
selector: 'app-incident-simulator',
selector: 'app-incident-simulator, app-cyber-defense',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './incident-simulator.component.html',
styleUrl: './incident-simulator.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class IncidentSimulatorComponent implements OnInit, OnDestroy {
protected readonly i18n = inject(I18nService);
export class IncidentSimulatorComponent {
private readonly securityService = inject(SecurityAgentService);
private readonly audio = inject(RetroAudioService);
protected readonly stage = signal<SimulationStage>('idle');
protected readonly viewMode = signal<SimulatorViewMode>('story');
protected readonly userEmail = signal<string>('');
protected readonly logs = signal<SecOpsLog[]>([]);
protected readonly threatIp = signal<string>('198.51.100.42');
protected readonly executionTimeMs = signal<number>(184);
protected readonly showReport = signal<boolean>(false);
protected readonly downloadFeedback = signal<boolean>(false);
// Parámetros reactivos de configuración
readonly mode = signal<SimulationMode>('PURPLE_TEAM');
readonly target = signal<string>('local_sandbox');
readonly requestedBy = signal<string>('auditor@bryan-bano.com');
readonly autoIsolate = signal<boolean>(true);
protected readonly isSimulating = computed<boolean>(() => {
const s = this.stage();
return s === 'breach' || s === 'detection' || s === 'remediation' || s === 'notification';
});
// Estados de ejecución y telemetría
readonly isLoading = signal<boolean>(false);
readonly errorMessage = signal<string | null>(null);
readonly result = signal<SecurityAgentResponse | null>(null);
protected readonly isContained = computed<boolean>(() => this.stage() === 'contained');
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;
setTargetPreset(value: string): void {
this.audio.playClick();
this.viewMode.set(mode);
this.target.set(value);
}
onEmailInput(event: Event): void {
const val = (event.target as HTMLInputElement).value;
this.userEmail.set(val);
}
executeSimulation(): void {
if (this.isLoading()) return;
startSimulation(): void {
if (this.isSimulating()) return;
this.clearTimeouts();
this.audio.playClick();
this.logs.set([]);
this.showReport.set(false);
this.downloadFeedback.set(false);
this.isLoading.set(true);
this.errorMessage.set(null);
this.result.set(null);
// Paso 1: Intento de Intrusión (Vector de Amenaza)
this.stage.set('breach');
this.appendLog({
level: 'WARN',
tag: 'IAM-ALERT',
message: `Anomalous API request: iam:CreateAccessKey from untrusted IP ${this.threatIp()} for principal "svc-deployer".`
const payload: SecurityAgentRequest = {
mode: this.mode(),
target: this.target().trim() || 'local_sandbox',
requested_by: this.requestedBy().trim() || 'auditor@bryan-bano.com',
parameters: {
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 {
this.clearTimeouts();
this.audio.playClick();
this.stage.set('idle');
this.showReport.set(false);
this.downloadFeedback.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();
this.errorMessage.set(null);
this.result.set(null);
this.isLoading.set(false);
}
}
export {
IncidentSimulatorComponent as CyberDefenseComponent,
IncidentSimulatorComponent as CyberDefenseDashboardComponent
};