mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 01:48:17 +00:00
feat(access-control-prover): 11.3 extract existing target knowledge into candidate records
This commit is contained in:
@@ -223,7 +223,7 @@ its final resolution, dominance, fact-acquisition and measurement semantics with
|
||||
- _Requirements: 1.2, 1.3, 1.4, 1.5, 1.6_
|
||||
- _Boundary: Registration Adapter_
|
||||
- _Depends: 8.4, 11.1_
|
||||
- [ ] 11.3 (P) Extract existing target knowledge into candidate records
|
||||
- [x] 11.3 (P) Extract existing target knowledge into candidate records
|
||||
- Read legacy recognition strategies and current facts without changing promoted outputs.
|
||||
- Done when extraction produces deterministic candidate records with source/version/region evidence.
|
||||
- _Requirements: 2.9_
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Tooling package for the access-control-prover acquisition workflow."""
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Task 11.3: extract existing target knowledge into candidate records (Req 2.9).
|
||||
|
||||
Extraction mode of the initialization job: the legacy recognition strategies in
|
||||
``mapping/legacy.py`` and the promoted fact rows in ``ruleset/obligations/*.toml``
|
||||
are read -- never modified -- and returned as deterministic candidate records
|
||||
carrying source, version and region evidence. Verification and promotion are the
|
||||
acquisition workflow's job (tasks 11.4+), not extraction's.
|
||||
|
||||
This is a tools module, not an engine module: naming recognition targets here is
|
||||
legitimate (the whole point is to extract their vocabulary into data).
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import tomllib
|
||||
from collections.abc import Iterable, Iterator
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
_RECOGNISER_PATTERN = re.compile(
|
||||
r"^(def (_[a-z][a-z0-9_]*_entries)\(|def (_python_registered_routes)\(|(_PHP_REGISTRATION) = )",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CandidateRecord:
|
||||
"""A unit of extracted target knowledge awaiting verification (Req 2.9)."""
|
||||
|
||||
kind: str # 'operation' | 'discharger' | 'implication' | 'entry-strategy'
|
||||
name: str # fact id or recogniser function/constant name
|
||||
source: str # file the knowledge was read from (relative-style path tail)
|
||||
version: str # sha256[:12] of the source file's bytes
|
||||
region: str # '<file>:<table>:<id>' or 'legacy.py:<symbol>:<start>-<end>'
|
||||
payload: tuple[tuple[str, str], ...] = () # sorted field pairs
|
||||
|
||||
|
||||
def extract_fact_rows(ruleset_dir: Path) -> tuple[CandidateRecord, ...]:
|
||||
"""Extract operation, discharger and implication rows as candidates."""
|
||||
records: list[CandidateRecord] = []
|
||||
for path in sorted(ruleset_dir.glob("*.toml")):
|
||||
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
||||
version = _digest(path)
|
||||
records.extend(_row_records(path.name, version, data))
|
||||
return tuple(records)
|
||||
|
||||
|
||||
def extract_legacy_strategies(mapping_path: Path) -> tuple[CandidateRecord, ...]:
|
||||
"""Extract the legacy recogniser inventory (symbol regions) as candidates."""
|
||||
text = mapping_path.read_text(encoding="utf-8")
|
||||
version = _digest(mapping_path)
|
||||
records = [
|
||||
CandidateRecord(
|
||||
kind="entry-strategy",
|
||||
name=symbol,
|
||||
source=mapping_path.name,
|
||||
version=version,
|
||||
region=f"legacy.py:{symbol}:{start}-{end}",
|
||||
)
|
||||
for symbol, start, end in _symbol_spans(text)
|
||||
]
|
||||
return tuple(records)
|
||||
|
||||
|
||||
def extract_all(repo_root: Path) -> tuple[CandidateRecord, ...]:
|
||||
"""Extract every knowledge source: fact rows and legacy strategies."""
|
||||
ruleset_dir = repo_root / "src" / "openultrasast" / "ruleset" / "obligations"
|
||||
mapping_path = repo_root / "src" / "openultrasast" / "mapping" / "legacy.py"
|
||||
records = [*extract_fact_rows(ruleset_dir), *extract_legacy_strategies(mapping_path)]
|
||||
return tuple(sorted(records, key=lambda record: (record.kind, record.name, record.region)))
|
||||
|
||||
|
||||
def _digest(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()[:12]
|
||||
|
||||
|
||||
def _row_records(file_name: str, version: str, data: dict) -> Iterator[CandidateRecord]:
|
||||
tables = (("operation", "operation"), ("discharger", "discharger"), ("implication", "implication"))
|
||||
for kind, table in tables:
|
||||
for row in data.get(table, []):
|
||||
row_id = str(row.get("id", row.get("kind", "")))
|
||||
yield CandidateRecord(
|
||||
kind=kind,
|
||||
name=row_id,
|
||||
source=file_name,
|
||||
version=version,
|
||||
region=f"{file_name}:{table}:{row_id}",
|
||||
payload=_payload(row),
|
||||
)
|
||||
|
||||
|
||||
def _payload(row: dict) -> tuple[tuple[str, str], ...]:
|
||||
pairs = []
|
||||
for key, value in row.items():
|
||||
if isinstance(value, (list, tuple)):
|
||||
pairs.append((key, ",".join(str(item) for item in value)))
|
||||
else:
|
||||
pairs.append((key, str(value)))
|
||||
return tuple(sorted(pairs))
|
||||
|
||||
|
||||
def _symbol_spans(text: str) -> Iterable[tuple[str, int, int]]:
|
||||
matches = list(_RECOGNISER_PATTERN.finditer(text))
|
||||
lines = text.splitlines()
|
||||
for index, match in enumerate(matches):
|
||||
symbol = match.group(2) or match.group(3) or match.group(4)
|
||||
start = text.count("\n", 0, match.start()) + 1
|
||||
end_line = text.count("\n", 0, matches[index + 1].start()) + 1 if index + 1 < len(matches) else len(lines)
|
||||
yield symbol, start, end_line - 1
|
||||
|
||||
|
||||
__all__ = ["CandidateRecord", "extract_all", "extract_fact_rows", "extract_legacy_strategies"]
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Task 11.3: extraction of existing target knowledge into candidate records (Req 2.9).
|
||||
|
||||
Extraction reads the legacy recognition strategies and the promoted fact rows and
|
||||
returns deterministic candidate records carrying source, version and region
|
||||
evidence. It never modifies promoted outputs.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from openultrasast.tools.fact_init import (
|
||||
CandidateRecord,
|
||||
extract_all,
|
||||
extract_fact_rows,
|
||||
extract_legacy_strategies,
|
||||
)
|
||||
|
||||
REPO_ROOT = Path(__file__).parents[1]
|
||||
RULESET_DIR = REPO_ROOT / "src" / "openultrasast" / "ruleset" / "obligations"
|
||||
MAPPING_FILE = REPO_ROOT / "src" / "openultrasast" / "mapping" / "legacy.py"
|
||||
|
||||
_VERSION_PATTERN = re.compile(r"^[0-9a-f]{12}$")
|
||||
|
||||
|
||||
def _file_digest(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()[:12]
|
||||
|
||||
|
||||
def _digest_dir(directory: Path) -> dict[str, str]:
|
||||
return {path.name: _file_digest(path) for path in sorted(directory.glob("*.toml"))}
|
||||
|
||||
|
||||
def test_fact_rows_extraction_yields_operation_and_discharger_records() -> None:
|
||||
records = extract_fact_rows(RULESET_DIR)
|
||||
kinds = {record.kind for record in records}
|
||||
assert "operation" in kinds
|
||||
assert "discharger" in kinds
|
||||
assert records
|
||||
|
||||
|
||||
def test_extraction_includes_java_operation_rows() -> None:
|
||||
records = extract_fact_rows(RULESET_DIR)
|
||||
java_operations = [record for record in records if record.kind == "operation" and record.source.endswith("java.toml")]
|
||||
assert java_operations
|
||||
|
||||
|
||||
def test_legacy_strategy_extraction_names_recognisers() -> None:
|
||||
records = extract_legacy_strategies(MAPPING_FILE)
|
||||
names = {record.name for record in records}
|
||||
assert "_stapler_entries" in names
|
||||
assert "_jetty_entries" in names
|
||||
assert "_python_registered_routes" in names
|
||||
for record in records:
|
||||
assert record.region.startswith("legacy.py:")
|
||||
|
||||
|
||||
def test_every_candidate_carries_source_version_and_region() -> None:
|
||||
records = extract_all(REPO_ROOT)
|
||||
assert records
|
||||
for record in records:
|
||||
assert record.source
|
||||
assert _VERSION_PATTERN.match(record.version)
|
||||
assert ":" in record.region
|
||||
|
||||
|
||||
def test_version_digest_is_file_derived_and_stable() -> None:
|
||||
first = extract_fact_rows(RULESET_DIR)
|
||||
second = extract_fact_rows(RULESET_DIR)
|
||||
java_rows = [record for record in first if record.source.endswith("java.toml")]
|
||||
assert java_rows
|
||||
expected = _file_digest(RULESET_DIR / "java.toml")
|
||||
assert all(record.version == expected for record in java_rows)
|
||||
assert first == second
|
||||
|
||||
|
||||
def test_extraction_is_deterministic_and_sorted() -> None:
|
||||
first = extract_all(REPO_ROOT)
|
||||
second = extract_all(REPO_ROOT)
|
||||
assert first == second
|
||||
keys = [(record.kind, record.name, record.region) for record in first]
|
||||
assert keys == sorted(keys)
|
||||
|
||||
|
||||
def test_extraction_changes_no_promoted_outputs() -> None:
|
||||
from openultrasast.mapping import registry
|
||||
|
||||
tomls_before = _digest_dir(RULESET_DIR)
|
||||
mapping_before = _file_digest(MAPPING_FILE)
|
||||
strategies_before = len(registry._STRATEGIES)
|
||||
|
||||
extract_all(REPO_ROOT)
|
||||
|
||||
assert _digest_dir(RULESET_DIR) == tomls_before
|
||||
assert _file_digest(MAPPING_FILE) == mapping_before
|
||||
assert len(registry._STRATEGIES) == strategies_before
|
||||
|
||||
|
||||
def test_operation_payload_carries_row_fields() -> None:
|
||||
records = extract_fact_rows(RULESET_DIR)
|
||||
java_operations = [record for record in records if record.kind == "operation" and record.source.endswith("java.toml")]
|
||||
fields = {name for name, _ in java_operations[0].payload}
|
||||
assert "calls" in fields
|
||||
assert "kind" in fields
|
||||
|
||||
|
||||
def test_candidate_record_is_frozen() -> None:
|
||||
record = CandidateRecord(
|
||||
kind="operation",
|
||||
name="example",
|
||||
source="java.toml",
|
||||
version="0" * 12,
|
||||
region="java.toml:operations:example",
|
||||
payload=(("language", "java"),),
|
||||
)
|
||||
with pytest.raises(Exception): # noqa: B017
|
||||
record.name = "other" # type: ignore[misc]
|
||||
Reference in New Issue
Block a user