[pitboss] sweep after phase 09: 4 deferred items resolved

This commit is contained in:
pitboss 2026-05-12 14:48:40 -04:00
parent 996bff5983
commit e9649ea099
5 changed files with 224 additions and 15 deletions

View file

@ -408,3 +408,6 @@ jobs:
run: cargo nextest run --lib -p nyx-scanner dynamic::corpus
env:
RUST_LOG: error
- name: Corpus dashboard sync check (Python/Rust payload table parity)
run: python3 scripts/check_corpus_sync.py

View file

@ -0,0 +1,66 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { NewScanModal } from '@/modals/NewScanModal';
const mockMutateAsync = vi.hoisted(() => vi.fn());
const mockNavigate = vi.hoisted(() => vi.fn());
const mockToastSuccess = vi.hoisted(() => vi.fn());
const mockToastError = vi.hoisted(() => vi.fn());
vi.mock('@/api/queries/health', () => ({
useHealth: () => ({ data: { scan_root: '/test/project' } }),
}));
vi.mock('@/api/mutations/scans', () => ({
useStartScan: () => ({
mutateAsync: mockMutateAsync,
isPending: false,
}),
}));
vi.mock('react-router-dom', () => ({
useNavigate: () => mockNavigate,
}));
vi.mock('@/contexts/ToastContext', () => ({
useToast: () => ({ success: mockToastSuccess, error: mockToastError }),
}));
vi.mock('@/components/ui/Modal', () => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Modal: ({ open, children }: { open: boolean; children?: any }) =>
open ? <>{children}</> : null,
}));
describe('NewScanModal', () => {
beforeEach(() => {
mockMutateAsync.mockReset();
mockMutateAsync.mockResolvedValue(undefined);
mockNavigate.mockReset();
mockToastSuccess.mockReset();
mockToastError.mockReset();
});
it('renders when open is true', () => {
render(<NewScanModal open={true} onClose={vi.fn()} />);
expect(screen.getByText('Start new scan')).toBeInTheDocument();
});
it('calls mutateAsync without verify key when checkbox is untouched', async () => {
render(<NewScanModal open={true} onClose={vi.fn()} />);
fireEvent.click(screen.getByRole('button', { name: 'Start scan' }));
await waitFor(() => expect(mockMutateAsync).toHaveBeenCalledOnce());
const payload = mockMutateAsync.mock.calls[0][0];
expect(payload).not.toHaveProperty('verify');
expect(payload).toEqual({ engine_profile: 'balanced' });
});
it('calls mutateAsync with verify: false when checkbox is checked', async () => {
render(<NewScanModal open={true} onClose={vi.fn()} />);
fireEvent.click(screen.getByRole('checkbox'));
fireEvent.click(screen.getByRole('button', { name: 'Start scan' }));
await waitFor(() => expect(mockMutateAsync).toHaveBeenCalledOnce());
const payload = mockMutateAsync.mock.calls[0][0];
expect(payload).toEqual({ engine_profile: 'balanced', verify: false });
});
});

View file

