mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 01:48:17 +00:00
feat(detection-core): isolate legacy v1 ownership
This commit is contained in:
@@ -197,6 +197,11 @@ def load_migration_inventory(path: str | Path) -> LegacyMigrationInventory:
|
||||
|
||||
def _defined_symbols(tree: ast.Module) -> set[str]:
|
||||
symbols = {"<module>"}
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
symbols.update(alias.asname or alias.name for alias in node.names)
|
||||
elif isinstance(node, ast.Import):
|
||||
symbols.update(alias.asname or alias.name.split(".", 1)[0] for alias in node.names)
|
||||
|
||||
def visit(nodes: list[ast.stmt], parents: tuple[str, ...] = ()) -> None:
|
||||
for node in nodes:
|
||||
@@ -271,6 +276,9 @@ def _capability_index(tree: ast.Module) -> dict[str, set[str]]:
|
||||
constants = {node.value for node in ast.walk(tree) if isinstance(node, ast.Constant) and isinstance(node.value, str)}
|
||||
names = {node.id for node in ast.walk(tree) if isinstance(node, ast.Name)}
|
||||
names.update(node.attr for node in ast.walk(tree) if isinstance(node, ast.Attribute))
|
||||
names.update(
|
||||
alias.asname or alias.name for node in ast.walk(tree) if isinstance(node, (ast.Import, ast.ImportFrom)) for alias in node.names
|
||||
)
|
||||
return {
|
||||
"call": {_call_leaf(node) for node in ast.walk(tree) if isinstance(node, ast.Call)},
|
||||
"constant": constants,
|
||||
@@ -293,6 +301,16 @@ def _missing_anchors(entry: LegacyInventoryEntry, tree: ast.Module) -> tuple[str
|
||||
return tuple(missing)
|
||||
|
||||
|
||||
def _explicit_legacy_shim(tree: ast.Module, symbol: str) -> bool:
|
||||
root_symbol = symbol.split(".", 1)[0]
|
||||
return any(
|
||||
isinstance(node, ast.ImportFrom)
|
||||
and (node.module or "").startswith("openultrasast.core.legacy_v1")
|
||||
and (symbol == "<module>" or any((alias.asname or alias.name) == root_symbol for alias in node.names))
|
||||
for node in tree.body
|
||||
)
|
||||
|
||||
|
||||
def validate_inventory_sources(inventory: LegacyMigrationInventory, *, root: Path) -> None:
|
||||
"""Prove every registered migration target is readable, nonzero, and present."""
|
||||
for entry in inventory.entries:
|
||||
@@ -307,10 +325,11 @@ def validate_inventory_sources(inventory: LegacyMigrationInventory, *, root: Pat
|
||||
tree = ast.parse(content, filename=entry.path)
|
||||
except (SyntaxError, UnicodeDecodeError) as exc:
|
||||
raise LegacyInventoryError(f"inventory source is not parseable: {entry.path}") from exc
|
||||
if entry.symbol not in _defined_symbols(tree):
|
||||
is_shim = _explicit_legacy_shim(tree, entry.symbol)
|
||||
if entry.symbol not in _defined_symbols(tree) and not is_shim:
|
||||
raise LegacyInventoryError(f"inventory target is absent: {entry.path}:{entry.symbol}")
|
||||
missing = _missing_anchors(entry, tree)
|
||||
if missing:
|
||||
if missing and not is_shim:
|
||||
raise LegacyInventoryError(f"inventory capability is absent: {entry.path}:{missing}")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
"""Task 10.1: isolate the live v1 contract and serialization owners."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
V1_FIXTURES = REPO / "tests/fixtures/detection_core/v1"
|
||||
PURE_COMPATIBILITY_MODULES = (
|
||||
"admission.py",
|
||||
"consumer.py",
|
||||
"trace.py",
|
||||
"verdict.py",
|
||||
"_legacy_factpack.py",
|
||||
)
|
||||
|
||||
|
||||
def _trace() -> object:
|
||||
from openultrasast.core.legacy_v1 import contracts
|
||||
|
||||
return contracts.TraceRecord(
|
||||
entry="src/app.py::handler",
|
||||
hops=(contracts.Hop(function="handler", call_line=10, complete=True),),
|
||||
operation_kind="protected_read",
|
||||
resource="credentials",
|
||||
authority="admin",
|
||||
witnesses=(contracts.Witness(kind="guard", provenance="fact:checkPermission", relation="all_paths"),),
|
||||
guards=(contracts.Guard(kind="permission_check", effect="branches"),),
|
||||
completeness_reasons=(),
|
||||
judgment=contracts.Judgment.UNGUARDED,
|
||||
digests={"input": "abc123"},
|
||||
)
|
||||
|
||||
|
||||
def test_legacy_contract_values_have_one_compatibility_owner() -> None:
|
||||
from openultrasast.core.legacy_v1 import contracts
|
||||
|
||||
names = {
|
||||
"AdmissionManifest",
|
||||
"AdmissionManifestError",
|
||||
"BoundedSilence",
|
||||
"FactPackError",
|
||||
"Guard",
|
||||
"Hop",
|
||||
"Judgment",
|
||||
"LegacyFactPack",
|
||||
"ProvenClaim",
|
||||
"PushRequest",
|
||||
"PushVerdict",
|
||||
"TraceRecord",
|
||||
"Witness",
|
||||
}
|
||||
assert {getattr(contracts, name).__module__ for name in names} == {"openultrasast.core.legacy_v1.contracts"}
|
||||
|
||||
|
||||
def test_pure_compatibility_modules_define_no_contract_or_serializer() -> None:
|
||||
for name in PURE_COMPATIBILITY_MODULES:
|
||||
path = REPO / "src/openultrasast/core" / name
|
||||
content = path.read_bytes()
|
||||
assert content, f"ownership instrument must read nonzero bytes from {name}"
|
||||
tree = ast.parse(content, filename=path.as_posix())
|
||||
assert not [node.name for node in tree.body if isinstance(node, ast.ClassDef)]
|
||||
assert not any(isinstance(node, (ast.Import, ast.ImportFrom)) and "json" in ast.unparse(node) for node in tree.body)
|
||||
assert any(isinstance(node, ast.ImportFrom) and node.module == "openultrasast.core.legacy_v1.contracts" for node in tree.body)
|
||||
|
||||
|
||||
def test_writer_is_the_only_compatibility_json_serializer() -> None:
|
||||
namespace = REPO / "src/openultrasast/core/legacy_v1"
|
||||
paths = sorted(namespace.glob("*.py"))
|
||||
assert paths and all(path.read_bytes() for path in paths)
|
||||
json_importers = []
|
||||
for path in paths:
|
||||
tree = ast.parse(path.read_bytes(), filename=path.as_posix())
|
||||
if any(isinstance(node, (ast.Import, ast.ImportFrom)) and "json" in ast.unparse(node) for node in tree.body):
|
||||
json_importers.append(path.name)
|
||||
assert json_importers == ["writer.py"]
|
||||
|
||||
|
||||
def test_namespace_and_v2_dependency_directions_are_closed() -> None:
|
||||
namespace = REPO / "src/openultrasast/core/legacy_v1"
|
||||
for path in namespace.glob("*.py"):
|
||||
tree = ast.parse(path.read_bytes(), filename=path.as_posix())
|
||||
imports = {node.module for node in ast.walk(tree) if isinstance(node, ast.ImportFrom) and node.module}
|
||||
assert all(module.startswith("openultrasast.core.legacy_v1") for module in imports if module.startswith("openultrasast"))
|
||||
|
||||
compatibility = {*PURE_COMPATIBILITY_MODULES, "legacy.py", "__init__.py"}
|
||||
for path in (REPO / "src/openultrasast/core").glob("*.py"):
|
||||
if path.name in compatibility:
|
||||
continue
|
||||
content = path.read_bytes()
|
||||
assert content, f"dependency instrument must read nonzero bytes from {path.name}"
|
||||
tree = ast.parse(content, filename=path.as_posix())
|
||||
imports = {node.module for node in ast.walk(tree) if isinstance(node, ast.ImportFrom) and node.module}
|
||||
assert not any(module.startswith("openultrasast.core.legacy_v1") for module in imports)
|
||||
|
||||
|
||||
def test_legacy_trace_write_remains_byte_identical() -> None:
|
||||
expected = (V1_FIXTURES / "trace.json").read_bytes()
|
||||
assert len(expected) == 401
|
||||
value = _trace()
|
||||
assert value.canonical() == expected.rstrip(b"\n") # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def test_historical_decoder_stays_independent_from_compatibility_namespace() -> None:
|
||||
path = REPO / "src/openultrasast/core/legacy.py"
|
||||
content = path.read_bytes()
|
||||
assert content
|
||||
tree = ast.parse(content, filename=path.as_posix())
|
||||
imports = {node.module for node in ast.walk(tree) if isinstance(node, ast.ImportFrom)}
|
||||
assert "openultrasast.core.legacy_v1" not in imports
|
||||
@@ -170,11 +170,22 @@ def test_independent_reader_audit_matches_split_inventory_capabilities() -> None
|
||||
inventory = load_migration_inventory(INVENTORY)
|
||||
reader_entries = tuple(entry for entry in inventory.entries if entry.dependency_kind.value == "reader")
|
||||
registered = {(entry.path, anchor) for entry in reader_entries for anchor in entry.capability_anchors}
|
||||
current = set(report.capabilities)
|
||||
open_registered = {
|
||||
(entry.path, anchor)
|
||||
for entry in reader_entries
|
||||
if entry.disposition is MigrationDisposition.OPEN
|
||||
for anchor in entry.capability_anchors
|
||||
}
|
||||
|
||||
assert report.files_read > 0 and report.bytes_read > 0
|
||||
assert ("tests/test_core_probes.py", "top_level:ProbeResult") in report.capabilities
|
||||
assert all(len(entry.capability_anchors) == 1 for entry in reader_entries)
|
||||
assert registered == set(report.capabilities)
|
||||
assert current <= registered, "the frozen inventory may never grow to admit a new reader"
|
||||
for path, _anchor in open_registered - current:
|
||||
source = (REPO / path).read_bytes()
|
||||
assert source, f"migration shim instrument must read nonzero bytes from {path}"
|
||||
assert b"openultrasast.core.legacy_v1" in source, f"open reader disappeared without an explicit compatibility shim: {path}"
|
||||
|
||||
|
||||
def test_inventory_targets_require_exact_capability_anchors(tmp_path: Path) -> None:
|
||||
|
||||
Reference in New Issue
Block a user