Files
Oleksandr Bezdieniezhnykh c56d4584e6 [AZ-436] [AZ-437] [AZ-438] [AZ-439] Add NFT-SEC-01..05 security scenarios
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>
2026-05-17 17:33:22 +03:00

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