mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-30 20:39:39 +02:00
updated CHANGELOG.md
This commit is contained in:
parent
25863d222a
commit
291fe5d7be
13 changed files with 320 additions and 73 deletions
|
|
@ -172,6 +172,7 @@ export interface TimingBreakdown {
|
|||
call_graph_ms: number;
|
||||
pass2_ms: number;
|
||||
post_process_ms: number;
|
||||
dynamic_verify_ms?: number;
|
||||
}
|
||||
|
||||
export interface ScanMetricsSnapshot {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ export interface ScanProgress {
|
|||
files_skipped: number;
|
||||
batches_total: number;
|
||||
batches_completed: number;
|
||||
dynamic_enabled?: boolean;
|
||||
dynamic_total: number;
|
||||
dynamic_completed: number;
|
||||
current_file: string;
|
||||
elapsed_ms: number;
|
||||
timing: TimingBreakdown;
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export function NewScanModal({ open, onClose }: NewScanModalProps) {
|
|||
const [noVerify, setNoVerify] = useState(false);
|
||||
const [verifyBackend, setVerifyBackend] = useState<VerifyBackend>('auto');
|
||||
const [hardenProfile, setHardenProfile] = useState<HardenProfile>('standard');
|
||||
const showProcessHardening = !noVerify && verifyBackend === 'process';
|
||||
|
||||
const handleStart = async () => {
|
||||
const root = scanRoot.trim();
|
||||
|
|
@ -66,7 +67,9 @@ export function NewScanModal({ open, onClose }: NewScanModalProps) {
|
|||
body.verify = false;
|
||||
} else {
|
||||
body.verify_backend = verifyBackend;
|
||||
body.harden_profile = hardenProfile;
|
||||
if (verifyBackend === 'process') {
|
||||
body.harden_profile = hardenProfile;
|
||||
}
|
||||
}
|
||||
const payload = Object.keys(body).length ? body : undefined;
|
||||
try {
|
||||
|
|
@ -162,20 +165,21 @@ export function NewScanModal({ open, onClose }: NewScanModalProps) {
|
|||
</select>
|
||||
<span className="form-hint">{BACKEND_HINTS[verifyBackend]}</span>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Process Hardening</label>
|
||||
<select
|
||||
value={hardenProfile}
|
||||
disabled={noVerify || verifyBackend !== 'process'}
|
||||
onChange={(e) =>
|
||||
setHardenProfile(e.target.value as HardenProfile)
|
||||
}
|
||||
>
|
||||
<option value="standard">Standard</option>
|
||||
<option value="strict">Strict</option>
|
||||
</select>
|
||||
<span className="form-hint">{HARDEN_HINTS[hardenProfile]}</span>
|
||||
</div>
|
||||
{showProcessHardening && (
|
||||
<div className="form-group">
|
||||
<label>Process Hardening</label>
|
||||
<select
|
||||
value={hardenProfile}
|
||||
onChange={(e) =>
|
||||
setHardenProfile(e.target.value as HardenProfile)
|
||||
}
|
||||
>
|
||||
<option value="standard">Standard</option>
|
||||
<option value="strict">Strict</option>
|
||||
</select>
|
||||
<span className="form-hint">{HARDEN_HINTS[hardenProfile]}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="scan-modal-actions">
|
||||
<button className="btn btn-sm" onClick={onClose}>
|
||||
Cancel
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ function SummaryTab({ scan }: { scan: ScanView }) {
|
|||
const langs = (scan.languages || []).join(', ') || '-';
|
||||
|
||||
const timing = scan.timing;
|
||||
const dynamicVerifyMs = timing?.dynamic_verify_ms ?? 0;
|
||||
let total = 0;
|
||||
if (timing) {
|
||||
total =
|
||||
|
|
@ -41,7 +42,8 @@ function SummaryTab({ scan }: { scan: ScanView }) {
|
|||
timing.pass1_ms +
|
||||
timing.call_graph_ms +
|
||||
timing.pass2_ms +
|
||||
timing.post_process_ms;
|
||||
timing.post_process_ms +
|
||||
dynamicVerifyMs;
|
||||
}
|
||||
const pct = (ms: number) => ((ms / total) * 100).toFixed(1);
|
||||
|
||||
|
|
@ -151,6 +153,13 @@ function SummaryTab({ scan }: { scan: ScanView }) {
|
|||
style={{ width: `${pct(timing.post_process_ms)}%` }}
|
||||
title={`Post-process: ${timing.post_process_ms}ms`}
|
||||
></div>
|
||||
{dynamicVerifyMs > 0 && (
|
||||
<div
|
||||
className="timing-bar-segment postprocess"
|
||||
style={{ width: `${pct(dynamicVerifyMs)}%` }}
|
||||
title={`Dynamic verification: ${dynamicVerifyMs}ms`}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
<div className="timing-legend">
|
||||
<span className="timing-legend-item">
|
||||
|
|
@ -188,6 +197,15 @@ function SummaryTab({ scan }: { scan: ScanView }) {
|
|||
></span>{' '}
|
||||
Post {timing.post_process_ms}ms
|
||||
</span>
|
||||
{dynamicVerifyMs > 0 && (
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--text-tertiary)' }}
|
||||
></span>{' '}
|
||||
Dynamic {dynamicVerifyMs}ms
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -29,36 +29,86 @@ function ScanProgress({
|
|||
}: {
|
||||
data: NonNullable<ReturnType<typeof useSSE>['scanProgress']>;
|
||||
}) {
|
||||
const stages = [
|
||||
type ProgressStage =
|
||||
| 'discovering'
|
||||
| 'indexing'
|
||||
| 'loading_summaries'
|
||||
| 'building_call_graph'
|
||||
| 'analyzing'
|
||||
| 'post_processing'
|
||||
| 'dynamic_verification'
|
||||
| 'complete';
|
||||
|
||||
const hasDynamicStage =
|
||||
data.dynamic_enabled ||
|
||||
data.dynamic_total > 0 ||
|
||||
data.stage === 'dynamic_verification';
|
||||
const stages: ProgressStage[] = [
|
||||
'discovering',
|
||||
'indexing',
|
||||
'loading_summaries',
|
||||
'building_call_graph',
|
||||
'analyzing',
|
||||
'post_processing',
|
||||
...(hasDynamicStage ? ['dynamic_verification' as ProgressStage] : []),
|
||||
'complete',
|
||||
] as const;
|
||||
const stageLabels: Record<string, string> = {
|
||||
];
|
||||
const stageLabels: Record<ProgressStage, string> = {
|
||||
discovering: 'Discovering',
|
||||
indexing: 'Indexing',
|
||||
loading_summaries: 'Loading Summaries',
|
||||
building_call_graph: 'Call Graph',
|
||||
analyzing: 'Analyzing',
|
||||
post_processing: 'Post-Process',
|
||||
dynamic_verification: 'Dynamic Verify',
|
||||
complete: 'Complete',
|
||||
};
|
||||
const currentIdx = stages.indexOf(data.stage as (typeof stages)[number]);
|
||||
const currentIdx = stages.indexOf(data.stage as ProgressStage);
|
||||
|
||||
const total = data.files_discovered || 1;
|
||||
const processed =
|
||||
const totalFiles = data.files_discovered || 0;
|
||||
const safeTotalFiles = totalFiles || 1;
|
||||
const processedFiles =
|
||||
data.stage === 'indexing'
|
||||
? data.files_parsed
|
||||
: data.stage === 'analyzing' || data.stage === 'post_processing'
|
||||
? data.files_analyzed
|
||||
: data.stage === 'complete'
|
||||
? total
|
||||
? totalFiles
|
||||
: 0;
|
||||
const pct = Math.min(100, (processed / total) * 100);
|
||||
const dynamicTotal = data.dynamic_total ?? 0;
|
||||
const dynamicCompleted = Math.min(
|
||||
data.dynamic_completed ?? 0,
|
||||
dynamicTotal || data.dynamic_completed || 0,
|
||||
);
|
||||
const clamp01 = (value: number) => Math.max(0, Math.min(1, value));
|
||||
const stageProgress =
|
||||
data.stage === 'indexing'
|
||||
? clamp01(data.files_parsed / safeTotalFiles)
|
||||
: data.stage === 'loading_summaries' ||
|
||||
data.stage === 'building_call_graph' ||
|
||||
data.stage === 'post_processing'
|
||||
? 0.5
|
||||
: data.stage === 'analyzing'
|
||||
? clamp01(data.files_analyzed / safeTotalFiles)
|
||||
: data.stage === 'dynamic_verification'
|
||||
? dynamicTotal > 0
|
||||
? clamp01(dynamicCompleted / dynamicTotal)
|
||||
: 0
|
||||
: data.stage === 'complete'
|
||||
? 1
|
||||
: 0;
|
||||
const stageTransitions = stages.length - 1;
|
||||
const rawPct =
|
||||
currentIdx >= 0
|
||||
? ((currentIdx + stageProgress) / stageTransitions) * 100
|
||||
: 0;
|
||||
const pct = data.stage === 'complete' ? 100 : Math.min(99, rawPct);
|
||||
const primaryProgressLabel =
|
||||
data.stage === 'dynamic_verification'
|
||||
? dynamicTotal > 0
|
||||
? `${dynamicCompleted} / ${dynamicTotal} findings verified`
|
||||
: 'Verifying findings'
|
||||
: `${processedFiles} / ${totalFiles} files`;
|
||||
const elapsed = data.elapsed_ms
|
||||
? (data.elapsed_ms / 1000).toFixed(1) + 's'
|
||||
: '-';
|
||||
|
|
@ -89,23 +139,26 @@ function ScanProgress({
|
|||
<div className="progress-bar-fill" style={{ width: `${pct}%` }}></div>
|
||||
</div>
|
||||
<div className="progress-stats">
|
||||
<span>
|
||||
{processed} / {data.files_discovered || 0} files
|
||||
</span>
|
||||
<span>{primaryProgressLabel}</span>
|
||||
<span>{pct.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="progress-stats">
|
||||
<span>{data.files_parsed || 0} indexed</span>
|
||||
<span>{data.files_skipped || 0} reused</span>
|
||||
<span>{data.files_analyzed || 0} analyzed</span>
|
||||
{dynamicTotal > 0 && <span>{dynamicCompleted} verified</span>}
|
||||
</div>
|
||||
{data.batches_total > 0 && (
|
||||
{(data.batches_total > 0 || data.stage === 'dynamic_verification') && (
|
||||
<div className="progress-stats">
|
||||
<span>
|
||||
Batch {Math.min(data.batches_completed, data.batches_total)} /{' '}
|
||||
{data.batches_total}
|
||||
</span>
|
||||
<span>{stageLabels[data.stage] || data.stage}</span>
|
||||
{data.batches_total > 0 ? (
|
||||
<span>
|
||||
Batch {Math.min(data.batches_completed, data.batches_total)} /{' '}
|
||||
{data.batches_total}
|
||||
</span>
|
||||
) : (
|
||||
<span>Dynamic verification</span>
|
||||
)}
|
||||
<span>{stageLabels[data.stage as ProgressStage] || data.stage}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="progress-stats">
|
||||
|
|
@ -113,6 +166,9 @@ function ScanProgress({
|
|||
<span>Index {data.timing.pass1_ms}ms</span>
|
||||
<span>Graph {data.timing.call_graph_ms}ms</span>
|
||||
<span>Analyze {data.timing.pass2_ms}ms</span>
|
||||
{(data.timing.dynamic_verify_ms ?? 0) > 0 && (
|
||||
<span>Verify {data.timing.dynamic_verify_ms}ms</span>
|
||||
)}
|
||||
</div>
|
||||
{data.current_file && (
|
||||
<div className="progress-current-file">
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ a:hover {
|
|||
}
|
||||
.target-switcher {
|
||||
position: relative;
|
||||
padding: 0 var(--space-3) var(--space-2);
|
||||
padding: 0 10px var(--space-3);
|
||||
}
|
||||
.target-trigger,
|
||||
.target-option,
|
||||
|
|
@ -191,27 +191,43 @@ a:hover {
|
|||
}
|
||||
.target-trigger {
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
min-height: 56px;
|
||||
display: grid;
|
||||
grid-template-columns: 32px minmax(0, 1fr) 12px;
|
||||
grid-template-columns: 42px minmax(0, 1fr) 14px;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: 7px 8px;
|
||||
border: 1px solid var(--border);
|
||||
gap: var(--space-3);
|
||||
padding: 6px 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface);
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
}
|
||||
.target-trigger:hover,
|
||||
.target-trigger[aria-expanded='true'] {
|
||||
border-color: var(--line-strong);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
.target-avatar,
|
||||
.target-trigger[aria-expanded='true'] {
|
||||
box-shadow: inset 0 0 0 1px var(--border);
|
||||
}
|
||||
.target-avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: var(--radius-sm);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--accent);
|
||||
font-weight: var(--weight-semibold);
|
||||
font-size: 1.05rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.target-option-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: var(--radius-sm);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -219,6 +235,7 @@ a:hover {
|
|||
background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
font-weight: var(--weight-semibold);
|
||||
font-size: 0.85rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.target-trigger-copy,
|
||||
|
|
@ -231,12 +248,18 @@ a:hover {
|
|||
.target-name,
|
||||
.target-option-name {
|
||||
color: var(--text);
|
||||
font-size: var(--text-sm);
|
||||
font-size: 1.05rem;
|
||||
font-weight: var(--weight-semibold);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.target-trigger .target-path {
|
||||
display: none;
|
||||
}
|
||||
.target-option-name {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.target-path,
|
||||
.target-option-path {
|
||||
color: var(--text-tertiary);
|
||||
|
|
@ -246,10 +269,10 @@ a:hover {
|
|||
white-space: nowrap;
|
||||
}
|
||||
.target-caret {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-right: 1.5px solid var(--text-tertiary);
|
||||
border-bottom: 1.5px solid var(--text-tertiary);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-right: 2px solid var(--text-secondary);
|
||||
border-bottom: 2px solid var(--text-secondary);
|
||||
transform: rotate(45deg) translateY(-2px);
|
||||
transition: transform var(--transition-base);
|
||||
}
|
||||
|
|
@ -258,8 +281,8 @@ a:hover {
|
|||
}
|
||||
.target-menu {
|
||||
position: absolute;
|
||||
left: var(--space-3);
|
||||
right: var(--space-3);
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
top: calc(100% - var(--space-1));
|
||||
z-index: 30;
|
||||
padding: var(--space-2);
|
||||
|
|
@ -277,12 +300,12 @@ a:hover {
|
|||
}
|
||||
.target-option {
|
||||
display: grid;
|
||||
grid-template-columns: 28px minmax(0, 1fr);
|
||||
grid-template-columns: 30px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
padding: 5px 6px;
|
||||
min-height: 44px;
|
||||
padding: 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
|
|
@ -298,11 +321,6 @@ a:hover {
|
|||
cursor: default;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.target-option-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.target-add-form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 30px;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ describe('NewScanModal', () => {
|
|||
|
||||
it('calls mutateAsync without verify key when checkbox is untouched', async () => {
|
||||
render(<NewScanModal open={true} onClose={vi.fn()} />);
|
||||
expect(screen.queryByText('Process Hardening')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Start scan' }));
|
||||
await waitFor(() => expect(mockMutateAsync).toHaveBeenCalledOnce());
|
||||
const payload = mockMutateAsync.mock.calls[0][0];
|
||||
|
|
@ -55,7 +56,6 @@ describe('NewScanModal', () => {
|
|||
expect(payload).toEqual({
|
||||
engine_profile: 'balanced',
|
||||
verify_backend: 'auto',
|
||||
harden_profile: 'standard',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -72,6 +72,7 @@ describe('NewScanModal', () => {
|
|||
render(<NewScanModal open={true} onClose={vi.fn()} />);
|
||||
const selects = screen.getAllByRole('combobox');
|
||||
fireEvent.change(selects[2], { target: { value: 'process' } });
|
||||
expect(screen.getByText('Process Hardening')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Start scan' }));
|
||||
await waitFor(() => expect(mockMutateAsync).toHaveBeenCalledOnce());
|
||||
const payload = mockMutateAsync.mock.calls[0][0];
|
||||
|
|
@ -80,4 +81,14 @@ describe('NewScanModal', () => {
|
|||
harden_profile: 'standard',
|
||||
});
|
||||
});
|
||||
|
||||
it('hides process hardening when leaving the process backend', () => {
|
||||
render(<NewScanModal open={true} onClose={vi.fn()} />);
|
||||
const selects = screen.getAllByRole('combobox');
|
||||
fireEvent.change(selects[2], { target: { value: 'process' } });
|
||||
expect(screen.getByText('Process Hardening')).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(selects[2], { target: { value: 'docker' } });
|
||||
expect(screen.queryByText('Process Hardening')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue