From d99361cff6c2a4546cdb51313a673f74e2a8ff1f Mon Sep 17 00:00:00 2001 From: pitboss Date: Thu, 21 May 2026 15:48:29 -0500 Subject: [PATCH] [pitboss/grind] deferred session-0002 (20260521T201327Z-3848) --- frontend/src/api/mutations/scans.ts | 6 ++ frontend/src/api/queries/findings.ts | 1 + frontend/src/api/types.ts | 4 +- frontend/src/components/VerdictBadge.tsx | 4 +- frontend/src/hooks/useFindingsURLState.ts | 3 + frontend/src/modals/NewScanModal.tsx | 53 +++++++++- frontend/src/pages/FindingDetailPage.tsx | 22 ++-- frontend/src/pages/FindingsPage.tsx | 20 +++- .../src/test/modals/NewScanModal.test.tsx | 19 +++- .../framework/adapters/middleware_django.rs | 49 ++++++++- .../framework/adapters/middleware_express.rs | 100 +++++++++++++----- .../framework/adapters/middleware_laravel.rs | 65 ++++++++---- .../framework/adapters/middleware_rails.rs | 90 +++++++++++----- .../framework/adapters/migration_laravel.rs | 49 +++++---- .../framework/adapters/migration_rails.rs | 74 ++++++++----- src/output/json.rs | 28 +++++ src/server/models.rs | 42 +++++++- src/server/routes/findings.rs | 14 ++- 18 files changed, 499 insertions(+), 144 deletions(-) diff --git a/frontend/src/api/mutations/scans.ts b/frontend/src/api/mutations/scans.ts index d6c13f11..467f2f83 100644 --- a/frontend/src/api/mutations/scans.ts +++ b/frontend/src/api/mutations/scans.ts @@ -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() { diff --git a/frontend/src/api/queries/findings.ts b/frontend/src/api/queries/findings.ts index b7e39f40..405a881f 100644 --- a/frontend/src/api/queries/findings.ts +++ b/frontend/src/api/queries/findings.ts @@ -11,6 +11,7 @@ export interface FindingsParams { language?: string; rule_id?: string; status?: string; + verification?: string; search?: string; sort_by?: string; sort_dir?: string; diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 063fd4bb..d6db58b6 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -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 diff --git a/frontend/src/components/VerdictBadge.tsx b/frontend/src/components/VerdictBadge.tsx index a6475a37..f6505f38 100644 --- a/frontend/src/components/VerdictBadge.tsx +++ b/frontend/src/components/VerdictBadge.tsx @@ -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'; diff --git a/frontend/src/hooks/useFindingsURLState.ts b/frontend/src/hooks/useFindingsURLState.ts index 7c90e645..23e3b4e4 100644 --- a/frontend/src/hooks/useFindingsURLState.ts +++ b/frontend/src/hooks/useFindingsURLState.ts @@ -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 = new Set([ 'language', 'rule_id', 'status', + 'verification', 'search', ]); diff --git a/frontend/src/modals/NewScanModal.tsx b/frontend/src/modals/NewScanModal.tsx index 73fd528b..806a504d 100644 --- a/frontend/src/modals/NewScanModal.tsx +++ b/frontend/src/modals/NewScanModal.tsx @@ -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 = { deep: 'Adds symex (cross-file + interproc) and demand-driven backwards taint. About 2 to 3x slower.', }; +const BACKEND_HINTS: Record = { + 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 = { + 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('full'); const [engineProfile, setEngineProfile] = useState('balanced'); const [noVerify, setNoVerify] = useState(false); + const [verifyBackend, setVerifyBackend] = useState('auto'); + const [hardenProfile, setHardenProfile] = useState('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. +
+ + + {BACKEND_HINTS[verifyBackend]} +
+
+ + + {HARDEN_HINTS[hardenProfile]} +
)} - {verdict.attempts.length > 0 && ( + {attempts.length > 0 && (
Payload attempts:
    - {verdict.attempts.map((a, i) => ( + {attempts.map((a, i) => (
  • {a.payload_label} @@ -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 && ( - + )} diff --git a/frontend/src/pages/FindingsPage.tsx b/frontend/src/pages/FindingsPage.tsx index f672198c..3e8cef1d 100644 --- a/frontend/src/pages/FindingsPage.tsx +++ b/frontend/src/pages/FindingsPage.tsx @@ -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({ {values.map((v) => ( ))} @@ -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)} /> + handleFilterChange('verification', v)} + formatValue={formatVerificationStatus} + /> {hasActiveFilters && (