@ -0,0 +1,84 @@
#!/usr/bin/env python3
# Usage: python3 scripts/check_corpus_sync.py
# Run from repo root or any subdirectory; the script relocates to repo root.
# Exits 0 if src/dynamic/corpus.rs and scripts/corpus_dashboard.py agree on
# CORPUS_VERSION and all payload labels. Exits 1 on any divergence.
import os
import re
import sys
from pathlib import Path
# ── locate repo root (parent of the scripts/ dir this file lives in) ─────────
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parent
os.chdir(REPO_ROOT)
CORPUS_RS = REPO_ROOT / "src" / "dynamic" / "corpus.rs"
DASHBOARD_PY = REPO_ROOT / "scripts" / "corpus_dashboard.py"
# ── parse helpers ─────────────────────────────────────────────────────────────
def parse_corpus_rs(path: Path):
text = path.read_text(encoding="utf-8")
version_match = re.search(r'pub const CORPUS_VERSION:\s*u32\s*=\s*(\d+);', text)
version = int(version_match.group(1)) if version_match else None
labels = set(re.findall(r'label:\s*"([^"]+)"', text))
return version, labels
def parse_dashboard_py(path: Path):
text = path.read_text(encoding="utf-8")
version_match = re.search(r'CORPUS_VERSION\s*=\s*(\d+)', text)
version = int(version_match.group(1)) if version_match else None
labels = set(re.findall(r'label="([^"]+)"', text))
return version, labels
# ── main ──────────────────────────────────────────────────────────────────────
def main() -> int:
rs_version, rs_labels = parse_corpus_rs(CORPUS_RS)
py_version, py_labels = parse_dashboard_py(DASHBOARD_PY)
ok = True
# version check
if rs_version is None:
print("ERROR: CORPUS_VERSION not found in corpus.rs")
ok = False
if py_version is None:
print("ERROR: CORPUS_VERSION not found in corpus_dashboard.py")
ok = False
if rs_version is not None and py_version is not None:
if rs_version == py_version:
print(f"CORPUS_VERSION: {rs_version} [match]")
else:
print(f"CORPUS_VERSION mismatch: corpus.rs={rs_version} corpus_dashboard.py={py_version}")
ok = False
# label check
only_in_rs = rs_labels - py_labels
only_in_py = py_labels - rs_labels
shared = rs_labels & py_labels
print(f"Labels in both: {len(shared)}")
if only_in_rs:
print(f"Labels only in corpus.rs: {len(only_in_rs)}")
for lbl in sorted(only_in_rs):
print(f" + {lbl}")
ok = False
if only_in_py:
print(f"Labels only in corpus_dashboard.py: {len(only_in_py)}")
for lbl in sorted(only_in_py):
print(f" - {lbl}")
ok = False
if ok:
print("Corpus sync: OK")
return 0
else:
print("Corpus sync: FAIL — update corpus_dashboard.py to match corpus.rs")
return 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -167,6 +167,13 @@ else
else
# Run each escape fixture under both Process and Docker backends.
# A Confirmed verdict on an escape fixture is a gate failure.
# Additionally, check that no sentinel files were written to the host.
ESCAPE_SENTINEL_PATHS=(
"/tmp/nyx_escape_file_write_marker"
)
for _s in "${ESCAPE_SENTINEL_PATHS[@]}"; do
rm -f "$_s"
done
ESCAPE_FAILED=0
for fixture in "$ESCAPE_DIR"/*/; do
[[ -d "$fixture" ]] || continue
@ -196,7 +203,16 @@ print(len(confirmed))
fi
done
done
for _s in "${ESCAPE_SENTINEL_PATHS[@]}"; do
if [[ -f "$_s" ]]; then
die "Gate 4: escape sentinel written to host: $_s"
ESCAPE_FAILED=1
fi
done
[[ $ESCAPE_FAILED -eq 0 ]] && pass "Gate 4: sandbox escape suite green"
for _s in "${ESCAPE_SENTINEL_PATHS[@]}"; do
rm -f "$_s"
done
fi
fi

View file

@ -17,6 +17,29 @@ import sys
from collections import defaultdict
from pathlib import Path
LINE_TOLERANCE = 5
_CAP_PREFIX_TABLE = [
("taint.path_traversal", "path_traversal"),
("taint.sql", "sqli"),
("taint.xss", "xss"),
("taint.ssrf", "ssrf"),
("taint.cmdi", "cmdi"),
("taint.deserialize", "deserialize"),
("taint.redirect", "redirect"),
("taint.xxe", "xxe"),
("path_traversal", "path_traversal"),
("sqli", "sqli"),
("xss", "xss"),
("ssrf", "ssrf"),
("cmdi", "cmdi"),
("deserialize", "deserialize"),
("redirect", "redirect"),
("xxe", "xxe"),
("auth", "auth"),
("taint", "taint"),
]
def load_json(path: str) -> object:
with open(path) as f:
@ -24,11 +47,9 @@ def load_json(path: str) -> object:
def cap_of(finding: dict) -> str:
rule = finding.get("rule_id", "")
# Map rule_id prefix to cap name.
for cap in ["sqli", "xss", "cmdi", "ssrf", "deserialize", "path_traversal",
"redirect", "xxe", "taint", "auth"]:
if cap in rule.lower():
rule = finding.get("rule_id", "").lower()
for prefix, cap in _CAP_PREFIX_TABLE:
if rule.startswith(prefix):
return cap
return "other"
@ -76,26 +97,45 @@ def main() -> int:
if not args.inhouse and args.ground_truth and Path(args.ground_truth).exists():
gt = load_json(args.ground_truth)
# Ground truth format: list of {"path": ..., "line": ..., "cap": ..., "vuln": bool}
gt_true: set[tuple[str, int, str]] = set()
gt_true: list[dict] = []
for entry in gt if isinstance(gt, list) else []:
if entry.get("vuln"):
gt_true.add((entry.get("path", ""), entry.get("line", 0), entry.get("cap", "")))
gt_true.append({
"path": entry.get("path", ""),
"line": entry.get("line", 0),
"cap": entry.get("cap", ""),
})
# Track which GT entries were matched (by index) to avoid double-counting.
matched_gt: set[int] = set()
# Track (path, cap) pairs that had at least one finding match.
found_path_caps: set[tuple[str, str]] = set()
found_keys: set[tuple[str, int, str]] = set()
for f in findings:
key_gt = (f.get("path", ""), f.get("line", 0), cap_of(f))
found_keys.add(key_gt)
cap = cap_of(f)
f_path = f.get("path", "")
f_line = f.get("line", 0)
f_cap = cap_of(f)
cap = f_cap
lang = lang_of(f)
cell_key = (cap, lang)
if key_gt in gt_true:
matched_idx = None
for idx, gt_entry in enumerate(gt_true):
if (gt_entry["path"] == f_path
and gt_entry["cap"] == f_cap
and abs(gt_entry["line"] - f_line) <= LINE_TOLERANCE
and idx not in matched_gt):
matched_idx = idx
break
if matched_idx is not None:
matched_gt.add(matched_idx)
found_path_caps.add((f_path, f_cap))
cells[cell_key]["tp"] += 1
else:
cells[cell_key]["fp"] += 1
for gt_key in gt_true:
if gt_key not in found_keys:
cap = gt_key[2]
for idx, gt_entry in enumerate(gt_true):
if idx not in matched_gt:
cap = gt_entry["cap"]
cells[(cap, "unknown")]["fn"] += 1
result = {