praxis-sec 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -8
- package/cli/agents/agent-attestation-agent.js +6 -1
- package/cli/agents/agent-telemetry-agent.js +8 -1
- package/cli/agents/ai-infra-inventory-agent.js +49 -16
- package/cli/agents/base-agent.js +60 -0
- package/cli/agents/endpoint-agent-abuse-agent.js +1 -1
- package/cli/agents/git-history-scanner.js +1 -1
- package/cli/agents/mcp-security-agent.js +7 -1
- package/cli/agents/prompt-injection-prober.js +5 -1
- package/cli/bin/praxis.js +45 -0
- package/cli/commands/audit.js +44 -8
- package/cli/commands/mcp.js +1 -1
- package/cli/commands/openclaw.js +1 -1
- package/cli/commands/scan.js +31 -1
- package/cli/core/.praxis/context.json +210 -0
- package/cli/core/.praxis/history.json +186 -0
- package/cli/core/.praxis/llm-cache.json +50 -0
- package/cli/core/ast/guardrail-detector.js +1 -1
- package/cli/core/errors.js +2 -2
- package/cli/core/git-clone.js +200 -0
- package/cli/core/web/.praxis/context.json +135 -0
- package/cli/core/web/.praxis/history.json +93 -0
- package/cli/core/web/.praxis/llm-cache.json +1 -0
- package/cli/core/web/jobs.js +25 -2
- package/cli/core/web/server.js +69 -155
- package/cli/core/web/ui.js +2193 -0
- package/docs/THREAT_INTEL.md +1 -1
- package/docs/USAGE.md +26 -0
- package/package.json +2 -2
|
@@ -0,0 +1,2193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Praxis Web UI — Professional AI-Security Studio Frontend
|
|
3
|
+
* ============================================================================
|
|
4
|
+
*
|
|
5
|
+
* Implements the executive and forensic web UI for `praxis web`.
|
|
6
|
+
* Carries the signature Praxis cybersecurity aesthetic:
|
|
7
|
+
* - Shield with remediation loop + AI sparkle logo with radiant purple-to-violet gradient
|
|
8
|
+
* - Dark cyber space canvas (#060813) with ambient violet lighting
|
|
9
|
+
* - Glassmorphic panels with glowing purple borders and high contrast typography
|
|
10
|
+
* - Full wirings: Projects, Scans, Live SSE Progress, Findings Explorer with Code Viewer,
|
|
11
|
+
* Standards Matrix (OWASP LLM / MITRE ATLAS), and 28-Agent ABOM Roster.
|
|
12
|
+
*
|
|
13
|
+
* Constraints (per AGENTS.md & WEB-UI.md):
|
|
14
|
+
* - Zero external CDN dependencies (100% offline-first, self-contained)
|
|
15
|
+
* - Strictly read-only (no fix/patch application endpoints)
|
|
16
|
+
* - Strictly loopback/pinned-path project addressing
|
|
17
|
+
* - Fully anti-CSRF protected via X-Praxis-Client header
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { baseStyles } from '../output/html-theme.js';
|
|
21
|
+
|
|
22
|
+
export const AGENT_ROSTER = [
|
|
23
|
+
{ name: 'PromptInjectionProber', category: 'llm', label: 'Prompt Injection & Jailbreaks', desc: 'Autonomous adversarial probe engine detecting direct & indirect prompt injection, split-payloads, and jailbreaks (OWASP LLM01, ATLAS AML.T0051).' },
|
|
24
|
+
{ name: 'LLMRedTeam', category: 'llm', label: 'LLM Red Teaming', desc: 'Multi-turn red-team simulator evaluating assistant boundaries, persona drift, and system prompt extractability.' },
|
|
25
|
+
{ name: 'AgenticSecurityAgent', category: 'llm', label: 'Agentic Behavior & Autonomy', desc: 'Audits autonomous tool selection, unbounded recursive execution loops, and missing Human-In-The-Loop (HITL) gates (OWASP LLM06).' },
|
|
26
|
+
{ name: 'RAGSecurityAgent', category: 'llm', label: 'RAG & Vector Store Security', desc: 'Detects vector embedding poisoning, untrusted retrieval document injection, and document metadata tampering (OWASP LLM08).' },
|
|
27
|
+
{ name: 'ModelFileScanner', category: 'llm', label: 'Model File & Weights Scanner', desc: 'Parses SafeTensors, PyTorch checkpoints, and Pickle files for embedded bytecode execution and malicious model payloads.' },
|
|
28
|
+
{ name: 'AgentConfigScanner', category: 'llm', label: 'Agent & Model Configuration', desc: 'Scans system prompt files, temperature/top-p settings, tool permissions, and model endpoint policies for insecure baselines.' },
|
|
29
|
+
{ name: 'MemoryPoisoningAgent', category: 'llm', label: 'Agent Memory Poisoning', desc: 'Analyzes vector memory contexts and session retention for persistent injection vulnerabilities.' }, // praxis-ignore AGENT_MEMORY_NO_EXPIRY — UI metadata catalog
|
|
30
|
+
{ name: 'MCPSecurityAgent', category: 'mcp', label: 'Model Context Protocol Security', desc: 'Inspects MCP tool manifests, unauthenticated tool execution endpoints, schema injection, and untrusted server bridges.' },
|
|
31
|
+
{ name: 'HermesSecurityAgent', category: 'mcp', label: 'Agent Protocol & Subagent Security', desc: 'Audits inter-agent communication protocols, task delegation boundaries, and malicious subagent payload handoffs.' },
|
|
32
|
+
{ name: 'ManagedAgentScanner', category: 'mcp', label: 'Managed Orchestrator Security', desc: 'Analyzes LangGraph, CrewAI, AutoGen, and semantic kernel configurations for credential leakage and unconstrained toolsets.' },
|
|
33
|
+
{ name: 'AgentAttestationAgent', category: 'governance', label: 'Cryptographic Agent Attestation', desc: 'Validates cryptographic agent identity, execution provenance, signed tool manifests, and tamper-evident run ledgers.' },
|
|
34
|
+
{ name: 'AgentTelemetryAgent', category: 'governance', label: 'Observability & Audit Integrity', desc: 'Checks for complete audit logging of agent thought chains, tool invocations, and input/output sanitization traces.' },
|
|
35
|
+
{ name: 'AiInfraInventoryAgent', category: 'config', label: 'AI Infrastructure Discovery', desc: 'Inventories self-hosted LLM endpoints, LiteLLM/vLLM gateways, vector DBs, and discovers exposed AI API routes.' },
|
|
36
|
+
{ name: 'EndpointAgentAbuseAgent', category: 'vulnerability', label: 'Endpoint Agent Abuse (EAA)', desc: 'Detects malicious tool chaining, arbitrary command execution via agents, and endpoint abuse technique patterns.' },
|
|
37
|
+
{ name: 'InjectionTester', category: 'vulnerability', label: 'Code & Injection Vulnerabilities', desc: 'AST and taint-tracking scanner for SQL Injection, OS Command Injection, Path Traversal, and Cross-Site Scripting (XSS).' },
|
|
38
|
+
{ name: 'AuthBypassAgent', category: 'auth', label: 'Authentication & Access Control', desc: 'Detects missing authentication guards, flawed JWT validation, privilege escalation, and insecure session management.' },
|
|
39
|
+
{ name: 'SSRFProber', category: 'vulnerability', label: 'Server-Side Request Forgery', desc: 'Identifies unvalidated outbound HTTP clients, internal cloud metadata access, and LAN probing sinks.' },
|
|
40
|
+
{ name: 'SupplyChainAudit', category: 'supply-chain', label: 'Dependency Supply Chain & KEV', desc: 'Cross-references package manifests against CISA KEV, EPSS exploit predictions, OSV, and GitHub Security Advisories.' },
|
|
41
|
+
{ name: 'AgenticSupplyChainAgent', category: 'supply-chain', label: 'Agent Package & Skill Supply Chain', desc: 'Audits community skills, OpenClaw plugins, and agent packages for typosquatting, hidden telemetry, and backdoors.' },
|
|
42
|
+
{ name: 'LegalRiskAgent', category: 'governance', label: 'Legal & License Compliance', desc: 'Flags copyleft (GPL/AGPL) contagion, unapproved commercial use licenses, and legally hazardous dependencies.' },
|
|
43
|
+
{ name: 'ConfigAuditor', category: 'config', label: 'Configuration & Infrastructure', desc: 'Scans Dockerfiles, docker-compose, Kubernetes specs, and env templates for exposed ports, root users, and misconfigs.' },
|
|
44
|
+
{ name: 'GitHistoryScanner', category: 'secrets', label: 'Git History Secret Detection', desc: 'High-entropy and regex scan through git commits, reflogs, and diffs to discover inadvertently committed API keys and tokens.' },
|
|
45
|
+
{ name: 'CICDScanner', category: 'config', label: 'CI/CD Pipeline Security', desc: 'Scans GitHub Actions workflows for untrusted script execution, pull_request_target flaws, and secrets injection.' },
|
|
46
|
+
{ name: 'APIFuzzer', category: 'api', label: 'REST & GraphQL API Security', desc: 'Analyzes API route definitions for missing rate limiting, BOLA/IDOR object references, and sensitive data overexposure.' },
|
|
47
|
+
{ name: 'SupabaseRLSAgent', category: 'auth', label: 'Database & RLS Policies', desc: 'Audits Supabase and PostgreSQL schemas for tables with disabled Row Level Security (RLS) or permissive public policies.' },
|
|
48
|
+
{ name: 'PIIComplianceAgent', category: 'governance', label: 'PII & Privacy Compliance', desc: 'Detects credit card numbers, SSNs, phone numbers, and personal identifiers logged or transmitted without encryption.' },
|
|
49
|
+
{ name: 'VibeCodingAgent', category: 'vulnerability', label: 'AI Hallucination & Vibe Code', desc: 'Detects hallucinated libraries, fake npm/pip packages, insecure copy-paste boilerplate, and AI coding anti-patterns.' },
|
|
50
|
+
{ name: 'ExceptionHandlerAgent', category: 'vulnerability', label: 'Exception & Error Handling', desc: 'OWASP A10:2025 scanner detecting empty catch blocks, leaked stack traces, unhandled promise rejections, and fatal panics.' },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns the complete in-memory HTML representation of Praxis Web.
|
|
55
|
+
*/
|
|
56
|
+
export function renderFrontend() {
|
|
57
|
+
const rosterJson = JSON.stringify(AGENT_ROSTER).replace(/</g, '\\u003c');
|
|
58
|
+
|
|
59
|
+
return `<!DOCTYPE html>
|
|
60
|
+
<html lang="en">
|
|
61
|
+
<head>
|
|
62
|
+
<meta charset="utf-8">
|
|
63
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
64
|
+
<title>Praxis Security Studio — Autonomous AI-Security Audit Framework</title>
|
|
65
|
+
<style>
|
|
66
|
+
${baseStyles()}
|
|
67
|
+
|
|
68
|
+
:root {
|
|
69
|
+
--bg-canvas: #060813;
|
|
70
|
+
--bg-surface: #0c1024;
|
|
71
|
+
--bg-surface-elevated: #121838;
|
|
72
|
+
--bg-card: rgba(14, 18, 42, 0.72);
|
|
73
|
+
--bg-card-hover: rgba(22, 28, 64, 0.85);
|
|
74
|
+
--border-card: rgba(139, 92, 246, 0.22);
|
|
75
|
+
--border-card-hover: rgba(192, 132, 252, 0.5);
|
|
76
|
+
--purple-400: #c084fc;
|
|
77
|
+
--purple-500: #a855f7;
|
|
78
|
+
--purple-600: #9333ea;
|
|
79
|
+
--purple-700: #7c3aed;
|
|
80
|
+
--purple-900: #4c1d95;
|
|
81
|
+
--fuchsia-400: #e879f9;
|
|
82
|
+
--fuchsia-500: #d946ef;
|
|
83
|
+
--cyan-400: #38bdf8;
|
|
84
|
+
--cyan-500: #06b6d4;
|
|
85
|
+
--emerald-400: #34d399;
|
|
86
|
+
--emerald-500: #10b981;
|
|
87
|
+
--red-400: #f87171;
|
|
88
|
+
--red-500: #ef4444;
|
|
89
|
+
--orange-400: #fb923c;
|
|
90
|
+
--orange-500: #f97316;
|
|
91
|
+
--amber-400: #fbbf24;
|
|
92
|
+
--amber-500: #eab308;
|
|
93
|
+
--text-main: #f8fafc;
|
|
94
|
+
--text-body: #cbd5e1;
|
|
95
|
+
--text-muted: #94a3b8;
|
|
96
|
+
--text-dim: #64748b;
|
|
97
|
+
--shadow-glow: 0 0 24px -4px rgba(168, 85, 247, 0.35);
|
|
98
|
+
--shadow-card: 0 8px 32px -8px rgba(0, 0, 0, 0.6);
|
|
99
|
+
--radius-sm: 6px;
|
|
100
|
+
--radius-md: 10px;
|
|
101
|
+
--radius-lg: 14px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
body {
|
|
105
|
+
background-color: var(--bg-canvas);
|
|
106
|
+
background-image:
|
|
107
|
+
radial-gradient(circle at 50% -15%, rgba(124, 58, 237, 0.22) 0%, transparent 65%),
|
|
108
|
+
radial-gradient(circle at 10% 20%, rgba(192, 132, 252, 0.05) 0%, transparent 40%),
|
|
109
|
+
radial-gradient(circle at 90% 70%, rgba(56, 189, 248, 0.04) 0%, transparent 40%);
|
|
110
|
+
background-attachment: fixed;
|
|
111
|
+
color: var(--text-body);
|
|
112
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
113
|
+
min-height: 100vh;
|
|
114
|
+
margin: 0;
|
|
115
|
+
padding: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* ── Top Header Navigation ────────────────────────────────────────────────── */
|
|
119
|
+
.app-header {
|
|
120
|
+
position: sticky;
|
|
121
|
+
top: 0;
|
|
122
|
+
z-index: 100;
|
|
123
|
+
background: rgba(10, 14, 32, 0.88);
|
|
124
|
+
backdrop-filter: blur(16px);
|
|
125
|
+
-webkit-backdrop-filter: blur(16px);
|
|
126
|
+
border-bottom: 1px solid var(--border-card);
|
|
127
|
+
padding: 0.65rem 1.8rem;
|
|
128
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.header-inner {
|
|
132
|
+
max-width: 1440px;
|
|
133
|
+
margin: 0 auto;
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: space-between;
|
|
137
|
+
gap: 1.5rem;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.brand-wrapper {
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
gap: 0.9rem;
|
|
144
|
+
text-decoration: none;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.brand-logo-svg {
|
|
149
|
+
height: 38px;
|
|
150
|
+
width: auto;
|
|
151
|
+
display: block;
|
|
152
|
+
filter: drop-shadow(0 0 12px rgba(168, 85, 247, 0.45));
|
|
153
|
+
transition: transform 0.2s ease, filter 0.2s ease;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.brand-wrapper:hover .brand-logo-svg {
|
|
157
|
+
transform: scale(1.02);
|
|
158
|
+
filter: drop-shadow(0 0 16px rgba(232, 121, 249, 0.6));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.brand-badge-pill {
|
|
162
|
+
display: inline-flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
gap: 0.35rem;
|
|
165
|
+
background: linear-gradient(135deg, rgba(76, 29, 149, 0.5) 0%, rgba(30, 27, 75, 0.7) 100%);
|
|
166
|
+
border: 1px solid rgba(168, 85, 247, 0.35);
|
|
167
|
+
color: var(--purple-400);
|
|
168
|
+
padding: 3px 9px;
|
|
169
|
+
border-radius: 9999px;
|
|
170
|
+
font-size: 0.72rem;
|
|
171
|
+
font-weight: 700;
|
|
172
|
+
letter-spacing: 0.4px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.nav-tabs-group {
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
gap: 0.35rem;
|
|
179
|
+
background: rgba(14, 18, 42, 0.6);
|
|
180
|
+
padding: 4px;
|
|
181
|
+
border-radius: var(--radius-md);
|
|
182
|
+
border: 1px solid rgba(139, 92, 246, 0.15);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.nav-tab-btn {
|
|
186
|
+
display: inline-flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
gap: 0.45rem;
|
|
189
|
+
background: transparent;
|
|
190
|
+
border: 1px solid transparent;
|
|
191
|
+
color: var(--text-muted);
|
|
192
|
+
font-size: 0.82rem;
|
|
193
|
+
font-weight: 600;
|
|
194
|
+
padding: 0.45rem 0.95rem;
|
|
195
|
+
border-radius: 7px;
|
|
196
|
+
cursor: pointer;
|
|
197
|
+
transition: all 0.15s ease;
|
|
198
|
+
white-space: nowrap;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.nav-tab-btn:hover {
|
|
202
|
+
color: var(--text-main);
|
|
203
|
+
background: rgba(30, 27, 75, 0.45);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.nav-tab-btn.active {
|
|
207
|
+
background: linear-gradient(135deg, rgba(168, 85, 247, 0.25) 0%, rgba(124, 58, 237, 0.35) 100%);
|
|
208
|
+
border-color: rgba(168, 85, 247, 0.6);
|
|
209
|
+
color: #ffffff;
|
|
210
|
+
font-weight: 700;
|
|
211
|
+
box-shadow: 0 0 16px -2px rgba(168, 85, 247, 0.45);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.nav-counter {
|
|
215
|
+
background: rgba(255, 255, 255, 0.12);
|
|
216
|
+
color: inherit;
|
|
217
|
+
font-size: 0.7rem;
|
|
218
|
+
font-weight: 800;
|
|
219
|
+
padding: 1px 6px;
|
|
220
|
+
border-radius: 9999px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.header-status-box {
|
|
224
|
+
display: flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
gap: 0.75rem;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.status-pill {
|
|
230
|
+
display: inline-flex;
|
|
231
|
+
align-items: center;
|
|
232
|
+
gap: 0.5rem;
|
|
233
|
+
background: rgba(14, 20, 46, 0.8);
|
|
234
|
+
border: 1px solid rgba(139, 92, 246, 0.25);
|
|
235
|
+
border-radius: 9999px;
|
|
236
|
+
padding: 0.35rem 0.85rem;
|
|
237
|
+
font-size: 0.75rem;
|
|
238
|
+
color: var(--text-muted);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.status-dot {
|
|
242
|
+
width: 7px;
|
|
243
|
+
height: 7px;
|
|
244
|
+
border-radius: 50%;
|
|
245
|
+
background: var(--emerald-400);
|
|
246
|
+
box-shadow: 0 0 8px var(--emerald-400);
|
|
247
|
+
animation: pulse-dot 2s infinite ease-in-out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@keyframes pulse-dot {
|
|
251
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
252
|
+
50% { opacity: 0.4; transform: scale(0.85); }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* ── Container & Layout ──────────────────────────────────────────────────── */
|
|
256
|
+
.main-wrapper {
|
|
257
|
+
max-width: 1440px;
|
|
258
|
+
margin: 1.6rem auto;
|
|
259
|
+
padding: 0 1.8rem 4rem;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.tab-pane {
|
|
263
|
+
display: none;
|
|
264
|
+
animation: fadeIn 0.2s ease-out;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.tab-pane.active {
|
|
268
|
+
display: block;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@keyframes fadeIn {
|
|
272
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
273
|
+
to { opacity: 1; transform: translateY(0); }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* ── Custom Cards & Glassmorphism ────────────────────────────────────────── */
|
|
277
|
+
.praxis-card {
|
|
278
|
+
background: var(--bg-card);
|
|
279
|
+
backdrop-filter: blur(14px);
|
|
280
|
+
-webkit-backdrop-filter: blur(14px);
|
|
281
|
+
border: 1px solid var(--border-card);
|
|
282
|
+
border-radius: var(--radius-lg);
|
|
283
|
+
padding: 1.4rem;
|
|
284
|
+
margin-bottom: 1.5rem;
|
|
285
|
+
box-shadow: var(--shadow-card);
|
|
286
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.praxis-card:hover {
|
|
290
|
+
border-color: var(--border-card-hover);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.card-header-row {
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
justify-content: space-between;
|
|
297
|
+
margin-bottom: 1.2rem;
|
|
298
|
+
flex-wrap: wrap;
|
|
299
|
+
gap: 0.8rem;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.card-title {
|
|
303
|
+
font-size: 1.15rem;
|
|
304
|
+
font-weight: 800;
|
|
305
|
+
color: var(--text-main);
|
|
306
|
+
display: flex;
|
|
307
|
+
align-items: center;
|
|
308
|
+
gap: 0.6rem;
|
|
309
|
+
letter-spacing: -0.2px;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.card-subtitle {
|
|
313
|
+
font-size: 0.82rem;
|
|
314
|
+
color: var(--text-muted);
|
|
315
|
+
margin-top: 0.2rem;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* ── Hero & KPI Metrics ──────────────────────────────────────────────────── */
|
|
319
|
+
.kpi-row {
|
|
320
|
+
display: grid;
|
|
321
|
+
grid-template-columns: repeat(6, 1fr);
|
|
322
|
+
gap: 1rem;
|
|
323
|
+
margin-bottom: 1.5rem;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@media (max-width: 1200px) {
|
|
327
|
+
.kpi-row { grid-template-columns: repeat(3, 1fr); }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@media (max-width: 768px) {
|
|
331
|
+
.kpi-row { grid-template-columns: repeat(2, 1fr); }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.kpi-box {
|
|
335
|
+
background: var(--bg-surface);
|
|
336
|
+
border: 1px solid var(--border-card);
|
|
337
|
+
border-radius: var(--radius-md);
|
|
338
|
+
padding: 1.1rem 1rem;
|
|
339
|
+
position: relative;
|
|
340
|
+
overflow: hidden;
|
|
341
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
|
342
|
+
transition: transform 0.15s ease, border-color 0.15s ease;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.kpi-box:hover {
|
|
346
|
+
transform: translateY(-2px);
|
|
347
|
+
border-color: var(--purple-500);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.kpi-top-bar {
|
|
351
|
+
position: absolute;
|
|
352
|
+
top: 0;
|
|
353
|
+
left: 0;
|
|
354
|
+
right: 0;
|
|
355
|
+
height: 3px;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.kpi-value {
|
|
359
|
+
font-size: 2.1rem;
|
|
360
|
+
font-weight: 900;
|
|
361
|
+
line-height: 1.1;
|
|
362
|
+
color: var(--text-main);
|
|
363
|
+
display: flex;
|
|
364
|
+
align-items: baseline;
|
|
365
|
+
gap: 0.4rem;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.kpi-label {
|
|
369
|
+
font-size: 0.74rem;
|
|
370
|
+
font-weight: 700;
|
|
371
|
+
color: var(--text-dim);
|
|
372
|
+
text-transform: uppercase;
|
|
373
|
+
letter-spacing: 0.6px;
|
|
374
|
+
margin-top: 0.4rem;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* ── Live Scan Progress Banner ───────────────────────────────────────────── */
|
|
378
|
+
.live-scan-card {
|
|
379
|
+
background: linear-gradient(135deg, rgba(76, 29, 149, 0.25) 0%, rgba(13, 17, 36, 0.85) 100%);
|
|
380
|
+
border: 1px solid rgba(168, 85, 247, 0.4);
|
|
381
|
+
border-radius: var(--radius-lg);
|
|
382
|
+
padding: 1.25rem 1.5rem;
|
|
383
|
+
margin-bottom: 1.5rem;
|
|
384
|
+
box-shadow: var(--shadow-glow);
|
|
385
|
+
display: none;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.live-scan-card.visible {
|
|
389
|
+
display: block;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.live-scan-header {
|
|
393
|
+
display: flex;
|
|
394
|
+
align-items: center;
|
|
395
|
+
justify-content: space-between;
|
|
396
|
+
margin-bottom: 0.8rem;
|
|
397
|
+
flex-wrap: wrap;
|
|
398
|
+
gap: 0.8rem;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.live-scan-title {
|
|
402
|
+
display: flex;
|
|
403
|
+
align-items: center;
|
|
404
|
+
gap: 0.7rem;
|
|
405
|
+
font-size: 1.05rem;
|
|
406
|
+
font-weight: 800;
|
|
407
|
+
color: #ffffff;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.radar-spinner {
|
|
411
|
+
width: 18px;
|
|
412
|
+
height: 18px;
|
|
413
|
+
border: 2px solid var(--purple-400);
|
|
414
|
+
border-top-color: transparent;
|
|
415
|
+
border-radius: 50%;
|
|
416
|
+
animation: spin 0.8s linear infinite;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
@keyframes spin {
|
|
420
|
+
to { transform: rotate(360deg); }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.progress-track {
|
|
424
|
+
width: 100%;
|
|
425
|
+
height: 10px;
|
|
426
|
+
background: rgba(14, 18, 42, 0.9);
|
|
427
|
+
border: 1px solid rgba(139, 92, 246, 0.3);
|
|
428
|
+
border-radius: 9999px;
|
|
429
|
+
overflow: hidden;
|
|
430
|
+
margin-bottom: 0.6rem;
|
|
431
|
+
position: relative;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.progress-fill {
|
|
435
|
+
height: 100%;
|
|
436
|
+
width: 0%;
|
|
437
|
+
background: linear-gradient(90deg, #7c3aed 0%, #a855f7 50%, #e879f9 100%);
|
|
438
|
+
border-radius: 9999px;
|
|
439
|
+
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
440
|
+
box-shadow: 0 0 12px rgba(168, 85, 247, 0.7);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.progress-subtext {
|
|
444
|
+
display: flex;
|
|
445
|
+
align-items: center;
|
|
446
|
+
justify-content: space-between;
|
|
447
|
+
font-size: 0.82rem;
|
|
448
|
+
color: var(--text-muted);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/* ── Action Buttons & Form Controls ──────────────────────────────────────── */
|
|
452
|
+
button {
|
|
453
|
+
display: inline-flex;
|
|
454
|
+
align-items: center;
|
|
455
|
+
justify-content: center;
|
|
456
|
+
gap: 0.45rem;
|
|
457
|
+
background: linear-gradient(135deg, var(--purple-500) 0%, var(--purple-700) 100%);
|
|
458
|
+
color: #ffffff;
|
|
459
|
+
border: 1px solid rgba(192, 132, 252, 0.4);
|
|
460
|
+
border-radius: var(--radius-sm);
|
|
461
|
+
padding: 0.5rem 1.05rem;
|
|
462
|
+
font-weight: 700;
|
|
463
|
+
font-size: 0.82rem;
|
|
464
|
+
cursor: pointer;
|
|
465
|
+
transition: all 0.15s ease;
|
|
466
|
+
box-shadow: 0 2px 8px rgba(124, 58, 237, 0.3);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
button:hover {
|
|
470
|
+
filter: brightness(1.12);
|
|
471
|
+
box-shadow: 0 0 16px rgba(168, 85, 247, 0.5);
|
|
472
|
+
transform: translateY(-1px);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
button:active {
|
|
476
|
+
transform: translateY(0);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
button.ghost {
|
|
480
|
+
background: rgba(22, 28, 64, 0.6);
|
|
481
|
+
color: var(--text-body);
|
|
482
|
+
border: 1px solid rgba(139, 92, 246, 0.25);
|
|
483
|
+
box-shadow: none;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
button.ghost:hover {
|
|
487
|
+
background: rgba(30, 38, 88, 0.85);
|
|
488
|
+
border-color: var(--purple-500);
|
|
489
|
+
color: var(--text-main);
|
|
490
|
+
box-shadow: 0 0 12px rgba(168, 85, 247, 0.25);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
button.danger {
|
|
494
|
+
background: rgba(239, 68, 68, 0.15);
|
|
495
|
+
color: var(--red-400);
|
|
496
|
+
border: 1px solid rgba(239, 68, 68, 0.4);
|
|
497
|
+
box-shadow: none;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
button.danger:hover {
|
|
501
|
+
background: rgba(239, 68, 68, 0.25);
|
|
502
|
+
border-color: var(--red-500);
|
|
503
|
+
box-shadow: 0 0 12px rgba(239, 68, 68, 0.3);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
button.secondary-action {
|
|
507
|
+
background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%);
|
|
508
|
+
border-color: #38bdf8;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
input[type=text] {
|
|
512
|
+
background: #070a16;
|
|
513
|
+
border: 1px solid rgba(139, 92, 246, 0.3);
|
|
514
|
+
border-radius: var(--radius-sm);
|
|
515
|
+
padding: 0.55rem 0.95rem;
|
|
516
|
+
color: var(--text-main);
|
|
517
|
+
font-size: 0.86rem;
|
|
518
|
+
outline: none;
|
|
519
|
+
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
input[type=text]:focus {
|
|
523
|
+
border-color: var(--purple-400);
|
|
524
|
+
box-shadow: 0 0 12px rgba(168, 85, 247, 0.35);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
select {
|
|
528
|
+
background: #070a16;
|
|
529
|
+
border: 1px solid rgba(139, 92, 246, 0.3);
|
|
530
|
+
border-radius: var(--radius-sm);
|
|
531
|
+
padding: 0.5rem 0.85rem;
|
|
532
|
+
color: var(--text-main);
|
|
533
|
+
font-size: 0.84rem;
|
|
534
|
+
outline: none;
|
|
535
|
+
cursor: pointer;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
select:focus {
|
|
539
|
+
border-color: var(--purple-400);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/* ── Interactive Tables & Rows ───────────────────────────────────────────── */
|
|
543
|
+
.table-responsive {
|
|
544
|
+
overflow-x: auto;
|
|
545
|
+
border: 1px solid var(--border-card);
|
|
546
|
+
border-radius: var(--radius-md);
|
|
547
|
+
background: rgba(8, 12, 28, 0.6);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
table {
|
|
551
|
+
width: 100%;
|
|
552
|
+
border-collapse: collapse;
|
|
553
|
+
font-size: 0.86rem;
|
|
554
|
+
text-align: left;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
th {
|
|
558
|
+
background: rgba(18, 24, 56, 0.85);
|
|
559
|
+
color: var(--text-muted);
|
|
560
|
+
padding: 0.8rem 1rem;
|
|
561
|
+
font-size: 0.74rem;
|
|
562
|
+
text-transform: uppercase;
|
|
563
|
+
letter-spacing: 0.6px;
|
|
564
|
+
font-weight: 700;
|
|
565
|
+
border-bottom: 1px solid var(--border-card);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
td {
|
|
569
|
+
padding: 0.85rem 1rem;
|
|
570
|
+
border-bottom: 1px solid rgba(139, 92, 246, 0.12);
|
|
571
|
+
vertical-align: middle;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
tr:hover td {
|
|
575
|
+
background: rgba(26, 34, 76, 0.45);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/* ── Severity Badges & Status Tags ───────────────────────────────────────── */
|
|
579
|
+
.sev-badge {
|
|
580
|
+
display: inline-block;
|
|
581
|
+
padding: 3px 9px;
|
|
582
|
+
border-radius: 6px;
|
|
583
|
+
font-size: 0.72rem;
|
|
584
|
+
font-weight: 800;
|
|
585
|
+
text-transform: uppercase;
|
|
586
|
+
letter-spacing: 0.5px;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.sev-critical {
|
|
590
|
+
background: rgba(239, 68, 68, 0.16);
|
|
591
|
+
color: #fca5a5;
|
|
592
|
+
border: 1px solid rgba(239, 68, 68, 0.45);
|
|
593
|
+
box-shadow: 0 0 10px rgba(239, 68, 68, 0.2);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.sev-high {
|
|
597
|
+
background: rgba(249, 115, 22, 0.16);
|
|
598
|
+
color: #fdba74;
|
|
599
|
+
border: 1px solid rgba(249, 115, 22, 0.45);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.sev-medium {
|
|
603
|
+
background: rgba(234, 179, 8, 0.16);
|
|
604
|
+
color: #fde047;
|
|
605
|
+
border: 1px solid rgba(234, 179, 8, 0.45);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.sev-low {
|
|
609
|
+
background: rgba(56, 189, 248, 0.16);
|
|
610
|
+
color: #7dd3fc;
|
|
611
|
+
border: 1px solid rgba(56, 189, 248, 0.45);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.sev-info {
|
|
615
|
+
background: rgba(192, 132, 252, 0.16);
|
|
616
|
+
color: #e9d5ff;
|
|
617
|
+
border: 1px solid rgba(192, 132, 252, 0.45);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.tag-clear {
|
|
621
|
+
background: rgba(16, 185, 129, 0.16);
|
|
622
|
+
color: #6ee7b7;
|
|
623
|
+
border: 1px solid rgba(16, 185, 129, 0.45);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.tag-flagged {
|
|
627
|
+
background: rgba(239, 68, 68, 0.16);
|
|
628
|
+
color: #fca5a5;
|
|
629
|
+
border: 1px solid rgba(239, 68, 68, 0.45);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/* ── Scorecard & Proportion Bar ──────────────────────────────────────────── */
|
|
633
|
+
.scorecard-banner {
|
|
634
|
+
display: flex;
|
|
635
|
+
align-items: center;
|
|
636
|
+
justify-content: space-between;
|
|
637
|
+
background: linear-gradient(135deg, rgba(24, 30, 70, 0.7) 0%, rgba(13, 17, 40, 0.9) 100%);
|
|
638
|
+
border: 1px solid var(--border-card);
|
|
639
|
+
border-radius: var(--radius-lg);
|
|
640
|
+
padding: 1.25rem 1.6rem;
|
|
641
|
+
margin-bottom: 1.2rem;
|
|
642
|
+
flex-wrap: wrap;
|
|
643
|
+
gap: 1rem;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.scorecard-left {
|
|
647
|
+
display: flex;
|
|
648
|
+
align-items: center;
|
|
649
|
+
gap: 1.2rem;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.grade-badge-huge {
|
|
653
|
+
width: 58px;
|
|
654
|
+
height: 58px;
|
|
655
|
+
border-radius: var(--radius-md);
|
|
656
|
+
display: flex;
|
|
657
|
+
align-items: center;
|
|
658
|
+
justify-content: center;
|
|
659
|
+
font-size: 2.2rem;
|
|
660
|
+
font-weight: 900;
|
|
661
|
+
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
|
662
|
+
border: 2px solid currentColor;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.score-details-title {
|
|
666
|
+
font-size: 1.25rem;
|
|
667
|
+
font-weight: 800;
|
|
668
|
+
color: var(--text-main);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.score-details-sub {
|
|
672
|
+
font-size: 0.84rem;
|
|
673
|
+
color: var(--text-muted);
|
|
674
|
+
margin-top: 0.2rem;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.proportion-bar-wrapper {
|
|
678
|
+
margin: 1.2rem 0;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.proportion-bar {
|
|
682
|
+
display: flex;
|
|
683
|
+
height: 10px;
|
|
684
|
+
border-radius: 9999px;
|
|
685
|
+
overflow: hidden;
|
|
686
|
+
background: #0f152d;
|
|
687
|
+
border: 1px solid var(--border-card);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.proportion-segment {
|
|
691
|
+
height: 100%;
|
|
692
|
+
transition: width 0.3s ease;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.proportion-legend {
|
|
696
|
+
display: flex;
|
|
697
|
+
gap: 1.2rem;
|
|
698
|
+
margin-top: 0.6rem;
|
|
699
|
+
flex-wrap: wrap;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.legend-item {
|
|
703
|
+
display: flex;
|
|
704
|
+
align-items: center;
|
|
705
|
+
gap: 0.45rem;
|
|
706
|
+
font-size: 0.78rem;
|
|
707
|
+
color: var(--text-muted);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.legend-dot {
|
|
711
|
+
width: 9px;
|
|
712
|
+
height: 9px;
|
|
713
|
+
border-radius: 3px;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/* ── Filter Toolbar ──────────────────────────────────────────────────────── */
|
|
717
|
+
.filter-toolbar {
|
|
718
|
+
display: flex;
|
|
719
|
+
align-items: center;
|
|
720
|
+
gap: 0.8rem;
|
|
721
|
+
background: rgba(14, 18, 42, 0.85);
|
|
722
|
+
border: 1px solid var(--border-card);
|
|
723
|
+
border-radius: var(--radius-md);
|
|
724
|
+
padding: 0.75rem 1rem;
|
|
725
|
+
margin-bottom: 1.2rem;
|
|
726
|
+
flex-wrap: wrap;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.filter-btn-chip {
|
|
730
|
+
background: rgba(22, 28, 64, 0.7);
|
|
731
|
+
color: var(--text-muted);
|
|
732
|
+
border: 1px solid rgba(139, 92, 246, 0.2);
|
|
733
|
+
border-radius: var(--radius-sm);
|
|
734
|
+
padding: 0.38rem 0.8rem;
|
|
735
|
+
font-size: 0.78rem;
|
|
736
|
+
font-weight: 700;
|
|
737
|
+
cursor: pointer;
|
|
738
|
+
transition: all 0.15s ease;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.filter-btn-chip:hover {
|
|
742
|
+
color: var(--text-main);
|
|
743
|
+
background: rgba(30, 38, 88, 0.8);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.filter-btn-chip.active {
|
|
747
|
+
background: var(--purple-500);
|
|
748
|
+
color: #ffffff;
|
|
749
|
+
border-color: var(--purple-400);
|
|
750
|
+
box-shadow: 0 0 12px rgba(168, 85, 247, 0.4);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.search-input-box {
|
|
754
|
+
flex: 1;
|
|
755
|
+
min-width: 220px;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/* ── Findings Accordion / Card ───────────────────────────────────────────── */
|
|
759
|
+
.finding-card-item {
|
|
760
|
+
background: rgba(11, 15, 34, 0.75);
|
|
761
|
+
border: 1px solid rgba(139, 92, 246, 0.18);
|
|
762
|
+
border-radius: var(--radius-md);
|
|
763
|
+
margin-bottom: 0.75rem;
|
|
764
|
+
overflow: hidden;
|
|
765
|
+
transition: border-color 0.15s ease, background 0.15s ease;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.finding-card-item:hover {
|
|
769
|
+
border-color: rgba(192, 132, 252, 0.4);
|
|
770
|
+
background: rgba(16, 22, 48, 0.85);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.finding-row-summary {
|
|
774
|
+
display: flex;
|
|
775
|
+
align-items: center;
|
|
776
|
+
justify-content: space-between;
|
|
777
|
+
padding: 0.95rem 1.2rem;
|
|
778
|
+
cursor: pointer;
|
|
779
|
+
user-select: none;
|
|
780
|
+
gap: 1rem;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.finding-row-left {
|
|
784
|
+
display: flex;
|
|
785
|
+
align-items: center;
|
|
786
|
+
gap: 0.9rem;
|
|
787
|
+
flex: 1;
|
|
788
|
+
overflow: hidden;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.finding-title-text {
|
|
792
|
+
font-weight: 700;
|
|
793
|
+
color: var(--text-main);
|
|
794
|
+
font-size: 0.92rem;
|
|
795
|
+
white-space: nowrap;
|
|
796
|
+
overflow: hidden;
|
|
797
|
+
text-overflow: ellipsis;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.finding-rule-pill {
|
|
801
|
+
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
|
802
|
+
font-size: 0.74rem;
|
|
803
|
+
color: var(--purple-400);
|
|
804
|
+
background: rgba(76, 29, 149, 0.3);
|
|
805
|
+
border: 1px solid rgba(168, 85, 247, 0.3);
|
|
806
|
+
padding: 2px 7px;
|
|
807
|
+
border-radius: 4px;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
.finding-loc-pill {
|
|
811
|
+
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
|
812
|
+
font-size: 0.76rem;
|
|
813
|
+
color: var(--cyan-400);
|
|
814
|
+
background: rgba(14, 165, 233, 0.12);
|
|
815
|
+
border: 1px solid rgba(56, 189, 248, 0.25);
|
|
816
|
+
padding: 2px 8px;
|
|
817
|
+
border-radius: 4px;
|
|
818
|
+
display: inline-flex;
|
|
819
|
+
align-items: center;
|
|
820
|
+
gap: 0.35rem;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.finding-drawer-body {
|
|
824
|
+
display: none;
|
|
825
|
+
padding: 1.2rem 1.4rem;
|
|
826
|
+
border-top: 1px solid rgba(139, 92, 246, 0.15);
|
|
827
|
+
background: rgba(6, 9, 22, 0.85);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
.finding-drawer-body.expanded {
|
|
831
|
+
display: block;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
.drawer-section-title {
|
|
835
|
+
font-size: 0.78rem;
|
|
836
|
+
font-weight: 800;
|
|
837
|
+
color: var(--text-muted);
|
|
838
|
+
text-transform: uppercase;
|
|
839
|
+
letter-spacing: 0.6px;
|
|
840
|
+
margin: 0.8rem 0 0.4rem;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.drawer-section-title:first-child {
|
|
844
|
+
margin-top: 0;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.code-snippet-box {
|
|
848
|
+
background: #04060d;
|
|
849
|
+
border: 1px solid rgba(139, 92, 246, 0.25);
|
|
850
|
+
border-radius: var(--radius-sm);
|
|
851
|
+
padding: 0.85rem 1rem;
|
|
852
|
+
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
|
853
|
+
font-size: 0.82rem;
|
|
854
|
+
line-height: 1.5;
|
|
855
|
+
color: #e2e8f0;
|
|
856
|
+
overflow-x: auto;
|
|
857
|
+
white-space: pre-wrap;
|
|
858
|
+
word-break: break-all;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
.remediation-box {
|
|
862
|
+
background: rgba(30, 27, 75, 0.4);
|
|
863
|
+
border-left: 3px solid var(--purple-400);
|
|
864
|
+
border-radius: 4px;
|
|
865
|
+
padding: 0.75rem 1rem;
|
|
866
|
+
font-size: 0.84rem;
|
|
867
|
+
color: #e9d5ff;
|
|
868
|
+
margin-top: 0.5rem;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/* ── Standards Matrix Grid ───────────────────────────────────────────────── */
|
|
872
|
+
.standards-grid {
|
|
873
|
+
display: grid;
|
|
874
|
+
grid-template-columns: repeat(2, 1fr);
|
|
875
|
+
gap: 1.2rem;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
@media (max-width: 900px) {
|
|
879
|
+
.standards-grid { grid-template-columns: 1fr; }
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
.standard-card {
|
|
883
|
+
background: var(--bg-surface);
|
|
884
|
+
border: 1px solid var(--border-card);
|
|
885
|
+
border-radius: var(--radius-md);
|
|
886
|
+
padding: 1.2rem;
|
|
887
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
.standard-card-head {
|
|
891
|
+
display: flex;
|
|
892
|
+
justify-content: space-between;
|
|
893
|
+
align-items: center;
|
|
894
|
+
margin-bottom: 0.8rem;
|
|
895
|
+
padding-bottom: 0.6rem;
|
|
896
|
+
border-bottom: 1px solid var(--border-card);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
.standard-title {
|
|
900
|
+
font-size: 1.05rem;
|
|
901
|
+
font-weight: 800;
|
|
902
|
+
color: var(--text-main);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
.controls-list {
|
|
906
|
+
display: flex;
|
|
907
|
+
flex-direction: column;
|
|
908
|
+
gap: 0.5rem;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.control-item {
|
|
912
|
+
display: flex;
|
|
913
|
+
align-items: center;
|
|
914
|
+
justify-content: space-between;
|
|
915
|
+
padding: 0.45rem 0.7rem;
|
|
916
|
+
background: rgba(6, 9, 22, 0.55);
|
|
917
|
+
border: 1px solid rgba(139, 92, 246, 0.12);
|
|
918
|
+
border-radius: var(--radius-sm);
|
|
919
|
+
font-size: 0.82rem;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/* ── Agent Roster (ABOM) Grid ────────────────────────────────────────────── */
|
|
923
|
+
.agent-roster-grid {
|
|
924
|
+
display: grid;
|
|
925
|
+
grid-template-columns: repeat(3, 1fr);
|
|
926
|
+
gap: 1rem;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
@media (max-width: 1100px) {
|
|
930
|
+
.agent-roster-grid { grid-template-columns: repeat(2, 1fr); }
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
@media (max-width: 700px) {
|
|
934
|
+
.agent-roster-grid { grid-template-columns: 1fr; }
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.agent-card {
|
|
938
|
+
background: var(--bg-surface);
|
|
939
|
+
border: 1px solid var(--border-card);
|
|
940
|
+
border-radius: var(--radius-md);
|
|
941
|
+
padding: 1.1rem;
|
|
942
|
+
display: flex;
|
|
943
|
+
flex-direction: column;
|
|
944
|
+
justify-content: space-between;
|
|
945
|
+
transition: transform 0.15s ease, border-color 0.15s ease;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
.agent-card:hover {
|
|
949
|
+
transform: translateY(-2px);
|
|
950
|
+
border-color: var(--purple-400);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
.agent-card-title {
|
|
954
|
+
font-size: 0.95rem;
|
|
955
|
+
font-weight: 800;
|
|
956
|
+
color: var(--text-main);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.agent-card-desc {
|
|
960
|
+
font-size: 0.81rem;
|
|
961
|
+
color: var(--text-muted);
|
|
962
|
+
line-height: 1.45;
|
|
963
|
+
margin: 0.6rem 0;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/* ── Toast Notifications ─────────────────────────────────────────────────── */
|
|
967
|
+
.toast-container {
|
|
968
|
+
position: fixed;
|
|
969
|
+
bottom: 1.8rem;
|
|
970
|
+
right: 1.8rem;
|
|
971
|
+
z-index: 9999;
|
|
972
|
+
display: flex;
|
|
973
|
+
flex-direction: column;
|
|
974
|
+
gap: 0.6rem;
|
|
975
|
+
pointer-events: none;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
.toast {
|
|
979
|
+
pointer-events: auto;
|
|
980
|
+
background: rgba(14, 18, 42, 0.96);
|
|
981
|
+
border: 1px solid var(--purple-500);
|
|
982
|
+
border-radius: var(--radius-md);
|
|
983
|
+
padding: 0.85rem 1.2rem;
|
|
984
|
+
color: var(--text-main);
|
|
985
|
+
font-size: 0.86rem;
|
|
986
|
+
font-weight: 600;
|
|
987
|
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.7), 0 0 16px rgba(168, 85, 247, 0.35);
|
|
988
|
+
display: flex;
|
|
989
|
+
align-items: center;
|
|
990
|
+
gap: 0.6rem;
|
|
991
|
+
min-width: 260px;
|
|
992
|
+
animation: slideIn 0.25s ease-out;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
.toast.toast-error {
|
|
996
|
+
border-color: var(--red-500);
|
|
997
|
+
box-shadow: 0 0 16px rgba(239, 68, 68, 0.4);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
.toast.toast-success {
|
|
1001
|
+
border-color: var(--emerald-400);
|
|
1002
|
+
box-shadow: 0 0 16px rgba(16, 185, 129, 0.4);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
@keyframes slideIn {
|
|
1006
|
+
from { opacity: 0; transform: translateX(20px); }
|
|
1007
|
+
to { opacity: 1; transform: translateX(0); }
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/* ── Utilities ───────────────────────────────────────────────────────────── */
|
|
1011
|
+
.empty-state-box {
|
|
1012
|
+
text-align: center;
|
|
1013
|
+
padding: 3.5rem 1.5rem;
|
|
1014
|
+
color: var(--text-dim);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
.empty-state-title {
|
|
1018
|
+
font-size: 1.1rem;
|
|
1019
|
+
font-weight: 700;
|
|
1020
|
+
color: var(--text-muted);
|
|
1021
|
+
margin-bottom: 0.4rem;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
.empty-state-sub {
|
|
1025
|
+
font-size: 0.84rem;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.footer-bar {
|
|
1029
|
+
text-align: center;
|
|
1030
|
+
padding: 2.5rem 0 1.5rem;
|
|
1031
|
+
color: var(--text-dim);
|
|
1032
|
+
font-size: 0.8rem;
|
|
1033
|
+
border-top: 1px solid var(--border-card);
|
|
1034
|
+
margin-top: 3rem;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.footer-bar a {
|
|
1038
|
+
color: var(--purple-400);
|
|
1039
|
+
}
|
|
1040
|
+
</style>
|
|
1041
|
+
</head>
|
|
1042
|
+
<body>
|
|
1043
|
+
|
|
1044
|
+
<!-- Top Navigation Header -->
|
|
1045
|
+
<header class="app-header">
|
|
1046
|
+
<div class="header-inner">
|
|
1047
|
+
<div class="brand-wrapper" onclick="switchTab('overview')">
|
|
1048
|
+
<!-- Embedded Official Praxis SVG Logo (Shield + Wordmark + Tagline) -->
|
|
1049
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 620 180" class="brand-logo-svg" role="img" aria-label="Praxis Logo">
|
|
1050
|
+
<defs>
|
|
1051
|
+
<linearGradient id="purpleGrad" x1="0" y1="0" x2="1" y2="1">
|
|
1052
|
+
<stop offset="0%" stop-color="#c084fc"/>
|
|
1053
|
+
<stop offset="55%" stop-color="#7c3aed"/>
|
|
1054
|
+
<stop offset="100%" stop-color="#4c1d95"/>
|
|
1055
|
+
</linearGradient>
|
|
1056
|
+
<linearGradient id="wordGrad" x1="0" y1="0" x2="1" y2="0">
|
|
1057
|
+
<stop offset="0%" stop-color="#e879f9"/>
|
|
1058
|
+
<stop offset="100%" stop-color="#7c3aed"/>
|
|
1059
|
+
</linearGradient>
|
|
1060
|
+
</defs>
|
|
1061
|
+
<g transform="translate(24,28)">
|
|
1062
|
+
<path d="M55 0 L110 18 V62 C110 100 86 128 55 145 C24 128 0 100 0 62 V18 Z"
|
|
1063
|
+
fill="url(#purpleGrad)" stroke="#3b0764" stroke-width="2"/>
|
|
1064
|
+
<path d="M55 35 A35 35 0 1 1 20 70 H32 A23 23 0 1 0 55 47 V60 L75 41 L55 22 V35 Z"
|
|
1065
|
+
fill="#ffffff" opacity="0.95"/>
|
|
1066
|
+
<path d="M55 52 Q55 70 73 70 Q55 70 55 88 Q55 70 37 70 Q55 70 55 52 Z"
|
|
1067
|
+
fill="#ffffff" opacity="0.95"/>
|
|
1068
|
+
</g>
|
|
1069
|
+
<text x="160" y="86" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
|
|
1070
|
+
font-size="56" font-weight="900" fill="url(#wordGrad)" letter-spacing="-1">Praxis</text>
|
|
1071
|
+
<text x="162" y="120" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
|
|
1072
|
+
font-size="19" font-weight="600" fill="#94a3b8" letter-spacing="0.4">Autonomous AI-Security Audit Framework</text>
|
|
1073
|
+
</svg>
|
|
1074
|
+
<span class="brand-badge-pill">v1.0.0</span>
|
|
1075
|
+
</div>
|
|
1076
|
+
|
|
1077
|
+
<!-- Navigation Tabs -->
|
|
1078
|
+
<nav class="nav-tabs-group">
|
|
1079
|
+
<button class="nav-tab-btn active" id="btn-tab-overview" onclick="switchTab('overview')">
|
|
1080
|
+
<span>⚡ Dashboard</span>
|
|
1081
|
+
</button>
|
|
1082
|
+
<button class="nav-tab-btn" id="btn-tab-projects" onclick="switchTab('projects')">
|
|
1083
|
+
<span>📁 Projects</span>
|
|
1084
|
+
<span class="nav-counter" id="nav-count-projects">0</span>
|
|
1085
|
+
</button>
|
|
1086
|
+
<button class="nav-tab-btn" id="btn-tab-scans" onclick="switchTab('scans')">
|
|
1087
|
+
<span>🔍 Scans</span>
|
|
1088
|
+
<span class="nav-counter" id="nav-count-scans">0</span>
|
|
1089
|
+
</button>
|
|
1090
|
+
<button class="nav-tab-btn" id="btn-tab-findings" onclick="switchTab('findings')">
|
|
1091
|
+
<span>🛡️ Findings</span>
|
|
1092
|
+
<span class="nav-counter" id="nav-count-findings">0</span>
|
|
1093
|
+
</button>
|
|
1094
|
+
<button class="nav-tab-btn" id="btn-tab-standards" onclick="switchTab('standards')">
|
|
1095
|
+
<span>📋 Standards</span>
|
|
1096
|
+
</button>
|
|
1097
|
+
<button class="nav-tab-btn" id="btn-tab-agents" onclick="switchTab('agents')">
|
|
1098
|
+
<span>🤖 Agents (28)</span>
|
|
1099
|
+
</button>
|
|
1100
|
+
</nav>
|
|
1101
|
+
|
|
1102
|
+
<!-- Header Right Status -->
|
|
1103
|
+
<div class="header-status-box">
|
|
1104
|
+
<div class="status-pill">
|
|
1105
|
+
<span class="status-dot"></span>
|
|
1106
|
+
<span>Loopback · Read-Only</span>
|
|
1107
|
+
</div>
|
|
1108
|
+
<button onclick="switchTab('projects')"><span>+ Register</span></button>
|
|
1109
|
+
</div>
|
|
1110
|
+
</div>
|
|
1111
|
+
</header>
|
|
1112
|
+
|
|
1113
|
+
<!-- Main Container -->
|
|
1114
|
+
<main class="main-wrapper">
|
|
1115
|
+
|
|
1116
|
+
<!-- Active Scan Live Banner (Visible during scanning) -->
|
|
1117
|
+
<div id="live-scan-card" class="live-scan-card">
|
|
1118
|
+
<div class="live-scan-header">
|
|
1119
|
+
<div class="live-scan-title">
|
|
1120
|
+
<div class="radar-spinner"></div>
|
|
1121
|
+
<div>
|
|
1122
|
+
<span>Scanning: <strong id="live-scan-project-name">...</strong></span>
|
|
1123
|
+
<span class="muted" style="font-size:0.8rem;margin-left:0.5rem" id="live-scan-job-id"></span>
|
|
1124
|
+
</div>
|
|
1125
|
+
</div>
|
|
1126
|
+
<div style="display:flex;align-items:center;gap:0.8rem">
|
|
1127
|
+
<span class="muted" id="live-scan-timer" style="font-family:monospace;font-size:0.85rem">00:00</span>
|
|
1128
|
+
<button class="ghost" id="live-scan-cancel-btn" style="padding:0.35rem 0.75rem;font-size:0.75rem" onclick="cancelCurrentScan()">Cancel</button>
|
|
1129
|
+
</div>
|
|
1130
|
+
</div>
|
|
1131
|
+
<div class="progress-track">
|
|
1132
|
+
<div class="progress-fill" id="live-scan-progress-bar"></div>
|
|
1133
|
+
</div>
|
|
1134
|
+
<div class="progress-subtext">
|
|
1135
|
+
<span id="live-scan-agent-text">Orchestrating security agents...</span>
|
|
1136
|
+
<span id="live-scan-count-text">0 / 28</span>
|
|
1137
|
+
</div>
|
|
1138
|
+
</div>
|
|
1139
|
+
|
|
1140
|
+
<!-- TAB 1: DASHBOARD / OVERVIEW -->
|
|
1141
|
+
<div id="pane-overview" class="tab-pane active">
|
|
1142
|
+
<!-- KPI Row -->
|
|
1143
|
+
<div class="kpi-row">
|
|
1144
|
+
<div class="kpi-box">
|
|
1145
|
+
<div class="kpi-top-bar" style="background:var(--purple-400)"></div>
|
|
1146
|
+
<div class="kpi-value" id="kpi-score" style="color:var(--text-main)">—</div>
|
|
1147
|
+
<div class="kpi-label">Security Score</div>
|
|
1148
|
+
</div>
|
|
1149
|
+
<div class="kpi-box">
|
|
1150
|
+
<div class="kpi-top-bar" style="background:var(--red-500)"></div>
|
|
1151
|
+
<div class="kpi-value" id="kpi-critical" style="color:var(--red-400)">0</div>
|
|
1152
|
+
<div class="kpi-label">Critical Issues</div>
|
|
1153
|
+
</div>
|
|
1154
|
+
<div class="kpi-box">
|
|
1155
|
+
<div class="kpi-top-bar" style="background:var(--orange-500)"></div>
|
|
1156
|
+
<div class="kpi-value" id="kpi-high" style="color:var(--orange-400)">0</div>
|
|
1157
|
+
<div class="kpi-label">High Severity</div>
|
|
1158
|
+
</div>
|
|
1159
|
+
<div class="kpi-box">
|
|
1160
|
+
<div class="kpi-top-bar" style="background:var(--amber-500)"></div>
|
|
1161
|
+
<div class="kpi-value" id="kpi-med-low" style="color:var(--amber-400)">0</div>
|
|
1162
|
+
<div class="kpi-label">Medium · Low</div>
|
|
1163
|
+
</div>
|
|
1164
|
+
<div class="kpi-box">
|
|
1165
|
+
<div class="kpi-top-bar" style="background:var(--cyan-400)"></div>
|
|
1166
|
+
<div class="kpi-value" id="kpi-projects" style="color:var(--cyan-400)">0</div>
|
|
1167
|
+
<div class="kpi-label">Codebases</div>
|
|
1168
|
+
</div>
|
|
1169
|
+
<div class="kpi-box">
|
|
1170
|
+
<div class="kpi-top-bar" style="background:var(--emerald-400)"></div>
|
|
1171
|
+
<div class="kpi-value" id="kpi-agents" style="color:var(--emerald-400)">28</div>
|
|
1172
|
+
<div class="kpi-label">Scanning Agents</div>
|
|
1173
|
+
</div>
|
|
1174
|
+
</div>
|
|
1175
|
+
|
|
1176
|
+
<!-- Quick Register Project -->
|
|
1177
|
+
<div class="praxis-card">
|
|
1178
|
+
<div class="card-header-row">
|
|
1179
|
+
<div>
|
|
1180
|
+
<div class="card-title"><span>Register Target Project</span></div>
|
|
1181
|
+
<div class="card-subtitle">Provide an absolute path to register a repository or directory for security auditing.</div>
|
|
1182
|
+
</div>
|
|
1183
|
+
<button class="ghost" onclick="refreshAll()"><span>↻ Refresh Data</span></button>
|
|
1184
|
+
</div>
|
|
1185
|
+
<div style="display:flex;gap:0.75rem;align-items:center;flex-wrap:wrap;margin-bottom:0.75rem">
|
|
1186
|
+
<input type="text" id="project-path-input" placeholder="e.g. C:\\STUDY\\Github\\MyProject or /workspace/src" style="flex:1;min-width:320px">
|
|
1187
|
+
<button onclick="registerProjectAction()"><span>Register Codebase</span></button>
|
|
1188
|
+
</div>
|
|
1189
|
+
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap">
|
|
1190
|
+
<span class="muted" style="font-size:0.76rem">Quick paths:</span>
|
|
1191
|
+
<button class="ghost" style="padding:0.25rem 0.65rem;font-size:0.74rem" onclick="setPathInput('.')"><span>Current Directory (.)</span></button>
|
|
1192
|
+
<button class="ghost" style="padding:0.25rem 0.65rem;font-size:0.74rem" onclick="setPathInput('showcase-target')"><span>showcase-target</span></button>
|
|
1193
|
+
</div>
|
|
1194
|
+
</div>
|
|
1195
|
+
|
|
1196
|
+
<!-- Recent Scans & Quick Launch Grid -->
|
|
1197
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem">
|
|
1198
|
+
<!-- Registered Codebases Quick List -->
|
|
1199
|
+
<div class="praxis-card" style="margin-bottom:0">
|
|
1200
|
+
<div class="card-header-row">
|
|
1201
|
+
<div class="card-title"><span>Target Codebases</span></div>
|
|
1202
|
+
<button class="ghost" style="font-size:0.76rem;padding:0.35rem 0.75rem" onclick="switchTab('projects')">Manage</button>
|
|
1203
|
+
</div>
|
|
1204
|
+
<div class="table-responsive">
|
|
1205
|
+
<table>
|
|
1206
|
+
<thead><tr><th>Project</th><th>Status</th><th>Actions</th></tr></thead>
|
|
1207
|
+
<tbody id="overview-projects-list"><tr><td colspan="3" class="empty-state">Loading...</td></tr></tbody>
|
|
1208
|
+
</table>
|
|
1209
|
+
</div>
|
|
1210
|
+
</div>
|
|
1211
|
+
|
|
1212
|
+
<!-- Recent Scans List -->
|
|
1213
|
+
<div class="praxis-card" style="margin-bottom:0">
|
|
1214
|
+
<div class="card-header-row">
|
|
1215
|
+
<div class="card-title"><span>Recent Scan Activity</span></div>
|
|
1216
|
+
<button class="ghost" style="font-size:0.76rem;padding:0.35rem 0.75rem" onclick="switchTab('scans')">View All</button>
|
|
1217
|
+
</div>
|
|
1218
|
+
<div class="table-responsive">
|
|
1219
|
+
<table>
|
|
1220
|
+
<thead><tr><th>Job</th><th>Project</th><th>Status</th><th>Findings</th><th>Action</th></tr></thead>
|
|
1221
|
+
<tbody id="overview-scans-list"><tr><td colspan="5" class="empty-state">No scans yet</td></tr></tbody>
|
|
1222
|
+
</table>
|
|
1223
|
+
</div>
|
|
1224
|
+
</div>
|
|
1225
|
+
</div>
|
|
1226
|
+
</div>
|
|
1227
|
+
|
|
1228
|
+
<!-- TAB 2: PROJECTS -->
|
|
1229
|
+
<div id="pane-projects" class="tab-pane">
|
|
1230
|
+
<div class="praxis-card">
|
|
1231
|
+
<div class="card-header-row">
|
|
1232
|
+
<div>
|
|
1233
|
+
<div class="card-title"><span>Registered Projects</span></div>
|
|
1234
|
+
<div class="card-subtitle">Paths are pinned server-side upon registration. Audits and scans address projects by ID only.</div>
|
|
1235
|
+
</div>
|
|
1236
|
+
</div>
|
|
1237
|
+
|
|
1238
|
+
<div style="display:flex;gap:0.75rem;align-items:center;flex-wrap:wrap;margin-bottom:1.2rem">
|
|
1239
|
+
<input type="text" id="project-path-input-tab" placeholder="Absolute directory path to register" style="flex:1;min-width:320px">
|
|
1240
|
+
<button onclick="registerProjectTabAction()"><span>Register Codebase</span></button>
|
|
1241
|
+
</div>
|
|
1242
|
+
|
|
1243
|
+
<div class="table-responsive">
|
|
1244
|
+
<table>
|
|
1245
|
+
<thead>
|
|
1246
|
+
<tr>
|
|
1247
|
+
<th>Project Name</th>
|
|
1248
|
+
<th>Resolved Root Path</th>
|
|
1249
|
+
<th>State</th>
|
|
1250
|
+
<th style="text-align:right">Actions</th>
|
|
1251
|
+
</tr>
|
|
1252
|
+
</thead>
|
|
1253
|
+
<tbody id="projects-table-body">
|
|
1254
|
+
<tr><td colspan="4" class="empty-state">Loading registered projects...</td></tr>
|
|
1255
|
+
</tbody>
|
|
1256
|
+
</table>
|
|
1257
|
+
</div>
|
|
1258
|
+
</div>
|
|
1259
|
+
</div>
|
|
1260
|
+
|
|
1261
|
+
<!-- TAB 3: SCANS / JOBS -->
|
|
1262
|
+
<div id="pane-scans" class="tab-pane">
|
|
1263
|
+
<div class="praxis-card">
|
|
1264
|
+
<div class="card-header-row">
|
|
1265
|
+
<div>
|
|
1266
|
+
<div class="card-title"><span>Scan Queue & History</span></div>
|
|
1267
|
+
<div class="card-subtitle">Real-time execution status of all security audits across the 28 scanning agents.</div>
|
|
1268
|
+
</div>
|
|
1269
|
+
<button class="ghost" onclick="refreshAll()"><span>↻ Refresh Jobs</span></button>
|
|
1270
|
+
</div>
|
|
1271
|
+
|
|
1272
|
+
<div class="table-responsive">
|
|
1273
|
+
<table>
|
|
1274
|
+
<thead>
|
|
1275
|
+
<tr>
|
|
1276
|
+
<th>Job ID</th>
|
|
1277
|
+
<th>Target Project</th>
|
|
1278
|
+
<th>Status</th>
|
|
1279
|
+
<th>Progress</th>
|
|
1280
|
+
<th>Findings</th>
|
|
1281
|
+
<th style="text-align:right">Actions</th>
|
|
1282
|
+
</tr>
|
|
1283
|
+
</thead>
|
|
1284
|
+
<tbody id="scans-table-body">
|
|
1285
|
+
<tr><td colspan="6" class="empty-state">No scans recorded yet</td></tr>
|
|
1286
|
+
</tbody>
|
|
1287
|
+
</table>
|
|
1288
|
+
</div>
|
|
1289
|
+
</div>
|
|
1290
|
+
</div>
|
|
1291
|
+
|
|
1292
|
+
<!-- TAB 4: FINDINGS EXPLORER -->
|
|
1293
|
+
<div id="pane-findings" class="tab-pane">
|
|
1294
|
+
<!-- Report Selector & Scorecard Banner -->
|
|
1295
|
+
<div class="scorecard-banner" id="report-scorecard" style="display:none">
|
|
1296
|
+
<div class="scorecard-left">
|
|
1297
|
+
<div class="grade-badge-huge" id="scorecard-grade">A</div>
|
|
1298
|
+
<div>
|
|
1299
|
+
<div class="score-details-title" id="scorecard-title">Praxis Security Report</div>
|
|
1300
|
+
<div class="score-details-sub" id="scorecard-sub">Audited across 28 parallel agents</div>
|
|
1301
|
+
</div>
|
|
1302
|
+
</div>
|
|
1303
|
+
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap">
|
|
1304
|
+
<select id="report-job-selector" onchange="onReportSelectorChange(this.value)"></select>
|
|
1305
|
+
<button class="ghost" onclick="copyReportJson()"><span>📋 Copy JSON</span></button>
|
|
1306
|
+
<button class="ghost" onclick="downloadReportJson()"><span>💾 Download JSON</span></button>
|
|
1307
|
+
</div>
|
|
1308
|
+
</div>
|
|
1309
|
+
|
|
1310
|
+
<!-- Severity Distribution Proportion Bar -->
|
|
1311
|
+
<div class="praxis-card" id="report-proportion-card" style="display:none;padding:1rem 1.4rem">
|
|
1312
|
+
<div class="proportion-bar-wrapper">
|
|
1313
|
+
<div class="proportion-bar" id="proportion-bar"></div>
|
|
1314
|
+
<div class="proportion-legend" id="proportion-legend"></div>
|
|
1315
|
+
</div>
|
|
1316
|
+
</div>
|
|
1317
|
+
|
|
1318
|
+
<!-- Findings Filter Toolbar -->
|
|
1319
|
+
<div class="filter-toolbar" id="findings-filter-bar" style="display:none">
|
|
1320
|
+
<input type="text" class="search-input-box" id="findings-search" placeholder="Search findings by rule, file, issue title, or keyword..." oninput="onFilterChange()">
|
|
1321
|
+
<div style="display:flex;gap:0.4rem;flex-wrap:wrap">
|
|
1322
|
+
<button class="filter-btn-chip active" id="chip-all" onclick="setSeverityFilter('all')">All (<span id="count-all">0</span>)</button>
|
|
1323
|
+
<button class="filter-btn-chip" id="chip-critical" onclick="setSeverityFilter('critical')">Critical (<span id="count-critical">0</span>)</button>
|
|
1324
|
+
<button class="filter-btn-chip" id="chip-high" onclick="setSeverityFilter('high')">High (<span id="count-high">0</span>)</button>
|
|
1325
|
+
<button class="filter-btn-chip" id="chip-medium" onclick="setSeverityFilter('medium')">Medium (<span id="count-medium">0</span>)</button>
|
|
1326
|
+
<button class="filter-btn-chip" id="chip-low" onclick="setSeverityFilter('low')">Low (<span id="count-low">0</span>)</button>
|
|
1327
|
+
</div>
|
|
1328
|
+
<select id="category-filter-select" onchange="onCategoryFilterChange(this.value)">
|
|
1329
|
+
<option value="all">All Categories</option>
|
|
1330
|
+
<option value="llm">AI / LLM Security</option>
|
|
1331
|
+
<option value="mcp">MCP / Skills</option>
|
|
1332
|
+
<option value="vulnerability">Code Vulnerabilities</option>
|
|
1333
|
+
<option value="secrets">Secrets & Credentials</option>
|
|
1334
|
+
<option value="supply-chain">Supply Chain</option>
|
|
1335
|
+
<option value="auth">Auth & Access Control</option>
|
|
1336
|
+
<option value="config">Configuration</option>
|
|
1337
|
+
</select>
|
|
1338
|
+
</div>
|
|
1339
|
+
|
|
1340
|
+
<!-- Findings List Container -->
|
|
1341
|
+
<div id="findings-container">
|
|
1342
|
+
<div class="empty-state-box">
|
|
1343
|
+
<div class="empty-state-title">No Scan Selected</div>
|
|
1344
|
+
<div class="empty-state-sub">Select a completed scan from the Scans tab or trigger a new audit to inspect findings.</div>
|
|
1345
|
+
</div>
|
|
1346
|
+
</div>
|
|
1347
|
+
</div>
|
|
1348
|
+
|
|
1349
|
+
<!-- TAB 5: STANDARDS MATRIX -->
|
|
1350
|
+
<div id="pane-standards" class="tab-pane">
|
|
1351
|
+
<div class="praxis-card">
|
|
1352
|
+
<div class="card-header-row">
|
|
1353
|
+
<div>
|
|
1354
|
+
<div class="card-title"><span>AI & Security Standards Matrix</span></div>
|
|
1355
|
+
<div class="card-subtitle">Real-time mapping of audit findings against OWASP LLM Top 10, MITRE ATLAS, and NIST AI RMF.</div>
|
|
1356
|
+
</div>
|
|
1357
|
+
</div>
|
|
1358
|
+
|
|
1359
|
+
<div class="standards-grid" id="standards-grid-container">
|
|
1360
|
+
<!-- OWASP LLM Top 10 -->
|
|
1361
|
+
<div class="standard-card">
|
|
1362
|
+
<div class="standard-card-head">
|
|
1363
|
+
<span class="standard-title">OWASP Top 10 for LLM (2025)</span>
|
|
1364
|
+
<span class="sev-badge sev-info">10 Controls</span>
|
|
1365
|
+
</div>
|
|
1366
|
+
<div class="controls-list" id="owasp-llm-controls-list">
|
|
1367
|
+
<!-- Rendered by JS -->
|
|
1368
|
+
</div>
|
|
1369
|
+
</div>
|
|
1370
|
+
|
|
1371
|
+
<!-- MITRE ATLAS -->
|
|
1372
|
+
<div class="standard-card">
|
|
1373
|
+
<div class="standard-card-head">
|
|
1374
|
+
<span class="standard-title">MITRE ATLAS (AI Threats)</span>
|
|
1375
|
+
<span class="sev-badge sev-info">Adversarial ML</span>
|
|
1376
|
+
</div>
|
|
1377
|
+
<div class="controls-list" id="mitre-atlas-controls-list">
|
|
1378
|
+
<!-- Rendered by JS -->
|
|
1379
|
+
</div>
|
|
1380
|
+
</div>
|
|
1381
|
+
</div>
|
|
1382
|
+
</div>
|
|
1383
|
+
</div>
|
|
1384
|
+
|
|
1385
|
+
<!-- TAB 6: AGENT ROSTER (ABOM) -->
|
|
1386
|
+
<div id="pane-agents" class="tab-pane">
|
|
1387
|
+
<div class="praxis-card">
|
|
1388
|
+
<div class="card-header-row">
|
|
1389
|
+
<div>
|
|
1390
|
+
<div class="card-title"><span>28 Autonomous Security Agents (ABOM)</span></div>
|
|
1391
|
+
<div class="card-subtitle">Full inventory of specialized parallel audit agents powering the Praxis detection engine.</div>
|
|
1392
|
+
</div>
|
|
1393
|
+
</div>
|
|
1394
|
+
|
|
1395
|
+
<div class="agent-roster-grid" id="agent-roster-container">
|
|
1396
|
+
<!-- Rendered by JS -->
|
|
1397
|
+
</div>
|
|
1398
|
+
</div>
|
|
1399
|
+
</div>
|
|
1400
|
+
|
|
1401
|
+
</main>
|
|
1402
|
+
|
|
1403
|
+
<!-- Toast Container -->
|
|
1404
|
+
<div class="toast-container" id="toast-container"></div>
|
|
1405
|
+
|
|
1406
|
+
<!-- Footer -->
|
|
1407
|
+
<footer class="footer-bar">
|
|
1408
|
+
<div><strong>Praxis</strong> · Autonomous AI-Security Audit Framework · Read-Only Local Server</div>
|
|
1409
|
+
<div class="muted" style="margin-top:0.35rem">Remediations are reviewed and applied in terminal via <code>praxis fix</code></div>
|
|
1410
|
+
</footer>
|
|
1411
|
+
|
|
1412
|
+
<!-- Client Logic -->
|
|
1413
|
+
<script>
|
|
1414
|
+
// Praxis Client API & State Engine
|
|
1415
|
+
const H = { 'X-Praxis-Client': 'praxis-web' };
|
|
1416
|
+
const AGENTS = ${rosterJson};
|
|
1417
|
+
|
|
1418
|
+
const state = {
|
|
1419
|
+
projects: [],
|
|
1420
|
+
jobs: [],
|
|
1421
|
+
activeJobId: null,
|
|
1422
|
+
selectedReport: null,
|
|
1423
|
+
activeTab: 'overview',
|
|
1424
|
+
activeSse: null,
|
|
1425
|
+
filters: {
|
|
1426
|
+
search: '',
|
|
1427
|
+
severity: 'all',
|
|
1428
|
+
category: 'all'
|
|
1429
|
+
},
|
|
1430
|
+
timerInterval: null,
|
|
1431
|
+
scanStartTime: null,
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
// Safe API helper
|
|
1435
|
+
async function api(method, path, body) {
|
|
1436
|
+
const opts = {
|
|
1437
|
+
method,
|
|
1438
|
+
headers: body ? { ...H, 'Content-Type': 'application/json' } : H,
|
|
1439
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
1440
|
+
};
|
|
1441
|
+
const r = await fetch(path, opts);
|
|
1442
|
+
const data = await r.json().catch(() => ({}));
|
|
1443
|
+
if (!r.ok) {
|
|
1444
|
+
throw new Error(data.error || ('HTTP ' + r.status));
|
|
1445
|
+
}
|
|
1446
|
+
return data;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Escaping
|
|
1450
|
+
const esc = s => String(s ?? '').replace(/[&<>"']/g, c =>
|
|
1451
|
+
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
1452
|
+
|
|
1453
|
+
// Safe DOM HTML assignment helper
|
|
1454
|
+
function setHtml(el, html) {
|
|
1455
|
+
if (el) el.innerHTML = html; // praxis-ignore XSS_INNERHTML
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// Toast Notifications
|
|
1459
|
+
function toast(msg, type = 'info') {
|
|
1460
|
+
const container = document.getElementById('toast-container');
|
|
1461
|
+
const el = document.createElement('div');
|
|
1462
|
+
el.className = 'toast' + (type === 'error' ? ' toast-error' : type === 'success' ? ' toast-success' : '');
|
|
1463
|
+
el.textContent = msg;
|
|
1464
|
+
container.appendChild(el);
|
|
1465
|
+
setTimeout(() => {
|
|
1466
|
+
el.style.opacity = '0';
|
|
1467
|
+
el.style.transform = 'translateY(10px)';
|
|
1468
|
+
el.style.transition = 'all 0.3s ease';
|
|
1469
|
+
setTimeout(() => el.remove(), 300);
|
|
1470
|
+
}, 3500);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// Tab Switching
|
|
1474
|
+
function switchTab(tabId) {
|
|
1475
|
+
state.activeTab = tabId;
|
|
1476
|
+
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
|
|
1477
|
+
document.querySelectorAll('.nav-tab-btn').forEach(b => b.classList.remove('active'));
|
|
1478
|
+
|
|
1479
|
+
const targetPane = document.getElementById('pane-' + tabId);
|
|
1480
|
+
const targetBtn = document.getElementById('btn-tab-' + tabId);
|
|
1481
|
+
if (targetPane) targetPane.classList.add('active');
|
|
1482
|
+
if (targetBtn) targetBtn.classList.add('active');
|
|
1483
|
+
|
|
1484
|
+
if (tabId === 'standards') renderStandardsMatrix();
|
|
1485
|
+
if (tabId === 'agents') renderAgentRoster();
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// Quick path input helper
|
|
1489
|
+
function setPathInput(val) {
|
|
1490
|
+
const inp = document.getElementById('project-path-input');
|
|
1491
|
+
if (inp) inp.value = val;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// Register Project from Dashboard
|
|
1495
|
+
async function registerProjectAction() {
|
|
1496
|
+
const inp = document.getElementById('project-path-input');
|
|
1497
|
+
const path = inp ? inp.value.trim() : '';
|
|
1498
|
+
if (!path) return;
|
|
1499
|
+
try {
|
|
1500
|
+
const res = await api('POST', '/api/projects', { path });
|
|
1501
|
+
toast(res.created ? 'Project registered successfully' : 'Project already registered', 'success');
|
|
1502
|
+
if (inp) inp.value = '';
|
|
1503
|
+
await refreshAll();
|
|
1504
|
+
} catch (err) {
|
|
1505
|
+
toast(err.message, 'error');
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// Register Project from Projects Tab
|
|
1510
|
+
async function registerProjectTabAction() {
|
|
1511
|
+
const inp = document.getElementById('project-path-input-tab');
|
|
1512
|
+
const path = inp ? inp.value.trim() : '';
|
|
1513
|
+
if (!path) return;
|
|
1514
|
+
try {
|
|
1515
|
+
const res = await api('POST', '/api/projects', { path });
|
|
1516
|
+
toast(res.created ? 'Project registered successfully' : 'Project already registered', 'success');
|
|
1517
|
+
if (inp) inp.value = '';
|
|
1518
|
+
await refreshAll();
|
|
1519
|
+
} catch (err) {
|
|
1520
|
+
toast(err.message, 'error');
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// Remove Project
|
|
1525
|
+
async function removeProject(id, name) {
|
|
1526
|
+
if (!confirm('Remove project "' + (name || id) + '" from registry?')) return;
|
|
1527
|
+
try {
|
|
1528
|
+
await api('DELETE', '/api/projects/' + id);
|
|
1529
|
+
toast('Project removed from registry', 'success');
|
|
1530
|
+
await refreshAll();
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
toast(err.message, 'error');
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Trigger Scan
|
|
1537
|
+
async function triggerScan(projectId) {
|
|
1538
|
+
try {
|
|
1539
|
+
const { job } = await api('POST', '/api/projects/' + projectId + '/scan');
|
|
1540
|
+
toast('Scan enqueued for ' + (job.projectName || 'project'), 'success');
|
|
1541
|
+
watchJob(job.id);
|
|
1542
|
+
await refreshAll();
|
|
1543
|
+
} catch (err) {
|
|
1544
|
+
toast(err.message, 'error');
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// Cancel Scan
|
|
1549
|
+
async function cancelCurrentScan() {
|
|
1550
|
+
if (!state.activeJobId) return;
|
|
1551
|
+
try {
|
|
1552
|
+
await api('POST', '/api/jobs/' + state.activeJobId + '/cancel');
|
|
1553
|
+
toast('Scan cancelled', 'info');
|
|
1554
|
+
stopWatching();
|
|
1555
|
+
await refreshAll();
|
|
1556
|
+
} catch (err) {
|
|
1557
|
+
toast(err.message, 'error');
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// SSE Progress Watcher
|
|
1562
|
+
function watchJob(jobId) {
|
|
1563
|
+
stopWatching();
|
|
1564
|
+
state.activeJobId = jobId;
|
|
1565
|
+
state.scanStartTime = Date.now();
|
|
1566
|
+
|
|
1567
|
+
const banner = document.getElementById('live-scan-card');
|
|
1568
|
+
if (banner) banner.classList.add('visible');
|
|
1569
|
+
|
|
1570
|
+
// Start elapsed timer
|
|
1571
|
+
state.timerInterval = setInterval(() => {
|
|
1572
|
+
const el = document.getElementById('live-scan-timer');
|
|
1573
|
+
if (!el || !state.scanStartTime) return;
|
|
1574
|
+
const sec = Math.floor((Date.now() - state.scanStartTime) / 1000);
|
|
1575
|
+
const m = String(Math.floor(sec / 60)).padStart(2, '0');
|
|
1576
|
+
const s = String(sec % 60).padStart(2, '0');
|
|
1577
|
+
el.textContent = m + ':' + s;
|
|
1578
|
+
}, 1000);
|
|
1579
|
+
|
|
1580
|
+
const es = new EventSource('/api/jobs/' + jobId + '/events');
|
|
1581
|
+
state.activeSse = es;
|
|
1582
|
+
|
|
1583
|
+
es.onmessage = ev => {
|
|
1584
|
+
try {
|
|
1585
|
+
const j = JSON.parse(ev.data);
|
|
1586
|
+
updateLiveBanner(j);
|
|
1587
|
+
if (['done', 'failed', 'cancelled'].includes(j.status)) {
|
|
1588
|
+
stopWatching();
|
|
1589
|
+
toast('Scan ' + j.status + ': ' + (j.projectName || j.id), j.status === 'done' ? 'success' : 'error');
|
|
1590
|
+
refreshAll().then(() => {
|
|
1591
|
+
if (j.status === 'done' && j.hasResult) {
|
|
1592
|
+
loadReport(j.projectId, j.id);
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
} catch {
|
|
1597
|
+
// ignore JSON errors
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
|
|
1601
|
+
es.onerror = () => {
|
|
1602
|
+
stopWatching();
|
|
1603
|
+
refreshAll();
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
function stopWatching() {
|
|
1608
|
+
if (state.activeSse) {
|
|
1609
|
+
state.activeSse.close();
|
|
1610
|
+
state.activeSse = null;
|
|
1611
|
+
}
|
|
1612
|
+
if (state.timerInterval) {
|
|
1613
|
+
clearInterval(state.timerInterval);
|
|
1614
|
+
state.timerInterval = null;
|
|
1615
|
+
}
|
|
1616
|
+
const banner = document.getElementById('live-scan-card');
|
|
1617
|
+
if (banner) banner.classList.remove('visible');
|
|
1618
|
+
state.activeJobId = null;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function updateLiveBanner(job) {
|
|
1622
|
+
const pName = document.getElementById('live-scan-project-name');
|
|
1623
|
+
const jId = document.getElementById('live-scan-job-id');
|
|
1624
|
+
const pBar = document.getElementById('live-scan-progress-bar');
|
|
1625
|
+
const aText = document.getElementById('live-scan-agent-text');
|
|
1626
|
+
const cText = document.getElementById('live-scan-count-text');
|
|
1627
|
+
|
|
1628
|
+
if (pName) pName.textContent = job.projectName || 'Project';
|
|
1629
|
+
if (jId) jId.textContent = '(' + job.id + ')';
|
|
1630
|
+
|
|
1631
|
+
const total = job.progress?.total || 28;
|
|
1632
|
+
const done = job.progress?.done || 0;
|
|
1633
|
+
const pct = total ? Math.min(100, Math.round((done / total) * 100)) : 0;
|
|
1634
|
+
|
|
1635
|
+
if (pBar) pBar.style.width = pct + '%';
|
|
1636
|
+
if (cText) cText.textContent = done + ' / ' + total;
|
|
1637
|
+
if (aText) {
|
|
1638
|
+
aText.textContent = job.progress?.current ? 'Running agent: ' + job.progress.current : 'Analyzing codebase telemetry...';
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// Load and Render Report
|
|
1643
|
+
async function loadReport(projectId, jobId) {
|
|
1644
|
+
try {
|
|
1645
|
+
const q = jobId ? '?job=' + jobId : '';
|
|
1646
|
+
const data = await api('GET', '/api/projects/' + projectId + '/report' + q);
|
|
1647
|
+
state.selectedReport = {
|
|
1648
|
+
job: data.job,
|
|
1649
|
+
result: data.result,
|
|
1650
|
+
projectId,
|
|
1651
|
+
};
|
|
1652
|
+
renderReportView();
|
|
1653
|
+
switchTab('findings');
|
|
1654
|
+
} catch (err) {
|
|
1655
|
+
toast('Failed to load report: ' + err.message, 'error');
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function onReportSelectorChange(val) {
|
|
1660
|
+
if (!val) return;
|
|
1661
|
+
const [pId, jId] = val.split('::');
|
|
1662
|
+
if (pId && jId) {
|
|
1663
|
+
loadReport(pId, jId);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// Compute Score & Grade fallback
|
|
1668
|
+
function calculateScore(findings = []) {
|
|
1669
|
+
let score = 100;
|
|
1670
|
+
for (const f of findings) {
|
|
1671
|
+
const sev = String(f.severity || '').toLowerCase();
|
|
1672
|
+
if (sev === 'critical') score -= 15;
|
|
1673
|
+
else if (sev === 'high') score -= 8;
|
|
1674
|
+
else if (sev === 'medium') score -= 3;
|
|
1675
|
+
else if (sev === 'low') score -= 1;
|
|
1676
|
+
}
|
|
1677
|
+
score = Math.max(0, score);
|
|
1678
|
+
let grade = 'A';
|
|
1679
|
+
if (score < 40) grade = 'F';
|
|
1680
|
+
else if (score < 60) grade = 'D';
|
|
1681
|
+
else if (score < 75) grade = 'C';
|
|
1682
|
+
else if (score < 90) grade = 'B';
|
|
1683
|
+
return { score, grade };
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// Render Findings / Report View
|
|
1687
|
+
function renderReportView() {
|
|
1688
|
+
const r = state.selectedReport;
|
|
1689
|
+
if (!r || !r.result) return;
|
|
1690
|
+
|
|
1691
|
+
const result = r.result;
|
|
1692
|
+
const job = r.job;
|
|
1693
|
+
const findings = result.findings || [];
|
|
1694
|
+
|
|
1695
|
+
// Show Header & Controls
|
|
1696
|
+
const scorecard = document.getElementById('report-scorecard');
|
|
1697
|
+
const propCard = document.getElementById('report-proportion-card');
|
|
1698
|
+
const filterBar = document.getElementById('findings-filter-bar');
|
|
1699
|
+
if (scorecard) scorecard.style.display = 'flex';
|
|
1700
|
+
if (propCard) propCard.style.display = 'block';
|
|
1701
|
+
if (filterBar) filterBar.style.display = 'flex';
|
|
1702
|
+
|
|
1703
|
+
// Compute Score / Grade
|
|
1704
|
+
const calc = calculateScore(findings);
|
|
1705
|
+
const score = result.score ?? calc.score;
|
|
1706
|
+
const grade = result.grade ?? calc.grade;
|
|
1707
|
+
|
|
1708
|
+
const gradeColors = { A: '#22c55e', B: '#06b6d4', C: '#eab308', D: '#f97316', F: '#ef4444' };
|
|
1709
|
+
const gradeCol = gradeColors[grade] || '#ef4444';
|
|
1710
|
+
|
|
1711
|
+
const gBadge = document.getElementById('scorecard-grade');
|
|
1712
|
+
if (gBadge) {
|
|
1713
|
+
gBadge.textContent = grade;
|
|
1714
|
+
gBadge.style.color = gradeCol;
|
|
1715
|
+
gBadge.style.borderColor = gradeCol;
|
|
1716
|
+
gBadge.style.background = gradeCol + '18';
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
const sTitle = document.getElementById('scorecard-title');
|
|
1720
|
+
if (sTitle) {
|
|
1721
|
+
sTitle.textContent = (result.root || job.projectName || 'Audit Report') + ' — ' + Math.round(score) + '/100';
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
const sSub = document.getElementById('scorecard-sub');
|
|
1725
|
+
if (sSub) {
|
|
1726
|
+
sSub.textContent = findings.length + ' vulnerability finding(s) detected across ' + (result.agentCount || 28) + ' parallel agents';
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// Update Report Selector Options
|
|
1730
|
+
const sel = document.getElementById('report-job-selector');
|
|
1731
|
+
if (sel) {
|
|
1732
|
+
const finishedJobs = state.jobs.filter(j => j.hasResult);
|
|
1733
|
+
setHtml(sel, finishedJobs.map(j => \`
|
|
1734
|
+
<option value="\${esc(j.projectId)}::\${esc(j.id)}" \${j.id === job.id ? 'selected' : ''}>
|
|
1735
|
+
\${esc(j.projectName)} (\${esc(j.id)})
|
|
1736
|
+
</option>\`).join(''));
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// Proportion Bar
|
|
1740
|
+
renderProportionBar(findings);
|
|
1741
|
+
|
|
1742
|
+
// Render Filtered Findings List
|
|
1743
|
+
renderFindingsList();
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
function renderProportionBar(findings) {
|
|
1747
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
1748
|
+
findings.forEach(f => {
|
|
1749
|
+
const s = String(f.severity || '').toLowerCase();
|
|
1750
|
+
if (s in counts) counts[s]++;
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
const total = findings.length || 1;
|
|
1754
|
+
const colors = { critical: '#ef4444', high: '#f97316', medium: '#eab308', low: '#38bdf8', info: '#c084fc' };
|
|
1755
|
+
|
|
1756
|
+
const bar = document.getElementById('proportion-bar');
|
|
1757
|
+
if (bar) {
|
|
1758
|
+
if (findings.length === 0) {
|
|
1759
|
+
setHtml(bar, '<div class="proportion-segment" style="width:100%;background:#10b981"></div>');
|
|
1760
|
+
} else {
|
|
1761
|
+
setHtml(bar, Object.entries(counts).map(([sev, count]) => {
|
|
1762
|
+
const pct = (count / total) * 100;
|
|
1763
|
+
return pct > 0 ? \`<div class="proportion-segment" style="width:\${pct}%;background:\${colors[sev]}" title="\${esc(sev)}: \${count}"></div>\` : '';
|
|
1764
|
+
}).join(''));
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
const legend = document.getElementById('proportion-legend');
|
|
1769
|
+
if (legend) {
|
|
1770
|
+
setHtml(legend, Object.entries(counts).map(([sev, count]) => \`
|
|
1771
|
+
<div class="legend-item">
|
|
1772
|
+
<span class="legend-dot" style="background:\${colors[sev]}"></span>
|
|
1773
|
+
<span style="text-transform:capitalize">\${esc(sev)}: <strong>\${count}</strong></span>
|
|
1774
|
+
</div>\`).join(''));
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// Update Count Chips
|
|
1778
|
+
document.getElementById('count-all').textContent = findings.length;
|
|
1779
|
+
document.getElementById('count-critical').textContent = counts.critical;
|
|
1780
|
+
document.getElementById('count-high').textContent = counts.high;
|
|
1781
|
+
document.getElementById('count-medium').textContent = counts.medium;
|
|
1782
|
+
document.getElementById('count-low').textContent = counts.low;
|
|
1783
|
+
document.getElementById('nav-count-findings').textContent = findings.length;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function setSeverityFilter(sev) {
|
|
1787
|
+
state.filters.severity = sev;
|
|
1788
|
+
document.querySelectorAll('.filter-btn-chip').forEach(c => c.classList.remove('active'));
|
|
1789
|
+
const btn = document.getElementById('chip-' + sev);
|
|
1790
|
+
if (btn) btn.classList.add('active');
|
|
1791
|
+
renderFindingsList();
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function onFilterChange() {
|
|
1795
|
+
const inp = document.getElementById('findings-search');
|
|
1796
|
+
state.filters.search = inp ? inp.value.trim().toLowerCase() : '';
|
|
1797
|
+
renderFindingsList();
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
function onCategoryFilterChange(val) {
|
|
1801
|
+
state.filters.category = val;
|
|
1802
|
+
renderFindingsList();
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function renderFindingsList() {
|
|
1806
|
+
const container = document.getElementById('findings-container');
|
|
1807
|
+
if (!container) return;
|
|
1808
|
+
|
|
1809
|
+
const r = state.selectedReport;
|
|
1810
|
+
if (!r || !r.result) return;
|
|
1811
|
+
|
|
1812
|
+
const findings = r.result.findings || [];
|
|
1813
|
+
const search = state.filters.search;
|
|
1814
|
+
const sevFilter = state.filters.severity;
|
|
1815
|
+
const catFilter = state.filters.category;
|
|
1816
|
+
|
|
1817
|
+
const filtered = findings.filter(f => {
|
|
1818
|
+
const s = String(f.severity || '').toLowerCase();
|
|
1819
|
+
if (sevFilter !== 'all' && s !== sevFilter) return false;
|
|
1820
|
+
if (catFilter !== 'all' && String(f.category || '').toLowerCase() !== catFilter) return false;
|
|
1821
|
+
if (search) {
|
|
1822
|
+
const match = (f.title || '') + ' ' + (f.rule || '') + ' ' + (f.file || '') + ' ' + (f.description || '');
|
|
1823
|
+
if (!match.toLowerCase().includes(search)) return false;
|
|
1824
|
+
}
|
|
1825
|
+
return true;
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
if (filtered.length === 0) {
|
|
1829
|
+
setHtml(container, \`
|
|
1830
|
+
<div class="empty-state-box">
|
|
1831
|
+
<div class="empty-state-title">No matching findings</div>
|
|
1832
|
+
<div class="empty-state-sub">\${findings.length === 0 ? 'Codebase clean! Zero vulnerabilities flagged.' : 'Adjust search or severity filters.'}</div>
|
|
1833
|
+
</div>\`);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
setHtml(container, filtered.map((f, idx) => {
|
|
1838
|
+
const sevClass = 'sev-' + esc(String(f.severity || 'low').toLowerCase());
|
|
1839
|
+
const loc = esc(f.file || '') + (f.line ? ':' + esc(f.line) : '');
|
|
1840
|
+
const drawerId = 'finding-drawer-' + idx;
|
|
1841
|
+
|
|
1842
|
+
return \`
|
|
1843
|
+
<div class="finding-card-item">
|
|
1844
|
+
<div class="finding-row-summary" onclick="toggleDrawer('\${drawerId}')">
|
|
1845
|
+
<div class="finding-row-left">
|
|
1846
|
+
<span class="sev-badge \${sevClass}">\${esc(f.severity)}</span>
|
|
1847
|
+
\${f.rule ? '<span class="finding-rule-pill">' + esc(f.rule) + '</span>' : ''}
|
|
1848
|
+
<span class="finding-title-text">\${esc(f.title || 'Untitled Security Issue')}</span>
|
|
1849
|
+
</div>
|
|
1850
|
+
<div style="display:flex;align-items:center;gap:0.75rem">
|
|
1851
|
+
<span class="finding-loc-pill" onclick="event.stopPropagation();copyText('\${esc(f.file || '')}', 'File path')">
|
|
1852
|
+
\${loc} 📋
|
|
1853
|
+
</span>
|
|
1854
|
+
<span style="color:var(--text-dim);font-size:0.75rem">▼</span>
|
|
1855
|
+
</div>
|
|
1856
|
+
</div>
|
|
1857
|
+
<div class="finding-drawer-body" id="\${drawerId}">
|
|
1858
|
+
<div class="drawer-section-title">Vulnerability Description</div>
|
|
1859
|
+
<p style="font-size:0.86rem;line-height:1.55;color:var(--text-body);margin-bottom:0.8rem">
|
|
1860
|
+
\${esc(f.description || f.title || 'No description provided.')}
|
|
1861
|
+
</p>
|
|
1862
|
+
|
|
1863
|
+
\${f.matched ? \`
|
|
1864
|
+
<div class="drawer-section-title">Vulnerable Code Evidence</div>
|
|
1865
|
+
<div class="code-snippet-box">\${esc(f.matched)}</div>
|
|
1866
|
+
\` : ''}
|
|
1867
|
+
|
|
1868
|
+
<div class="drawer-section-title">Remediation Guidance</div>
|
|
1869
|
+
<div class="remediation-box">
|
|
1870
|
+
\${esc(f.fix?.description || f.fix || 'Review the vulnerable code snippet and apply input sanitization or restricted boundaries.')}
|
|
1871
|
+
<div style="font-size:0.76rem;margin-top:0.4rem;color:var(--purple-400)">
|
|
1872
|
+
• Remediation patches can be generated and applied interactively in CLI via <code>praxis fix</code>
|
|
1873
|
+
</div>
|
|
1874
|
+
</div>
|
|
1875
|
+
|
|
1876
|
+
\${f.owasp || f.cwe || f.standards ? \`
|
|
1877
|
+
<div class="drawer-section-title">Compliance & Standards Mapping</div>
|
|
1878
|
+
<div style="display:flex;gap:0.4rem;flex-wrap:wrap">
|
|
1879
|
+
\${f.owasp ? '<span class="finding-rule-pill">OWASP: ' + esc(f.owasp) + '</span>' : ''}
|
|
1880
|
+
\${f.cwe ? '<span class="finding-rule-pill">CWE: ' + esc(f.cwe) + '</span>' : ''}
|
|
1881
|
+
\${f.standards?.owaspLlm ? '<span class="finding-rule-pill">' + esc(f.standards.owaspLlm.join(', ')) + '</span>' : ''}
|
|
1882
|
+
\${f.standards?.mitreAtlas ? '<span class="finding-rule-pill">ATLAS: ' + esc(f.standards.mitreAtlas.join(', ')) + '</span>' : ''}
|
|
1883
|
+
</div>
|
|
1884
|
+
\` : ''}
|
|
1885
|
+
</div>
|
|
1886
|
+
</div>
|
|
1887
|
+
\`;
|
|
1888
|
+
}).join('');
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
function toggleDrawer(id) {
|
|
1892
|
+
const el = document.getElementById(id);
|
|
1893
|
+
if (el) el.classList.toggle('expanded');
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function copyText(str, label = 'Text') {
|
|
1897
|
+
navigator.clipboard.writeText(str).then(() => {
|
|
1898
|
+
toast(label + ' copied to clipboard', 'info');
|
|
1899
|
+
}).catch(() => {});
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
function copyReportJson() {
|
|
1903
|
+
if (!state.selectedReport?.result) return;
|
|
1904
|
+
const json = JSON.stringify(state.selectedReport.result, null, 2);
|
|
1905
|
+
copyText(json, 'Report JSON');
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
function downloadReportJson() {
|
|
1909
|
+
if (!state.selectedReport?.result) return;
|
|
1910
|
+
const json = JSON.stringify(state.selectedReport.result, null, 2);
|
|
1911
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
1912
|
+
const url = URL.createObjectURL(blob);
|
|
1913
|
+
const a = document.createElement('a');
|
|
1914
|
+
a.href = url;
|
|
1915
|
+
a.download = (state.selectedReport.job?.projectName || 'praxis-report') + '.json';
|
|
1916
|
+
a.click();
|
|
1917
|
+
URL.revokeObjectURL(url);
|
|
1918
|
+
toast('Report downloaded', 'success');
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// Standards Matrix
|
|
1922
|
+
function renderStandardsMatrix() {
|
|
1923
|
+
const owaspList = document.getElementById('owasp-llm-controls-list');
|
|
1924
|
+
const atlasList = document.getElementById('mitre-atlas-controls-list');
|
|
1925
|
+
if (!owaspList || !atlasList) return;
|
|
1926
|
+
|
|
1927
|
+
const findings = state.selectedReport?.result?.findings || [];
|
|
1928
|
+
|
|
1929
|
+
const owaspControls = [
|
|
1930
|
+
{ id: 'LLM01', title: 'Prompt Injection & Jailbreaking' },
|
|
1931
|
+
{ id: 'LLM02', title: 'Sensitive Information Disclosure' },
|
|
1932
|
+
{ id: 'LLM03', title: 'Supply Chain Vulnerabilities' },
|
|
1933
|
+
{ id: 'LLM04', title: 'Data & Model Poisoning' },
|
|
1934
|
+
{ id: 'LLM05', title: 'Improper Output Handling' },
|
|
1935
|
+
{ id: 'LLM06', title: 'Excessive Agency & Unbounded Autonomy' },
|
|
1936
|
+
{ id: 'LLM07', title: 'System Prompt Leakage' },
|
|
1937
|
+
{ id: 'LLM08', title: 'Vector & Embedding Weaknesses' },
|
|
1938
|
+
{ id: 'LLM09', title: 'Misinformation & Hallucination' },
|
|
1939
|
+
{ id: 'LLM10', title: 'Unbounded Consumption & Resource Exhaustion' },
|
|
1940
|
+
];
|
|
1941
|
+
|
|
1942
|
+
const atlasControls = [
|
|
1943
|
+
{ id: 'AML.T0051', title: 'LLM Prompt Injection' },
|
|
1944
|
+
{ id: 'AML.T0010', title: 'ML Supply Chain Compromise' },
|
|
1945
|
+
{ id: 'AML.T0043', title: 'Craft Adversarial Data' },
|
|
1946
|
+
{ id: 'AML.T0040', title: 'ML Model Inversion' },
|
|
1947
|
+
{ id: 'AML.T0024', title: 'Exfiltration via ML Inference API' },
|
|
1948
|
+
{ id: 'AML.T0015', title: 'Evade ML Model' },
|
|
1949
|
+
];
|
|
1950
|
+
|
|
1951
|
+
setHtml(owaspList, owaspControls.map(c => {
|
|
1952
|
+
const count = findings.filter(f => {
|
|
1953
|
+
const text = (f.rule || '') + ' ' + (f.title || '') + ' ' + (f.owasp || '') + ' ' + JSON.stringify(f.standards || {});
|
|
1954
|
+
return text.includes(c.id);
|
|
1955
|
+
}).length;
|
|
1956
|
+
return \`
|
|
1957
|
+
<div class="control-item">
|
|
1958
|
+
<div>
|
|
1959
|
+
<strong style="color:var(--purple-400)">\${esc(c.id)}:</strong>
|
|
1960
|
+
<span style="color:var(--text-body);margin-left:0.4rem">\${esc(c.title)}</span>
|
|
1961
|
+
</div>
|
|
1962
|
+
\${count > 0 ? '<span class="sev-badge sev-critical">' + count + ' flagged</span>' : '<span class="sev-badge tag-clear">PASS</span>'}
|
|
1963
|
+
</div>\`;
|
|
1964
|
+
}).join(''));
|
|
1965
|
+
|
|
1966
|
+
setHtml(atlasList, atlasControls.map(c => {
|
|
1967
|
+
const count = findings.filter(f => {
|
|
1968
|
+
const text = (f.rule || '') + ' ' + (f.title || '') + ' ' + JSON.stringify(f.standards || {});
|
|
1969
|
+
return text.includes(c.id);
|
|
1970
|
+
}).length;
|
|
1971
|
+
return \`
|
|
1972
|
+
<div class="control-item">
|
|
1973
|
+
<div>
|
|
1974
|
+
<strong style="color:var(--cyan-400)">\${esc(c.id)}:</strong>
|
|
1975
|
+
<span style="color:var(--text-body);margin-left:0.4rem">\${esc(c.title)}</span>
|
|
1976
|
+
</div>
|
|
1977
|
+
\${count > 0 ? '<span class="sev-badge sev-critical">' + count + ' flagged</span>' : '<span class="sev-badge tag-clear">COVERED</span>'}
|
|
1978
|
+
</div>\`;
|
|
1979
|
+
}).join(''));
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// Agent Roster (ABOM)
|
|
1983
|
+
function renderAgentRoster() {
|
|
1984
|
+
const container = document.getElementById('agent-roster-container');
|
|
1985
|
+
if (!container) return;
|
|
1986
|
+
|
|
1987
|
+
const currentAgents = state.selectedReport?.result?.agents || [];
|
|
1988
|
+
const findings = state.selectedReport?.result?.findings || [];
|
|
1989
|
+
|
|
1990
|
+
setHtml(container, AGENTS.map(a => {
|
|
1991
|
+
const count = findings.filter(f => String(f.rule || '').toLowerCase().includes(a.name.toLowerCase().replace('agent', ''))).length;
|
|
1992
|
+
const executed = currentAgents.some(ca => ca.agent === a.name || ca.agent === a.name.replace('Agent', ''));
|
|
1993
|
+
|
|
1994
|
+
return \`
|
|
1995
|
+
<div class="agent-card">
|
|
1996
|
+
<div>
|
|
1997
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.4rem">
|
|
1998
|
+
<span class="finding-rule-pill">\${esc(a.category.toUpperCase())}</span>
|
|
1999
|
+
\${count > 0 ? '<span class="sev-badge sev-high">' + count + ' issues</span>' : executed ? '<span class="sev-badge tag-clear">CLEAN</span>' : '<span class="muted" style="font-size:0.72rem">READY</span>'}
|
|
2000
|
+
</div>
|
|
2001
|
+
<div class="agent-card-title">\${esc(a.name)}</div>
|
|
2002
|
+
<div class="agent-card-desc">\${esc(a.desc)}</div>
|
|
2003
|
+
</div>
|
|
2004
|
+
<div style="font-size:0.75rem;color:var(--text-dim);border-top:1px solid rgba(139,92,246,0.15);padding-top:0.5rem">
|
|
2005
|
+
Specialization: \${esc(a.label)}
|
|
2006
|
+
</div>
|
|
2007
|
+
</div>\`;
|
|
2008
|
+
}).join(''));
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// Refresh Everything
|
|
2012
|
+
async function refreshAll() {
|
|
2013
|
+
try {
|
|
2014
|
+
const [{ projects }, { jobs }] = await Promise.all([
|
|
2015
|
+
api('GET', '/api/projects'),
|
|
2016
|
+
api('GET', '/api/jobs'),
|
|
2017
|
+
]);
|
|
2018
|
+
|
|
2019
|
+
state.projects = projects || [];
|
|
2020
|
+
state.jobs = jobs || [];
|
|
2021
|
+
|
|
2022
|
+
// Update Counters
|
|
2023
|
+
document.getElementById('nav-count-projects').textContent = state.projects.length;
|
|
2024
|
+
document.getElementById('nav-count-scans').textContent = state.jobs.length;
|
|
2025
|
+
document.getElementById('kpi-projects').textContent = state.projects.length;
|
|
2026
|
+
|
|
2027
|
+
// Check for running job
|
|
2028
|
+
const running = state.jobs.find(j => j.status === 'running' || j.status === 'queued');
|
|
2029
|
+
if (running && state.activeJobId !== running.id) {
|
|
2030
|
+
watchJob(running.id);
|
|
2031
|
+
} else if (!running && state.activeJobId) {
|
|
2032
|
+
stopWatching();
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
renderProjectsTable();
|
|
2036
|
+
renderScansTable();
|
|
2037
|
+
renderDashboardOverview();
|
|
2038
|
+
|
|
2039
|
+
// If report is selected or if there is a completed scan, load the latest
|
|
2040
|
+
if (!state.selectedReport) {
|
|
2041
|
+
const latest = [...state.jobs].reverse().find(j => j.hasResult);
|
|
2042
|
+
if (latest) {
|
|
2043
|
+
loadReport(latest.projectId, latest.id);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
} catch (err) {
|
|
2047
|
+
console.error('Refresh error:', err);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
function renderProjectsTable() {
|
|
2052
|
+
const tbody = document.getElementById('projects-table-body');
|
|
2053
|
+
if (!tbody) return;
|
|
2054
|
+
|
|
2055
|
+
if (state.projects.length === 0) {
|
|
2056
|
+
setHtml(tbody, '<tr><td colspan="4" class="empty-state">No projects registered yet</td></tr>');
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
setHtml(tbody, state.projects.map(p => \`
|
|
2061
|
+
<tr>
|
|
2062
|
+
<td><strong>\${esc(p.name)}</strong></td>
|
|
2063
|
+
<td><code>\${esc(p.root)}</code></td>
|
|
2064
|
+
<td>\${p.present ? '<span class="sev-badge tag-clear">● Available</span>' : '<span class="sev-badge tag-flagged">✕ Missing</span>'}</td>
|
|
2065
|
+
<td style="text-align:right">
|
|
2066
|
+
<button style="padding:0.35rem 0.75rem;font-size:0.76rem" onclick="triggerScan('\${esc(p.id)}')">⚡ Run Scan</button>
|
|
2067
|
+
<button class="ghost" style="padding:0.35rem 0.75rem;font-size:0.76rem" onclick="loadProjectLatestReport('\${esc(p.id)}')">Report</button>
|
|
2068
|
+
<button class="danger" style="padding:0.35rem 0.75rem;font-size:0.76rem" onclick="removeProject('\${esc(p.id)}', '\${esc(p.name)}')">Remove</button>
|
|
2069
|
+
</td>
|
|
2070
|
+
</tr>\`).join(''));
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
function renderScansTable() {
|
|
2074
|
+
const tbody = document.getElementById('scans-table-body');
|
|
2075
|
+
if (!tbody) return;
|
|
2076
|
+
|
|
2077
|
+
if (state.jobs.length === 0) {
|
|
2078
|
+
setHtml(tbody, '<tr><td colspan="6" class="empty-state">No scans recorded yet</td></tr>');
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
setHtml(tbody, state.jobs.slice().reverse().map(j => {
|
|
2083
|
+
const statusBadges = {
|
|
2084
|
+
done: '<span class="sev-badge tag-clear">DONE</span>',
|
|
2085
|
+
running: '<span class="sev-badge sev-info">RUNNING</span>',
|
|
2086
|
+
queued: '<span class="sev-badge sev-low">QUEUED</span>',
|
|
2087
|
+
failed: '<span class="sev-badge sev-critical">FAILED</span>',
|
|
2088
|
+
cancelled: '<span class="sev-badge sev-high">CANCELLED</span>',
|
|
2089
|
+
};
|
|
2090
|
+
const sBadge = statusBadges[j.status] || '<span class="sev-badge">' + esc(j.status) + '</span>';
|
|
2091
|
+
const progressText = j.progress?.total ? j.progress.done + '/' + j.progress.total + (j.progress.current ? ' (' + esc(j.progress.current) + ')' : '') : '—';
|
|
2092
|
+
|
|
2093
|
+
return \`
|
|
2094
|
+
<tr>
|
|
2095
|
+
<td><code>\${esc(j.id)}</code></td>
|
|
2096
|
+
<td><strong>\${esc(j.projectName)}</strong></td>
|
|
2097
|
+
<td>\${sBadge}</td>
|
|
2098
|
+
<td>\${progressText}</td>
|
|
2099
|
+
<td>\${j.hasResult ? 'Available' : '—'}</td>
|
|
2100
|
+
<td style="text-align:right">
|
|
2101
|
+
\${j.hasResult ? '<button style="padding:0.35rem 0.75rem;font-size:0.76rem" onclick="loadReport(\\'' + esc(j.projectId) + '\\', \\'' + esc(j.id) + '\\')">View Report</button>' : ''}
|
|
2102
|
+
\${j.status === 'queued' ? '<button class="danger" style="padding:0.35rem 0.75rem;font-size:0.76rem" onclick="cancelJobAction(\\'' + esc(j.id) + '\\')">Cancel</button>' : ''}
|
|
2103
|
+
</td>
|
|
2104
|
+
</tr>\`;
|
|
2105
|
+
}).join(''));
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
function renderDashboardOverview() {
|
|
2109
|
+
// Overview Projects List
|
|
2110
|
+
const pList = document.getElementById('overview-projects-list');
|
|
2111
|
+
if (pList) {
|
|
2112
|
+
if (state.projects.length === 0) {
|
|
2113
|
+
setHtml(pList, '<tr><td colspan="3" class="empty-state">No projects registered</td></tr>');
|
|
2114
|
+
} else {
|
|
2115
|
+
setHtml(pList, state.projects.slice(0, 5).map(p => \`
|
|
2116
|
+
<tr>
|
|
2117
|
+
<td><strong>\${esc(p.name)}</strong></td>
|
|
2118
|
+
<td>\${p.present ? '<span class="sev-badge tag-clear">Available</span>' : '<span class="sev-badge tag-flagged">Missing</span>'}</td>
|
|
2119
|
+
<td><button style="padding:0.25rem 0.6rem;font-size:0.74rem" onclick="triggerScan('\${esc(p.id)}')">Scan</button></td>
|
|
2120
|
+
</tr>\`).join(''));
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// Overview Scans List
|
|
2125
|
+
const sList = document.getElementById('overview-scans-list');
|
|
2126
|
+
if (sList) {
|
|
2127
|
+
if (state.jobs.length === 0) {
|
|
2128
|
+
setHtml(sList, '<tr><td colspan="5" class="empty-state">No scans executed yet</td></tr>');
|
|
2129
|
+
} else {
|
|
2130
|
+
setHtml(sList, state.jobs.slice().reverse().slice(0, 5).map(j => \`
|
|
2131
|
+
<tr>
|
|
2132
|
+
<td><code>\${esc(j.id)}</code></td>
|
|
2133
|
+
<td>\${esc(j.projectName)}</td>
|
|
2134
|
+
<td>\${esc(j.status)}</td>
|
|
2135
|
+
<td>\${j.hasResult ? 'Ready' : '—'}</td>
|
|
2136
|
+
<td>\${j.hasResult ? '<a href="#" onclick="event.preventDefault();loadReport(\\'' + esc(j.projectId) + '\\', \\'' + esc(j.id) + '\\')">View</a>' : '—'}</td>
|
|
2137
|
+
</tr>\`).join(''));
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// Update Score & KPI Boxes
|
|
2142
|
+
const completedWithFindings = state.jobs.find(j => j.hasResult);
|
|
2143
|
+
if (state.selectedReport?.result) {
|
|
2144
|
+
const findings = state.selectedReport.result.findings || [];
|
|
2145
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
2146
|
+
findings.forEach(f => {
|
|
2147
|
+
const s = String(f.severity || '').toLowerCase();
|
|
2148
|
+
if (s in counts) counts[s]++;
|
|
2149
|
+
});
|
|
2150
|
+
|
|
2151
|
+
const calc = calculateScore(findings);
|
|
2152
|
+
const score = state.selectedReport.result.score ?? calc.score;
|
|
2153
|
+
const grade = state.selectedReport.result.grade ?? calc.grade;
|
|
2154
|
+
|
|
2155
|
+
document.getElementById('kpi-score').textContent = score + '/100 (' + grade + ')';
|
|
2156
|
+
document.getElementById('kpi-critical').textContent = counts.critical;
|
|
2157
|
+
document.getElementById('kpi-high').textContent = counts.high;
|
|
2158
|
+
document.getElementById('kpi-med-low').textContent = counts.medium + counts.low;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
function loadProjectLatestReport(projectId) {
|
|
2163
|
+
const job = [...state.jobs].reverse().find(j => j.projectId === projectId && j.hasResult);
|
|
2164
|
+
if (job) {
|
|
2165
|
+
loadReport(projectId, job.id);
|
|
2166
|
+
} else {
|
|
2167
|
+
toast('No completed scans found for this project yet. Click "Run Scan" to start one.', 'info');
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
async function cancelJobAction(jobId) {
|
|
2172
|
+
try {
|
|
2173
|
+
await api('POST', '/api/jobs/' + jobId + '/cancel');
|
|
2174
|
+
toast('Job cancelled', 'info');
|
|
2175
|
+
await refreshAll();
|
|
2176
|
+
} catch (err) {
|
|
2177
|
+
toast(err.message, 'error');
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// Polling interval for background job refresh
|
|
2182
|
+
setInterval(() => {
|
|
2183
|
+
if (!state.activeJobId) {
|
|
2184
|
+
refreshAll();
|
|
2185
|
+
}
|
|
2186
|
+
}, 4000);
|
|
2187
|
+
|
|
2188
|
+
// Initial Load
|
|
2189
|
+
refreshAll();
|
|
2190
|
+
</script>
|
|
2191
|
+
</body>
|
|
2192
|
+
</html>`;
|
|
2193
|
+
}
|