mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 01:48:17 +00:00
feat(access-control-prover): 7.2 resource abstract values and operation-fact fields
This commit is contained in:
@@ -1,19 +1,25 @@
|
||||
"""Evidence relations and the completeness value (access-control-prover, Req 11.1, 11.6, 11.7).
|
||||
"""Evidence relations, the completeness value and resource abstract values (access-control-prover).
|
||||
|
||||
Covers Req 2.1, 2.5, 2.6, 2.7, 11.1, 11.6, 11.7.
|
||||
|
||||
``EvidenceRelation`` is the abstract evidence domain: definite presence (``MUST``), definite absence
|
||||
(``NO``), possibility (``MAY``) and unknown (``UNKNOWN``). Three separate operations keep one table
|
||||
from being used for the wrong quantifier: ``join_alternatives`` merges evidence from alternative call
|
||||
targets or control paths, ``conjoin_dimensions`` combines one witness's kind, scope, resource, authority
|
||||
and dominance dimensions, and ``aggregate_witnesses`` asks whether any witness definitely governs one
|
||||
obligation. Every operation is deterministic, pure and independent of target names; join never converts
|
||||
obligation. ``ResourceRef`` abstracts one resource binding into an exact token, a finite alias set,
|
||||
request-global scope or an unknown value; ``join_resources`` unions alias sets finitely, lets
|
||||
request-global scope absorb concrete bindings and never lets an unresolvable value resolve. Every
|
||||
operation is deterministic, pure and independent of target names; join never converts
|
||||
``MAY`` or ``UNKNOWN`` into ``MUST`` or ``NO``, so losing precision cannot manufacture a verdict.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Mapping
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Literal
|
||||
|
||||
|
||||
class EvidenceRelation(Enum):
|
||||
@@ -38,6 +44,70 @@ class Completeness:
|
||||
object.__setattr__(self, "reasons", tuple(dict.fromkeys(self.reasons)))
|
||||
|
||||
|
||||
_RESOURCE_KINDS = ("exact", "aliases", "request_global", "unknown")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResourceRef:
|
||||
"""The abstract value of one resource binding (design, "Abstract evidence domain"; Req 2.1, 2.5, 2.6, 2.7, 11.1).
|
||||
|
||||
``exact`` binds one named resource, ``aliases`` binds a finite set of them, ``request_global`` binds the
|
||||
whole request without resource equality, and ``unknown`` is an unresolvable or unbounded binding. Values
|
||||
are deduplicated and sorted, so equal bindings are equal however they were constructed; a misshapen kind
|
||||
or arity fails loud rather than degrading silently.
|
||||
"""
|
||||
|
||||
kind: Literal["exact", "aliases", "request_global", "unknown"]
|
||||
values: tuple[str, ...] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.kind not in _RESOURCE_KINDS:
|
||||
raise ValueError(f"unknown resource kind: {self.kind!r}")
|
||||
tokens = sorted(set(self.values))
|
||||
if self.kind == "exact" and len(tokens) != 1:
|
||||
raise ValueError(f"exact resource needs exactly one token: {self.values!r}")
|
||||
if self.kind == "aliases" and not tokens:
|
||||
raise ValueError("alias set needs at least one token")
|
||||
if self.kind in ("request_global", "unknown") and tokens:
|
||||
raise ValueError(f"{self.kind} resource binds no tokens: {self.values!r}")
|
||||
object.__setattr__(self, "values", tuple(tokens))
|
||||
|
||||
|
||||
REQUEST_GLOBAL_RESOURCE = ResourceRef(kind="request_global")
|
||||
UNKNOWN_RESOURCE = ResourceRef(kind="unknown")
|
||||
|
||||
|
||||
def parse_resource(binding: str | Sequence[str] | None) -> ResourceRef:
|
||||
"""Parse one resource binding from fact or IR evidence into its abstract value.
|
||||
|
||||
A single identifier token is ``exact``, a finite token collection is a finite alias set, and ``None``
|
||||
(no binding could be resolved) is ``unknown``; anything misshapen raises instead of quietly degrading.
|
||||
"""
|
||||
if binding is None:
|
||||
return UNKNOWN_RESOURCE
|
||||
tokens = (binding,) if isinstance(binding, str) else tuple(binding)
|
||||
if not tokens or not all(isinstance(token, str) and token for token in tokens):
|
||||
raise ValueError(f"misshapen resource binding: {binding!r}")
|
||||
kind: Literal["exact", "aliases"] = "exact" if isinstance(binding, str) else "aliases"
|
||||
return ResourceRef(kind=kind, values=tokens)
|
||||
|
||||
|
||||
def join_resources(left: ResourceRef, right: ResourceRef) -> ResourceRef:
|
||||
"""Join resource bindings from alternative paths or call targets.
|
||||
|
||||
Alias sets join by finite union and identical exact tokens stay exact; request-global scope absorbs any
|
||||
concrete binding because it already covers every resource in the request (Req 2.6), and an unbounded or
|
||||
unresolvable value joins to ``unknown`` so losing precision cannot manufacture an exact match (Req 2.7, 11.1).
|
||||
"""
|
||||
if left.kind == "unknown" or right.kind == "unknown":
|
||||
return UNKNOWN_RESOURCE
|
||||
if left.kind == "request_global" or right.kind == "request_global":
|
||||
return REQUEST_GLOBAL_RESOURCE
|
||||
if left.kind == right.kind == "exact" and left.values == right.values:
|
||||
return left
|
||||
return ResourceRef(kind="aliases", values=left.values + right.values)
|
||||
|
||||
|
||||
_ALTERNATIVE_JOIN: Mapping[tuple[EvidenceRelation, EvidenceRelation], EvidenceRelation] = {
|
||||
(EvidenceRelation.MUST, EvidenceRelation.MUST): EvidenceRelation.MUST,
|
||||
(EvidenceRelation.MUST, EvidenceRelation.NO): EvidenceRelation.MAY,
|
||||
|
||||
@@ -22,7 +22,18 @@ SENSITIVITIES = ("low", "medium", "high")
|
||||
DEFAULT_OBLIGATION_FACTS_DIR = Path(__file__).resolve().parents[2] / "ruleset" / "obligations"
|
||||
|
||||
_OPERATION_FIELDS = frozenset(
|
||||
{"id", "kind", "calls", "resource_arg", "requires", "sensitivity", "query_document_arg", "handlers", "framework"}
|
||||
{
|
||||
"id",
|
||||
"kind",
|
||||
"calls",
|
||||
"resource_arg",
|
||||
"requires",
|
||||
"sensitivity",
|
||||
"query_document_arg",
|
||||
"handlers",
|
||||
"framework",
|
||||
"request_global_kinds",
|
||||
}
|
||||
)
|
||||
_DISCHARGER_FIELDS = frozenset(
|
||||
{"id", "kind", "calls", "decorators", "identity_sources", "constraint_params", "request_sources", "permissive_values"}
|
||||
@@ -50,6 +61,9 @@ class OperationFact:
|
||||
query_document_arg: int | None = None
|
||||
handlers: tuple[str, ...] = ()
|
||||
framework: str | None = None
|
||||
# Requirement 2.6 (access-control-prover): discharger kinds a request-global witness may discharge for
|
||||
# this operation at request scope, without resource equality. Declared by the fact, never inferred.
|
||||
request_global_kinds: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -97,48 +111,7 @@ def _load_obligation_facts_uncached(directory: Path | None = None) -> Obligation
|
||||
language = str(payload.get("language") or path.stem)
|
||||
versions.append(str(payload.get("version", "1")))
|
||||
for row in _rows(payload.get("operation"), path):
|
||||
_only_fields(row, _OPERATION_FIELDS, path)
|
||||
kind = str(row.get("kind", ""))
|
||||
if kind not in OPERATION_KINDS:
|
||||
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} has unknown kind {kind!r}")
|
||||
requires = _strings(row.get("requires"), "requires", path)
|
||||
for item in requires:
|
||||
if item not in DISCHARGER_KINDS:
|
||||
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} requires unknown discharger kind {item!r}")
|
||||
sensitivity = str(row.get("sensitivity", "medium"))
|
||||
if sensitivity not in SENSITIVITIES:
|
||||
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} has unknown sensitivity {sensitivity!r}")
|
||||
resource_arg = row.get("resource_arg")
|
||||
if resource_arg is not None and not isinstance(resource_arg, int):
|
||||
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} resource_arg must be an integer")
|
||||
document_arg = row.get("query_document_arg")
|
||||
if document_arg is not None and (not isinstance(document_arg, int) or document_arg < 1):
|
||||
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} query_document_arg must be a positive integer")
|
||||
handlers = _strings(row.get("handlers"), "handlers", path)
|
||||
framework = row.get("framework")
|
||||
if framework is not None and (not isinstance(framework, str) or not framework):
|
||||
raise ObligationFactsError("invalid operation framework")
|
||||
if handlers and framework is None:
|
||||
raise ObligationFactsError("handler operation needs a framework")
|
||||
for pattern in handlers:
|
||||
try:
|
||||
re.compile(pattern)
|
||||
except re.error as exc:
|
||||
raise ObligationFactsError(type(exc).__name__) from exc
|
||||
operations.append(
|
||||
OperationFact(
|
||||
id=str(row["id"]),
|
||||
kind=kind,
|
||||
language=language,
|
||||
calls=_strings(row.get("calls"), "calls", path),
|
||||
resource_arg=resource_arg,
|
||||
requires=requires,
|
||||
sensitivity=sensitivity,
|
||||
query_document_arg=document_arg,
|
||||
handlers=handlers,
|
||||
framework=framework,
|
||||
)
|
||||
)
|
||||
operations.append(_operation_fact(row, path, language))
|
||||
for row in _rows(payload.get("discharger"), path):
|
||||
_only_fields(row, _DISCHARGER_FIELDS, path)
|
||||
kind = str(row.get("kind", ""))
|
||||
@@ -160,6 +133,64 @@ def _load_obligation_facts_uncached(directory: Path | None = None) -> Obligation
|
||||
return ObligationFacts(version=versions[0] if versions else "1", operations=tuple(operations), dischargers=tuple(dischargers))
|
||||
|
||||
|
||||
def _operation_fact(row: dict[str, object], path: Path, language: str) -> OperationFact:
|
||||
"""Validate one operation row against the closed sets and parse it; any unknown kind or field fails loud."""
|
||||
_only_fields(row, _OPERATION_FIELDS, path)
|
||||
kind = str(row.get("kind", ""))
|
||||
if kind not in OPERATION_KINDS:
|
||||
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} has unknown kind {kind!r}")
|
||||
requires = _strings(row.get("requires"), "requires", path)
|
||||
for item in requires:
|
||||
if item not in DISCHARGER_KINDS:
|
||||
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} requires unknown discharger kind {item!r}")
|
||||
sensitivity = str(row.get("sensitivity", "medium"))
|
||||
if sensitivity not in SENSITIVITIES:
|
||||
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} has unknown sensitivity {sensitivity!r}")
|
||||
request_global_kinds = _strings(row.get("request_global_kinds"), "request_global_kinds", path)
|
||||
for item in request_global_kinds:
|
||||
if item not in DISCHARGER_KINDS:
|
||||
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} request_global_kinds names unknown discharger kind {item!r}")
|
||||
handler_patterns, handler_framework = _handler_shape(row, path)
|
||||
return OperationFact(
|
||||
id=str(row["id"]),
|
||||
kind=kind,
|
||||
language=language,
|
||||
calls=_strings(row.get("calls"), "calls", path),
|
||||
resource_arg=_optional_int(row, "resource_arg", path, positive=False),
|
||||
requires=requires,
|
||||
sensitivity=sensitivity,
|
||||
query_document_arg=_optional_int(row, "query_document_arg", path, positive=True),
|
||||
handlers=handler_patterns,
|
||||
framework=handler_framework,
|
||||
request_global_kinds=request_global_kinds,
|
||||
)
|
||||
|
||||
|
||||
def _optional_int(row: dict[str, object], field: str, path: Path, *, positive: bool) -> int | None:
|
||||
value = row.get(field)
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, int) or (positive and value < 1):
|
||||
requirement = "a positive integer" if positive else "an integer"
|
||||
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} {field} must be {requirement}")
|
||||
return value
|
||||
|
||||
|
||||
def _handler_shape(row: dict[str, object], path: Path) -> tuple[tuple[str, ...], str | None]:
|
||||
handler_patterns = _strings(row.get("handlers"), "handlers", path)
|
||||
framework = row.get("framework")
|
||||
if framework is not None and (not isinstance(framework, str) or not framework):
|
||||
raise ObligationFactsError("invalid operation framework")
|
||||
if handler_patterns and framework is None:
|
||||
raise ObligationFactsError("handler operation needs a framework")
|
||||
for pattern in handler_patterns:
|
||||
try:
|
||||
re.compile(pattern)
|
||||
except re.error as exc:
|
||||
raise ObligationFactsError(type(exc).__name__) from exc
|
||||
return handler_patterns, framework
|
||||
|
||||
|
||||
def _rows(value: object, path: Path) -> list[dict[str, object]]:
|
||||
if value is None:
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user