Yog965/grc-ai-command-deck
0
1// State Management2let authToken = "";3let currentUser = null;4let activeJobId = "";5let chart = null;6let currentMappings = [];7let selectedMapping = null;8 9// Tab Configurations10const tabs = {11 overview: {12 title: "Compliance Cockpit",13 subtitle: "Real-time telemetry of security drift, NIST mapping coverage, and file audits."14 },15 mappings: {16 title: "Mappings & HITL Verification",17 subtitle: "Resolve low-confidence SBERT predictions and authorize policy alignments."18 },19 report: {20 title: "Compliance Drift Report",21 subtitle: "Review and export official, audit-ready compliance analysis evidence."22 },23 history: {24 title: "Job Run Audit History",25 subtitle: "Inspect execution metrics, configurations, and job output datasets."26 },27 instructions: {28 title: "System Operations Manual",29 subtitle: "Operational documentation, pipeline architectures, and quick-start instructions."30 }31};32 33// UI Selectors34const appContainer = document.getElementById("appContainer");35const loginScreen = document.getElementById("loginScreen");36const loginForm = document.getElementById("loginForm");37const usernameInput = document.getElementById("username");38const passwordInput = document.getElementById("password");39 40const navItems = document.querySelectorAll(".nav-item");41const tabContents = document.querySelectorAll(".tab-content");42const tabTitle = document.getElementById("tabTitle");43const tabSubtitle = document.getElementById("tabSubtitle");44 45const outputSetSelect = document.getElementById("outputSetSelect");46const refreshBtn = document.getElementById("refreshBtn");47const runBtn = document.getElementById("runBtn");48const runStatus = document.getElementById("runStatus");49const jobCard = document.getElementById("jobCard");50const jobProgressBar = document.getElementById("jobProgressBar");51const jobStage = document.getElementById("jobStage");52const jobProgressText = document.getElementById("jobProgressText");53const jobStatusMsg = document.getElementById("jobStatusMsg");54 55const logsCountInput = document.getElementById("logsCount");56const similarityThresholdInput = document.getElementById("similarityThreshold");57const logoutBtn = document.getElementById("logoutBtn");58 59const userAvatar = document.getElementById("userAvatar");60const userDisplayName = document.getElementById("userDisplayName");61const userRoleBadge = document.getElementById("userRoleBadge");62 63// Metric cards64const controlsCount = document.getElementById("controlsCount");65const logsCountMetric = document.getElementById("logsCountMetric");66const driftCount = document.getElementById("driftCount");67const hitlCount = document.getElementById("hitlCount");68const driftRatePercent = document.getElementById("driftRatePercent");69 70const artifactsTableWrap = document.getElementById("artifactsTableWrap");71const mappingsList = document.getElementById("mappingsList");72 73// HITL Editor selectors74const hitlEditorEmpty = document.getElementById("hitlEditorEmpty");75const hitlEditorContent = document.getElementById("hitlEditorContent");76const hitlDetailRawLog = document.getElementById("hitlDetailRawLog");77const hitlDetailStatus = document.getElementById("hitlDetailStatus");78const hitlSuggestionsList = document.getElementById("hitlSuggestionsList");79const overrideControlId = document.getElementById("overrideControlId");80const overrideMitreId = document.getElementById("overrideMitreId");81const overrideNotes = document.getElementById("overrideNotes");82const btnHitlAccept = document.getElementById("btnHitlAccept");83const btnHitlSubmit = document.getElementById("btnHitlSubmit");84 85// Report selectors86const reportRenderedHtml = document.getElementById("reportRenderedHtml");87const reportDriftStatusBadge = document.getElementById("reportDriftStatusBadge");88const reportMetaTime = document.getElementById("reportMetaTime");89const reportMetaControls = document.getElementById("reportMetaControls");90const reportMetaAnomalies = document.getElementById("reportMetaAnomalies");91const reportMetaPending = document.getElementById("reportMetaPending");92const downloadPdfBtn = document.getElementById("downloadPdfBtn");93const downloadBundleBtn = document.getElementById("downloadBundleBtn");94 95// History selectors96const historyTableWrap = document.getElementById("historyTableWrap");97const runDetailWrap = document.getElementById("runDetailWrap");98 99// ==========================================100// Toast System101// ==========================================102function showToast(message, type = "info") {103 const container = document.getElementById("toastContainer");104 const toast = document.createElement("div");105 toast.className = `toast toast-${type}`;106 107 let icon = "info";108 if (type === "success") icon = "check-circle";109 if (type === "error") icon = "x-circle";110 if (type === "warning") icon = "alert-triangle";111 112 toast.innerHTML = `113 <i data-lucide="${icon}"></i>114 <span>${message}</span>115 `;116 117 container.appendChild(toast);118 lucide.createIcons();119 120 setTimeout(() => {121 toast.style.opacity = "0";122 toast.style.transform = "translateY(-15px) scale(0.95)";123 toast.style.transition = "all 0.3s ease";124 setTimeout(() => toast.remove(), 300);125 }, 4000);126}127 128// ==========================================129// API Helpers130// ==========================================131async function fetchJSON(url, options = {}) {132 const headers = options.headers ? { ...options.headers } : {};133 if (authToken) {134 headers.Authorization = `Bearer ${authToken}`;135 }136 137 const response = await fetch(url, { ...options, headers });138 if (!response.ok) {139 const detail = await response.text();140 let parsedDetail = detail;141 try {142 const jsonDetail = JSON.parse(detail);143 parsedDetail = jsonDetail.detail || detail;144 } catch(e) {}145 throw new Error(parsedDetail || `Request failed: ${response.status}`);146 }147 return response.json();148}149 150async function downloadWithAuth(url, filename) {151 const headers = authToken ? { Authorization: `Bearer ${authToken}` } : {};152 const response = await fetch(url, { headers });153 if (!response.ok) {154 const detail = await response.text();155 throw new Error(detail || `Download failed: ${response.status}`);156 }157 158 const blob = await response.blob();159 const objectUrl = URL.createObjectURL(blob);160 const link = document.createElement("a");161 link.href = objectUrl;162 link.download = filename;163 document.body.appendChild(link);164 link.click();165 link.remove();166 URL.revokeObjectURL(objectUrl);167}168 169// ==========================================170// Metric Counter Animation171// ==========================================172function animateMetric(element, nextValue) {173 const target = Number(nextValue) || 0;174 const current = Number((element.textContent || "0").replace(/,/g, "")) || 0;175 const durationMs = 450;176 const start = performance.now();177 178 function step(timestamp) {179 const t = Math.min((timestamp - start) / durationMs, 1);180 const eased = 1 - Math.pow(1 - t, 3); // Cubic Ease Out181 const value = Math.round(current + (target - current) * eased);182 element.textContent = value.toLocaleString();183 if (t < 1) {184 requestAnimationFrame(step);185 }186 }187 188 requestAnimationFrame(step);189}190 191// ==========================================192// Chart.js Configuration193// ==========================================194function updateChart(totalLogs, driftLogs) {195 const normalLogs = Math.max(totalLogs - driftLogs, 0);196 const driftRate = totalLogs > 0 ? ((driftLogs / totalLogs) * 100).toFixed(1) : "0.0";197 driftRatePercent.textContent = `${driftRate}%`;198 199 const ctx = document.getElementById("driftChart").getContext("2d");200 201 if (chart) {202 chart.destroy();203 }204 205 chart = new Chart(ctx, {206 type: "doughnut",207 data: {208 labels: ["Compliant Operations", "Drift Anomalies"],209 datasets: [210 {211 data: [normalLogs, driftLogs],212 backgroundColor: ["#00b2c8", "#ff7a1a"],213 hoverBackgroundColor: ["#00cfe8", "#ff933b"],214 borderWidth: 1,215 borderColor: "#0d1527",216 },217 ],218 },219 options: {220 responsive: true,221 maintainAspectRatio: false,222 cutout: "80%",223 animation: {224 duration: 800,225 easing: "easeOutQuart",226 },227 plugins: {228 legend: {229 display: true,230 position: "bottom",231 labels: {232 color: "#8ca3c7",233 font: {234 family: "Space Grotesk",235 size: 11236 },237 padding: 15238 },239 },240 tooltip: {241 backgroundColor: "#0d1527",242 borderColor: "rgba(129, 170, 225, 0.15)",243 borderWidth: 1,244 titleColor: "#f0f5ff",245 bodyColor: "#8ca3c7",246 titleFont: { family: "Space Grotesk" },247 bodyFont: { family: "Space Grotesk" }248 }249 },250 },251 });252}253 254// ==========================================255// Render Layout Functions256// ==========================================257function renderArtifacts(artifacts) {258 if (!artifacts || !artifacts.length) {259 artifactsTableWrap.innerHTML = "<p style='padding: 1rem; text-align: center; color: var(--muted);'>No artifacts found.</p>";260 return;261 }262 263 const rows = artifacts264 .map((item) => {265 const statusBadge = item.exists 266 ? `<span class="badge badge-success"><i data-lucide="check-circle-2"></i> Ready</span>`267 : `<span class="badge badge-neutral"><i data-lucide="help-circle"></i> Missing</span>`;268 const sizeKb = (item.size_bytes / 1024).toFixed(2);269 270 return `271 <tr>272 <td style="font-weight: 600; color: #fff;"><i data-lucide="file" style="width:14px; display:inline-block; vertical-align:middle; margin-right:4px;"></i> ${item.name}</td>273 <td>${statusBadge}</td>274 <td><code>${sizeKb} KB</code></td>275 <td style="font-family:'IBM Plex Mono', monospace; font-size:0.75rem; opacity:0.8;">${item.path}</td>276 </tr>277 `;278 })279 .join("");280 281 artifactsTableWrap.innerHTML = `282 <table>283 <thead>284 <tr><th>Artifact File</th><th>Status</th><th>Size</th><th>Path Target</th></tr>285 </thead>286 <tbody>${rows}</tbody>287 </table>288 `;289 lucide.createIcons();290}291 292function renderMappingsList(mappings) {293 if (!mappings || !mappings.length) {294 mappingsList.innerHTML = "<div class='hitl-empty-state'><i data-lucide='inbox'></i><p>No drift mappings found in outputs.</p></div>";295 lucide.createIcons();296 return;297 }298 299 mappingsList.innerHTML = mappings300 .map((item, index) => {301 const isSelected = selectedMapping && selectedMapping.RawLog === item.RawLog;302 const selectClass = isSelected ? "selected" : "";303 304 const nistSim = item.selected_similarity || 0;305 const mitreSim = item.selected_mitre_similarity || 0;306 const needsHitl = item.hitl_required || item.mitre_hitl_required;307 308 let simClass = "similarity-high";309 if (needsHitl) simClass = "similarity-low";310 else if (nistSim < 0.8 || mitreSim < 0.8) simClass = "similarity-warn";311 312 const hitlBadge = needsHitl313 ? `<span class="hitl-status-badge hitl-status-required"><i data-lucide="alert-circle" style="width:11px;height:11px;"></i> Pending Review</span>`314 : `<span class="hitl-status-badge hitl-status-resolved"><i data-lucide="check-circle" style="width:11px;height:11px;"></i> Resolved</span>`;315 316 return `317 <article class="mapping-card-item ${selectClass}" data-index="${index}">318 <div class="mapping-item-meta" style="flex-wrap: wrap; gap: 0.3rem;">319 <span class="mapping-control-id" style="background: rgba(162,89,255,0.15); color: #c084fc;">NIST: ${item.selected_control_id || "UNKNOWN"}</span>320 <span class="mapping-control-id" style="background: rgba(255,122,26,0.15); color: #ff9d4d;">MITRE: ${item.selected_mitre_id || "UNKNOWN"}</span>321 <span class="mapping-similarity ${simClass}">NIST: ${nistSim.toFixed(2)} | MITRE: ${mitreSim.toFixed(2)}</span>322 </div>323 <div class="mapping-item-log">${item.RawLog}</div>324 <div style="display:flex; justify-content:space-between; align-items:center; margin-top:0.4rem;">325 <span style="font-size:0.75rem; color:var(--muted);"><i data-lucide="cpu" style="width:11px;height:11px;display:inline-block;vertical-align:middle;margin-right:2px;"></i> ${item.Resource || "Unknown"}</span>326 ${hitlBadge}327 </div>328 </article>329 `;330 })331 .join("");332 333 lucide.createIcons();334 335 // Add click events to mapping cards336 mappingsList.querySelectorAll(".mapping-card-item").forEach((card) => {337 card.addEventListener("click", () => {338 const idx = card.getAttribute("data-index");339 selectMappingItem(mappings[idx]);340 });341 });342}343 344function selectMappingItem(item) {345 selectedMapping = item;346 347 // Highlight selection348 document.querySelectorAll(".mapping-card-item").forEach((card, idx) => {349 if (currentMappings[idx] && currentMappings[idx].RawLog === item.RawLog) {350 card.classList.add("selected");351 } else {352 card.classList.remove("selected");353 }354 });355 356 // Populate editor357 hitlEditorEmpty.style.display = "none";358 hitlEditorContent.style.display = "block";359 360 hitlDetailRawLog.textContent = item.RawLog;361 362 const needsHitl = item.hitl_required || item.mitre_hitl_required;363 if (needsHitl) {364 hitlDetailStatus.className = "badge badge-warning";365 hitlDetailStatus.innerHTML = "<i data-lucide='alert-circle'></i> Pending Human Verification";366 } else {367 hitlDetailStatus.className = "badge badge-success";368 hitlDetailStatus.innerHTML = `<i data-lucide='check-circle-2'></i> Verified (NIST: ${item.hitl_decision || 'Auto'} | MITRE: ${item.mitre_hitl_decision || 'Auto'})`;369 }370 371 // Clear overrides form inputs372 overrideControlId.value = item.selected_control_id || "";373 if (overrideMitreId) {374 overrideMitreId.value = item.selected_mitre_id || "";375 }376 overrideNotes.value = needsHitl ? "" : (item.hitl_decision || item.mitre_hitl_decision || "");377 378 // Generate recommendation lists379 let suggestionsHtml = "";380 381 if (item.top_matches && item.top_matches.length) {382 suggestionsHtml += `<h4 style="font-size:0.75rem; color:var(--muted); text-transform:uppercase; margin-bottom:0.4rem;">NIST SP 800-53 Suggestions</h4>`;383 suggestionsHtml += item.top_matches384 .map((match, idx) => {385 return `386 <div class="rec-item nist-rec" data-control-id="${match.control_id}" style="border-left: 2px solid #a259ff; padding: 0.5rem; margin-bottom: 0.4rem; background: rgba(162, 89, 255, 0.05); border-radius: 4px; cursor: pointer;">387 <div class="rec-item-title">388 <span style="color:#fff; font-weight:600;">${idx+1}. ${match.control_id.toUpperCase()}</span> 389 <span style="color:var(--muted); font-size:0.75rem; margin-left:6px;">${match.title}</span>390 </div>391 <div class="rec-item-score" style="font-size:0.72rem; color:var(--accent);">Match: ${(match.similarity * 100).toFixed(1)}%</div>392 </div>393 `;394 })395 .join("");396 }397 398 if (item.mitre_top_matches && item.mitre_top_matches.length) {399 suggestionsHtml += `<h4 style="font-size:0.75rem; color:var(--muted); text-transform:uppercase; margin: 0.8rem 0 0.4rem 0;">MITRE ATT&CK Cloud Suggestions</h4>`;400 suggestionsHtml += item.mitre_top_matches401 .map((match, idx) => {402 return `403 <div class="rec-item mitre-rec" data-mitre-id="${match.technique_id}" style="border-left: 2px solid #ff7a1a; padding: 0.5rem; margin-bottom: 0.4rem; background: rgba(255, 122, 26, 0.05); border-radius: 4px; cursor: pointer;">404 <div class="rec-item-title">405 <span style="color:#fff; font-weight:600;">${idx+1}. ${match.technique_id}</span> 406 <span style="color:var(--muted); font-size:0.75rem; margin-left:6px;">${match.name}</span>407 </div>408 <div class="rec-item-score" style="font-size:0.72rem; color: #ff9d4d;">Match: ${(match.similarity * 100).toFixed(1)}%</div>409 </div>410 `;411 })412 .join("");413 }414 415 if (!suggestionsHtml) {416 suggestionsHtml = "<p style='font-size:0.8rem; color:var(--muted); padding:0.4rem;'>No match recommendations returned.</p>";417 }418 419 hitlSuggestionsList.innerHTML = suggestionsHtml;420 421 // Make recommendations clickable to pre-fill inputs422 hitlSuggestionsList.querySelectorAll(".nist-rec").forEach((rec) => {423 rec.addEventListener("click", () => {424 const cId = rec.getAttribute("data-control-id");425 overrideControlId.value = cId;426 showToast(`Selected suggested NIST control: ${cId.toUpperCase()}`, "info");427 });428 });429 430 hitlSuggestionsList.querySelectorAll(".mitre-rec").forEach((rec) => {431 rec.addEventListener("click", () => {432 const mId = rec.getAttribute("data-mitre-id");433 if (overrideMitreId) {434 overrideMitreId.value = mId;435 showToast(`Selected suggested MITRE Technique: ${mId}`, "info");436 }437 });438 });439 lucide.createIcons();440}441 442function renderHistory(historyItems) {443 if (!historyItems || !historyItems.length) {444 historyTableWrap.innerHTML = "<p style='padding: 1rem; text-align: center; color: var(--muted);'>No execution logs found.</p>";445 return;446 }447 448 const rows = historyItems449 .map((item) => {450 const outSet = item.params?.output_set || "n/a";451 const logs = item.params?.logs_count || "n/a";452 const drift = item.summary?.drift_count ?? "n/a";453 const duration = item.duration_seconds ? `${item.duration_seconds}s` : "n/a";454 455 let statusBadge = `<span class="badge badge-success"><i data-lucide="check"></i> Success</span>`;456 if (item.status === "failed") {457 statusBadge = `<span class="badge badge-danger"><i data-lucide="x"></i> Failed</span>`;458 } else if (item.status === "running") {459 statusBadge = `<span class="badge badge-warning"><i data-lucide="refresh-cw" class="spin"></i> Running</span>`;460 }461 462 return `463 <tr>464 <td style="font-size:0.8rem; color:#fff;">${formatTimestamp(item.started_at)}</td>465 <td>${statusBadge}</td>466 <td><code>${outSet}</code></td>467 <td>${logs}</td>468 <td style="font-weight:600; color:var(--accent);">${drift}</td>469 <td>${duration}</td>470 <td><button class="btn btn-secondary inline-btn" data-run-id="${item.run_id || ""}">Inspect</button></td>471 </tr>472 `;473 })474 .join("");475 476 historyTableWrap.innerHTML = `477 <table>478 <thead>479 <tr>480 <th>Execution Date</th>481 <th>Status</th>482 <th>Dataset Name</th>483 <th>Logs Ingested</th>484 <th>Drifts Flagged</th>485 <th>Time Taken</th>486 <th>Action</th>487 </tr>488 </thead>489 <tbody>${rows}</tbody>490 </table>491 `;492 lucide.createIcons();493 494 // Attach click listener to history rows495 historyTableWrap.querySelectorAll("button[data-run-id]").forEach((button) => {496 button.addEventListener("click", () => {497 const runId = button.getAttribute("data-run-id");498 if (runId) {499 loadRunDetail(runId).catch((error) => {500 showToast(`Detail inspect failed: ${error.message}`, "error");501 });502 }503 });504 });505}506 507function renderRunDetail(payload) {508 const run = payload.run || {};509 const artifacts = payload.artifacts || [];510 const summary = run.summary || {};511 512 const artifactRows = artifacts513 .map((item) => {514 const statusBadge = item.exists 515 ? `<span class="badge badge-success" style="font-size:0.7rem; padding:0.1rem 0.4rem;">Exists</span>`516 : `<span class="badge badge-neutral" style="font-size:0.7rem; padding:0.1rem 0.4rem;">Missing</span>`;517 return `<tr><td><code>${item.name}</code></td><td>${statusBadge}</td><td style="font-size:0.72rem; color:var(--muted); font-family:monospace;">${item.path}</td></tr>`;518 })519 .join("");520 521 runDetailWrap.innerHTML = `522 <div class="detail-card">523 <div class="detail-header-item">524 <h3>Pipeline Run GUID</h3>525 <p>${run.run_id || "n/a"}</p>526 </div>527 528 <div class="detail-grid">529 <div class="detail-cell">530 <label>Triggered By</label>531 <span>${run.triggered_by || "n/a"} (${run.role || "n/a"})</span>532 </div>533 <div class="detail-cell">534 <label>Execution Status</label>535 <span>${(run.status || "n/a").toUpperCase()}</span>536 </div>537 <div class="detail-cell">538 <label>Execution Duration</label>539 <span>${run.duration_seconds ?? "n/a"}s</span>540 </div>541 <div class="detail-cell">542 <label>Output Dataset</label>543 <span>${summary.output_set || "n/a"}</span>544 </div>545 <div class="detail-cell">546 <label>NIST Control References</label>547 <span>${summary.controls_count ?? "n/a"} parsed</span>548 </div>549 <div class="detail-cell">550 <label>Log Scopes / Drifts</label>551 <span>${summary.logs_count ?? "n/a"} / ${summary.drift_count ?? "n/a"}</span>552 </div>553 </div>554 555 <h4 style="font-size:0.8rem; color:var(--muted); text-transform:uppercase; margin:1rem 0 0.4rem;">Dataset Output Files</h4>556 <div class="table-wrap" style="margin-bottom:0; background:rgba(0,0,0,0.2);">557 <table>558 <thead>559 <tr><th>Name</th><th>State</th><th>Location</th></tr>560 </thead>561 <tbody>${artifactRows}</tbody>562 </table>563 </div>564 ${run.error ? `<div style="margin-top:1rem; padding:0.8rem; background:rgba(239,68,68,0.08); border:1px solid rgba(239,68,68,0.2); border-radius:8px; color:var(--danger); font-size:0.82rem; font-family:monospace; word-break:break-all;"><strong>Execution Error:</strong> ${run.error}</div>` : ''}565 </div>566 `;567}568 569// ==========================================570// Dashboard Sync & Loading Actions571// ==========================================572async function loadOutputSets() {573 try {574 const data = await fetchJSON("/api/output-sets");575 outputSetSelect.innerHTML = data.output_sets576 .map((setName) => `<option value="${setName}">${setName}</option>`)577 .join("");578 579 // Set default value if 'outputs' exists580 if (data.output_sets.includes("outputs")) {581 outputSetSelect.value = "outputs";582 }583 } catch(error) {584 showToast(`Load datasets failed: ${error.message}`, "error");585 }586}587 588async function refreshDashboard() {589 if (!authToken) return;590 591 const outputSet = outputSetSelect.value || "outputs";592 593 // Set current similarity threshold label594 currentThresholdVal.textContent = similarityThresholdInput.value;595 596 try {597 // Show skeleton/loading state598 refreshBtn.disabled = true;599 600 const [summary, artifacts, mappings, report, history] = await Promise.all([601 fetchJSON(`/api/summary?output_set=${encodeURIComponent(outputSet)}`),602 fetchJSON(`/api/artifacts?output_set=${encodeURIComponent(outputSet)}`),603 fetchJSON(`/api/mappings?output_set=${encodeURIComponent(outputSet)}&limit=100`),604 fetchJSON(`/api/report?output_set=${encodeURIComponent(outputSet)}`),605 fetchJSON("/api/run-history?limit=15"),606 ]);607 608 // Animate telemetry panels609 animateMetric(controlsCount, summary.controls_count);610 animateMetric(logsCountMetric, summary.logs_count);611 animateMetric(driftCount, summary.drift_count);612 animateMetric(hitlCount, summary.hitl_required_count);613 614 // Update Telemetry Chart615 updateChart(summary.logs_count, summary.drift_count);616 617 // Render artifacts list618 renderArtifacts(artifacts.artifacts);619 620 // Render history and mappings621 currentMappings = mappings.items || [];622 renderMappingsList(currentMappings);623 renderHistory(history.items || []);624 625 // Render markdown report to beautiful HTML626 if (report && report.report_markdown) {627 reportRenderedHtml.innerHTML = marked.parse(report.report_markdown);628 } else {629 reportRenderedHtml.innerHTML = "<p style='color:var(--muted);'>No compliance report has been compiled yet. Run the pipeline.</p>";630 }631 632 // Set report metadata side pane633 if (summary.drift_count > 0) {634 reportDriftStatusBadge.className = "badge badge-danger";635 reportDriftStatusBadge.innerHTML = "<i data-lucide='alert-triangle'></i> DRIFT DETECTED";636 } else {637 reportDriftStatusBadge.className = "badge badge-success";638 reportDriftStatusBadge.innerHTML = "<i data-lucide='shield-check'></i> COMPLIANT";639 }640 641 reportMetaTime.textContent = new Date().toLocaleTimeString();642 reportMetaControls.textContent = summary.controls_count;643 reportMetaAnomalies.textContent = summary.drift_count;644 reportMetaPending.textContent = summary.hitl_required_count;645 646 // Reset HITL override editor side pane647 selectedMapping = null;648 hitlEditorContent.style.display = "none";649 hitlEditorEmpty.style.display = "flex";650 651 showToast("Telemetry dashboard synchronized successfully.", "success");652 } catch (error) {653 showToast(`Dashboard sync failed: ${error.message}`, "error");654 } finally {655 refreshBtn.disabled = false;656 lucide.createIcons();657 }658}659 660async function loadRunDetail(runId) {661 try {662 const detail = await fetchJSON(`/api/run-history/${encodeURIComponent(runId)}`);663 renderRunDetail(detail);664 showToast(`Loaded details for run: ${runId.substring(0, 8)}...`, "info");665 } catch (error) {666 showToast(`Load run details failed: ${error.message}`, "error");667 }668}669 670// ==========================================671// Pipeline Worker Interactions (Poller)672// ==========================================673function updateJobProgress(progress, stage, status) {674 const clamped = Math.max(0, Math.min(100, Number(progress) || 0));675 jobProgressBar.style.width = `${clamped}%`;676 jobProgressText.textContent = `${clamped}%`;677 jobStage.textContent = stage || "In Queue";678 679 if (status === "running") {680 jobStatusMsg.className = "job-status-text pulse";681 jobStatusMsg.innerHTML = `<i data-lucide="refresh-cw" class="spin"></i> ${stage}...`;682 } else if (status === "completed") {683 jobStatusMsg.className = "job-status-text";684 jobStatusMsg.style.color = "var(--success)";685 jobStatusMsg.innerHTML = `<i data-lucide="check-circle-2"></i> Pipeline executed successfully.`;686 } else if (status === "failed") {687 jobStatusMsg.className = "job-status-text";688 jobStatusMsg.style.color = "var(--danger)";689 jobStatusMsg.innerHTML = `<i data-lucide="x-circle"></i> Pipeline failed.`;690 }691 lucide.createIcons();692}693 694async function pollJobUntilDone(jobId) {695 activeJobId = jobId;696 jobCard.style.display = "block";697 runStatus.textContent = "Pipeline executing in background worker...";698 699 while (activeJobId === jobId) {700 try {701 const job = await fetchJSON(`/api/jobs/${encodeURIComponent(jobId)}`);702 updateJobProgress(job.progress, job.stage, job.status);703 704 if (job.status === "completed") {705 runStatus.textContent = "Pipeline completed successfully. Updating local workspace...";706 showToast("Compliance pipeline task succeeded.", "success");707 await refreshDashboard();708 if (job.run_id) {709 await loadRunDetail(job.run_id);710 // Switch to run history tab to show details711 switchTab("history");712 }713 714 // Hide job progress card after a small delay715 setTimeout(() => {716 jobCard.style.display = "none";717 runStatus.textContent = "Ready.";718 }, 5000);719 return;720 }721 722 if (job.status === "failed") {723 runStatus.textContent = `Pipeline failed: ${job.error || "Unknown error"}`;724 showToast(`Pipeline execution error: ${job.error}`, "error");725 await refreshDashboard();726 return;727 }728 } catch(err) {729 runStatus.textContent = `Polling error: ${err.message}`;730 }731 732 await new Promise((resolve) => setTimeout(resolve, 1500));733 }734}735 736async function runPipeline() {737 runBtn.disabled = true;738 runStatus.textContent = "Submitting pipeline configuration...";739 740 try {741 const payload = {742 logs_count: Number(logsCountInput.value),743 similarity_threshold: Number(similarityThresholdInput.value),744 output_set: outputSetSelect.value || "outputs",745 };746 747 const queued = await fetchJSON("/api/run-pipeline", {748 method: "POST",749 headers: { "Content-Type": "application/json" },750 body: JSON.stringify(payload),751 });752 753 showToast("Pipeline job queued successfully.", "info");754 await pollJobUntilDone(queued.job_id);755 } catch (error) {756 runStatus.textContent = `Submission failed: ${error.message}`;757 showToast(`Execution failed: ${error.message}`, "error");758 } finally {759 applyRoleState();760 }761}762 763// ==========================================764// HITL Submission Override765// ==========================================766async function submitHitlResolution(controlId, mitreId, notes) {767 if (!selectedMapping) {768 showToast("No mapping item selected for resolution.", "warning");769 return;770 }771 772 const formattedControlId = controlId ? controlId.trim().toLowerCase() : "";773 const formattedMitreId = mitreId ? mitreId.trim() : "";774 775 if (!formattedControlId && !formattedMitreId) {776 showToast("Please assign a valid NIST Control ID or MITRE Technique ID.", "warning");777 return;778 }779 780 try {781 btnHitlSubmit.disabled = true;782 const outputSet = outputSetSelect.value || "outputs";783 784 const payload = {785 output_set: outputSet,786 raw_log: selectedMapping.RawLog,787 selected_control_id: formattedControlId || null,788 selected_mitre_id: formattedMitreId || null,789 hitl_decision: notes || "Human verified and matching framework mapping assigned"790 };791 792 const response = await fetchJSON("/api/mappings/resolve", {793 method: "POST",794 headers: { "Content-Type": "application/json" },795 body: JSON.stringify(payload),796 });797 798 showToast("HITL override applied and saved successfully.", "success");799 800 // Refresh dashboard data to sync mappings list and report content801 await refreshDashboard();802 } catch(error) {803 showToast(`Failed to save resolution: ${error.message}`, "error");804 } finally {805 btnHitlSubmit.disabled = false;806 }807}808 809// ==========================================810// Authentication State Management811// ==========================================812async function handleLogin(event) {813 event.preventDefault();814 runStatus.textContent = "Checking authorization...";815 816 try {817 const payload = {818 username: usernameInput.value.trim(),819 password: passwordInput.value,820 };821 822 const session = await fetchJSON("/api/login", {823 method: "POST",824 headers: { "Content-Type": "application/json" },825 body: JSON.stringify(payload),826 });827 828 authToken = session.access_token;829 currentUser = { username: session.username, role: session.role };830 831 // Transition UI832 loginScreen.classList.add("hidden");833 appContainer.classList.remove("blur-bg");834 835 showToast(`Authenticated as ${session.username}. Console loaded.`, "success");836 837 // Populate display profile838 userAvatar.textContent = session.username.charAt(0).toUpperCase();839 userDisplayName.textContent = session.username;840 userRoleBadge.textContent = session.role;841 842 applyRoleState();843 await loadOutputSets();844 await refreshDashboard();845 } catch (error) {846 authToken = "";847 currentUser = null;848 applyRoleState();849 showToast(`Authentication failed: ${error.message}`, "error");850 }851}852 853function handleLogout() {854 activeJobId = "";855 authToken = "";856 currentUser = null;857 858 // Transition UI859 loginScreen.classList.remove("hidden");860 appContainer.classList.add("blur-bg");861 862 outputSetSelect.innerHTML = "<option value='outputs'>outputs</option>";863 historyTableWrap.innerHTML = "";864 runDetailWrap.innerHTML = `865 <div style="color: var(--muted); padding: 2rem 1rem; text-align: center;">866 <i data-lucide="list" style="font-size: 2rem; margin-bottom: 0.5rem; color: rgba(129, 170, 225, 0.15); display: block;"></i>867 Select a pipeline run from history to view details.868 </div>869 `;870 reportRenderedHtml.innerHTML = "<p>Authentication Required.</p>";871 872 applyRoleState();873 showToast("Session disconnected. Logged out.", "info");874}875 876function applyRoleState() {877 if (!currentUser) {878 runBtn.disabled = true;879 refreshBtn.disabled = true;880 downloadPdfBtn.disabled = true;881 downloadBundleBtn.disabled = true;882 return;883 }884 885 refreshBtn.disabled = false;886 downloadPdfBtn.disabled = false;887 downloadBundleBtn.disabled = false;888 889 // Role verification: Analysts and Admins can trigger pipeline. Reviewers can only read/override HITL.890 const hasExecuteAccess = currentUser.role === "admin" || currentUser.role === "analyst";891 runBtn.disabled = !hasExecuteAccess;892}893 894// ==========================================895// Tabs Switching Navigation896// ==========================================897function switchTab(tabId) {898 // Update nav active classes899 navItems.forEach((item) => {900 if (item.getAttribute("data-tab") === tabId) {901 item.classList.add("active");902 } else {903 item.classList.remove("active");904 }905 });906 907 // Show/Hide tab content panels908 tabContents.forEach((content) => {909 if (content.id === `tab-${tabId}`) {910 content.classList.add("active");911 } else {912 content.classList.remove("active");913 }914 });915 916 // Update top title titles917 if (tabs[tabId]) {918 tabTitle.textContent = tabs[tabId].title;919 tabSubtitle.textContent = tabs[tabId].subtitle;920 }921}922 923// ==========================================924// Utilities925// ==========================================926function formatTimestamp(isoString) {927 if (!isoString) return "n/a";928 try {929 const d = new Date(isoString);930 return d.toLocaleString();931 } catch(e) {932 return isoString;933 }934}935 936// ==========================================937// Event Listeners Initialization938// ==========================================939navItems.forEach((item) => {940 item.addEventListener("click", () => {941 const tabId = item.getAttribute("data-tab");942 switchTab(tabId);943 });944});945 946refreshBtn.addEventListener("click", () => {947 refreshDashboard();948});949 950outputSetSelect.addEventListener("change", () => {951 refreshDashboard();952});953 954runBtn.addEventListener("click", runPipeline);955loginForm.addEventListener("submit", handleLogin);956logoutBtn.addEventListener("click", handleLogout);957 958// HITL Overrides Buttons959btnHitlAccept.addEventListener("click", () => {960 if (!selectedMapping) return;961 const topControlId = selectedMapping.top_matches && selectedMapping.top_matches[0]962 ? selectedMapping.top_matches[0].control_id963 : "";964 const topMitreId = selectedMapping.mitre_top_matches && selectedMapping.mitre_top_matches[0]965 ? selectedMapping.mitre_top_matches[0].technique_id966 : "";967 968 if (topControlId || topMitreId) {969 if (topControlId) overrideControlId.value = topControlId;970 if (topMitreId && overrideMitreId) overrideMitreId.value = topMitreId;971 submitHitlResolution(topControlId, topMitreId, "Accepted SBERT model recommendation");972 } else {973 showToast("No recommendation matches to accept.", "warning");974 }975});976 977btnHitlSubmit.addEventListener("click", () => {978 const mitreVal = overrideMitreId ? overrideMitreId.value : "";979 submitHitlResolution(overrideControlId.value, mitreVal, overrideNotes.value);980});981 982// Download/Export Files983downloadPdfBtn.addEventListener("click", async () => {984 try {985 const outputSet = outputSetSelect.value || "outputs";986 showToast("Generating compliance report PDF...", "info");987 await downloadWithAuth(988 `/api/export/report.pdf?output_set=${encodeURIComponent(outputSet)}`,989 `${outputSet}_compliance_report.pdf`990 );991 showToast("Report PDF downloaded successfully.", "success");992 } catch (error) {993 showToast(`PDF Export failed: ${error.message}`, "error");994 }995});996 997downloadBundleBtn.addEventListener("click", async () => {998 try {999 const outputSet = outputSetSelect.value || "outputs";1000 showToast("Compressing CSV artifact files...", "info");1001 await downloadWithAuth(1002 `/api/export/csv-bundle?output_set=${encodeURIComponent(outputSet)}`,1003 `${outputSet}_grc_bundle.zip`1004 );1005 showToast("Evidence ZIP bundle downloaded successfully.", "success");1006 } catch (error) {1007 showToast(`ZIP Bundle download failed: ${error.message}`, "error");1008 }1009});1010 1011// App Startup Init1012(function init() {1013 // Clear any existing session settings1014 applyRoleState();1015 // Load Lucide Icons1016 lucide.createIcons();1017})();1018 