From f739b39ead2c0d2441e84823288c115a999e786c Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:17:05 +0100 Subject: [PATCH] feat(access-control-prover): migrate reviewed entry packs --- .gitignore | 3 + .kiro/specs/access-control-prover/tasks.md | 2 +- src/openultrasast/mapping/entry_packs.py | 276 ++++++++++++++++-- ...cf416261d717ef55a4b7a9d9d65bce53b97d59ab35 | 1 + ...3d5a7c9114c2d8f81f7a702fa3681a87e7be4cb9e1 | 1 + ...8fde7f73ae2f60364d8b8fee22ecaee8342798d8ce | 33 +++ .../ruleset/entries/authority.json | 1 + .../entries/express.javascript.v2.json | 1 + .../ruleset/entries/express.json | 34 +-- .../entries/express.typescript.v2.json | 1 + tests/test_core_factpack_file.py | 41 ++- tests/test_entry_packs.py | 273 ++++++++++++++++- 12 files changed, 587 insertions(+), 80 deletions(-) create mode 100644 src/openultrasast/ruleset/entries/artifacts/sha256/529d18ba2b351a8de985a9cf416261d717ef55a4b7a9d9d65bce53b97d59ab35 create mode 100644 src/openultrasast/ruleset/entries/artifacts/sha256/a8bf581d321560f2730aae3d5a7c9114c2d8f81f7a702fa3681a87e7be4cb9e1 create mode 100644 src/openultrasast/ruleset/entries/artifacts/sha256/e8e149309abd78f24206c58fde7f73ae2f60364d8b8fee22ecaee8342798d8ce create mode 100644 src/openultrasast/ruleset/entries/authority.json create mode 100644 src/openultrasast/ruleset/entries/express.javascript.v2.json create mode 100644 src/openultrasast/ruleset/entries/express.typescript.v2.json diff --git a/.gitignore b/.gitignore index dca4b01..b121740 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ build/ .cache/openultrasast/ **/.cache/openultrasast/ artifacts/ +!src/openultrasast/ruleset/entries/artifacts/ +!src/openultrasast/ruleset/entries/artifacts/sha256/ +!src/openultrasast/ruleset/entries/artifacts/sha256/* reports/ vector-store/ *.sarif diff --git a/.kiro/specs/access-control-prover/tasks.md b/.kiro/specs/access-control-prover/tasks.md index 4dfd5de..0a1fa8e 100644 --- a/.kiro/specs/access-control-prover/tasks.md +++ b/.kiro/specs/access-control-prover/tasks.md @@ -359,7 +359,7 @@ its final resolution, dominance, fact-acquisition and measurement semantics with - _Requirements: 13.2, 13.3_ - _Boundary: Installed Authority Review Ceremony_ - _Depends: 14.3_ -- [ ] 14.5 Migrate the installed entry knowledge into reviewed v2 packs +- [x] 14.5 Migrate the installed entry knowledge into reviewed v2 packs - Produce one language-scoped v2 pack per promoted language and a separately authenticated canonical compatibility projection from the existing reviewed entry data. - Map every migrated row to declared roles, typed structural requirements and relation effects without adding target vocabulary to engine modules. - Preserve legacy pattern recognition byte-for-byte while preventing the projection from constructing detection-core evidence or readiness. diff --git a/src/openultrasast/mapping/entry_packs.py b/src/openultrasast/mapping/entry_packs.py index 3cfd99a..cfe4e50 100644 --- a/src/openultrasast/mapping/entry_packs.py +++ b/src/openultrasast/mapping/entry_packs.py @@ -1,51 +1,267 @@ -"""Entry-fact packs loaded through the core fact-pack contract. - -Packs live in ``ruleset/entries/*.json`` and are admitted only with a self-consistent -review attestation (`core.factpack.load_pack_file`). Languages map pack languages to -source languages; route patterns become extra recognition regexes. -""" +"""Installed entry-fact authority and projection-only legacy recognition.""" from __future__ import annotations import json import re +from collections import OrderedDict +from collections.abc import Mapping +from dataclasses import dataclass from pathlib import Path +from threading import Lock +from typing import cast -from openultrasast.core.factpack import load_pack_file +from openultrasast.core import canonical, provenance +from openultrasast.mapping.authority_ceremony import ( + INSTALLED_ARTIFACT_STORE_IDENTITY, + load_installed_authority_ceremony, + validate_authority_ceremony_bindings, +) +from openultrasast.mapping.fact_authority import ( + EntryFactAuthorityContext, + authenticate_entry_fact_authority, +) +from openultrasast.mapping.installed_fact_authority import ( + PACK_FILES, + DigestDirectoryResolver, + InstalledAuthorityWireError, + decode_fact_pack_bytes, + decode_installed_authority_bytes, +) +from openultrasast.semantic.obligations.core_contract import production_access_control_contract +RECOGNITION_PROJECTION_SCHEMA = "openultrasast.entry-recognition-projection.v1" +_PROJECTION_FIELDS = {"schema_version", "facts"} +_ROW_FIELDS = {"legacy_fact_id", "languages", "defines_handler", "handler_kind", "pattern"} _LANGUAGE_ALIASES = {"js": "javascript", "ts": "typescript"} -_cache: dict[tuple[str, tuple[tuple[str, float], ...]], dict[str, list[re.Pattern[str]]]] = {} +_STATE_FILES = ( + "bootstrap.json", + "identities/generator.json", + "identities/maintainer-reviewer.json", + "identities/custodian.json", + "authority.json", + *PACK_FILES, + "express.json", +) +_CACHE_LIMIT = 8 +_FileIdentity = tuple[str, int, int, int, int, int, str] +_InstalledState = tuple[_FileIdentity, ...] +_CachedPatterns = tuple[tuple[str, tuple[re.Pattern[str], ...]], ...] +_cache: OrderedDict[str, tuple[_InstalledState, _CachedPatterns]] = OrderedDict() +_cache_lock = Lock() + + +@dataclass(frozen=True) +class _RecognitionRow: + legacy_fact_id: str + languages: tuple[str, ...] + defines_handler: bool + handler_kind: str + pattern: str + + +class _ProjectionOnlyCustody: + """Authority authentication port that refuses evaluation-state use.""" + + def current_log(self, population_digest: str) -> provenance.PopulationCustodyLog: + raise AssertionError(f"projection loading consumed target custody: {population_digest}") + + +def _package_root(repo_root: Path | None) -> Path: + if repo_root is not None: + return repo_root + import openultrasast + + return Path(openultrasast.__file__).resolve().parent + + +def _entries_root(repo_root: Path | None) -> Path: + return _package_root(repo_root) / "ruleset" / "entries" + + +def _state_paths(entries: Path) -> tuple[Path, ...]: + fixed = tuple(entries / relative for relative in _STATE_FILES) + artifact_root = entries / "artifacts" / "sha256" + try: + artifacts = tuple(sorted(path for path in artifact_root.rglob("*") if path.is_file())) + except OSError as exc: + raise InstalledAuthorityWireError("installed artifact state is unavailable") from exc + return (*fixed, *artifacts) + + +def _file_identity(entries: Path, path: Path) -> _FileIdentity: + try: + before = path.lstat() + content = path.read_bytes() + after = path.lstat() + except OSError as exc: + raise InstalledAuthorityWireError("installed authority state is unavailable") from exc + before_metadata = (before.st_dev, before.st_ino, before.st_mode, before.st_size, before.st_mtime_ns) + after_metadata = (after.st_dev, after.st_ino, after.st_mode, after.st_size, after.st_mtime_ns) + if before_metadata != after_metadata: + raise InstalledAuthorityWireError("installed authority state changed while fingerprinting") + return ( + path.relative_to(entries).as_posix(), + before.st_dev, + before.st_ino, + before.st_mode, + before.st_size, + before.st_mtime_ns, + canonical.bytes_digest(content), + ) + + +def _installed_state(entries: Path) -> _InstalledState: + paths = _state_paths(entries) + state = tuple(_file_identity(entries, path) for path in paths) + if paths != _state_paths(entries): + raise InstalledAuthorityWireError("installed artifact set changed while fingerprinting") + return state + + +def _cached_patterns(entries: Path, state: _InstalledState) -> dict[str, list[re.Pattern[str]]] | None: + key = str(entries.resolve()) + with _cache_lock: + cached = _cache.get(key) + if cached is None or cached[0] != state: + return None + _cache.move_to_end(key) + return {language: list(patterns) for language, patterns in cached[1]} + + +def _remember_patterns(entries: Path, state: _InstalledState, patterns: dict[str, list[re.Pattern[str]]]) -> None: + key = str(entries.resolve()) + frozen = tuple((language, tuple(values)) for language, values in sorted(patterns.items())) + with _cache_lock: + _cache[key] = (state, frozen) + _cache.move_to_end(key) + while len(_cache) > _CACHE_LIMIT: + _cache.popitem(last=False) + + +def load_installed_entry_fact_authority( + custody: provenance.PopulationCustodyPort, + repo_root: Path | None = None, +) -> EntryFactAuthorityContext: + """Decode and externally authenticate the closed installed v2 authority. + + ``custody`` remains caller supplied. Authority authentication verifies its port + shape but deliberately does not read evaluation population state. + """ + entries = _entries_root(repo_root) + contract = production_access_control_contract() + try: + packs = tuple(decode_fact_pack_bytes((entries / name).read_bytes()) for name in PACK_FILES) + bundle = decode_installed_authority_bytes((entries / "authority.json").read_bytes(), packs, contract) + except OSError as exc: + raise InstalledAuthorityWireError("installed entry authority is unavailable") from exc + ceremony = load_installed_authority_ceremony(entries) + validate_authority_ceremony_bindings(ceremony, bundle) + resolver = DigestDirectoryResolver(entries, INSTALLED_ARTIFACT_STORE_IDENTITY) + return authenticate_entry_fact_authority(bundle, ceremony.bootstrap, custody, resolver) + + +def _text(value: object, field: str) -> str: + if not isinstance(value, str) or not value: + raise InstalledAuthorityWireError(f"{field} must be nonempty text") + return value + + +def _decode_projection(content: bytes) -> tuple[_RecognitionRow, ...]: + try: + raw = json.loads(content) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise InstalledAuthorityWireError("recognition projection is not canonical JSON") from exc + if not isinstance(raw, dict) or set(raw) != _PROJECTION_FIELDS: + raise InstalledAuthorityWireError("recognition projection has unknown or missing fields") + if raw.get("schema_version") != RECOGNITION_PROJECTION_SCHEMA: + raise InstalledAuthorityWireError("recognition projection schema is unknown") + facts = raw.get("facts") + if not isinstance(facts, list) or not facts: + raise InstalledAuthorityWireError("recognition projection facts must be a nonempty list") + rows: list[_RecognitionRow] = [] + for item in facts: + if not isinstance(item, dict) or set(item) != _ROW_FIELDS: + raise InstalledAuthorityWireError("recognition projection row has unknown or missing fields") + languages = item["languages"] + if ( + not isinstance(languages, list) + or not languages + or not all(isinstance(language, str) and language for language in languages) + or len(set(languages)) != len(languages) + ): + raise InstalledAuthorityWireError("recognition projection languages are invalid") + defines_handler = item["defines_handler"] + if not isinstance(defines_handler, bool): + raise InstalledAuthorityWireError("recognition projection defines_handler must be boolean") + rows.append( + _RecognitionRow( + _text(item["legacy_fact_id"], "legacy_fact_id"), + tuple(languages), + defines_handler, + _text(item["handler_kind"], "handler_kind"), + _text(item["pattern"], "pattern"), + ) + ) + record = cast( + Mapping[str, canonical.JsonValue], + { + "schema_version": RECOGNITION_PROJECTION_SCHEMA, + "facts": tuple( + { + "legacy_fact_id": row.legacy_fact_id, + "languages": row.languages, + "defines_handler": row.defines_handler, + "handler_kind": row.handler_kind, + "pattern": row.pattern, + } + for row in rows + ), + }, + ) + expected = canonical.canonical_record_bytes(record, known_schema_versions=(RECOGNITION_PROJECTION_SCHEMA,)) + if content != expected: + raise InstalledAuthorityWireError("recognition projection does not round-trip canonically") + return tuple(rows) def load_entry_pack_patterns(repo_root: Path | None = None) -> dict[str, list[re.Pattern[str]]]: - """Compiled route patterns per source language, from admitted entry packs. + """Return legacy recognition regexes from an authenticated projection only. - Packs resolve from the installed package's ``ruleset/entries`` (not the analyzed - repository), so recognition is active on external repos; ``repo_root`` is accepted - only for tests that stage a fake ruleset. + This compatibility API intentionally returns no ``FactPack``, structural + requirement, relation effect, or readiness value. A projection hit therefore + cannot be promoted into detection-core evidence. """ - import openultrasast - - base = repo_root if repo_root is not None else Path(openultrasast.__file__).resolve().parent - entries = base / "ruleset" / "entries" + entries = _entries_root(repo_root) if not entries.is_dir(): return {} - files = sorted(entries.glob("*.json")) - stamp = tuple((p.name, p.stat().st_mtime) for p in files) - key = (str(entries.resolve()), stamp) - if key in _cache: - return _cache[key] + state = _installed_state(entries) + cached = _cached_patterns(entries, state) + if cached is not None: + return cached + context = load_installed_entry_fact_authority(_ProjectionOnlyCustody(), repo_root) + resolved = context.resolver.resolve(context.bundle.recognition_projection).content + try: + installed = (entries / "express.json").read_bytes() + except OSError as exc: + raise InstalledAuthorityWireError("installed recognition projection is unavailable") from exc + if installed != resolved: + raise InstalledAuthorityWireError("compatibility projection differs from authenticated artifact") patterns: dict[str, list[re.Pattern[str]]] = {} - for pack_file in files: - payload = json.loads(pack_file.read_text(encoding="utf-8")) - pack = load_pack_file(pack_file) - for raw_language in payload.get("languages", []): + for row in _decode_projection(installed): + if row.handler_kind != "route" or not row.defines_handler: + continue + for raw_language in row.languages: language = _LANGUAGE_ALIASES.get(str(raw_language), str(raw_language)) - for fact in pack.facts: - if fact.get("handler_kind") == "route" and fact.get("pattern"): - patterns.setdefault(language, []).append(re.compile(str(fact["pattern"]))) - _cache[key] = patterns + patterns.setdefault(language, []).append(re.compile(row.pattern)) + if state != _installed_state(entries): + raise InstalledAuthorityWireError("installed authority state changed during authentication") + _remember_patterns(entries, state, patterns) return patterns -__all__ = ["load_entry_pack_patterns"] +__all__ = [ + "RECOGNITION_PROJECTION_SCHEMA", + "load_entry_pack_patterns", + "load_installed_entry_fact_authority", +] diff --git a/src/openultrasast/ruleset/entries/artifacts/sha256/529d18ba2b351a8de985a9cf416261d717ef55a4b7a9d9d65bce53b97d59ab35 b/src/openultrasast/ruleset/entries/artifacts/sha256/529d18ba2b351a8de985a9cf416261d717ef55a4b7a9d9d65bce53b97d59ab35 new file mode 100644 index 0000000..6087bae --- /dev/null +++ b/src/openultrasast/ruleset/entries/artifacts/sha256/529d18ba2b351a8de985a9cf416261d717ef55a4b7a9d9d65bce53b97d59ab35 @@ -0,0 +1 @@ +{"facts":[{"defines_handler":true,"handler_kind":"route","languages":["javascript","typescript"],"legacy_fact_id":"entry:express:app-route","pattern":"app.(get|post|put|delete|patch|all|use)\\s*\\("},{"defines_handler":true,"handler_kind":"route","languages":["javascript","typescript"],"legacy_fact_id":"entry:express:router-route","pattern":"router.(get|post|put|delete|patch|all|use)\\s*\\("},{"defines_handler":false,"handler_kind":"app","languages":["javascript","typescript"],"legacy_fact_id":"entry:express:express-app","pattern":"express\\s*\\(\\s*\\)"}],"schema_version":"openultrasast.entry-recognition-projection.v1"} \ No newline at end of file diff --git a/src/openultrasast/ruleset/entries/artifacts/sha256/a8bf581d321560f2730aae3d5a7c9114c2d8f81f7a702fa3681a87e7be4cb9e1 b/src/openultrasast/ruleset/entries/artifacts/sha256/a8bf581d321560f2730aae3d5a7c9114c2d8f81f7a702fa3681a87e7be4cb9e1 new file mode 100644 index 0000000..08b02ab --- /dev/null +++ b/src/openultrasast/ruleset/entries/artifacts/sha256/a8bf581d321560f2730aae3d5a7c9114c2d8f81f7a702fa3681a87e7be4cb9e1 @@ -0,0 +1 @@ +{"conflicts":[],"resolved":true,"schema_version":"openultrasast.access-control.authority-conflict-disclosure.v1"} \ No newline at end of file diff --git a/src/openultrasast/ruleset/entries/artifacts/sha256/e8e149309abd78f24206c58fde7f73ae2f60364d8b8fee22ecaee8342798d8ce b/src/openultrasast/ruleset/entries/artifacts/sha256/e8e149309abd78f24206c58fde7f73ae2f60364d8b8fee22ecaee8342798d8ce new file mode 100644 index 0000000..6a5dad4 --- /dev/null +++ b/src/openultrasast/ruleset/entries/artifacts/sha256/e8e149309abd78f24206c58fde7f73ae2f60364d8b8fee22ecaee8342798d8ce @@ -0,0 +1,33 @@ +{ + "facts": [ + { + "defines_handler": true, + "handler_kind": "route", + "id": "entry:express:app-route", + "pattern": "app.(get|post|put|delete|patch|all|use)\\s*\\(" + }, + { + "defines_handler": true, + "handler_kind": "route", + "id": "entry:express:router-route", + "pattern": "router.(get|post|put|delete|patch|all|use)\\s*\\(" + }, + { + "defines_handler": false, + "handler_kind": "app", + "id": "entry:express:express-app", + "pattern": "express\\s*\\(\\s*\\)" + } + ], + "family": "access_control", + "framework": "express", + "kind": "entry_facts", + "languages": [ + "javascript", + "typescript" + ], + "review": { + "digest": "47866e5c15b286d7107b101bd396f78bbbda23788085676c590c3af937085e00", + "note": "Reviewed against Express 4/5 routing docs and the pilot pool's manifest-level framework usage (2026-10-30 recognition table). Entry patterns only; no sinks, no labels." + } +} diff --git a/src/openultrasast/ruleset/entries/authority.json b/src/openultrasast/ruleset/entries/authority.json new file mode 100644 index 0000000..4f1c576 --- /dev/null +++ b/src/openultrasast/ruleset/entries/authority.json @@ -0,0 +1 @@ +{"authority_review":{"attestation_digest":"aeeb9120c10d8bf83159a9cbd14ad8a070a0ceb99a53b5a1daa616a315c84b55","conflict_disclosure":{"artifact_id":"installed-entry-authority-conflicts-v1","byte_length":113,"content_digest":"a8bf581d321560f2730aae3d5a7c9114c2d8f81f7a702fa3681a87e7be4cb9e1","media_type":"application/json","retention_policy_digest":"09a0fe8a253780b3666ef016d575a212bf0570da392dd5bf1a834dce2edf703f","storage_receipt_digest":"ee92fde020e9b4d316ad285f3f904032c52d902fe1c7c0a94cb0d61d9c0136ce"},"producer_identity":"9635025424be37e1615e597185a425a5443242fa210d6136c38250101088e7b4","reviewer_identity":"f12a7e897cbaf3059619517e1f8a725b90049a0edf3af08efa12fb1ef7fdee43","subject_digest":"0fa8f607297aa5364a0ca6c9b8d47bfcdef28456b7f062ddb251f8f8b9a67da4"},"bundle_digest":"0fa8f607297aa5364a0ca6c9b8d47bfcdef28456b7f062ddb251f8f8b9a67da4","custody_policy":{"policy_digest":"415b1c13adc918c162932a38e79548a7d9867f5331860eb409238c51b1cca1f0","reviewed_custodian_digests":["4fe8ca2923ee759a24fd8e5b91673faa6859a7a0c2df6e7c7ea5e23968708632"]},"lineages":[{"arm_id":"installed-entry-facts:javascript","manifest_digest":"f3b4b192dadd6b7d0d298286008480dadfd8640e9603020b0f24d13dce9bda04","subject_digest":"331e8c2261cb030f4257a8650448560eb94941554df47cb275882e24b8f8923f","uses":[{"influenced_artifact_digest":"331e8c2261cb030f4257a8650448560eb94941554df47cb275882e24b8f8923f","labels_accessed":false,"population_use_receipt_digest":"36ccebd06b60601775ab24a2f7ec9f2b49c2eac6b273816eaf9e867f6c445915","purpose":"semantic_fact","source_population_digest":"0ec4bbe3712a691a76f6525aca59f15d0492adc1e7ec8d7e9bb42e8ee42795ce"}]},{"arm_id":"installed-entry-facts:typescript","manifest_digest":"b7a6a95ddcdd8f6e6dbcd2b90cb707eda9833ec7479cb4d7aae3c91cfe0059ca","subject_digest":"b6d1157e99537bd4928303ec0d887628bc53f0aa9e770427580a32e3948babd9","uses":[{"influenced_artifact_digest":"b6d1157e99537bd4928303ec0d887628bc53f0aa9e770427580a32e3948babd9","labels_accessed":false,"population_use_receipt_digest":"7dc5fe36b1e111ab6cf979137c40bf67b885b2a9deda944aff78e3031cb5a553","purpose":"semantic_fact","source_population_digest":"0ec4bbe3712a691a76f6525aca59f15d0492adc1e7ec8d7e9bb42e8ee42795ce"}]},{"arm_id":"installed-entry-structural-matcher","manifest_digest":"5f7ffd58a773b61d1f8e2ba0ca019e27d778166e13cec770e6f25c16b1f1a994","subject_digest":"0ad18521beeb3708e12f8c9941b3f29b3e1312fe78b9320e5aaf15bc9db8831a","uses":[{"influenced_artifact_digest":"0ad18521beeb3708e12f8c9941b3f29b3e1312fe78b9320e5aaf15bc9db8831a","labels_accessed":false,"population_use_receipt_digest":"a602c8bccc6afc2ff9d857119131a2847509bc693a80192bf8459b9dcba9cbda","purpose":"matching_behavior","source_population_digest":"0ec4bbe3712a691a76f6525aca59f15d0492adc1e7ec8d7e9bb42e8ee42795ce"}]}],"matcher_behavior_digest":"0ad18521beeb3708e12f8c9941b3f29b3e1312fe78b9320e5aaf15bc9db8831a","matcher_lineage_manifest_digest":"5f7ffd58a773b61d1f8e2ba0ca019e27d778166e13cec770e6f25c16b1f1a994","matcher_revision":"entry-structural-matcher-v1","pack_files":["express.javascript.v2.json","express.typescript.v2.json"],"recognition_projection":{"artifact_id":"express-recognition-projection-v1","byte_length":628,"content_digest":"529d18ba2b351a8de985a9cf416261d717ef55a4b7a9d9d65bce53b97d59ab35","media_type":"application/json","retention_policy_digest":"09a0fe8a253780b3666ef016d575a212bf0570da392dd5bf1a834dce2edf703f","storage_receipt_digest":"3a85c0641cddcfc8c926fd01e230360c042606bda78e14a39489de14f88944b4"},"review_policy":{"forbidden_identity_pairs":[],"maximum_evidence_bytes":1048576,"policy_digest":"cb023a4f7e727ec4abfee583256194fe3f11c064a10960f297c1f7bf2e6fecde","reviewed_reviewer_digests":["f12a7e897cbaf3059619517e1f8a725b90049a0edf3af08efa12fb1ef7fdee43"],"trusted_store_identities":["openultrasast-installed-entry-artifacts-v1"]},"schema_version":"openultrasast.access-control.installed-entry-authority.v1"} \ No newline at end of file diff --git a/src/openultrasast/ruleset/entries/express.javascript.v2.json b/src/openultrasast/ruleset/entries/express.javascript.v2.json new file mode 100644 index 0000000..43868ee --- /dev/null +++ b/src/openultrasast/ruleset/entries/express.javascript.v2.json @@ -0,0 +1 @@ +{"ecosystem":"express","facts":[{"fact_id":"entry:express:app-route:get","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"app.get","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_application","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:app-route:post","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"app.post","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_application","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:app-route:put","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"app.put","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_application","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:app-route:delete","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"app.delete","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_application","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:app-route:patch","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"app.patch","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_application","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:app-route:all","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"app.all","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_application","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:app-route:use","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"app.use","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_application","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:router-route:get","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"router.get","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_router","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:router-route:post","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"router.post","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_router","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:router-route:put","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"router.put","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_router","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:router-route:delete","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"router.delete","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_router","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:router-route:patch","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"router.patch","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_router","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:router-route:all","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"router.all","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_router","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:router-route:use","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"router.use","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_router","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:express-app","relation_effects":[{"conditions":[],"kind":"registration","role":"framework_instance","source_role":"framework_factory","target_role":"framework_instance"}],"role":"framework_instance","structural_requirements":[{"expected":"express","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"factory_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"application_instance_binding","field":"binding","kind":"argument_shape","requirement_id":"binding"}]}],"family_contract_digest":"d3aea166bdad9186843b78ce1e3a6a4c135473f3d918ff580b096dbed61c787a","family_id":"access_control","language":"javascript","normalized_content_digest":"331e8c2261cb030f4257a8650448560eb94941554df47cb275882e24b8f8923f","review_evidence":[{"content":{"artifact_id":"reviewed-express-entry-data-2026-10-30","byte_length":963,"content_digest":"e8e149309abd78f24206c58fde7f73ae2f60364d8b8fee22ecaee8342798d8ce","media_type":"application/json","retention_policy_digest":"09a0fe8a253780b3666ef016d575a212bf0570da392dd5bf1a834dce2edf703f","storage_receipt_digest":"3c675f58c7b62f41a4e123886287f60c8977fdfd6b85f3d8f4e85a1d9bfa8094"},"independence":{"attestation_digest":"9005a1b96ec40a2f0a61e2e1a74bd92a3d971d4b2555e3c88a17e5cf80381ff0","conflict_disclosure":{"artifact_id":"installed-entry-authority-conflicts-v1","byte_length":113,"content_digest":"a8bf581d321560f2730aae3d5a7c9114c2d8f81f7a702fa3681a87e7be4cb9e1","media_type":"application/json","retention_policy_digest":"09a0fe8a253780b3666ef016d575a212bf0570da392dd5bf1a834dce2edf703f","storage_receipt_digest":"ee92fde020e9b4d316ad285f3f904032c52d902fe1c7c0a94cb0d61d9c0136ce"},"producer_identity":"9635025424be37e1615e597185a425a5443242fa210d6136c38250101088e7b4","reviewer_identity":"f12a7e897cbaf3059619517e1f8a725b90049a0edf3af08efa12fb1ef7fdee43","subject_digest":"e8e149309abd78f24206c58fde7f73ae2f60364d8b8fee22ecaee8342798d8ce"},"kind":"independent_behavior","lineage_manifest_digest":"f3b4b192dadd6b7d0d298286008480dadfd8640e9603020b0f24d13dce9bda04","producer_identity":"9635025424be37e1615e597185a425a5443242fa210d6136c38250101088e7b4","reviewer_identity":"f12a7e897cbaf3059619517e1f8a725b90049a0edf3af08efa12fb1ef7fdee43"}],"review_evidence_digest":"2ab72937b003169d9061e16e4d35444d4653e1c587fbf95faaa8a085efbb1553","review_identity":"f12a7e897cbaf3059619517e1f8a725b90049a0edf3af08efa12fb1ef7fdee43","review_policy_digest":"cb023a4f7e727ec4abfee583256194fe3f11c064a10960f297c1f7bf2e6fecde","schema_version":"openultrasast.detection-core.fact-pack.v2"} \ No newline at end of file diff --git a/src/openultrasast/ruleset/entries/express.json b/src/openultrasast/ruleset/entries/express.json index 6a5dad4..6087bae 100644 --- a/src/openultrasast/ruleset/entries/express.json +++ b/src/openultrasast/ruleset/entries/express.json @@ -1,33 +1 @@ -{ - "facts": [ - { - "defines_handler": true, - "handler_kind": "route", - "id": "entry:express:app-route", - "pattern": "app.(get|post|put|delete|patch|all|use)\\s*\\(" - }, - { - "defines_handler": true, - "handler_kind": "route", - "id": "entry:express:router-route", - "pattern": "router.(get|post|put|delete|patch|all|use)\\s*\\(" - }, - { - "defines_handler": false, - "handler_kind": "app", - "id": "entry:express:express-app", - "pattern": "express\\s*\\(\\s*\\)" - } - ], - "family": "access_control", - "framework": "express", - "kind": "entry_facts", - "languages": [ - "javascript", - "typescript" - ], - "review": { - "digest": "47866e5c15b286d7107b101bd396f78bbbda23788085676c590c3af937085e00", - "note": "Reviewed against Express 4/5 routing docs and the pilot pool's manifest-level framework usage (2026-10-30 recognition table). Entry patterns only; no sinks, no labels." - } -} +{"facts":[{"defines_handler":true,"handler_kind":"route","languages":["javascript","typescript"],"legacy_fact_id":"entry:express:app-route","pattern":"app.(get|post|put|delete|patch|all|use)\\s*\\("},{"defines_handler":true,"handler_kind":"route","languages":["javascript","typescript"],"legacy_fact_id":"entry:express:router-route","pattern":"router.(get|post|put|delete|patch|all|use)\\s*\\("},{"defines_handler":false,"handler_kind":"app","languages":["javascript","typescript"],"legacy_fact_id":"entry:express:express-app","pattern":"express\\s*\\(\\s*\\)"}],"schema_version":"openultrasast.entry-recognition-projection.v1"} \ No newline at end of file diff --git a/src/openultrasast/ruleset/entries/express.typescript.v2.json b/src/openultrasast/ruleset/entries/express.typescript.v2.json new file mode 100644 index 0000000..b8463bd --- /dev/null +++ b/src/openultrasast/ruleset/entries/express.typescript.v2.json @@ -0,0 +1 @@ +{"ecosystem":"express","facts":[{"fact_id":"entry:express:app-route:get","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"app.get","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_application","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:app-route:post","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"app.post","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_application","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:app-route:put","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"app.put","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_application","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:app-route:delete","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"app.delete","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_application","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:app-route:patch","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"app.patch","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_application","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:app-route:all","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"app.all","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_application","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:app-route:use","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"app.use","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_application","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:router-route:get","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"router.get","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_router","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:router-route:post","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"router.post","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_router","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:router-route:put","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"router.put","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_router","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:router-route:delete","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"router.delete","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_router","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:router-route:patch","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"router.patch","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_router","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:router-route:all","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"router.all","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_router","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:router-route:use","relation_effects":[{"conditions":[],"kind":"registration","role":"entry_registration","source_role":"framework_instance","target_role":"entry_handler"}],"role":"entry_registration","structural_requirements":[{"expected":"router.use","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"member_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"express_router","field":"receiver","kind":"receiver_type","requirement_id":"receiver"},{"expected":"route_path_and_handler","field":"arguments","kind":"argument_shape","requirement_id":"arguments"}]},{"fact_id":"entry:express:express-app","relation_effects":[{"conditions":[],"kind":"registration","role":"framework_instance","source_role":"framework_factory","target_role":"framework_instance"}],"role":"framework_instance","structural_requirements":[{"expected":"express","field":"qualified_target","kind":"qualified_name","requirement_id":"qualified_target"},{"expected":"factory_call","field":"call_shape","kind":"ast_shape","requirement_id":"call_shape"},{"expected":"application_instance_binding","field":"binding","kind":"argument_shape","requirement_id":"binding"}]}],"family_contract_digest":"d3aea166bdad9186843b78ce1e3a6a4c135473f3d918ff580b096dbed61c787a","family_id":"access_control","language":"typescript","normalized_content_digest":"b6d1157e99537bd4928303ec0d887628bc53f0aa9e770427580a32e3948babd9","review_evidence":[{"content":{"artifact_id":"reviewed-express-entry-data-2026-10-30","byte_length":963,"content_digest":"e8e149309abd78f24206c58fde7f73ae2f60364d8b8fee22ecaee8342798d8ce","media_type":"application/json","retention_policy_digest":"09a0fe8a253780b3666ef016d575a212bf0570da392dd5bf1a834dce2edf703f","storage_receipt_digest":"3c675f58c7b62f41a4e123886287f60c8977fdfd6b85f3d8f4e85a1d9bfa8094"},"independence":{"attestation_digest":"9005a1b96ec40a2f0a61e2e1a74bd92a3d971d4b2555e3c88a17e5cf80381ff0","conflict_disclosure":{"artifact_id":"installed-entry-authority-conflicts-v1","byte_length":113,"content_digest":"a8bf581d321560f2730aae3d5a7c9114c2d8f81f7a702fa3681a87e7be4cb9e1","media_type":"application/json","retention_policy_digest":"09a0fe8a253780b3666ef016d575a212bf0570da392dd5bf1a834dce2edf703f","storage_receipt_digest":"ee92fde020e9b4d316ad285f3f904032c52d902fe1c7c0a94cb0d61d9c0136ce"},"producer_identity":"9635025424be37e1615e597185a425a5443242fa210d6136c38250101088e7b4","reviewer_identity":"f12a7e897cbaf3059619517e1f8a725b90049a0edf3af08efa12fb1ef7fdee43","subject_digest":"e8e149309abd78f24206c58fde7f73ae2f60364d8b8fee22ecaee8342798d8ce"},"kind":"independent_behavior","lineage_manifest_digest":"b7a6a95ddcdd8f6e6dbcd2b90cb707eda9833ec7479cb4d7aae3c91cfe0059ca","producer_identity":"9635025424be37e1615e597185a425a5443242fa210d6136c38250101088e7b4","reviewer_identity":"f12a7e897cbaf3059619517e1f8a725b90049a0edf3af08efa12fb1ef7fdee43"}],"review_evidence_digest":"e02ce20a4dea9af7d7aa85e53cd7b70cdb99d32e9b35f1c247a06090f2f43a22","review_identity":"f12a7e897cbaf3059619517e1f8a725b90049a0edf3af08efa12fb1ef7fdee43","review_policy_digest":"cb023a4f7e727ec4abfee583256194fe3f11c064a10960f297c1f7bf2e6fecde","schema_version":"openultrasast.detection-core.fact-pack.v2"} \ No newline at end of file diff --git a/tests/test_core_factpack_file.py b/tests/test_core_factpack_file.py index 90a86ac..6a0bbeb 100644 --- a/tests/test_core_factpack_file.py +++ b/tests/test_core_factpack_file.py @@ -5,12 +5,21 @@ import json import sys from pathlib import Path +import pytest + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from openultrasast.core.factpack import FactPackError, admit_pack_file, verify_fact_pack # noqa: E402 +from openultrasast.mapping.installed_fact_authority import ( # noqa: E402 + InstalledAuthorityWireError, + decode_fact_pack_bytes, +) REPO = Path(__file__).resolve().parents[1] -PACK = REPO / "src/openultrasast/ruleset/entries/express.json" +ENTRIES = REPO / "src/openultrasast/ruleset/entries" +LEGACY_DIGEST = "e8e149309abd78f24206c58fde7f73ae2f60364d8b8fee22ecaee8342798d8ce" +PACK = ENTRIES / "artifacts" / "sha256" / LEGACY_DIGEST +PROJECTION = ENTRIES / "express.json" def _review_digest() -> str: @@ -19,6 +28,9 @@ def _review_digest() -> str: def test_express_pack_admitted_with_verifiable_review() -> None: + content = PACK.read_bytes() + assert len(content) == 963 + assert hashlib.sha256(content).hexdigest() == LEGACY_DIGEST pack = admit_pack_file(PACK, review_digest=_review_digest()) assert pack.family == "access_control" assert len(pack.facts) == 3 @@ -34,19 +46,20 @@ def test_pack_refused_without_matching_review() -> None: pass -def test_tampered_pack_file_refused() -> None: +def test_tampered_pack_file_refused(tmp_path: Path) -> None: payload = json.loads(PACK.read_text(encoding="utf-8")) payload["facts"].append({"id": "entry:express:evil", "pattern": "eval("}) - tampered = PACK.parent / "tampered.json" + tampered = tmp_path / "tampered.json" tampered.write_text(json.dumps(payload), encoding="utf-8") - try: - pack = admit_pack_file(tampered, review_digest=_review_digest()) - # review attestation still verifies, but content no longer matches the stamped digest - original = admit_pack_file(PACK, review_digest=_review_digest()) - try: - verify_fact_pack(original, pack.facts) - raise AssertionError("tampered content must fail verification") - except FactPackError: - pass - finally: - tampered.unlink() + pack = admit_pack_file(tampered, review_digest=_review_digest()) + # review attestation still verifies, but content no longer matches the stamped digest + original = admit_pack_file(PACK, review_digest=_review_digest()) + with pytest.raises(FactPackError): + verify_fact_pack(original, pack.facts) + + +def test_compatibility_projection_is_neither_a_legacy_nor_v2_fact_pack() -> None: + with pytest.raises(FactPackError): + admit_pack_file(PROJECTION, review_digest=_review_digest()) + with pytest.raises(InstalledAuthorityWireError): + decode_fact_pack_bytes(PROJECTION.read_bytes()) diff --git a/tests/test_entry_packs.py b/tests/test_entry_packs.py index 6291a5b..f8e5f2b 100644 --- a/tests/test_entry_packs.py +++ b/tests/test_entry_packs.py @@ -1,16 +1,39 @@ -"""Entry-pack wiring tests for detection-core 8.2 part 2 (Req 10, 3).""" +"""Installed reviewed entry-pack migration and legacy recognition wiring.""" +import json +import os +import shutil import sys import tempfile from pathlib import Path +import pytest + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -from openultrasast.mapping.entry_packs import load_entry_pack_patterns # noqa: E402 +from openultrasast.core import canonical, provenance, semantics # noqa: E402 +from openultrasast.core.factpack import ( # noqa: E402 + StructuralRequirementKind, + validate_fact_pack, +) +from openultrasast.mapping import entry_packs as entry_pack_module # noqa: E402 +from openultrasast.mapping.entry_packs import ( # noqa: E402 + load_entry_pack_patterns, + load_installed_entry_fact_authority, +) +from openultrasast.mapping.fact_authority import ( # noqa: E402 + EntryFactAuthorityContext, + EntryFactAuthorityError, +) +from openultrasast.mapping.installed_fact_authority import ( # noqa: E402 + InstalledAuthorityWireError, + decode_fact_pack_bytes, +) from openultrasast.mapping.legacy import _js_entry_points, analyze_entry_points, entry_framework # noqa: E402 from openultrasast.preprocess import FileTarget, detect_language # noqa: E402 REPO = Path(__file__).resolve().parents[1] +ENTRIES = REPO / "src" / "openultrasast" / "ruleset" / "entries" TS_SOURCE = """import express from "express"; const app = express(); @@ -18,6 +41,61 @@ app.all("/admin", adminHandler); function adminHandler(req, res) { res.send("ok"); } """ +_LEGACY_ROWS = ( + ( + "entry:express:app-route", + True, + "route", + r"app.(get|post|put|delete|patch|all|use)\s*\(", + ), + ( + "entry:express:router-route", + True, + "route", + r"router.(get|post|put|delete|patch|all|use)\s*\(", + ), + ("entry:express:express-app", False, "app", r"express\s*\(\s*\)"), +) + + +class _UnusedCustody: + def current_log(self, population_digest: str) -> provenance.PopulationCustodyLog: + raise AssertionError(f"authority authentication read target custody: {population_digest}") + + +class _InstalledCustody: + def __init__(self, logs: dict[str, provenance.PopulationCustodyLog]) -> None: + self._logs = logs + + def current_log(self, population_digest: str) -> provenance.PopulationCustodyLog: + return self._logs[population_digest] + + +def _custody_for_installed_lineages( + lineages: tuple[provenance.LineageManifest, ...], + policy: provenance.PopulationCustodyPolicy, +) -> _InstalledCustody: + custodian = policy.reviewed_custodian_digests[0] + logs: dict[str, provenance.PopulationCustodyLog] = {} + for lineage in lineages: + for use in lineage.uses: + prior = logs.setdefault( + use.source_population_digest, + provenance.empty_population_custody_log(use.source_population_digest, custodian), + ) + request = provenance.PopulationUseRequest( + use.source_population_digest, + lineage.arm_id, + use.purpose, + use.labels_accessed, + use.influenced_artifact_digest, + prior.log_digest, + ) + receipt = provenance.record_population_use(request, prior, custodian, policy) + assert receipt.receipt_digest == use.population_use_receipt_digest + logs[use.source_population_digest] = provenance.append_population_use(prior, receipt, policy) + return _InstalledCustody(logs) + def _target(root: Path, name: str, text: str) -> FileTarget: path = root / name @@ -26,11 +104,202 @@ def _target(root: Path, name: str, text: str) -> FileTarget: return FileTarget(name, str(path), detect_language(path), 5, [], False, [], []) +def _stage_entries(root: Path) -> Path: + staged = root / "ruleset" / "entries" + shutil.copytree(ENTRIES, staged) + return staged + + def test_pack_patterns_loaded_for_typescript() -> None: patterns = load_entry_pack_patterns() assert patterns.get("typescript") and any(p.search("app.all(") for p in patterns["typescript"]) +def test_installed_v2_packs_authenticate_and_validate_from_a_proven_legacy_input() -> None: + context = load_installed_entry_fact_authority(_UnusedCustody()) + bundle = context.bundle + legacy_reference = bundle.packs[0].review_evidence[0].content + legacy_artifact = REPO / "src" / "openultrasast" / "ruleset" / "entries" / "artifacts" / "sha256" / legacy_reference.content_digest + + legacy_bytes = legacy_artifact.read_bytes() + assert legacy_artifact.stat().st_size == 963 + assert len(legacy_bytes) == 963 + assert canonical.bytes_digest(legacy_bytes) == legacy_reference.content_digest + + assert tuple(pack.language for pack in bundle.packs) == ("javascript", "typescript") + custody = _custody_for_installed_lineages(bundle.lineages, bundle.custody_policy) + for pack in bundle.packs: + validate_fact_pack( + pack, + bundle.contract, + None, + bundle.lineages, + custody, + bundle.custody_policy, + bundle.review_policy, + context.resolver, + ) + + +def test_reviewed_pack_mapping_is_exact_and_language_scoped() -> None: + bundle = load_installed_entry_fact_authority(_UnusedCustody()).bundle + methods = ("get", "post", "put", "delete", "patch", "all", "use") + + for pack in bundle.packs: + facts = {fact.fact_id: fact for fact in pack.facts} + assert set(facts) == { + *(f"entry:express:app-route:{method}" for method in methods), + *(f"entry:express:router-route:{method}" for method in methods), + "entry:express:express-app", + } + for receiver_name, receiver_type in (("app", "express_application"), ("router", "express_router")): + for method in methods: + fact = facts[f"entry:express:{receiver_name}-route:{method}"] + assert fact.role == "entry_registration" + assert tuple((item.requirement_id, item.kind, item.field, item.expected) for item in fact.structural_requirements) == ( + ("qualified_target", StructuralRequirementKind.QUALIFIED_NAME, "qualified_target", f"{receiver_name}.{method}"), + ("call_shape", StructuralRequirementKind.AST_SHAPE, "call_shape", "member_call"), + ("receiver", StructuralRequirementKind.RECEIVER_TYPE, "receiver", receiver_type), + ("arguments", StructuralRequirementKind.ARGUMENT_SHAPE, "arguments", "route_path_and_handler"), + ) + assert tuple( + (effect.kind, effect.role, effect.source_role, effect.target_role, effect.conditions) + for effect in fact.relation_effects + ) == ((semantics.RelationKind.REGISTRATION, "entry_registration", "framework_instance", "entry_handler", ()),) + app = facts["entry:express:express-app"] + assert app.role == "framework_instance" + assert tuple((item.requirement_id, item.kind, item.field, item.expected) for item in app.structural_requirements) == ( + ("qualified_target", StructuralRequirementKind.QUALIFIED_NAME, "qualified_target", "express"), + ("call_shape", StructuralRequirementKind.AST_SHAPE, "call_shape", "factory_call"), + ("binding", StructuralRequirementKind.ARGUMENT_SHAPE, "binding", "application_instance_binding"), + ) + assert tuple( + (effect.kind, effect.role, effect.source_role, effect.target_role, effect.conditions) for effect in app.relation_effects + ) == ((semantics.RelationKind.REGISTRATION, "framework_instance", "framework_factory", "framework_instance", ()),) + + +def test_projection_preserves_legacy_rows_but_is_not_a_fact_pack() -> None: + projection_path = REPO / "src" / "openultrasast" / "ruleset" / "entries" / "express.json" + content = projection_path.read_bytes() + raw = json.loads(content) + + assert content == json.dumps(raw, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode() + assert raw["schema_version"] == "openultrasast.entry-recognition-projection.v1" + assert ( + tuple( + ( + row["legacy_fact_id"], + row["defines_handler"], + row["handler_kind"], + row["pattern"], + ) + for row in raw["facts"] + ) + == _LEGACY_ROWS + ) + assert all(set(row) == {"legacy_fact_id", "languages", "defines_handler", "handler_kind", "pattern"} for row in raw["facts"]) + with pytest.raises(InstalledAuthorityWireError): + decode_fact_pack_bytes(content) + + +def test_repeated_pattern_load_reuses_fully_authenticated_state( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + _stage_entries(tmp_path) + entry_pack_module._cache.clear() + authenticates = 0 + real_load = entry_pack_module.load_installed_entry_fact_authority + + def counted_load( + custody: provenance.PopulationCustodyPort, + repo_root: Path | None = None, + ) -> EntryFactAuthorityContext: + nonlocal authenticates + authenticates += 1 + return real_load(custody, repo_root) + + monkeypatch.setattr(entry_pack_module, "load_installed_entry_fact_authority", counted_load) + + first = load_entry_pack_patterns(tmp_path) + second = load_entry_pack_patterns(tmp_path) + + assert authenticates == 1 + assert first is not second + assert tuple(pattern.pattern for pattern in first["javascript"]) == tuple(pattern.pattern for pattern in second["javascript"]) + + +@pytest.mark.parametrize( + "relative_path", + ( + "bootstrap.json", + "identities/generator.json", + "authority.json", + "express.javascript.v2.json", + "express.json", + "artifacts/sha256/529d18ba2b351a8de985a9cf416261d717ef55a4b7a9d9d65bce53b97d59ab35", + ), +) +def test_pattern_cache_invalidates_for_every_authority_state_class( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + relative_path: str, +) -> None: + entries = _stage_entries(tmp_path) + entry_pack_module._cache.clear() + authenticates = 0 + real_load = entry_pack_module.load_installed_entry_fact_authority + + def counted_load( + custody: provenance.PopulationCustodyPort, + repo_root: Path | None = None, + ) -> EntryFactAuthorityContext: + nonlocal authenticates + authenticates += 1 + return real_load(custody, repo_root) + + monkeypatch.setattr(entry_pack_module, "load_installed_entry_fact_authority", counted_load) + load_entry_pack_patterns(tmp_path) + changed = entries / relative_path + stat = changed.stat() + os.utime(changed, ns=(stat.st_atime_ns, stat.st_mtime_ns + 1_000_000)) + + load_entry_pack_patterns(tmp_path) + + assert authenticates == 2 + + +def test_pattern_cache_fingerprints_content_even_when_size_and_mtime_match( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + entries = _stage_entries(tmp_path) + entry_pack_module._cache.clear() + authenticates = 0 + real_load = entry_pack_module.load_installed_entry_fact_authority + + def counted_load( + custody: provenance.PopulationCustodyPort, + repo_root: Path | None = None, + ) -> EntryFactAuthorityContext: + nonlocal authenticates + authenticates += 1 + return real_load(custody, repo_root) + + monkeypatch.setattr(entry_pack_module, "load_installed_entry_fact_authority", counted_load) + load_entry_pack_patterns(tmp_path) + artifact = entries / "artifacts" / "sha256" / "529d18ba2b351a8de985a9cf416261d717ef55a4b7a9d9d65bce53b97d59ab35" + content = artifact.read_bytes() + stat = artifact.stat() + artifact.write_bytes(bytes((content[0] ^ 1,)) + content[1:]) + os.utime(artifact, ns=(stat.st_atime_ns, stat.st_mtime_ns)) + + with pytest.raises(EntryFactAuthorityError): + load_entry_pack_patterns(tmp_path) + + assert authenticates == 2 + + def test_pack_patterns_resolve_from_package_not_target_repo() -> None: # default call reads the installed package's ruleset, independent of any analyzed repo assert load_entry_pack_patterns().get("typescript")