mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-24 20:28:06 +02:00
refactor: Update UI components for consistency and improve layout
This commit is contained in:
parent
da619171cf
commit
77be7f10d9
74 changed files with 3186 additions and 618 deletions
|
|
@ -170,7 +170,7 @@ function KvGrid({ entries }: { entries: Array<[string, React.ReactNode]> }) {
|
|||
}
|
||||
|
||||
function fmt(v: unknown): React.ReactNode {
|
||||
if (v === null || v === undefined) return <span className="muted">—</span>;
|
||||
if (v === null || v === undefined) return <span className="muted">-</span>;
|
||||
if (typeof v === 'boolean')
|
||||
return (
|
||||
<span className={v ? 'pill pill-on' : 'pill pill-off'}>
|
||||
|
|
@ -387,7 +387,7 @@ function RawEditor() {
|
|||
value={draft ?? ''}
|
||||
spellCheck={false}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
placeholder="# nyx.local — overrides for the default config. # Anything you set here wins over nyx.conf."
|
||||
placeholder="# nyx.local - overrides for the default config. # Anything you set here wins over nyx.conf."
|
||||
/>
|
||||
<p className="config-help">
|
||||
Edits are validated against the full config schema before being written.
|
||||
|
|
@ -608,7 +608,7 @@ export function ConfigPage() {
|
|||
setProfileName('');
|
||||
}, [profileName, addProfile]);
|
||||
|
||||
if (configLoading) return <LoadingState message="Loading configuration…" />;
|
||||
if (configLoading) return <LoadingState message="Loading configuration..." />;
|
||||
if (configError) return <ErrorState message={configError.message} />;
|
||||
|
||||
const cfg = config as Record<string, Record<string, unknown>> | undefined;
|
||||
|
|
@ -616,15 +616,7 @@ export function ConfigPage() {
|
|||
const triageSyncOn = !!server?.triage_sync;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>Config</h2>
|
||||
<span className="page-header-sub">
|
||||
Edit defaults, rules, profiles, and the raw <code>nyx.local</code>{' '}
|
||||
file
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="config-page page-shell">
|
||||
<div className="config-tabs">
|
||||
{(
|
||||
[
|
||||
|
|
@ -866,6 +858,6 @@ export function ConfigPage() {
|
|||
)}
|
||||
|
||||
{tab === 'raw' && <RawEditor />}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ const STATE_REMEDIATION_HINTS: Record<string, string[]> = {
|
|||
'Prefer a language-native cleanup pattern (defer, with, try-with-resources, RAII).',
|
||||
],
|
||||
'state-resource-leak-possible': [
|
||||
'Ensure the resource is closed on all code paths — including error and early-return paths.',
|
||||
'Ensure the resource is closed on all code paths, including error and early-return paths.',
|
||||
'Put cleanup in a finally/defer block rather than after the happy path.',
|
||||
],
|
||||
'state-unauthed-access': [
|
||||
|
|
@ -636,7 +636,7 @@ const TAINT_REMEDIATION: Record<string, string[]> = {
|
|||
'If HTML is unavoidable, run input through a well-maintained sanitizer (DOMPurify, Bleach).',
|
||||
],
|
||||
sql: [
|
||||
'Use parameterized queries or a prepared statement — never concatenate user input into SQL.',
|
||||
'Use parameterized queries or a prepared statement. Never concatenate user input into SQL.',
|
||||
'Prefer an ORM or query builder that escapes parameters automatically.',
|
||||
'Validate input type (integer, enum, allowlist) before the query.',
|
||||
],
|
||||
|
|
@ -878,6 +878,12 @@ export function FindingDetailPage() {
|
|||
const hasRelated = f.related_findings && f.related_findings.length > 0;
|
||||
const hasLabels = f.labels && f.labels.length > 0;
|
||||
const hasCode = !!f.code_context;
|
||||
const sourcePath = evidence?.source
|
||||
? `${evidence.source.path}:${evidence.source.line}:${evidence.source.col}`
|
||||
: null;
|
||||
const sinkPath = evidence?.sink
|
||||
? `${evidence.sink.path}:${evidence.sink.line}:${evidence.sink.col}`
|
||||
: null;
|
||||
|
||||
const metaParts: string[] = [];
|
||||
if (f.category) metaParts.push(f.category);
|
||||
|
|
@ -894,7 +900,7 @@ export function FindingDetailPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="detail-panel finding-detail">
|
||||
<div className="detail-panel finding-detail page-shell">
|
||||
<div className="detail-title-row">
|
||||
<h2 className="finding-heading">
|
||||
<span
|
||||
|
|
@ -931,6 +937,22 @@ export function FindingDetailPage() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{(sourcePath || sinkPath) && (
|
||||
<div className="path-trace" aria-label="Source to sink path">
|
||||
<div className="path-trace-card">
|
||||
<span className="path-trace-label">Source</span>
|
||||
<code className="path-trace-path">{sourcePath || 'Unknown'}</code>
|
||||
</div>
|
||||
<div className="path-trace-arrow" aria-hidden>
|
||||
→
|
||||
</div>
|
||||
<div className="path-trace-card">
|
||||
<span className="path-trace-label">Sink</span>
|
||||
<code className="path-trace-path">{sinkPath || 'Unknown'}</code>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<StatusControl
|
||||
finding={f}
|
||||
onTriage={handleTriage}
|
||||
|
|
@ -974,13 +996,6 @@ export function FindingDetailPage() {
|
|||
<HowToFix finding={f} />
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Evidence (collapsed by default — overlaps with taint flow) */}
|
||||
{hasEvidence && (
|
||||
<CollapsibleSection title="Evidence" defaultOpen={false}>
|
||||
<EvidenceSection evidence={evidence!} skipStateCard={isState} />
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Analysis Notes */}
|
||||
{hasNotes && (
|
||||
<CollapsibleSection title="Analysis Notes" defaultOpen={false}>
|
||||
|
|
@ -1002,20 +1017,6 @@ export function FindingDetailPage() {
|
|||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Labels */}
|
||||
{hasLabels && (
|
||||
<CollapsibleSection title="Labels" defaultOpen={false}>
|
||||
<div className="label-list">
|
||||
{f.labels.map(([k, v], i) => (
|
||||
<span key={i} className="label-item">
|
||||
<span className="label-key">{k}:</span>{' '}
|
||||
<span className="label-value">{v}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Code Preview */}
|
||||
{hasCode && (
|
||||
<CollapsibleSection title="Code Preview" defaultOpen={false}>
|
||||
|
|
|
|||
|
|
@ -567,15 +567,7 @@ export function FindingsPage() {
|
|||
const totalPages = Math.ceil(data.total / data.per_page) || 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>Findings</h2>
|
||||
<span className="filter-count">
|
||||
{data.total} finding{data.total !== 1 ? 's' : ''}
|
||||
{hasActiveFilters ? ' (filtered)' : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="findings-page page-shell">
|
||||
{/* Filter bar */}
|
||||
<div className="filter-bar">
|
||||
<input
|
||||
|
|
@ -793,6 +785,6 @@ export function FindingsPage() {
|
|||
onClose={() => setSuppressModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export function OverviewPage() {
|
|||
|
||||
const categoryItems = (overview.issue_categories || [])
|
||||
.slice(0, 8)
|
||||
.map((b) => ({ label: b.label, value: b.count, color: '#72f3d7' }));
|
||||
.map((b) => ({ label: b.label, value: b.count, color: 'var(--accent)' }));
|
||||
|
||||
const trendData = (trends || []).map((t) => ({
|
||||
label: t.timestamp,
|
||||
|
|
@ -74,11 +74,7 @@ export function OverviewPage() {
|
|||
const hotSinks = overview.hot_sinks || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>Overview</h2>
|
||||
</div>
|
||||
|
||||
<div className="overview-page page-shell">
|
||||
{/* Baseline strip */}
|
||||
<BaselinePinControl
|
||||
baseline={overview.baseline}
|
||||
|
|
@ -117,7 +113,7 @@ export function OverviewPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Stat cards — kept lean: 5 cards, severity stacks live in Top Files
|
||||
{/* Stat cards kept lean: 5 cards, severity stacks live in Top Files
|
||||
and Per-Language. Cross-file / Symex moved into Scanner Quality. */}
|
||||
<div className="overview-stat-grid overview-stat-grid-5">
|
||||
<StatCard
|
||||
|
|
@ -145,7 +141,7 @@ export function OverviewPage() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
{/* Charts — 3-col: Findings (col1 span2) | OWASP+Confidence (col2) | Categories (col3 span2) */}
|
||||
<div className="overview-chart-grid">
|
||||
<div className="card">
|
||||
<div className="card-header">Findings Over Time</div>
|
||||
|
|
@ -158,14 +154,8 @@ export function OverviewPage() {
|
|||
)}
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="card-header">OWASP Top 10 (2021)</div>
|
||||
{overview.owasp_buckets && overview.owasp_buckets.length > 0 ? (
|
||||
<OwaspChart buckets={overview.owasp_buckets} />
|
||||
) : (
|
||||
<div className="empty-state" style={{ padding: 16 }}>
|
||||
<p>No OWASP-mapped findings.</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="card-header">Issue Categories</div>
|
||||
<HorizontalBarChart items={categoryItems} />
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="card-header">Confidence Distribution</div>
|
||||
|
|
@ -180,8 +170,14 @@ export function OverviewPage() {
|
|||
)}
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="card-header">Issue Categories</div>
|
||||
<HorizontalBarChart items={categoryItems} />
|
||||
<div className="card-header">OWASP Top 10 (2021)</div>
|
||||
{overview.owasp_buckets && overview.owasp_buckets.length > 0 ? (
|
||||
<OwaspChart buckets={overview.owasp_buckets} />
|
||||
) : (
|
||||
<div className="empty-state" style={{ padding: 16 }}>
|
||||
<p>No OWASP-mapped findings.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -289,7 +285,7 @@ export function OverviewPage() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -271,20 +271,7 @@ export function RulesPage() {
|
|||
if (error) return <ErrorState message={error.message} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>Rules</h2>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--text-secondary)',
|
||||
fontSize: 'var(--text-sm)',
|
||||
marginLeft: 'var(--space-3)',
|
||||
}}
|
||||
>
|
||||
{(rules || []).length} rules
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="rules-page page-shell">
|
||||
<div className="rules-layout">
|
||||
<div className="rules-list-panel">
|
||||
<div className="rules-filters">
|
||||
|
|
@ -310,14 +297,7 @@ export function RulesPage() {
|
|||
</option>
|
||||
))}
|
||||
</select>
|
||||
<label
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
fontSize: 'var(--text-sm)',
|
||||
}}
|
||||
>
|
||||
<label className="rules-custom-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={customOnly}
|
||||
|
|
@ -357,6 +337,6 @@ export function RulesPage() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ function CompareByGroup({
|
|||
}, [data, groupField]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="scan-compare-page page-shell">
|
||||
{groups.map(([key, items]) => {
|
||||
const counts = { new: 0, fixed: 0, changed: 0, unchanged: 0 };
|
||||
items.forEach(
|
||||
|
|
@ -267,7 +267,7 @@ function CompareByGroup({
|
|||
</CollapsibleSection>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -300,16 +300,10 @@ export function ScanComparePage() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
style={{ marginBottom: 'var(--space-4)' }}
|
||||
onClick={() => navigate('/scans')}
|
||||
>
|
||||
Back to Scans
|
||||
</button>
|
||||
|
||||
<div className="page-header">
|
||||
<h2>Scan Comparison</h2>
|
||||
<div className="page-action-row">
|
||||
<button className="btn btn-sm" onClick={() => navigate('/scans')}>
|
||||
Back to Scans
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="compare-header">
|
||||
|
|
|
|||
|
|
@ -71,125 +71,127 @@ function SummaryTab({ scan }: { scan: ScanView }) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="card-header">Details</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)', width: 140 }}>
|
||||
Scan ID
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 'var(--text-xs)',
|
||||
}}
|
||||
>
|
||||
{scan.id}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Root</td>
|
||||
<td
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 'var(--text-sm)',
|
||||
}}
|
||||
>
|
||||
{scan.scan_root}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Engine</td>
|
||||
<td>{scan.engine_version || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Started</td>
|
||||
<td>{fmtDate(scan.started_at)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Finished</td>
|
||||
<td>{fmtDate(scan.finished_at)}</td>
|
||||
</tr>
|
||||
{scan.error && (
|
||||
<div className="scan-summary-grid">
|
||||
<div className="card scan-detail-card">
|
||||
<div className="card-header">Details</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Error</td>
|
||||
<td style={{ color: 'var(--sev-high)' }}>{scan.error}</td>
|
||||
<td style={{ color: 'var(--text-secondary)', width: 140 }}>
|
||||
Scan ID
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 'var(--text-xs)',
|
||||
}}
|
||||
>
|
||||
{scan.id}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{timing && total > 0 && (
|
||||
<div className="card" style={{ marginTop: 'var(--space-4)' }}>
|
||||
<div className="card-header">Timing Breakdown</div>
|
||||
<div className="timing-bar">
|
||||
<div
|
||||
className="timing-bar-segment walk"
|
||||
style={{ width: `${pct(timing.walk_ms)}%` }}
|
||||
title={`Walk: ${timing.walk_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment pass1"
|
||||
style={{ width: `${pct(timing.pass1_ms)}%` }}
|
||||
title={`Pass 1: ${timing.pass1_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment callgraph"
|
||||
style={{ width: `${pct(timing.call_graph_ms)}%` }}
|
||||
title={`Call Graph: ${timing.call_graph_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment pass2"
|
||||
style={{ width: `${pct(timing.pass2_ms)}%` }}
|
||||
title={`Pass 2: ${timing.pass2_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment postprocess"
|
||||
style={{ width: `${pct(timing.post_process_ms)}%` }}
|
||||
title={`Post-process: ${timing.post_process_ms}ms`}
|
||||
></div>
|
||||
</div>
|
||||
<div className="timing-legend">
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--sev-low)' }}
|
||||
></span>{' '}
|
||||
Walk {timing.walk_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--accent)' }}
|
||||
></span>{' '}
|
||||
Pass 1 {timing.pass1_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--sev-medium)' }}
|
||||
></span>{' '}
|
||||
Call Graph {timing.call_graph_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--success)' }}
|
||||
></span>{' '}
|
||||
Pass 2 {timing.pass2_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--text-tertiary)' }}
|
||||
></span>{' '}
|
||||
Post {timing.post_process_ms}ms
|
||||
</span>
|
||||
</div>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Root</td>
|
||||
<td
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 'var(--text-sm)',
|
||||
}}
|
||||
>
|
||||
{scan.scan_root}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Engine</td>
|
||||
<td>{scan.engine_version || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Started</td>
|
||||
<td>{fmtDate(scan.started_at)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Finished</td>
|
||||
<td>{fmtDate(scan.finished_at)}</td>
|
||||
</tr>
|
||||
{scan.error && (
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Error</td>
|
||||
<td style={{ color: 'var(--sev-high)' }}>{scan.error}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{timing && total > 0 && (
|
||||
<div className="card scan-timing-card">
|
||||
<div className="card-header">Timing Breakdown</div>
|
||||
<div className="timing-bar">
|
||||
<div
|
||||
className="timing-bar-segment walk"
|
||||
style={{ width: `${pct(timing.walk_ms)}%` }}
|
||||
title={`Walk: ${timing.walk_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment pass1"
|
||||
style={{ width: `${pct(timing.pass1_ms)}%` }}
|
||||
title={`Pass 1: ${timing.pass1_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment callgraph"
|
||||
style={{ width: `${pct(timing.call_graph_ms)}%` }}
|
||||
title={`Call Graph: ${timing.call_graph_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment pass2"
|
||||
style={{ width: `${pct(timing.pass2_ms)}%` }}
|
||||
title={`Pass 2: ${timing.pass2_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment postprocess"
|
||||
style={{ width: `${pct(timing.post_process_ms)}%` }}
|
||||
title={`Post-process: ${timing.post_process_ms}ms`}
|
||||
></div>
|
||||
</div>
|
||||
<div className="timing-legend">
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--sev-low)' }}
|
||||
></span>{' '}
|
||||
Walk {timing.walk_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--accent)' }}
|
||||
></span>{' '}
|
||||
Pass 1 {timing.pass1_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--sev-medium)' }}
|
||||
></span>{' '}
|
||||
Call Graph {timing.call_graph_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--success)' }}
|
||||
></span>{' '}
|
||||
Pass 2 {timing.pass2_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--text-tertiary)' }}
|
||||
></span>{' '}
|
||||
Post {timing.post_process_ms}ms
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -212,7 +214,7 @@ function FindingsTab({ scanId }: { scanId: string }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="scan-detail-page page-shell">
|
||||
<div className="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
|
|
@ -266,7 +268,7 @@ function FindingsTab({ scanId }: { scanId: string }) {
|
|||
>
|
||||
Showing {data.findings.length} of {data.total} findings
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -416,31 +418,19 @@ export function ScanDetailPage() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--space-2)',
|
||||
marginBottom: 'var(--space-4)',
|
||||
}}
|
||||
>
|
||||
<div className="page-action-row">
|
||||
<button className="btn btn-sm" onClick={() => navigate('/scans')}>
|
||||
Back to Scans
|
||||
</button>
|
||||
{prevScanId && (
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
style={{ marginLeft: 'auto' }}
|
||||
className="btn btn-sm page-action-push"
|
||||
onClick={() => navigate(`/scans/compare/${prevScanId}/${id}`)}
|
||||
>
|
||||
Compare with Previous
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="page-header">
|
||||
<h2>Scan Detail</h2>
|
||||
<span className={`status-badge ${scan.status}`}>
|
||||
<span className={`status-badge ${scan.status}`} style={{ marginLeft: 'auto' }}>
|
||||
<span className={`status-dot ${scan.status}`}></span>
|
||||
{scan.status}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -174,11 +174,7 @@ export function ScansPage() {
|
|||
const showCheckboxes = completedScans.length >= 2;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>Scans</h2>
|
||||
</div>
|
||||
|
||||
<div className="scans-page page-shell">
|
||||
{(runningScans.length > 0 || isScanRunning) && scanProgress && (
|
||||
<ScanProgress data={scanProgress} />
|
||||
)}
|
||||
|
|
@ -210,90 +206,103 @@ export function ScansPage() {
|
|||
</div>
|
||||
) : (
|
||||
<div className="table-wrap">
|
||||
<table>
|
||||
<table className="scans-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{showCheckboxes && <th style={{ width: 32 }}></th>}
|
||||
<th>Status</th>
|
||||
<th>Root</th>
|
||||
<th>Duration</th>
|
||||
<th>Findings</th>
|
||||
<th>Languages</th>
|
||||
<th>Started</th>
|
||||
<th style={{ width: 60 }}></th>
|
||||
{showCheckboxes && <th className="scan-select-col"></th>}
|
||||
<th className="scan-status-col">Status</th>
|
||||
<th className="scan-root-col">Root</th>
|
||||
<th className="scan-duration-col">Duration</th>
|
||||
<th className="scan-findings-col">Findings</th>
|
||||
<th className="scan-languages-col">Languages</th>
|
||||
<th className="scan-started-col">Started</th>
|
||||
<th className="scan-actions-col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{scans.map((s: ScanView) => (
|
||||
<tr
|
||||
key={s.id}
|
||||
className="clickable"
|
||||
onClick={() => navigate(`/scans/${s.id}`)}
|
||||
>
|
||||
{showCheckboxes && (
|
||||
{scans.map((s: ScanView) => {
|
||||
const languages = s.languages || [];
|
||||
const visibleLanguages = languages.slice(0, 4);
|
||||
const hiddenLanguageCount =
|
||||
languages.length - visibleLanguages.length;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={s.id}
|
||||
className="clickable"
|
||||
onClick={() => navigate(`/scans/${s.id}`)}
|
||||
>
|
||||
{showCheckboxes && (
|
||||
<td>
|
||||
{s.status === 'completed' && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="scan-compare-cb"
|
||||
checked={selectedScans.has(s.id)}
|
||||
onClick={(e) => handleCheckbox(e, s.id)}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
{s.status === 'completed' && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="scan-compare-cb"
|
||||
checked={selectedScans.has(s.id)}
|
||||
onClick={(e) => handleCheckbox(e, s.id)}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<span className={`status-badge ${s.status}`}>
|
||||
<span className={`status-dot ${s.status}`}></span>
|
||||
{s.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="scan-root-cell" title={s.scan_root}>
|
||||
{truncPath(s.scan_root)}
|
||||
</td>
|
||||
<td className="scan-number-cell">
|
||||
{s.duration_secs != null
|
||||
? s.duration_secs.toFixed(2) + 's'
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="scan-number-cell">
|
||||
{s.finding_count ?? '-'}
|
||||
</td>
|
||||
<td className="scan-languages-cell">
|
||||
{languages.length > 0 ? (
|
||||
<span className="scan-language-list">
|
||||
{visibleLanguages.map((l) => (
|
||||
<span key={l} className="lang-badge">
|
||||
{l}
|
||||
</span>
|
||||
))}
|
||||
{hiddenLanguageCount > 0 && (
|
||||
<span className="lang-badge lang-badge-more">
|
||||
+{hiddenLanguageCount}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
<span className={`status-badge ${s.status}`}>
|
||||
<span className={`status-dot ${s.status}`}></span>
|
||||
{s.status}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: '0.82rem',
|
||||
}}
|
||||
>
|
||||
{truncPath(s.scan_root)}
|
||||
</td>
|
||||
<td>
|
||||
{s.duration_secs != null
|
||||
? s.duration_secs.toFixed(2) + 's'
|
||||
: '-'}
|
||||
</td>
|
||||
<td>{s.finding_count ?? '-'}</td>
|
||||
<td>
|
||||
{(s.languages || []).length > 0
|
||||
? (s.languages || []).map((l) => (
|
||||
<span key={l} className="lang-badge">
|
||||
{l}
|
||||
</span>
|
||||
))
|
||||
: '-'}
|
||||
</td>
|
||||
<td>{relTime(s.started_at)}</td>
|
||||
<td>
|
||||
{s.status !== 'running' && (
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (confirm('Delete this scan?')) {
|
||||
deleteScan.mutate(s.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<td>{relTime(s.started_at)}</td>
|
||||
<td className="scan-actions-cell">
|
||||
{s.status !== 'running' && (
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (confirm('Delete this scan?')) {
|
||||
deleteScan.mutate(s.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,22 @@ function TriageSummary({
|
|||
<div className="triage-hero">
|
||||
<div className="triage-hero-row">
|
||||
<h1 className="triage-hero-title">{headline}</h1>
|
||||
{showSeverity && totalCount > 0 && (
|
||||
<div className="triage-hero-severity">
|
||||
{SEVERITIES.map((sev) => (
|
||||
<span
|
||||
key={sev}
|
||||
className={`triage-sev-stat triage-sev-${sev.toLowerCase()}`}
|
||||
>
|
||||
<span className="triage-sev-dot" aria-hidden />
|
||||
<span className="triage-sev-count">
|
||||
{(openBySev[sev] ?? 0).toLocaleString()}
|
||||
</span>
|
||||
<span className="triage-sev-name">{sev}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{totalCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -142,22 +158,6 @@ function TriageSummary({
|
|||
</button>
|
||||
)}
|
||||
</div>
|
||||
{showSeverity && totalCount > 0 && (
|
||||
<div className="triage-hero-severity">
|
||||
{SEVERITIES.map((sev) => (
|
||||
<span
|
||||
key={sev}
|
||||
className={`triage-sev-stat triage-sev-${sev.toLowerCase()}`}
|
||||
>
|
||||
<span className="triage-sev-dot" aria-hidden />
|
||||
<span className="triage-sev-count">
|
||||
{(openBySev[sev] ?? 0).toLocaleString()}
|
||||
</span>
|
||||
<span className="triage-sev-name">{sev}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{expanded && (
|
||||
<div className="triage-state-row">
|
||||
<button
|
||||
|
|
@ -1124,7 +1124,7 @@ export function TriagePage() {
|
|||
GROUP_MODES.find((g) => g.value === groupMode)?.label ?? 'None';
|
||||
|
||||
return (
|
||||
<div className="triage-page">
|
||||
<div className="triage-page page-shell">
|
||||
<TriageSummary
|
||||
totalCount={totalCount}
|
||||
needsAttention={needsAttention}
|
||||
|
|
@ -1260,7 +1260,7 @@ export function TriagePage() {
|
|||
<input
|
||||
className="triage-search"
|
||||
type="search"
|
||||
placeholder="Search rule, file, message…"
|
||||
placeholder="Search rule, file, message..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export function FunctionSelector({
|
|||
}
|
||||
|
||||
function formatFunctionLabel(fn: FunctionInfo): string {
|
||||
const sig = `(${fn.param_count} params) — L${fn.line}`;
|
||||
const sig = `(${fn.param_count} params), L${fn.line}`;
|
||||
if (fn.func_kind === 'closure' && fn.container) {
|
||||
return `${fn.name} [closure in ${fn.container}] ${sig}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export function TypeFactsAnalysisPanel({
|
|||
{securityFacts.length > 0 && (
|
||||
<TypeFactGroup
|
||||
title="Security-Relevant Types"
|
||||
subtitle="HttpClient, DatabaseConnection, Url, … — drive type-qualified callee resolution and sink suppression"
|
||||
subtitle="HttpClient, DatabaseConnection, Url, and related types drive type-qualified callee resolution and sink suppression"
|
||||
facts={securityFacts}
|
||||
highlight
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue