mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 17:58:18 +00:00
feat(access-control-prover): 11.4 verify and promote proposed target knowledge
This commit is contained in:
@@ -229,7 +229,7 @@ its final resolution, dominance, fact-acquisition and measurement semantics with
|
|||||||
- _Requirements: 2.9_
|
- _Requirements: 2.9_
|
||||||
- _Boundary: Fact Initialization_
|
- _Boundary: Fact Initialization_
|
||||||
- _Depends: 7.1_
|
- _Depends: 7.1_
|
||||||
- [ ] 11.4 Verify and promote proposed target knowledge
|
- [x] 11.4 Verify and promote proposed target knowledge
|
||||||
- Accept source- or approved-documentation proposals, reject contradictions, and write only verified outputs.
|
- Accept source- or approved-documentation proposals, reject contradictions, and write only verified outputs.
|
||||||
- Done when matching proposals promote and contradicted or evidence-free proposals leave promoted knowledge unchanged.
|
- Done when matching proposals promote and contradicted or evidence-free proposals leave promoted knowledge unchanged.
|
||||||
- _Requirements: 2.9, 2.10_
|
- _Requirements: 2.9, 2.10_
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
"""Task 11.3: extract existing target knowledge into candidate records (Req 2.9).
|
"""Tasks 11.3 + 11.4: extraction and verified promotion of target knowledge (Reqs 2.9, 2.10).
|
||||||
|
|
||||||
Extraction mode of the initialization job: the legacy recognition strategies in
|
Extraction mode of the initialization job: the legacy recognition strategies in
|
||||||
``mapping/legacy.py`` and the promoted fact rows in ``ruleset/obligations/*.toml``
|
``mapping/legacy.py`` and the promoted fact rows in ``ruleset/obligations/*.toml``
|
||||||
are read -- never modified -- and returned as deterministic candidate records
|
are read -- never modified -- and returned as deterministic candidate records
|
||||||
carrying source, version and region evidence. Verification and promotion are the
|
carrying source, version and region evidence.
|
||||||
acquisition workflow's job (tasks 11.4+), not extraction's.
|
|
||||||
|
Verification and promotion (task 11.4): a proposal is verified when its evidence
|
||||||
|
is complete (source, version, region) and it does not contradict promoted
|
||||||
|
knowledge. Promotion accepts only verified proposals; contradicted or
|
||||||
|
evidence-free proposals are rejected with a reason and leave the promoted
|
||||||
|
corpus unchanged (Req 2.10).
|
||||||
|
|
||||||
This is a tools module, not an engine module: naming recognition targets here is
|
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).
|
legitimate (the whole point is to extract their vocabulary into data).
|
||||||
@@ -109,4 +114,54 @@ def _symbol_spans(text: str) -> Iterable[tuple[str, int, int]]:
|
|||||||
yield symbol, start, end_line - 1
|
yield symbol, start, end_line - 1
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["CandidateRecord", "extract_all", "extract_fact_rows", "extract_legacy_strategies"]
|
@dataclass(frozen=True)
|
||||||
|
class PromotionResult:
|
||||||
|
"""Outcome of promoting a batch of proposals (Reqs 2.9, 2.10)."""
|
||||||
|
|
||||||
|
accepted: tuple[CandidateRecord, ...]
|
||||||
|
rejected: tuple[tuple[CandidateRecord, str], ...] # (record, reason)
|
||||||
|
promoted_after: tuple[CandidateRecord, ...]
|
||||||
|
|
||||||
|
|
||||||
|
def verify_candidate(record: CandidateRecord, promoted: tuple[CandidateRecord, ...]) -> tuple[bool, str | None]:
|
||||||
|
"""Verify one proposal against the promoted corpus.
|
||||||
|
|
||||||
|
Evidence must be complete (source, version, region) and the proposal must not
|
||||||
|
contradict promoted knowledge. Rejections carry a reason (Req 2.10).
|
||||||
|
"""
|
||||||
|
if not record.source or not record.region or not re.fullmatch(r"[0-9a-f]{12}", record.version):
|
||||||
|
return False, "missing-evidence"
|
||||||
|
for existing in promoted:
|
||||||
|
if existing.kind == record.kind and existing.name == record.name:
|
||||||
|
if existing.payload != record.payload:
|
||||||
|
return False, f"contradicts-promoted:{record.kind}:{record.name}"
|
||||||
|
return True, None
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
|
def promote_proposals(candidates: tuple[CandidateRecord, ...], promoted: tuple[CandidateRecord, ...]) -> PromotionResult:
|
||||||
|
"""Promote only verified proposals; rejects never modify the corpus (Req 2.10)."""
|
||||||
|
accepted: list[CandidateRecord] = []
|
||||||
|
rejected: list[tuple[CandidateRecord, str]] = []
|
||||||
|
promoted_after = list(promoted)
|
||||||
|
for record in candidates:
|
||||||
|
is_valid, reason = verify_candidate(record, tuple(promoted_after))
|
||||||
|
if not is_valid or reason is not None:
|
||||||
|
rejected.append((record, reason or "rejected"))
|
||||||
|
continue
|
||||||
|
accepted.append(record)
|
||||||
|
already_promoted = any(existing.kind == record.kind and existing.name == record.name for existing in promoted_after)
|
||||||
|
if not already_promoted:
|
||||||
|
promoted_after.append(record)
|
||||||
|
return PromotionResult(tuple(accepted), tuple(rejected), tuple(promoted_after))
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"CandidateRecord",
|
||||||
|
"PromotionResult",
|
||||||
|
"extract_all",
|
||||||
|
"extract_fact_rows",
|
||||||
|
"extract_legacy_strategies",
|
||||||
|
"promote_proposals",
|
||||||
|
"verify_candidate",
|
||||||
|
]
|
||||||
|
|||||||
+123
-2
@@ -1,8 +1,10 @@
|
|||||||
"""Task 11.3: extraction of existing target knowledge into candidate records (Req 2.9).
|
"""Tasks 11.3 + 11.4: extraction and verified promotion of target knowledge (Reqs 2.9, 2.10).
|
||||||
|
|
||||||
Extraction reads the legacy recognition strategies and the promoted fact rows and
|
Extraction reads the legacy recognition strategies and the promoted fact rows and
|
||||||
returns deterministic candidate records carrying source, version and region
|
returns deterministic candidate records carrying source, version and region
|
||||||
evidence. It never modifies promoted outputs.
|
evidence. It never modifies promoted outputs. Verification and promotion accept
|
||||||
|
only evidence-complete, non-contradicting proposals; rejected proposals leave the
|
||||||
|
promoted corpus unchanged.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -13,9 +15,12 @@ import pytest
|
|||||||
|
|
||||||
from openultrasast.tools.fact_init import (
|
from openultrasast.tools.fact_init import (
|
||||||
CandidateRecord,
|
CandidateRecord,
|
||||||
|
PromotionResult,
|
||||||
extract_all,
|
extract_all,
|
||||||
extract_fact_rows,
|
extract_fact_rows,
|
||||||
extract_legacy_strategies,
|
extract_legacy_strategies,
|
||||||
|
promote_proposals,
|
||||||
|
verify_candidate,
|
||||||
)
|
)
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).parents[1]
|
REPO_ROOT = Path(__file__).parents[1]
|
||||||
@@ -33,6 +38,122 @@ def _digest_dir(directory: Path) -> dict[str, str]:
|
|||||||
return {path.name: _file_digest(path) for path in sorted(directory.glob("*.toml"))}
|
return {path.name: _file_digest(path) for path in sorted(directory.glob("*.toml"))}
|
||||||
|
|
||||||
|
|
||||||
|
def _proposal(
|
||||||
|
name: str = "proposed-read", payload: tuple[tuple[str, str], ...] = (("calls", "read"), ("kind", "protected_read"))
|
||||||
|
) -> CandidateRecord:
|
||||||
|
return CandidateRecord(
|
||||||
|
kind="operation",
|
||||||
|
name=name,
|
||||||
|
source="docs/target.md",
|
||||||
|
version="a1b2c3d4e5f6",
|
||||||
|
region="docs/target.md:proposed-read",
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _promoted_corpus() -> tuple[CandidateRecord, ...]:
|
||||||
|
return (
|
||||||
|
CandidateRecord(
|
||||||
|
kind="operation",
|
||||||
|
name="promoted-read",
|
||||||
|
source="src/openultrasast/ruleset/obligations/java.toml",
|
||||||
|
version="0123456789ab",
|
||||||
|
region="java.toml:operation:promoted-read",
|
||||||
|
payload=(("calls", "read"), ("kind", "protected_read")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_verified_novel_proposal_is_accepted_and_promoted() -> None:
|
||||||
|
promoted = _promoted_corpus()
|
||||||
|
is_valid, reason = verify_candidate(_proposal(), promoted)
|
||||||
|
assert is_valid is True
|
||||||
|
assert reason is None
|
||||||
|
result = promote_proposals((_proposal(),), promoted)
|
||||||
|
assert result.accepted == (_proposal(),)
|
||||||
|
assert result.rejected == ()
|
||||||
|
assert _proposal() in result.promoted_after
|
||||||
|
assert promoted[0] in result.promoted_after
|
||||||
|
|
||||||
|
|
||||||
|
def test_contradicted_proposal_is_rejected_and_promoted_knowledge_unchanged() -> None:
|
||||||
|
promoted = _promoted_corpus()
|
||||||
|
contradicted = CandidateRecord(
|
||||||
|
kind="operation",
|
||||||
|
name="promoted-read",
|
||||||
|
source="docs/target.md",
|
||||||
|
version="a1b2c3d4e5f6",
|
||||||
|
region="docs/target.md:promoted-read",
|
||||||
|
payload=(("calls", "write"),),
|
||||||
|
)
|
||||||
|
is_valid, reason = verify_candidate(contradicted, promoted)
|
||||||
|
assert is_valid is False
|
||||||
|
assert reason is not None and reason.startswith("contradicts-promoted")
|
||||||
|
result = promote_proposals((contradicted,), promoted)
|
||||||
|
assert result.rejected[0][0] == contradicted
|
||||||
|
assert result.promoted_after == promoted
|
||||||
|
|
||||||
|
|
||||||
|
def test_evidence_free_proposal_is_rejected() -> None:
|
||||||
|
promoted = _promoted_corpus()
|
||||||
|
for broken in (
|
||||||
|
_proposal().__class__(**{**_proposal().__dict__, "source": ""}),
|
||||||
|
CandidateRecord(kind="operation", name="x", source="s", version="short", region="r"),
|
||||||
|
CandidateRecord(kind="operation", name="y", source="s", version="a1b2c3d4e5f6", region=""),
|
||||||
|
):
|
||||||
|
is_valid, reason = verify_candidate(broken, promoted)
|
||||||
|
assert is_valid is False
|
||||||
|
assert reason is not None and reason.startswith("missing-evidence")
|
||||||
|
result = promote_proposals(
|
||||||
|
(CandidateRecord(kind="operation", name="z", source="", version="a1b2c3d4e5f6", region="r"),),
|
||||||
|
promoted,
|
||||||
|
)
|
||||||
|
assert result.promoted_after == promoted
|
||||||
|
|
||||||
|
|
||||||
|
def test_exact_match_proposal_is_accepted_without_duplicate() -> None:
|
||||||
|
promoted = _promoted_corpus()
|
||||||
|
twin = CandidateRecord(
|
||||||
|
kind="operation",
|
||||||
|
name="promoted-read",
|
||||||
|
source="docs/target.md",
|
||||||
|
version="a1b2c3d4e5f6",
|
||||||
|
region="docs/target.md:promoted-read",
|
||||||
|
payload=promoted[0].payload,
|
||||||
|
)
|
||||||
|
result = promote_proposals((twin,), promoted)
|
||||||
|
assert result.accepted == (twin,)
|
||||||
|
assert result.rejected == ()
|
||||||
|
same_knowledge = [record for record in result.promoted_after if record.kind == twin.kind and record.name == twin.name]
|
||||||
|
assert len(same_knowledge) == 1
|
||||||
|
assert len(result.promoted_after) == len(promoted)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rejected_proposals_never_modify_promoted_corpus() -> None:
|
||||||
|
promoted = _promoted_corpus()
|
||||||
|
contradicted = CandidateRecord(
|
||||||
|
kind="operation",
|
||||||
|
name="promoted-read",
|
||||||
|
source="docs/target.md",
|
||||||
|
version="a1b2c3d4e5f6",
|
||||||
|
region="docs/target.md:promoted-read",
|
||||||
|
payload=(("calls", "purge"),),
|
||||||
|
)
|
||||||
|
result = promote_proposals((_proposal(), contradicted), promoted)
|
||||||
|
assert result.accepted == (_proposal(),)
|
||||||
|
assert len(result.rejected) == 1
|
||||||
|
assert contradicted not in result.promoted_after
|
||||||
|
assert promoted[0] in result.promoted_after
|
||||||
|
|
||||||
|
|
||||||
|
def test_promotion_is_deterministic() -> None:
|
||||||
|
promoted = _promoted_corpus()
|
||||||
|
first = promote_proposals((_proposal(),), promoted)
|
||||||
|
second = promote_proposals((_proposal(),), promoted)
|
||||||
|
assert first == second
|
||||||
|
assert isinstance(first, PromotionResult)
|
||||||
|
|
||||||
|
|
||||||
def test_fact_rows_extraction_yields_operation_and_discharger_records() -> None:
|
def test_fact_rows_extraction_yields_operation_and_discharger_records() -> None:
|
||||||
records = extract_fact_rows(RULESET_DIR)
|
records = extract_fact_rows(RULESET_DIR)
|
||||||
kinds = {record.kind for record in records}
|
kinds = {record.kind for record in records}
|
||||||
|
|||||||
Reference in New Issue
Block a user