mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 12:39:28 +00:00
feat(facts): the [[layout]] fact -- vendored trees are out, test paths are not product
Two decisions the maintainer made, as rows rather than constants. A vendored tree (vendor/, node_modules/, third_party/) is somebody else's unit: out of the targets in preprocess and out of the graph through the frontend's --exclude, which the scan hands to the backend because the CPG seam imports only the standard library. A test path (tests/, test-*.php, *Test.php) stays in scope -- a test that reaches a sink still names the sink -- but its regions are not shipped and order after the product's. Measured before the row: 207 of WP Statistics's 357 PHP files were vendored, and 14 of the 67 regions ahead of MW WP Form's CVE were test files. The position benchmark applies both, so it measures the order the scan spends. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
9142cb1fb4
commit
b821ac6e4e
@@ -74,6 +74,27 @@ class DispatchFact:
|
||||
language: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LayoutFact:
|
||||
"""What a repository's PATHS say about scope, per ecosystem: which trees are somebody else's code and
|
||||
which files are tests.
|
||||
|
||||
Both are policy, which is why they are rows and not constants. Vendored code is a separate unit --
|
||||
"divide and rule": a library is analysed as a library, or not at all, never as part of the project
|
||||
that bundles it -- so a `vendored` tree is out of the targets AND out of the graph. A `tests` path is
|
||||
in scope but not product: its regions are ordered after the product's, never removed, because a test
|
||||
that reaches a sink still names the sink.
|
||||
|
||||
Patterns are gitignore-style, matched against the repository-relative path and against each directory
|
||||
on it: `vendor/` names any directory called vendor at any depth, `test-*.php` any file so named.
|
||||
"""
|
||||
|
||||
id: str
|
||||
vendored: tuple[str, ...]
|
||||
tests: tuple[str, ...]
|
||||
language: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SemanticFacts:
|
||||
version: str
|
||||
@@ -81,6 +102,7 @@ class SemanticFacts:
|
||||
sinks: tuple[SinkFact, ...]
|
||||
sanitizers: tuple[SanitizerFact, ...]
|
||||
dispatches: tuple[DispatchFact, ...] = ()
|
||||
layouts: tuple[LayoutFact, ...] = ()
|
||||
|
||||
def for_language(self, language: str) -> SemanticFacts:
|
||||
key = _LANGUAGE_ALIASES.get(language, language)
|
||||
@@ -90,6 +112,7 @@ class SemanticFacts:
|
||||
sinks=tuple(item for item in self.sinks if item.language == key),
|
||||
sanitizers=tuple(item for item in self.sanitizers if item.language == key),
|
||||
dispatches=tuple(item for item in self.dispatches if item.language == key),
|
||||
layouts=tuple(item for item in self.layouts if item.language == key),
|
||||
)
|
||||
|
||||
|
||||
@@ -101,6 +124,7 @@ def load_facts(directory: Path | None = None) -> SemanticFacts:
|
||||
sinks: list[SinkFact] = []
|
||||
sanitizers: list[SanitizerFact] = []
|
||||
dispatches: list[DispatchFact] = []
|
||||
layouts: list[LayoutFact] = []
|
||||
versions: list[str] = []
|
||||
found = False
|
||||
for path in sorted(root.glob("*.toml")):
|
||||
@@ -115,6 +139,7 @@ def load_facts(directory: Path | None = None) -> SemanticFacts:
|
||||
sinks.extend(_sinks(payload.get("sink"), language, path))
|
||||
sanitizers.extend(_sanitizers(payload.get("sanitizer"), language, path))
|
||||
dispatches.extend(_dispatches(payload.get("dispatch"), language, path))
|
||||
layouts.extend(_layouts(payload.get("layout"), language, path))
|
||||
if not found:
|
||||
raise FactLoadError(f"no semantic fact files in {root}")
|
||||
return SemanticFacts(
|
||||
@@ -123,6 +148,7 @@ def load_facts(directory: Path | None = None) -> SemanticFacts:
|
||||
sinks=tuple(sinks),
|
||||
sanitizers=tuple(sanitizers),
|
||||
dispatches=tuple(dispatches),
|
||||
layouts=tuple(layouts),
|
||||
)
|
||||
|
||||
|
||||
@@ -143,6 +169,23 @@ def _dispatches(value: object, language: str, path: Path) -> list[DispatchFact]:
|
||||
return facts
|
||||
|
||||
|
||||
def _layouts(value: object, language: str, path: Path) -> list[LayoutFact]:
|
||||
facts: list[LayoutFact] = []
|
||||
for item in _items(value):
|
||||
identifier = str(item.get("id", "")).strip()
|
||||
if not identifier:
|
||||
raise FactLoadError(f"layout without an id in {path}")
|
||||
facts.append(
|
||||
LayoutFact(
|
||||
id=identifier,
|
||||
vendored=_strings(item.get("vendored", []), "vendored", path),
|
||||
tests=_strings(item.get("tests", []), "tests", path),
|
||||
language=language,
|
||||
)
|
||||
)
|
||||
return facts
|
||||
|
||||
|
||||
def _items(value: object) -> list[dict[str, object]]:
|
||||
if value is None:
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user