mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-18 20:15:14 +02:00
* feat: Add const_bound_vars tracking to prevent false positives in ownership checks
* feat: Introduce field interner and typed bounded vars for enhanced type tracking
* feat: Add typed_call_receivers and typed_bounded_dto_fields for enhanced type tracking
* feat: Centralize method name extraction with bare_method_name helper
* feat: Implement Phase-6 hierarchy fan-out for runtime virtual dispatch
* feat: Enhance C++ taint tracking with additional container operations and inline method resolution
* feat: Introduce field-sensitive points-to analysis for enhanced resource tracking
* feat: Implement Pointer-Phase 6 subscript handling for enhanced container analysis
* test: Add comprehensive tests for JavaScript control flow constructs and lattice operations
* docs: Update advanced analysis documentation with field-sensitive points-to and hierarchy fan-out details
* test: Add comprehensive tests for lattice algebra laws and SSA edge cases
* feat: Add destructured session user handling and safe user ID access patterns
* feat: Implement row-population reverse-walk for enhanced authorization checks
* feat: Enhance authorization checks with local alias chain for self-actor types
* feat: Introduce ActiveRecord query safety checks and enhance snippet extraction
* feat: Implement chained method call inner-gate rebinding for SSRF prevention
* feat: Add observability and error modules, enhance debug functionality, and implement theme context
* feat: Remove Auth Analysis page and update navigation to redirect to Explorer
* feat: Optimize SSA lowering by sharing results between taint engine and artifact extractor
* feat: Optimize SSA lowering by sharing results between taint engine and artifact extractor
* feat: Reset path-safe-suppressed spans before lowering to maintain analysis integrity
* fix(ssa): ungate debug_assert_bfs_ordering for release-tests build
The helper at src/ssa/lower.rs was gated `#[cfg(debug_assertions)]` while
the unit test at the bottom of the file was gated only `#[cfg(test)]`.
Since `cfg(test)` is set in release builds with `--tests` but
`cfg(debug_assertions)` is not, `cargo build --release --tests` failed
with E0425. Removing the gate fixes the build; the body is `debug_assert!`
only, so the helper is free in release. Also drop the gate at the call
site to avoid a `dead_code` warning when the lib is built without
`--tests`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(closure-capture): flip JS/TS fixtures to required-finding
The JS and TS closure-capture fixtures pinned the old broken behaviour
via `forbidden_findings: [{ "id_prefix": "taint-" }]`. The engine now
correctly traces taint through the closure boundary (env source captured
by an arrow function, sunk via `child_process.exec` inside the body), so
the formerly-forbidden finding is a true positive.
Match the Python sibling's shape — `required_findings` with
`id_prefix` + `min_count` plus a small `noise_budget` — and rewrite the
companion READMEs and the phase8_fragility_tests doc-comments from
"known gap" to "regression guard".
Verified:
- cargo test --release --test phase8_fragility_tests → 8/8 pass
- cargo test --release --lib bfs_assertion → pass
- corpus benchmark F1 = 0.9976 (TP=205, FP=1, FN=0) — unchanged
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: Add OWASP mapping and baseline mutation hooks for enhanced security analysis
* feat: Introduce health module and enhance health score computation with calibration tests
* feat: Add expectations configuration and cleanup .gitignore for log files
* feat: Implement theme selection and enhance settings panel for triage sync
* feat: Suppress false positives for strcpy calls with literal sources in AST
* feat: Update analyse_function_ssa to return body CFG for accurate analysis
* feat: Add bug report and feature request templates for improved issue tracking
* feat: removed dev scripts
* feat: update README.md for clarity and consistency in fixture descriptions
* feat: removed dev docs
* feat: clean up error handling and UI elements for improved user experience
* feat: adjust button sizes in HeaderBar for better UI consistency
* feat: enhance taint analysis with additional context for sanitizer and taint findings
* cargo fmt
* prettier
* refactor: simplify conditional checks and improve code readability in AST and screenshot capture scripts
* feat: add script to frame PNG screenshots with brand gradient
* feat: add fuzzing support with new targets and CI workflows
* refactor: streamline match expressions and improve formatting in CLI and output handling
* feat: enhance configuration display with detailed output options
* feat: stage demo configuration for improved CLI screenshot output
* feat: expose merge_configs function for user-configurable settings
* refactor: simplify code structure and improve readability in config handling
* refactor: improve descriptions for vulnerability patterns in various languages
* feat: update MIT License section with additional usage details and copyright information
* feat: update screenshots
* refactor: update build process and paths for frontend assets
* feat: add cross-file taint fuzzing target and supporting dictionary
* refactor: clean up formatting and comments in fuzz configuration and example files
* refactor: remove outdated comments and clean up CI configuration files
* chore: update changelog dates and improve formatting in documentation
* refactor: update Cargo.toml and CI configuration for improved packaging and build process
* refactor: enhance quote-stripping logic to prevent panics and add regression tests
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
469 lines
15 KiB
TypeScript
469 lines
15 KiB
TypeScript
import { useState, useMemo } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import {
|
|
useScan,
|
|
useScans,
|
|
useScanFindings,
|
|
useScanLogs,
|
|
useScanMetrics,
|
|
} from '../api/queries/scans';
|
|
import { LoadingState } from '../components/ui/LoadingState';
|
|
import { ErrorState } from '../components/ui/ErrorState';
|
|
import { usePageTitle } from '../hooks/usePageTitle';
|
|
import type { ScanView, ScanLogEntry, ScanMetricsSnapshot } from '../api/types';
|
|
|
|
function truncPath(p?: string, max = 50): string {
|
|
if (!p) return '';
|
|
if (p.length <= max) return p;
|
|
return '...' + p.slice(p.length - max + 3);
|
|
}
|
|
|
|
function fmtDate(iso?: string): string {
|
|
return iso ? new Date(iso).toLocaleString() : '-';
|
|
}
|
|
|
|
function fmtNum(n?: number | null): string {
|
|
return n != null ? n.toLocaleString() : '-';
|
|
}
|
|
|
|
// ── Summary Tab ──────────────────────────────────────────────────────────────
|
|
|
|
function SummaryTab({ scan }: { scan: ScanView }) {
|
|
const duration =
|
|
scan.duration_secs != null ? scan.duration_secs.toFixed(2) + 's' : '-';
|
|
const langs = (scan.languages || []).join(', ') || '-';
|
|
|
|
const timing = scan.timing;
|
|
let total = 0;
|
|
if (timing) {
|
|
total =
|
|
timing.walk_ms +
|
|
timing.pass1_ms +
|
|
timing.call_graph_ms +
|
|
timing.pass2_ms +
|
|
timing.post_process_ms;
|
|
}
|
|
const pct = (ms: number) => ((ms / total) * 100).toFixed(1);
|
|
|
|
return (
|
|
<>
|
|
<div className="scan-stat-grid">
|
|
<div className="scan-stat-card">
|
|
<div className="scan-stat-label">Files Scanned</div>
|
|
<div className="scan-stat-value">{scan.files_scanned ?? '-'}</div>
|
|
</div>
|
|
<div className="scan-stat-card">
|
|
<div className="scan-stat-label">Findings</div>
|
|
<div className="scan-stat-value">{scan.finding_count ?? '-'}</div>
|
|
</div>
|
|
<div className="scan-stat-card">
|
|
<div className="scan-stat-label">Duration</div>
|
|
<div className="scan-stat-value">{duration}</div>
|
|
</div>
|
|
<div className="scan-stat-card">
|
|
<div className="scan-stat-label">Languages</div>
|
|
<div
|
|
className="scan-stat-value"
|
|
style={{ fontSize: 'var(--text-base)' }}
|
|
>
|
|
{langs}
|
|
</div>
|
|
</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 && (
|
|
<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" 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>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ── Findings Tab ─────────────────────────────────────────────────────────────
|
|
|
|
function FindingsTab({ scanId }: { scanId: string }) {
|
|
const navigate = useNavigate();
|
|
const { data, isLoading, error } = useScanFindings(scanId);
|
|
|
|
if (isLoading) return <LoadingState message="Loading findings..." />;
|
|
if (error) return <ErrorState message={error.message} />;
|
|
if (!data?.findings || data.findings.length === 0) {
|
|
return (
|
|
<div className="empty-state">
|
|
<h3>No findings</h3>
|
|
<p>This scan produced no findings.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Severity</th>
|
|
<th>Rule</th>
|
|
<th>File</th>
|
|
<th>Line</th>
|
|
<th>Confidence</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.findings.map((f) => (
|
|
<tr
|
|
key={f.index}
|
|
className="clickable"
|
|
onClick={() => navigate(`/findings/${f.index}`)}
|
|
>
|
|
<td>
|
|
<span className={`badge badge-${f.severity.toLowerCase()}`}>
|
|
{f.severity}
|
|
</span>
|
|
</td>
|
|
<td>{f.rule_id}</td>
|
|
<td className="cell-path" title={f.path}>
|
|
{truncPath(f.path)}
|
|
</td>
|
|
<td>{f.line}</td>
|
|
<td>
|
|
{f.confidence ? (
|
|
<span
|
|
className={`badge badge-conf-${f.confidence.toLowerCase()}`}
|
|
>
|
|
{f.confidence}
|
|
</span>
|
|
) : (
|
|
'-'
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div
|
|
style={{
|
|
marginTop: 'var(--space-2)',
|
|
fontSize: 'var(--text-sm)',
|
|
color: 'var(--text-secondary)',
|
|
}}
|
|
>
|
|
Showing {data.findings.length} of {data.total} findings
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ── Logs Tab ─────────────────────────────────────────────────────────────────
|
|
|
|
function LogsTab({ scanId }: { scanId: string }) {
|
|
const [levelFilter, setLevelFilter] = useState<string | undefined>(undefined);
|
|
const { data: logs, isLoading, error } = useScanLogs(scanId, levelFilter);
|
|
|
|
if (isLoading) return <LoadingState message="Loading logs..." />;
|
|
if (error) return <ErrorState message={error.message} />;
|
|
|
|
const levels: Array<{ value: string | undefined; label: string }> = [
|
|
{ value: undefined, label: 'All' },
|
|
{ value: 'info', label: 'Info' },
|
|
{ value: 'warn', label: 'Warn' },
|
|
{ value: 'error', label: 'Error' },
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<div className="log-filters">
|
|
{levels.map((l) => (
|
|
<button
|
|
key={l.label}
|
|
className={`log-filter-btn ${levelFilter === l.value ? 'active' : ''}`}
|
|
onClick={() => setLevelFilter(l.value)}
|
|
>
|
|
{l.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
{!logs || logs.length === 0 ? (
|
|
<div className="empty-state">
|
|
<p>No log entries</p>
|
|
</div>
|
|
) : (
|
|
<div className="log-viewer">
|
|
{logs.map((l: ScanLogEntry, i: number) => (
|
|
<div key={i} className={`log-entry log-${l.level}`}>
|
|
<span className={`log-level ${l.level}`}>{l.level}</span>
|
|
<span className="log-time">
|
|
{new Date(l.timestamp).toLocaleTimeString()}
|
|
</span>
|
|
<span className="log-message">
|
|
{l.message}
|
|
{l.file_path && (
|
|
<span style={{ color: 'var(--text-tertiary)' }}>
|
|
{' '}
|
|
{l.file_path}
|
|
</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ── Metrics Tab ──────────────────────────────────────────────────────────────
|
|
|
|
function MetricsTab({ scanId, scan }: { scanId: string; scan: ScanView }) {
|
|
const { data: fetchedMetrics } = useScanMetrics(scanId);
|
|
const metrics: ScanMetricsSnapshot | undefined =
|
|
scan.metrics || fetchedMetrics || undefined;
|
|
|
|
if (!metrics) {
|
|
return (
|
|
<div className="empty-state">
|
|
<p>No metrics available for this scan.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="metric-grid">
|
|
<div className="metric-card">
|
|
<div className="metric-card-label">CFG Nodes</div>
|
|
<div className="metric-card-value">{fmtNum(metrics.cfg_nodes)}</div>
|
|
</div>
|
|
<div className="metric-card">
|
|
<div className="metric-card-label">Call Edges</div>
|
|
<div className="metric-card-value">{fmtNum(metrics.call_edges)}</div>
|
|
</div>
|
|
<div className="metric-card">
|
|
<div className="metric-card-label">Functions Analyzed</div>
|
|
<div className="metric-card-value">
|
|
{fmtNum(metrics.functions_analyzed)}
|
|
</div>
|
|
</div>
|
|
<div className="metric-card">
|
|
<div className="metric-card-label">Summaries Reused</div>
|
|
<div className="metric-card-value">
|
|
{fmtNum(metrics.summaries_reused)}
|
|
</div>
|
|
</div>
|
|
<div className="metric-card">
|
|
<div className="metric-card-label">Unresolved Calls</div>
|
|
<div className="metric-card-value">
|
|
{fmtNum(metrics.unresolved_calls)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Scan Detail Page ─────────────────────────────────────────────────────────
|
|
|
|
type TabId = 'summary' | 'findings' | 'logs' | 'metrics';
|
|
|
|
export function ScanDetailPage() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const { data: scan, isLoading, error } = useScan(id || '');
|
|
const { data: allScans } = useScans();
|
|
const [activeTab, setActiveTab] = useState<TabId>('summary');
|
|
usePageTitle(scan ? `Scan ${scan.id.slice(0, 8)}` : 'Scan');
|
|
|
|
const prevScanId = useMemo(() => {
|
|
if (!scan || scan.status !== 'completed' || !allScans) return null;
|
|
const completed = allScans
|
|
.filter((s) => s.status === 'completed' && s.started_at)
|
|
.sort((a, b) => (a.started_at || '').localeCompare(b.started_at || ''));
|
|
const myIdx = completed.findIndex((s) => s.id === id);
|
|
if (myIdx > 0) return completed[myIdx - 1].id;
|
|
return null;
|
|
}, [scan, allScans, id]);
|
|
|
|
if (isLoading) return <LoadingState message="Loading scan..." />;
|
|
if (error || !scan) {
|
|
return (
|
|
<ErrorState
|
|
title="Scan not found"
|
|
message={error?.message || 'Not found'}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const tabs: { id: TabId; label: string }[] = [
|
|
{ id: 'summary', label: 'Summary' },
|
|
{ id: 'findings', label: 'Findings' },
|
|
{ id: 'logs', label: 'Logs' },
|
|
{ id: 'metrics', label: 'Metrics' },
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 'var(--space-2)',
|
|
marginBottom: 'var(--space-4)',
|
|
}}
|
|
>
|
|
<button className="btn btn-sm" onClick={() => navigate('/scans')}>
|
|
Back to Scans
|
|
</button>
|
|
{prevScanId && (
|
|
<button
|
|
className="btn btn-sm"
|
|
style={{ marginLeft: 'auto' }}
|
|
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-dot ${scan.status}`}></span>
|
|
{scan.status}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="scan-detail-tabs">
|
|
{tabs.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
className={`scan-detail-tab ${activeTab === tab.id ? 'active' : ''}`}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div id="scan-tab-content">
|
|
{activeTab === 'summary' && <SummaryTab scan={scan} />}
|
|
{activeTab === 'findings' && <FindingsTab scanId={id!} />}
|
|
{activeTab === 'logs' && <LogsTab scanId={id!} />}
|
|
{activeTab === 'metrics' && <MetricsTab scanId={id!} scan={scan} />}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|