feat(dynamic): add PartiallyConfirmed status for finer-grained sink-reachability categorization, update dynamic verification, telemetry, and reporting systems

This commit is contained in:
elipeter 2026-05-29 14:35:39 -05:00
parent 635b213825
commit c0501884ae
23 changed files with 658 additions and 142 deletions

View file

@ -3,7 +3,12 @@ export type Confidence = 'Low' | 'Medium' | 'High';
export type FlowStepKind = 'source' | 'assignment' | 'call' | 'phi' | 'sink';
// Dynamic verification types (from src/evidence.rs VerifyStatus / VerifyResult)
export type VerifyStatus = 'Confirmed' | 'NotConfirmed' | 'Inconclusive' | 'Unsupported';
export type VerifyStatus =
| 'Confirmed'
| 'PartiallyConfirmed'
| 'NotConfirmed'
| 'Inconclusive'
| 'Unsupported';
export interface AttemptSummary {
payload_label: string;
@ -29,6 +34,7 @@ export interface VerifyResult {
export interface DynamicVerificationSummary {
total: number;
confirmed: number;
partially_confirmed: number;
not_confirmed: number;
inconclusive: number;
unsupported: number;

View file

@ -2,6 +2,7 @@ import type { VerifyResult, VerifyStatus } from '../api/types';
const STATUS_LABELS: Record<VerifyStatus, string> = {
Confirmed: 'Confirmed',
PartiallyConfirmed: 'Partially confirmed',
NotConfirmed: 'Not confirmed',
Inconclusive: 'Inconclusive',
Unsupported: 'Unsupported',
@ -15,6 +16,10 @@ function verdictTooltip(verdict: VerifyResult): string {
return triggered_payload
? `Confirmed via payload: ${triggered_payload}`
: 'Dynamically confirmed exploitable';
case 'PartiallyConfirmed':
return detail
? `Partially confirmed (sink reached): ${detail}`
: 'Partially confirmed: sink reached but exploit chain did not complete';
case 'NotConfirmed':
return (verdict.attempts?.length ?? 0) > 0
? `Not confirmed after ${verdict.attempts?.length ?? 0} payload attempt(s)`

View file

@ -244,13 +244,14 @@ export function ScannerQualityPanel({
const dynamic = quality.dynamic_verification ?? {
total: 0,
confirmed: 0,
partially_confirmed: 0,
not_confirmed: 0,
inconclusive: 0,
unsupported: 0,
};
const dynamicDetail =
dynamic.total > 0
? `${dynamic.total.toLocaleString()} verdicts · ${dynamic.not_confirmed.toLocaleString()} not confirmed · ${dynamic.inconclusive.toLocaleString()} inconclusive · ${dynamic.unsupported.toLocaleString()} unsupported`
? `${dynamic.total.toLocaleString()} verdicts · ${dynamic.partially_confirmed.toLocaleString()} partially confirmed · ${dynamic.not_confirmed.toLocaleString()} not confirmed · ${dynamic.inconclusive.toLocaleString()} inconclusive · ${dynamic.unsupported.toLocaleString()} unsupported`
: 'no dynamic verdicts in latest scan';
const rows: Array<{

View file

@ -31,6 +31,7 @@ function formatTriageState(state: string): string {
function formatVerificationStatus(status: string): string {
if (status === 'NotConfirmed') return 'Not confirmed';
if (status === 'PartiallyConfirmed') return 'Partially confirmed';
return status || 'Unverified';
}

View file

@ -2668,6 +2668,10 @@ tr.selected td {
background: var(--success-bg);
color: var(--success);
}
.badge-dyn-partiallyconfirmed {
background: var(--conf-medium-bg);
color: var(--conf-medium);
}
.badge-dyn-notconfirmed {
background: var(--bg-secondary);
color: var(--text-secondary);

View file

@ -43,6 +43,19 @@ describe('DynamicVerdictSection', () => {
).toBeInTheDocument();
});
it('renders PartiallyConfirmed badge', () => {
render(
<DynamicVerdictSection
verdict={makeVerdict('PartiallyConfirmed', {
detail: 'sink reached but exploit chain did not complete',
})}
/>,
);
expect(
screen.getByTestId('verdict-badge-partiallyconfirmed'),
).toBeInTheDocument();
});
it('does not crash when the API omits an empty attempts array', () => {
render(
<DynamicVerdictSection
@ -82,6 +95,7 @@ describe('DynamicVerdictSection', () => {
unmount();
for (const status of [
'PartiallyConfirmed',
'NotConfirmed',
'Unsupported',
'Inconclusive',

View file

@ -35,6 +35,21 @@ describe('VerdictBadge', () => {
expect(badge.textContent).toContain('🔥');
});
it('renders PartiallyConfirmed badge with amber class and no flame', () => {
render(
<VerdictBadge
verdict={makeVerdict('PartiallyConfirmed', {
detail: 'sink-reachability probe fired but the oracle marker was not observed',
})}
/>,
);
const badge = screen.getByTestId('verdict-badge-partiallyconfirmed');
expect(badge).toBeInTheDocument();
expect(badge.className).toContain('badge-dyn-partiallyconfirmed');
expect(badge.textContent).not.toContain('🔥');
expect(badge.getAttribute('title')).toContain('sink reached');
});
it('renders NotConfirmed badge with correct class', () => {
render(<VerdictBadge verdict={makeVerdict('NotConfirmed')} />);
const badge = screen.getByTestId('verdict-badge-notconfirmed');
@ -107,9 +122,10 @@ describe('VerdictBadge', () => {
expect(badge.textContent?.replace('🔥 ', '')).toBe('C');
});
it('renders all four VerifyStatus variants without crashing', () => {
it('renders all five VerifyStatus variants without crashing', () => {
const statuses: VerifyResult['status'][] = [
'Confirmed',
'PartiallyConfirmed',
'NotConfirmed',
'Unsupported',
'Inconclusive',