mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
[pitboss/grind] deferred session-0002 (20260521T201327Z-3848)
This commit is contained in:
parent
159a779f31
commit
d99361cff6
18 changed files with 499 additions and 144 deletions
|
|
@ -4,6 +4,8 @@ import type { ScanView } from '../types';
|
|||
|
||||
export type ScanMode = 'full' | 'ast' | 'cfg' | 'taint';
|
||||
export type EngineProfile = 'fast' | 'balanced' | 'deep';
|
||||
export type VerifyBackend = 'auto' | 'docker' | 'process' | 'firecracker';
|
||||
export type HardenProfile = 'standard' | 'strict';
|
||||
|
||||
export interface StartScanBody {
|
||||
scan_root?: string;
|
||||
|
|
@ -18,6 +20,10 @@ export interface StartScanBody {
|
|||
verify?: boolean;
|
||||
/** Also verify Confidence < Medium findings. Default false. */
|
||||
verify_all_confidence?: boolean;
|
||||
/** Sandbox backend for dynamic verification. */
|
||||
verify_backend?: VerifyBackend;
|
||||
/** Process-backend hardening profile. */
|
||||
harden_profile?: HardenProfile;
|
||||
}
|
||||
|
||||
export function useStartScan() {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface FindingsParams {
|
|||
language?: string;
|
||||
rule_id?: string;
|
||||
status?: string;
|
||||
verification?: string;
|
||||
search?: string;
|
||||
sort_by?: string;
|
||||
sort_dir?: string;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export interface VerifyResult {
|
|||
/** Typed InconclusiveReason (PascalCase string) */
|
||||
inconclusive_reason?: string;
|
||||
detail?: string;
|
||||
attempts: AttemptSummary[];
|
||||
attempts?: AttemptSummary[];
|
||||
toolchain_match?: string;
|
||||
}
|
||||
|
||||
|
|
@ -134,6 +134,7 @@ export interface FindingView {
|
|||
triage_note?: string;
|
||||
code_context?: CodeContextView;
|
||||
evidence?: Evidence;
|
||||
dynamic_verdict?: VerifyResult;
|
||||
guard_kind?: string;
|
||||
rank_reason?: [string, string][];
|
||||
sanitizer_status?: string;
|
||||
|
|
@ -155,6 +156,7 @@ export interface FilterValues {
|
|||
languages: string[];
|
||||
rules: string[];
|
||||
statuses: string[];
|
||||
verification_statuses: string[];
|
||||
}
|
||||
|
||||
// Scan types
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ function verdictTooltip(verdict: VerifyResult): string {
|
|||
? `Confirmed via payload: ${triggered_payload}`
|
||||
: 'Dynamically confirmed exploitable';
|
||||
case 'NotConfirmed':
|
||||
return verdict.attempts.length > 0
|
||||
? `Not confirmed after ${verdict.attempts.length} payload attempt(s)`
|
||||
return (verdict.attempts?.length ?? 0) > 0
|
||||
? `Not confirmed after ${verdict.attempts?.length ?? 0} payload attempt(s)`
|
||||
: 'Not confirmed';
|
||||
case 'Unsupported':
|
||||
return reason ? `Unsupported: ${reason}` : 'Dynamic verification not supported';
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface FindingsURLState {
|
|||
language: string;
|
||||
rule_id: string;
|
||||
status: string;
|
||||
verification: string;
|
||||
search: string;
|
||||
}
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ const FINDINGS_DEFAULTS: FindingsURLState = {
|
|||
language: '',
|
||||
rule_id: '',
|
||||
status: '',
|
||||
verification: '',
|
||||
search: '',
|
||||
};
|
||||
|
||||
|
|
@ -52,6 +54,7 @@ const FILTER_KEYS: ReadonlySet<string> = new Set([
|
|||
'language',
|
||||
'rule_id',
|
||||
'status',
|
||||
'verification',
|
||||
'search',
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import {
|
|||
useStartScan,
|
||||
type ScanMode,
|
||||
type EngineProfile,
|
||||
type VerifyBackend,
|
||||
type HardenProfile,
|
||||
type StartScanBody,
|
||||
} from '../api/mutations/scans';
|
||||
|
||||
|
|
@ -29,6 +31,18 @@ const PROFILE_HINTS: Record<EngineProfile, string> = {
|
|||
deep: 'Adds symex (cross-file + interproc) and demand-driven backwards taint. About 2 to 3x slower.',
|
||||
};
|
||||
|
||||
const BACKEND_HINTS: Record<VerifyBackend, string> = {
|
||||
auto: 'Use Docker when it fits, otherwise fall back to process.',
|
||||
docker: 'Require Docker-backed harness execution.',
|
||||
process: 'Unsafe local process backend for quick test runs.',
|
||||
firecracker: 'Use the Firecracker backend when available.',
|
||||
};
|
||||
|
||||
const HARDEN_HINTS: Record<HardenProfile, string> = {
|
||||
standard: 'Baseline process limits.',
|
||||
strict: 'Stricter process confinement when supported.',
|
||||
};
|
||||
|
||||
export function NewScanModal({ open, onClose }: NewScanModalProps) {
|
||||
const { data: health } = useHealth();
|
||||
const startScan = useStartScan();
|
||||
|
|
@ -39,6 +53,8 @@ export function NewScanModal({ open, onClose }: NewScanModalProps) {
|
|||
const [mode, setMode] = useState<ScanMode>('full');
|
||||
const [engineProfile, setEngineProfile] = useState<EngineProfile>('balanced');
|
||||
const [noVerify, setNoVerify] = useState(false);
|
||||
const [verifyBackend, setVerifyBackend] = useState<VerifyBackend>('auto');
|
||||
const [hardenProfile, setHardenProfile] = useState<HardenProfile>('standard');
|
||||
|
||||
const handleStart = async () => {
|
||||
const root = scanRoot.trim();
|
||||
|
|
@ -46,7 +62,12 @@ export function NewScanModal({ open, onClose }: NewScanModalProps) {
|
|||
if (root && root !== defaultRoot) body.scan_root = root;
|
||||
if (mode !== 'full') body.mode = mode;
|
||||
body.engine_profile = engineProfile;
|
||||
if (noVerify) body.verify = false;
|
||||
if (noVerify) {
|
||||
body.verify = false;
|
||||
} else {
|
||||
body.verify_backend = verifyBackend;
|
||||
body.harden_profile = hardenProfile;
|
||||
}
|
||||
const payload = Object.keys(body).length ? body : undefined;
|
||||
try {
|
||||
await startScan.mutateAsync(payload);
|
||||
|
|
@ -125,6 +146,36 @@ export function NewScanModal({ open, onClose }: NewScanModalProps) {
|
|||
findings. Check to skip and get a fast static-only result.
|
||||
</span>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Verification Backend</label>
|
||||
<select
|
||||
value={verifyBackend}
|
||||
disabled={noVerify}
|
||||
onChange={(e) =>
|
||||
setVerifyBackend(e.target.value as VerifyBackend)
|
||||
}
|
||||
>
|
||||
<option value="auto">Auto</option>
|
||||
<option value="docker">Docker</option>
|
||||
<option value="process">Process (unsafe)</option>
|
||||
<option value="firecracker">Firecracker</option>
|
||||
</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>
|
||||
<div className="scan-modal-actions">
|
||||
<button className="btn btn-sm" onClick={onClose}>
|
||||
Cancel
|
||||
|
|
|
|||
|
|
@ -707,16 +707,21 @@ function HowToFix({ finding }: { finding: FindingView }) {
|
|||
|
||||
export function DynamicVerdictSection({ verdict }: { verdict: VerifyResult }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const attempts = verdict.attempts ?? [];
|
||||
// The repro bundle is keyed by spec_hash (not finding_id) inside the Nyx
|
||||
// cache. Rather than showing a path that may not match, surface the CLI
|
||||
// command that locates and opens the bundle regardless of the hash.
|
||||
const reproCmd = `nyx repro --finding ${verdict.finding_id}`;
|
||||
|
||||
const copyCmd = () => {
|
||||
navigator.clipboard.writeText(reproCmd).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
if (!navigator.clipboard) return;
|
||||
navigator.clipboard.writeText(reproCmd).then(
|
||||
() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -767,11 +772,11 @@ export function DynamicVerdictSection({ verdict }: { verdict: VerifyResult }) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{verdict.attempts.length > 0 && (
|
||||
{attempts.length > 0 && (
|
||||
<div className="dynamic-attempts">
|
||||
<strong>Payload attempts:</strong>
|
||||
<ul className="dynamic-attempt-list">
|
||||
{verdict.attempts.map((a, i) => (
|
||||
{attempts.map((a, i) => (
|
||||
<li key={i} className={`attempt-row ${a.triggered ? 'triggered' : ''}`}>
|
||||
<code>{a.payload_label}</code>
|
||||
<span className="attempt-outcome">
|
||||
|
|
@ -953,6 +958,7 @@ export function FindingDetailPage() {
|
|||
|
||||
const f = finding;
|
||||
const evidence = f.evidence;
|
||||
const dynamicVerdict = evidence?.dynamic_verdict ?? f.dynamic_verdict;
|
||||
const isState = isStateFinding(f);
|
||||
const hasWhySection =
|
||||
f.message ||
|
||||
|
|
@ -1110,9 +1116,9 @@ export function FindingDetailPage() {
|
|||
)}
|
||||
|
||||
{/* Dynamic Verification */}
|
||||
{evidence?.dynamic_verdict && (
|
||||
{dynamicVerdict && (
|
||||
<CollapsibleSection title="Dynamic Verification">
|
||||
<DynamicVerdictSection verdict={evidence.dynamic_verdict} />
|
||||
<DynamicVerdictSection verdict={dynamicVerdict} />
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ function formatTriageState(state: string): string {
|
|||
return (state || 'open').replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
function formatVerificationStatus(status: string): string {
|
||||
if (status === 'NotConfirmed') return 'Not confirmed';
|
||||
return status || 'Unverified';
|
||||
}
|
||||
|
||||
// ── Filter Bar ──────────────────────────────────────────────────────────────
|
||||
|
||||
interface FilterSelectProps {
|
||||
|
|
@ -37,6 +42,7 @@ interface FilterSelectProps {
|
|||
values: string[] | undefined;
|
||||
current: string;
|
||||
onChange: (value: string) => void;
|
||||
formatValue?: (value: string) => string;
|
||||
}
|
||||
|
||||
function FilterSelect({
|
||||
|
|
@ -45,6 +51,7 @@ function FilterSelect({
|
|||
values,
|
||||
current,
|
||||
onChange,
|
||||
formatValue,
|
||||
}: FilterSelectProps) {
|
||||
if (!values || values.length === 0) return null;
|
||||
return (
|
||||
|
|
@ -52,7 +59,7 @@ function FilterSelect({
|
|||
<option value="">All {label}</option>
|
||||
{values.map((v) => (
|
||||
<option key={v} value={v}>
|
||||
{v}
|
||||
{formatValue ? formatValue(v) : v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -322,6 +329,7 @@ export function FindingsPage() {
|
|||
language: state.language || undefined,
|
||||
rule_id: state.rule_id || undefined,
|
||||
status: state.status || undefined,
|
||||
verification: state.verification || undefined,
|
||||
search: state.search || undefined,
|
||||
}),
|
||||
[state],
|
||||
|
|
@ -621,6 +629,14 @@ export function FindingsPage() {
|
|||
current={state.status}
|
||||
onChange={(v) => handleFilterChange('status', v)}
|
||||
/>
|
||||
<FilterSelect
|
||||
id="filter-verification"
|
||||
label="Verification"
|
||||
values={filters?.verification_statuses}
|
||||
current={state.verification}
|
||||
onChange={(v) => handleFilterChange('verification', v)}
|
||||
formatValue={formatVerificationStatus}
|
||||
/>
|
||||
{hasActiveFilters && (
|
||||
<button className="btn btn-sm btn-clear" onClick={resetFilters}>
|
||||
Clear All
|
||||
|
|
@ -764,7 +780,7 @@ export function FindingsPage() {
|
|||
</td>
|
||||
<td>
|
||||
<VerdictBadge
|
||||
verdict={f.evidence?.dynamic_verdict}
|
||||
verdict={f.dynamic_verdict ?? f.evidence?.dynamic_verdict}
|
||||
compact
|
||||
/>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,11 @@ describe('NewScanModal', () => {
|
|||
await waitFor(() => expect(mockMutateAsync).toHaveBeenCalledOnce());
|
||||
const payload = mockMutateAsync.mock.calls[0][0];
|
||||
expect(payload).not.toHaveProperty('verify');
|
||||
expect(payload).toEqual({ engine_profile: 'balanced' });
|
||||
expect(payload).toEqual({
|
||||
engine_profile: 'balanced',
|
||||
verify_backend: 'auto',
|
||||
harden_profile: 'standard',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls mutateAsync with verify: false when checkbox is checked', async () => {
|
||||
|
|
@ -63,4 +67,17 @@ describe('NewScanModal', () => {
|
|||
const payload = mockMutateAsync.mock.calls[0][0];
|
||||
expect(payload).toEqual({ engine_profile: 'balanced', verify: false });
|
||||
});
|
||||
|
||||
it('allows selecting the unsafe process verification backend', async () => {
|
||||
render(<NewScanModal open={true} onClose={vi.fn()} />);
|
||||
const selects = screen.getAllByRole('combobox');
|
||||
fireEvent.change(selects[2], { target: { value: 'process' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Start scan' }));
|
||||
await waitFor(() => expect(mockMutateAsync).toHaveBeenCalledOnce());
|
||||
const payload = mockMutateAsync.mock.calls[0][0];
|
||||
expect(payload).toMatchObject({
|
||||
verify_backend: 'process',
|
||||
harden_profile: 'standard',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue