mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 15:41:12 +00:00
c56d4584e6
Batch 87: 6 NFT-SEC blackbox scenarios + 5 helper evaluators + 75 unit tests + cumulative review batches 85-87. * AZ-436 NFT-SEC-01: cache-poisoning safety budget (AC-NEW-9); aggregate false_trust_count ≤ N×1e-6; zero-tolerance default. Canonical-only by default; E2E_NFT_SEC_01_RELEASE_GATE=1 unlocks full matrix. * AZ-437 NFT-SEC-02 + NFT-SEC-05: shared egress-observation evaluator (AC-NEW-10); SEC-02 = 0 packets to non-e2e-net over 5min replay; SEC-05 = DNS-blackhole sidecar healthy + lookup fails + UDP-53 silent. * AZ-438 NFT-SEC-03: AP-only signing rejection (AC-NEW-11); 3 sub-cases (unsigned/wrong-key/replayed) each reject ≤500ms + no position drift. * AZ-439 NFT-SEC-04: probe (always-run) = no-crash + deterministic decode outcome; ASan-fuzz (release-gate) = 0 findings ≥4h; AC-3 corpus floor informational only per spec. Verdict per-batch: PASS_WITH_WARNINGS (5 Low). Cumulative review for batches 85-87 (K=3 window) also PASS_WITH_WARNINGS with 5 cross-batch findings — recommends hygiene PBIs for write_csv_evidence duplication (13 helpers) and _resolve_fixture_path duplication (13 scenarios), plus new tickets for AZ-595 fixture builder + DNS-blackhole sidecar service. Also adds _docs/LESSONS.md documenting the Jira transition-ID lesson (always call getTransitionsForJiraIssue first, never memorize numeric IDs across sessions). Co-authored-by: Cursor <cursoragent@cursor.com>
144 lines
4.7 KiB
Python
144 lines
4.7 KiB
Python
"""OpenCV CVE-2025-53644 probe evaluator for NFT-SEC-04 (AZ-439 / RESTRICT-CVE-1).
|
|
|
|
The probe feeds the canonical ``cve-jpeg-fixture`` (a single crafted
|
|
JPEG known to trigger the upstream OpenCV vulnerability) into the SUT's
|
|
nav-camera input and asserts:
|
|
|
|
* AC-1a: the SUT process remains **alive** after the frame (no crash,
|
|
no abort, no segfault — the FDR archive contains AT LEAST one record
|
|
whose monotonic_ms strictly post-dates the probe);
|
|
* AC-1b: the FDR contains a deterministic outcome for the probe frame
|
|
— either a ``decode-success`` record (the patched OpenCV decoded
|
|
the crafted JPEG) OR a ``frame-decode-error`` record (the SUT
|
|
gracefully rejected the malformed payload). A silent drop (no FDR
|
|
record at all) is a failure, because it means the SUT swallowed the
|
|
frame without honoring its error-reporting contract.
|
|
|
|
Public-boundary discipline: does NOT import any
|
|
``src/gps_denied_onboard`` symbol. The evaluator consumes only the
|
|
runner-collected ``FdrSurvivalRecord`` summaries the FDR-reader helper
|
|
already produces for other scenarios.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
from typing import Sequence
|
|
|
|
|
|
class ProbeFrameOutcome(str, Enum):
|
|
"""The deterministic per-frame outcome the SUT must record."""
|
|
|
|
DECODE_SUCCESS = "decode-success"
|
|
FRAME_DECODE_ERROR = "frame-decode-error"
|
|
MISSING = "missing" # silent drop — fails AC-1b
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FdrSurvivalRecord:
|
|
"""One FDR record from the runner-collected archive."""
|
|
|
|
monotonic_ms: int
|
|
kind: str # e.g. "frame-decode-success" or "frame-decode-error"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CveProbeReport:
|
|
"""Verdict for one ``cve-jpeg-fixture`` injection."""
|
|
|
|
probe_injected_at_ms: int
|
|
last_fdr_record_at_ms: int | None
|
|
probe_outcome: ProbeFrameOutcome
|
|
|
|
@property
|
|
def passes_no_crash(self) -> bool:
|
|
return (
|
|
self.last_fdr_record_at_ms is not None
|
|
and self.last_fdr_record_at_ms >= self.probe_injected_at_ms
|
|
)
|
|
|
|
@property
|
|
def passes_graceful_outcome(self) -> bool:
|
|
return self.probe_outcome in (
|
|
ProbeFrameOutcome.DECODE_SUCCESS,
|
|
ProbeFrameOutcome.FRAME_DECODE_ERROR,
|
|
)
|
|
|
|
@property
|
|
def passes(self) -> bool:
|
|
return self.passes_no_crash and self.passes_graceful_outcome
|
|
|
|
|
|
def classify_probe_outcome(
|
|
fdr_records: Sequence[FdrSurvivalRecord],
|
|
*,
|
|
probe_injected_at_ms: int,
|
|
tolerance_ms: int = 50,
|
|
) -> ProbeFrameOutcome:
|
|
"""Pick the FDR record nearest the probe injection and classify it.
|
|
|
|
A record is considered ``for the probe`` if its monotonic timestamp
|
|
lies within ``[probe_injected_at_ms, probe_injected_at_ms + tolerance_ms]``.
|
|
If no record falls in that window the outcome is ``MISSING`` —
|
|
which fails AC-1b regardless of the no-crash check.
|
|
"""
|
|
for record in fdr_records:
|
|
if record.monotonic_ms < probe_injected_at_ms:
|
|
continue
|
|
if record.monotonic_ms > probe_injected_at_ms + tolerance_ms:
|
|
continue
|
|
if "decode-success" in record.kind:
|
|
return ProbeFrameOutcome.DECODE_SUCCESS
|
|
if "decode-error" in record.kind:
|
|
return ProbeFrameOutcome.FRAME_DECODE_ERROR
|
|
return ProbeFrameOutcome.MISSING
|
|
|
|
|
|
def evaluate(
|
|
fdr_records: Sequence[FdrSurvivalRecord],
|
|
*,
|
|
probe_injected_at_ms: int,
|
|
tolerance_ms: int = 50,
|
|
) -> CveProbeReport:
|
|
last_record_at = max((r.monotonic_ms for r in fdr_records), default=None)
|
|
outcome = classify_probe_outcome(
|
|
fdr_records,
|
|
probe_injected_at_ms=probe_injected_at_ms,
|
|
tolerance_ms=tolerance_ms,
|
|
)
|
|
return CveProbeReport(
|
|
probe_injected_at_ms=probe_injected_at_ms,
|
|
last_fdr_record_at_ms=last_record_at,
|
|
probe_outcome=outcome,
|
|
)
|
|
|
|
|
|
def write_csv_evidence(out_path: Path, report: CveProbeReport) -> Path:
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
with out_path.open("w", newline="") as fh:
|
|
writer = csv.writer(fh)
|
|
writer.writerow(
|
|
[
|
|
"probe_injected_at_ms",
|
|
"last_fdr_record_at_ms",
|
|
"probe_outcome",
|
|
"passes_no_crash",
|
|
"passes_graceful_outcome",
|
|
"passes",
|
|
]
|
|
)
|
|
writer.writerow(
|
|
[
|
|
report.probe_injected_at_ms,
|
|
"" if report.last_fdr_record_at_ms is None else report.last_fdr_record_at_ms,
|
|
report.probe_outcome.value,
|
|
"true" if report.passes_no_crash else "false",
|
|
"true" if report.passes_graceful_outcome else "false",
|
|
"true" if report.passes else "false",
|
|
]
|
|
)
|
|
return out_path
|