feat: pure Dioxus attack chain visualization, PDF report redesign, and orchestrator data fixes
Some checks failed
CI / Format (push) Has been cancelled
CI / Deploy Docs (push) Has been cancelled
CI / Tests (push) Has been cancelled
CI / Detect Changes (push) Has been cancelled
CI / Deploy Agent (push) Has been cancelled
CI / Deploy Dashboard (push) Has been cancelled
CI / Deploy MCP (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Format (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
CI / Security Audit (pull_request) Has been cancelled
CI / Tests (pull_request) Has been cancelled
CI / Detect Changes (pull_request) Has been cancelled
CI / Deploy Agent (pull_request) Has been cancelled
CI / Deploy Dashboard (pull_request) Has been cancelled
CI / Deploy Docs (pull_request) Has been cancelled
CI / Deploy MCP (pull_request) Has been cancelled
Some checks failed
CI / Format (push) Has been cancelled
CI / Deploy Docs (push) Has been cancelled
CI / Tests (push) Has been cancelled
CI / Detect Changes (push) Has been cancelled
CI / Deploy Agent (push) Has been cancelled
CI / Deploy Dashboard (push) Has been cancelled
CI / Deploy MCP (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Format (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
CI / Security Audit (pull_request) Has been cancelled
CI / Tests (pull_request) Has been cancelled
CI / Detect Changes (pull_request) Has been cancelled
CI / Deploy Agent (pull_request) Has been cancelled
CI / Deploy Dashboard (pull_request) Has been cancelled
CI / Deploy Docs (pull_request) Has been cancelled
CI / Deploy MCP (pull_request) Has been cancelled
- Replace vis-network JS graph with pure RSX attack chain component featuring KPI header, phase rail, expandable accordion with tool category chips, risk scores, and findings pills - Redesign pentest report as professional PDF-first document with cover page, table of contents, severity bar chart, phased attack chain timeline, and print-friendly light theme - Fix orchestrator to populate findings_produced, risk_score, and llm_reasoning on attack chain nodes - Capture LLM reasoning text alongside tool calls in LlmResponse enum - Add session-level KPI fallback for older pentest data - Remove attack-chain-viz.js and prototype files - Add encrypted ZIP report export endpoint with password protection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,234 +0,0 @@
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Attack Chain DAG Visualization — vis-network wrapper
|
||||
// Obsidian Control theme
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// Status color palette matching Obsidian Control
|
||||
var STATUS_COLORS = {
|
||||
completed: { bg: "#16a34a", border: "#12873c", font: "#060a13" },
|
||||
running: { bg: "#d97706", border: "#b56205", font: "#060a13" },
|
||||
failed: { bg: "#dc2626", border: "#b91c1c", font: "#ffffff" },
|
||||
pending: { bg: "#5e7291", border: "#3d506b", font: "#e4eaf4" },
|
||||
skipped: { bg: "#374151", border: "#1f2937", font: "#e4eaf4" },
|
||||
};
|
||||
|
||||
var EDGE_COLOR = "rgba(94, 114, 145, 0.5)";
|
||||
|
||||
var network = null;
|
||||
var nodesDataset = null;
|
||||
var edgesDataset = null;
|
||||
var rawNodesMap = {};
|
||||
|
||||
function getStatusColor(status) {
|
||||
return STATUS_COLORS[status] || STATUS_COLORS.pending;
|
||||
}
|
||||
|
||||
function truncate(str, maxLen) {
|
||||
if (!str) return "";
|
||||
return str.length > maxLen ? str.substring(0, maxLen) + "…" : str;
|
||||
}
|
||||
|
||||
function buildTooltip(node) {
|
||||
var lines = [];
|
||||
lines.push("Tool: " + (node.tool_name || "unknown"));
|
||||
lines.push("Status: " + (node.status || "pending"));
|
||||
if (node.llm_reasoning) {
|
||||
lines.push("Reasoning: " + truncate(node.llm_reasoning, 200));
|
||||
}
|
||||
var findingsCount = node.findings_produced ? node.findings_produced.length : 0;
|
||||
lines.push("Findings: " + findingsCount);
|
||||
lines.push("Risk: " + (node.risk_score != null ? node.risk_score : "N/A"));
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function toVisNode(node) {
|
||||
var color = getStatusColor(node.status);
|
||||
// Scale node size by risk_score: min 12, max 40
|
||||
var risk = typeof node.risk_score === "number" ? node.risk_score : 0;
|
||||
var size = Math.max(12, Math.min(40, 12 + (risk / 100) * 28));
|
||||
|
||||
return {
|
||||
id: node.node_id,
|
||||
label: node.tool_name || "unknown",
|
||||
title: buildTooltip(node),
|
||||
size: size,
|
||||
color: {
|
||||
background: color.bg,
|
||||
border: color.border,
|
||||
highlight: { background: color.bg, border: "#ffffff" },
|
||||
hover: { background: color.bg, border: "#ffffff" },
|
||||
},
|
||||
font: {
|
||||
color: color.font,
|
||||
size: 11,
|
||||
face: "'JetBrains Mono', monospace",
|
||||
strokeWidth: 2,
|
||||
strokeColor: "#060a13",
|
||||
},
|
||||
borderWidth: 1,
|
||||
borderWidthSelected: 3,
|
||||
shape: "dot",
|
||||
_raw: node,
|
||||
};
|
||||
}
|
||||
|
||||
function buildEdges(nodes) {
|
||||
var edges = [];
|
||||
var seen = {};
|
||||
nodes.forEach(function (node) {
|
||||
if (!node.parent_node_ids) return;
|
||||
node.parent_node_ids.forEach(function (parentId) {
|
||||
var key = parentId + "|" + node.node_id;
|
||||
if (seen[key]) return;
|
||||
seen[key] = true;
|
||||
edges.push({
|
||||
from: parentId,
|
||||
to: node.node_id,
|
||||
color: {
|
||||
color: EDGE_COLOR,
|
||||
highlight: "#ffffff",
|
||||
hover: EDGE_COLOR,
|
||||
},
|
||||
width: 2,
|
||||
arrows: {
|
||||
to: { enabled: true, scaleFactor: 0.5 },
|
||||
},
|
||||
smooth: {
|
||||
enabled: true,
|
||||
type: "cubicBezier",
|
||||
roundness: 0.5,
|
||||
forceDirection: "vertical",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
return edges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and render an attack chain DAG.
|
||||
* Called from Rust via eval().
|
||||
* @param {Array} nodes - Array of AttackChainNode objects
|
||||
*/
|
||||
window.__loadAttackChain = function (nodes) {
|
||||
var container = document.getElementById("attack-chain-canvas");
|
||||
if (!container) {
|
||||
console.error("[attack-chain-viz] #attack-chain-canvas not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build lookup map
|
||||
rawNodesMap = {};
|
||||
nodes.forEach(function (n) {
|
||||
rawNodesMap[n.node_id] = n;
|
||||
});
|
||||
|
||||
var visNodes = nodes.map(toVisNode);
|
||||
var visEdges = buildEdges(nodes);
|
||||
|
||||
nodesDataset = new vis.DataSet(visNodes);
|
||||
edgesDataset = new vis.DataSet(visEdges);
|
||||
|
||||
var options = {
|
||||
nodes: {
|
||||
font: { color: "#e4eaf4", size: 11 },
|
||||
scaling: { min: 12, max: 40 },
|
||||
},
|
||||
edges: {
|
||||
font: { color: "#5e7291", size: 9, strokeWidth: 0 },
|
||||
selectionWidth: 3,
|
||||
},
|
||||
physics: {
|
||||
enabled: false,
|
||||
},
|
||||
layout: {
|
||||
hierarchical: {
|
||||
enabled: true,
|
||||
direction: "UD",
|
||||
sortMethod: "directed",
|
||||
levelSeparation: 120,
|
||||
nodeSpacing: 160,
|
||||
treeSpacing: 200,
|
||||
blockShifting: true,
|
||||
edgeMinimization: true,
|
||||
parentCentralization: true,
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
hover: true,
|
||||
tooltipDelay: 200,
|
||||
hideEdgesOnDrag: false,
|
||||
hideEdgesOnZoom: false,
|
||||
multiselect: false,
|
||||
navigationButtons: false,
|
||||
keyboard: { enabled: true },
|
||||
},
|
||||
};
|
||||
|
||||
// Destroy previous instance
|
||||
if (network) {
|
||||
network.destroy();
|
||||
}
|
||||
|
||||
network = new vis.Network(
|
||||
container,
|
||||
{ nodes: nodesDataset, edges: edgesDataset },
|
||||
options
|
||||
);
|
||||
|
||||
// Click handler — sends data to Rust
|
||||
network.on("click", function (params) {
|
||||
if (params.nodes.length > 0) {
|
||||
var nodeId = params.nodes[0];
|
||||
var visNode = nodesDataset.get(nodeId);
|
||||
if (visNode && visNode._raw && window.__onAttackNodeClick) {
|
||||
window.__onAttackNodeClick(JSON.stringify(visNode._raw));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(
|
||||
"[attack-chain-viz] Loaded " + nodes.length + " nodes, " + visEdges.length + " edges"
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback placeholder for Rust to set.
|
||||
* Called with JSON string of the clicked node's data.
|
||||
*/
|
||||
window.__onAttackNodeClick = null;
|
||||
|
||||
/**
|
||||
* Fit entire attack chain DAG in view.
|
||||
*/
|
||||
window.__fitAttackChain = function () {
|
||||
if (!network) return;
|
||||
network.fit({
|
||||
animation: { duration: 400, easingFunction: "easeInOutQuad" },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Select and focus on a specific node by node_id.
|
||||
*/
|
||||
window.__highlightAttackNode = function (nodeId) {
|
||||
if (!network || !nodesDataset) return;
|
||||
|
||||
var node = nodesDataset.get(nodeId);
|
||||
if (!node) return;
|
||||
|
||||
network.selectNodes([nodeId]);
|
||||
network.focus(nodeId, {
|
||||
scale: 1.5,
|
||||
animation: { duration: 500, easingFunction: "easeInOutQuad" },
|
||||
});
|
||||
|
||||
// Trigger click callback too
|
||||
if (node._raw && window.__onAttackNodeClick) {
|
||||
window.__onAttackNodeClick(JSON.stringify(node._raw));
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -2767,3 +2767,467 @@ tbody tr:last-child td {
|
||||
.sbom-diff-row-changed {
|
||||
border-left: 3px solid var(--warning);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════
|
||||
ATTACK CHAIN VISUALIZATION
|
||||
═══════════════════════════════════ */
|
||||
|
||||
/* KPI bar */
|
||||
.ac-kpi-bar {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.ac-kpi-card {
|
||||
flex: 1;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 12px 14px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ac-kpi-card:first-child { border-radius: 10px 0 0 10px; }
|
||||
.ac-kpi-card:last-child { border-radius: 0 10px 10px 0; }
|
||||
.ac-kpi-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
}
|
||||
.ac-kpi-card:nth-child(1)::before { background: var(--accent, #3b82f6); opacity: 0.4; }
|
||||
.ac-kpi-card:nth-child(2)::before { background: var(--danger, #dc2626); opacity: 0.5; }
|
||||
.ac-kpi-card:nth-child(3)::before { background: var(--success, #16a34a); opacity: 0.4; }
|
||||
.ac-kpi-card:nth-child(4)::before { background: var(--warning, #d97706); opacity: 0.4; }
|
||||
|
||||
.ac-kpi-value {
|
||||
font-family: var(--font-display);
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.ac-kpi-label {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 9px;
|
||||
color: var(--text-tertiary, #6b7280);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Phase progress rail */
|
||||
.ac-phase-rail {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 14px;
|
||||
position: relative;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.ac-phase-rail::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
height: 2px;
|
||||
background: var(--border-color);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.ac-rail-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
min-width: 56px;
|
||||
flex: 1;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.ac-rail-node:hover .ac-rail-dot { transform: scale(1.25); }
|
||||
.ac-rail-node.active .ac-rail-label { color: var(--accent, #3b82f6); }
|
||||
.ac-rail-node.active .ac-rail-dot { box-shadow: 0 0 0 3px rgba(59,130,246,0.2), 0 0 12px rgba(59,130,246,0.15); }
|
||||
|
||||
.ac-rail-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
border: 2.5px solid var(--bg-primary, #0f172a);
|
||||
transition: transform 0.2s cubic-bezier(0.16,1,0.3,1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ac-rail-dot.done { background: var(--success, #16a34a); box-shadow: 0 0 8px rgba(22,163,74,0.25); }
|
||||
.ac-rail-dot.running { background: var(--warning, #d97706); box-shadow: 0 0 10px rgba(217,119,6,0.35); animation: ac-dot-pulse 2s ease-in-out infinite; }
|
||||
.ac-rail-dot.pending { background: var(--text-tertiary, #6b7280); opacity: 0.5; }
|
||||
.ac-rail-dot.mixed { background: conic-gradient(var(--success, #16a34a) 0deg 270deg, var(--danger, #dc2626) 270deg 360deg); box-shadow: 0 0 8px rgba(22,163,74,0.2); }
|
||||
|
||||
@keyframes ac-dot-pulse {
|
||||
0%, 100% { box-shadow: 0 0 8px rgba(217,119,6,0.35); }
|
||||
50% { box-shadow: 0 0 18px rgba(217,119,6,0.55); }
|
||||
}
|
||||
|
||||
.ac-rail-label {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 9px;
|
||||
color: var(--text-tertiary, #6b7280);
|
||||
margin-top: 5px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.ac-rail-findings {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.ac-rail-findings.has { color: var(--danger, #dc2626); }
|
||||
.ac-rail-findings.none { color: var(--text-tertiary, #6b7280); opacity: 0.4; }
|
||||
|
||||
.ac-rail-heatmap {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.ac-hm-cell {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 1.5px;
|
||||
}
|
||||
.ac-hm-cell.ok { background: var(--success, #16a34a); opacity: 0.5; }
|
||||
.ac-hm-cell.fail { background: var(--danger, #dc2626); opacity: 0.65; }
|
||||
.ac-hm-cell.run { background: var(--warning, #d97706); opacity: 0.5; animation: ac-pulse 1.5s ease-in-out infinite; }
|
||||
.ac-hm-cell.wait { background: var(--text-tertiary, #6b7280); opacity: 0.15; }
|
||||
|
||||
.ac-rail-bar {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
margin-top: 7px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.ac-rail-bar-inner {
|
||||
height: 100%;
|
||||
border-radius: 1px;
|
||||
}
|
||||
.ac-rail-bar-inner.done { background: var(--success, #16a34a); opacity: 0.35; }
|
||||
.ac-rail-bar-inner.running { background: linear-gradient(to right, var(--success, #16a34a), var(--warning, #d97706)); opacity: 0.35; }
|
||||
|
||||
/* Progress track */
|
||||
.ac-progress-track {
|
||||
height: 3px;
|
||||
background: var(--border-color);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ac-progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(90deg, var(--success, #16a34a) 0%, var(--accent, #3b82f6) 100%);
|
||||
transition: width 0.6s cubic-bezier(0.16,1,0.3,1);
|
||||
}
|
||||
|
||||
/* Expand all controls */
|
||||
.ac-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.ac-btn-toggle {
|
||||
font-family: var(--font-body);
|
||||
font-size: 11px;
|
||||
color: var(--accent, #3b82f6);
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.ac-btn-toggle:hover {
|
||||
background: rgba(59,130,246,0.08);
|
||||
border-color: rgba(59,130,246,0.12);
|
||||
}
|
||||
|
||||
/* Phase accordion */
|
||||
.ac-phases {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.ac-phase {
|
||||
animation: ac-phase-in 0.35s cubic-bezier(0.16,1,0.3,1) both;
|
||||
}
|
||||
@keyframes ac-phase-in {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.ac-phase-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 14px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.ac-phase.open .ac-phase-header {
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
.ac-phase-header:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.ac-phase-num {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--accent, #3b82f6);
|
||||
background: rgba(59,130,246,0.08);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
border: 1px solid rgba(59,130,246,0.1);
|
||||
}
|
||||
|
||||
.ac-phase-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ac-phase-dots {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
}
|
||||
.ac-phase-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ac-phase-dot.completed { background: var(--success, #16a34a); }
|
||||
.ac-phase-dot.failed { background: var(--danger, #dc2626); }
|
||||
.ac-phase-dot.running { background: var(--warning, #d97706); animation: ac-pulse 1.5s ease-in-out infinite; }
|
||||
.ac-phase-dot.pending { background: var(--text-tertiary, #6b7280); opacity: 0.4; }
|
||||
|
||||
@keyframes ac-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
|
||||
.ac-phase-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.ac-phase-meta .findings-ct { color: var(--danger, #dc2626); font-weight: 600; }
|
||||
.ac-phase-meta .running-ct { color: var(--warning, #d97706); font-weight: 500; }
|
||||
|
||||
.ac-phase-chevron {
|
||||
color: var(--text-tertiary, #6b7280);
|
||||
font-size: 11px;
|
||||
transition: transform 0.25s cubic-bezier(0.16,1,0.3,1);
|
||||
width: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
.ac-phase.open .ac-phase-chevron {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.ac-phase-body {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.35s cubic-bezier(0.16,1,0.3,1);
|
||||
background: var(--bg-secondary);
|
||||
border-left: 1px solid var(--border-color);
|
||||
border-right: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
.ac-phase.open .ac-phase-body {
|
||||
max-height: 2000px;
|
||||
}
|
||||
.ac-phase-body-inner {
|
||||
padding: 4px 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
/* Tool rows */
|
||||
.ac-tool-row {
|
||||
display: grid;
|
||||
grid-template-columns: 5px 26px 1fr auto auto auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
.ac-tool-row:hover {
|
||||
background: rgba(255,255,255,0.02);
|
||||
}
|
||||
.ac-tool-row.expanded {
|
||||
background: rgba(59,130,246,0.03);
|
||||
}
|
||||
.ac-tool-row.is-pending {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ac-status-bar {
|
||||
width: 4px;
|
||||
height: 26px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ac-status-bar.completed { background: var(--success, #16a34a); }
|
||||
.ac-status-bar.failed { background: var(--danger, #dc2626); }
|
||||
.ac-status-bar.running { background: var(--warning, #d97706); animation: ac-pulse 1.5s ease-in-out infinite; }
|
||||
.ac-status-bar.pending { background: var(--text-tertiary, #6b7280); opacity: 0.25; }
|
||||
|
||||
.ac-tool-icon {
|
||||
font-size: 17px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
.ac-tool-info { min-width: 0; }
|
||||
.ac-tool-name {
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Category chips */
|
||||
.ac-cat-chip {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.ac-cat-chip.recon { color: #38bdf8; background: rgba(56,189,248,0.1); }
|
||||
.ac-cat-chip.api { color: #818cf8; background: rgba(129,140,248,0.1); }
|
||||
.ac-cat-chip.headers { color: #06b6d4; background: rgba(6,182,212,0.1); }
|
||||
.ac-cat-chip.csp { color: #d946ef; background: rgba(217,70,239,0.1); }
|
||||
.ac-cat-chip.cookies { color: #f59e0b; background: rgba(245,158,11,0.1); }
|
||||
.ac-cat-chip.logs { color: #78716c; background: rgba(120,113,108,0.1); }
|
||||
.ac-cat-chip.ratelimit { color: #64748b; background: rgba(100,116,139,0.1); }
|
||||
.ac-cat-chip.cors { color: #8b5cf6; background: rgba(139,92,246,0.1); }
|
||||
.ac-cat-chip.tls { color: #14b8a6; background: rgba(20,184,166,0.1); }
|
||||
.ac-cat-chip.redirect { color: #fb923c; background: rgba(251,146,60,0.1); }
|
||||
.ac-cat-chip.email { color: #0ea5e9; background: rgba(14,165,233,0.1); }
|
||||
.ac-cat-chip.auth { color: #f43f5e; background: rgba(244,63,94,0.1); }
|
||||
.ac-cat-chip.xss { color: #f97316; background: rgba(249,115,22,0.1); }
|
||||
.ac-cat-chip.sqli { color: #ef4444; background: rgba(239,68,68,0.1); }
|
||||
.ac-cat-chip.ssrf { color: #a855f7; background: rgba(168,85,247,0.1); }
|
||||
.ac-cat-chip.idor { color: #ec4899; background: rgba(236,72,153,0.1); }
|
||||
.ac-cat-chip.fuzzer { color: #a78bfa; background: rgba(167,139,250,0.1); }
|
||||
.ac-cat-chip.cve { color: #dc2626; background: rgba(220,38,38,0.1); }
|
||||
.ac-cat-chip.default { color: #94a3b8; background: rgba(148,163,184,0.1); }
|
||||
|
||||
.ac-tool-duration {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 10px;
|
||||
color: var(--text-tertiary, #6b7280);
|
||||
white-space: nowrap;
|
||||
min-width: 48px;
|
||||
text-align: right;
|
||||
}
|
||||
.ac-tool-duration.running-text {
|
||||
color: var(--warning, #d97706);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ac-findings-pill {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 22px;
|
||||
padding: 1px 7px;
|
||||
border-radius: 9px;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
}
|
||||
.ac-findings-pill.has { background: rgba(220,38,38,0.12); color: var(--danger, #dc2626); }
|
||||
.ac-findings-pill.zero { background: transparent; color: var(--text-tertiary, #6b7280); font-weight: 400; opacity: 0.5; }
|
||||
|
||||
.ac-risk-val {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
min-width: 32px;
|
||||
text-align: right;
|
||||
}
|
||||
.ac-risk-val.high { color: var(--danger, #dc2626); }
|
||||
.ac-risk-val.medium { color: var(--warning, #d97706); }
|
||||
.ac-risk-val.low { color: var(--text-secondary); }
|
||||
.ac-risk-val.none { color: transparent; }
|
||||
|
||||
/* Tool detail (expanded) */
|
||||
.ac-tool-detail {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.28s cubic-bezier(0.16,1,0.3,1);
|
||||
}
|
||||
.ac-tool-detail.open {
|
||||
max-height: 300px;
|
||||
}
|
||||
.ac-tool-detail-inner {
|
||||
padding: 6px 10px 10px 49px;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.ac-reasoning-block {
|
||||
background: rgba(59,130,246,0.03);
|
||||
border-left: 2px solid var(--accent, #3b82f6);
|
||||
padding: 7px 12px;
|
||||
border-radius: 0 6px 6px 0;
|
||||
font-style: italic;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.ac-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 3px 14px;
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 10px;
|
||||
}
|
||||
.ac-detail-label {
|
||||
color: var(--text-tertiary, #6b7280);
|
||||
text-transform: uppercase;
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.ac-detail-value {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user