mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
engine: admit TypeScript (jssrc2cpg, JavaScript taint specs via one alias), .mts/.cts, mixed JS/TS builds one CPG
Implemented with Codex. Not merged: wall-clock deadline tests failed at host load 47; to be re-checked against main at normal load. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
20777d8863
commit
e13ce35a75
@@ -126,9 +126,8 @@ def resolve_scan(scans: Path, case: str, side: str) -> dict[str, Any] | None:
|
|||||||
|
|
||||||
|
|
||||||
def recorded_engine(language: str, findings: list, result: Mapping | None, repository: bool) -> Part:
|
def recorded_engine(language: str, findings: list, result: Mapping | None, repository: bool) -> Part:
|
||||||
# The language list describes standalone scans. Repository scans also analyze
|
# Recorded findings take precedence over the shared engine language coverage.
|
||||||
# TypeScript; recorded function findings take precedence over that list.
|
if language not in ENGINE_LANGUAGES and not findings:
|
||||||
if language not in ENGINE_LANGUAGES and not findings and not (repository and language == "typescript" and result is not None):
|
|
||||||
return NONE
|
return NONE
|
||||||
state = engine_for(result)
|
state = engine_for(result)
|
||||||
if state is not None:
|
if state is not None:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from .layout import is_test_path, is_vendored, layout_facts
|
|||||||
FRONTENDS = {
|
FRONTENDS = {
|
||||||
"php": "php",
|
"php": "php",
|
||||||
"javascript": "javascript",
|
"javascript": "javascript",
|
||||||
"typescript": "typescript",
|
"typescript": "javascript",
|
||||||
"python": "python",
|
"python": "python",
|
||||||
"java": "java",
|
"java": "java",
|
||||||
"c": "c",
|
"c": "c",
|
||||||
@@ -109,14 +109,19 @@ class PartitionGraph:
|
|||||||
|
|
||||||
def _dispatch(self, kind: str, requests: Mapping[str, Mapping[str, object]], method: str) -> dict[str, list[object]]:
|
def _dispatch(self, kind: str, requests: Mapping[str, Mapping[str, object]], method: str) -> dict[str, list[object]]:
|
||||||
answers: dict[str, list[object]] = {}
|
answers: dict[str, list[object]] = {}
|
||||||
languages = dict.fromkeys(self.path_languages.get(str(params.get("file", "")), "") for params in requests.values())
|
|
||||||
|
def graph_language(path: str) -> str:
|
||||||
|
language = self.path_languages.get(path, "")
|
||||||
|
return "javascript" if language == "typescript" else language
|
||||||
|
|
||||||
|
languages = dict.fromkeys(graph_language(str(params.get("file", ""))) for params in requests.values())
|
||||||
for language in languages:
|
for language in languages:
|
||||||
graph = self.graphs.get(language)
|
graph = self.graphs.get(language)
|
||||||
if graph is None:
|
if graph is None:
|
||||||
continue
|
continue
|
||||||
if self.budget is not None and time.monotonic() >= self.budget.deadline_monotonic:
|
if self.budget is not None and time.monotonic() >= self.budget.deadline_monotonic:
|
||||||
break
|
break
|
||||||
selected = {rid: params for rid, params in requests.items() if self.path_languages.get(str(params.get("file", ""))) == language}
|
selected = {rid: params for rid, params in requests.items() if graph_language(str(params.get("file", ""))) == language}
|
||||||
if not selected:
|
if not selected:
|
||||||
continue
|
continue
|
||||||
batch = getattr(graph, method, None) or getattr(graph, "run_batch", None)
|
batch = getattr(graph, method, None) or getattr(graph, "run_batch", None)
|
||||||
@@ -133,7 +138,12 @@ class PartitionGraph:
|
|||||||
# Carried, not normalized: it names request ids and costs, never a path.
|
# Carried, not normalized: it names request ids and costs, never a path.
|
||||||
answers.setdefault(TIMING, []).extend(rows)
|
answers.setdefault(TIMING, []).extend(rows)
|
||||||
elif rid == "__census__" and isinstance(rows, list):
|
elif rid == "__census__" and isinstance(rows, list):
|
||||||
expected = set(next(p.paths for p in self.partitions if p.language == language))
|
expected = {
|
||||||
|
path
|
||||||
|
for p in self.partitions
|
||||||
|
if p.language == language or (language == "javascript" and p.language == "typescript")
|
||||||
|
for path in p.paths
|
||||||
|
}
|
||||||
normalized = self._normalize(rows)
|
normalized = self._normalize(rows)
|
||||||
names = [r.get("file_names") for r in normalized if isinstance(r, Mapping)]
|
names = [r.get("file_names") for r in normalized if isinstance(r, Mapping)]
|
||||||
named = bool(names) and all(isinstance(n, list) and all(isinstance(p, str) for p in n) for n in names)
|
named = bool(names) and all(isinstance(n, list) and all(isinstance(p, str) for p in n) for n in names)
|
||||||
@@ -234,10 +244,17 @@ def build_partitions(
|
|||||||
graph.path_languages[relative] = languages[0]
|
graph.path_languages[relative] = languages[0]
|
||||||
elif languages:
|
elif languages:
|
||||||
boundaries.add("ambiguous_frontend_path")
|
boundaries.add("ambiguous_frontend_path")
|
||||||
if len(paths) > 1:
|
# JavaScript and TypeScript share one frontend input tree and graph. Keep
|
||||||
|
# original per-file languages for scoped coverage and property limitations.
|
||||||
|
groups: dict[str, list[str]] = {}
|
||||||
|
for language in paths:
|
||||||
|
key = "javascript" if language == "typescript" else language
|
||||||
|
groups.setdefault(key, []).append(language)
|
||||||
|
if len(groups) > 1:
|
||||||
boundaries.add("cross_partition_semantics_unresolved")
|
boundaries.add("cross_partition_semantics_unresolved")
|
||||||
records: list[PartitionCoverage] = []
|
records: list[PartitionCoverage] = []
|
||||||
for language, sources in sorted(paths.items()):
|
for language, members in sorted(groups.items()):
|
||||||
|
sources = [source for member in members for source in paths[member]]
|
||||||
relative_paths = tuple(sorted(p.relative_to(root).as_posix() for p in sources))
|
relative_paths = tuple(sorted(p.relative_to(root).as_posix() for p in sources))
|
||||||
frontend = FRONTENDS.get(language)
|
frontend = FRONTENDS.get(language)
|
||||||
if frontend is None:
|
if frontend is None:
|
||||||
@@ -247,7 +264,7 @@ def build_partitions(
|
|||||||
continue
|
continue
|
||||||
scratch = Path(tempfile.mkdtemp(prefix="ousast-input-"))
|
scratch = Path(tempfile.mkdtemp(prefix="ousast-input-"))
|
||||||
graph.roots[language] = scratch
|
graph.roots[language] = scratch
|
||||||
size = 0
|
sizes: dict[str, int] = {}
|
||||||
complete = True
|
complete = True
|
||||||
for source in sources:
|
for source in sources:
|
||||||
if budget is not None and time.monotonic() >= budget.deadline_monotonic:
|
if budget is not None and time.monotonic() >= budget.deadline_monotonic:
|
||||||
@@ -268,25 +285,31 @@ def build_partitions(
|
|||||||
complete = False
|
complete = False
|
||||||
break
|
break
|
||||||
output.write(chunk)
|
output.write(chunk)
|
||||||
size += len(chunk)
|
relative = source.relative_to(root).as_posix()
|
||||||
|
sizes[relative] = sizes.get(relative, 0) + len(chunk)
|
||||||
if not complete:
|
if not complete:
|
||||||
break
|
break
|
||||||
built = build(scratch, frontend) if complete else None
|
built = build(scratch, frontend) if complete else None
|
||||||
if built is not None:
|
if built is not None:
|
||||||
graph.graphs[language] = built
|
graph.graphs[language] = built
|
||||||
graph.unparsed += tuple(graph._normalize(list(getattr(built, "unparsed", ()))))
|
graph.unparsed += tuple(graph._normalize(list(getattr(built, "unparsed", ()))))
|
||||||
record_limits: tuple[str, ...] = ("runtime_unspecified",) if language in {"javascript", "typescript"} else ()
|
for language in sorted(members):
|
||||||
if language == "typescript":
|
relative_paths = tuple(sorted(p.relative_to(root).as_posix() for p in paths[language]))
|
||||||
record_limits += ("typescript_property_support_unvalidated",)
|
source_size = sum(sizes.get(p, 0) for p in relative_paths)
|
||||||
boundaries.add("typescript_property_support_unvalidated")
|
record_limits: tuple[str, ...] = ("runtime_unspecified",) if language in {"javascript", "typescript"} else ()
|
||||||
# About TypeScript answers. YesWiki is a PHP application that ships a few `.ts` files, and this
|
if language == "typescript":
|
||||||
# boundary made all 1,128 of its PHP questions unresolved.
|
record_limits += ("typescript_property_support_unvalidated",)
|
||||||
owned_languages.setdefault("typescript_property_support_unvalidated", set()).add("typescript")
|
boundaries.add("typescript_property_support_unvalidated")
|
||||||
if language in {"c", "cpp"}:
|
# About TypeScript answers. YesWiki is a PHP application that ships a few `.ts` files, and this
|
||||||
record_limits += ("c_bounds_arithmetic_unmodeled",)
|
# boundary made all 1,128 of its PHP questions unresolved.
|
||||||
records.append(
|
owned_languages.setdefault("typescript_property_support_unvalidated", set()).add("typescript")
|
||||||
PartitionCoverage(language, frontend, relative_paths, size, "built" if built is not None else "build_failed", record_limits)
|
if language in {"c", "cpp"}:
|
||||||
)
|
record_limits += ("c_bounds_arithmetic_unmodeled",)
|
||||||
|
records.append(
|
||||||
|
PartitionCoverage(
|
||||||
|
language, frontend, relative_paths, source_size, "built" if built is not None else "build_failed", record_limits
|
||||||
|
)
|
||||||
|
)
|
||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
|
|
||||||
graph.partitions = tuple(
|
graph.partitions = tuple(
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from collections.abc import Mapping, Sequence
|
|||||||
from dataclasses import dataclass, replace
|
from dataclasses import dataclass, replace
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ..semantic.facts import TAINT_LANGUAGE_ALIAS
|
||||||
from .contracts import AffectedRelationship, ChangeContext, ExecutionBudget, QuestionIdentity
|
from .contracts import AffectedRelationship, ChangeContext, ExecutionBudget, QuestionIdentity
|
||||||
from .specs import config_specs, dominance_specs, taint_specs
|
from .specs import config_specs, dominance_specs, taint_specs
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ _ACCESS_RANK: dict[str, float] = {
|
|||||||
# Languages whose facts the model layer carries. Anything else yields no region rather than an empty scan
|
# Languages whose facts the model layer carries. Anything else yields no region rather than an empty scan
|
||||||
# that looks like a clean bill of health.
|
# that looks like a clean bill of health.
|
||||||
_LANGUAGES = {"python", "javascript", "typescript", "java", "c", "c_cpp", "php"}
|
_LANGUAGES = {"python", "javascript", "typescript", "java", "c", "c_cpp", "php"}
|
||||||
_NORMALISE = {"typescript": "javascript", "c_cpp": "c"}
|
_NORMALISE = {**TAINT_LANGUAGE_ALIAS, "c_cpp": "c"}
|
||||||
|
|
||||||
# What a CPG calls a file's top-level code. A module body is a method like any other to the engine, and
|
# What a CPG calls a file's top-level code. A module body is a method like any other to the engine, and
|
||||||
# naming it here is what lets a region ask about the code that is not inside any function.
|
# naming it here is what lets a region ask about the code that is not inside any function.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import re
|
|||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from ..semantic.facts import SemanticFacts, load_facts
|
from ..semantic.facts import TAINT_LANGUAGE_ALIAS, SemanticFacts, load_facts
|
||||||
from ..semantic.obligations.facts import ObligationFacts, load_obligation_facts
|
from ..semantic.obligations.facts import ObligationFacts, load_obligation_facts
|
||||||
from .taxonomy import FamilyTaxonomy, load_families
|
from .taxonomy import FamilyTaxonomy, load_families
|
||||||
|
|
||||||
@@ -245,7 +245,10 @@ def taint_specs(
|
|||||||
Sources and sanitizers are language-wide: a family does not get its own notion of what untrusted input is,
|
Sources and sanitizers are language-wide: a family does not get its own notion of what untrusted input is,
|
||||||
and a sanitizer that breaks one flow breaks it whatever the sink's family.
|
and a sanitizer that breaks one flow breaks it whatever the sink's family.
|
||||||
"""
|
"""
|
||||||
scoped = (facts if facts is not None else load_facts()).for_language(language)
|
facts = facts if facts is not None else load_facts()
|
||||||
|
if language in TAINT_LANGUAGE_ALIAS:
|
||||||
|
language = facts.taint_language(language)
|
||||||
|
scoped = facts.for_language(language)
|
||||||
families = taxonomy if taxonomy is not None else load_families()
|
families = taxonomy if taxonomy is not None else load_families()
|
||||||
sources = tuple(sorted({pattern for fact in scoped.sources for pattern in fact.patterns}))
|
sources = tuple(sorted({pattern for fact in scoped.sources for pattern in fact.patterns}))
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ from .preprocess import FileTarget, preprocess_repository
|
|||||||
from .rank import rank_targets
|
from .rank import rank_targets
|
||||||
from .ruleset import PatternRule
|
from .ruleset import PatternRule
|
||||||
from .semantic import OverlayRecord, adjudicate
|
from .semantic import OverlayRecord, adjudicate
|
||||||
from .semantic.facts import FactLoadError, load_facts
|
from .semantic.facts import TAINT_LANGUAGE_ALIAS, FactLoadError, load_facts
|
||||||
from .semantic.functions import RESERVED_FUNCTION_LABELS, function_at, named_function_ranges, spans_named
|
from .semantic.functions import RESERVED_FUNCTION_LABELS, function_at, named_function_ranges, spans_named
|
||||||
from .semantic.obligations.facts import OPERATION_KINDS
|
from .semantic.obligations.facts import OPERATION_KINDS
|
||||||
|
|
||||||
@@ -1071,7 +1071,7 @@ def _sink_ids_by_rule() -> dict[tuple[str, str], frozenset[str]]:
|
|||||||
|
|
||||||
|
|
||||||
def _fact_language(language: str) -> str:
|
def _fact_language(language: str) -> str:
|
||||||
return {"c_cpp": "c", "cpp": "c", "typescript": "javascript"}.get(language, language)
|
return {"c_cpp": "c", "cpp": "c", **TAINT_LANGUAGE_ALIAS}.get(language, language)
|
||||||
|
|
||||||
|
|
||||||
def _in_named_function(record: OverlayRecord, function: str, ranges: FunctionRanges | None) -> bool:
|
def _in_named_function(record: OverlayRecord, function: str, ranges: FunctionRanges | None) -> bool:
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ from ...mapping import analyze_entry_points, attach_reachability_hints
|
|||||||
from ...preprocess import detect_language, preprocess_repository
|
from ...preprocess import detect_language, preprocess_repository
|
||||||
from ...rank import rank_targets
|
from ...rank import rank_targets
|
||||||
from ...ruleset import DEFAULT_RULESET_DIR, load_ruleset
|
from ...ruleset import DEFAULT_RULESET_DIR, load_ruleset
|
||||||
|
from ...semantic.facts import TAINT_LANGUAGE_ALIAS
|
||||||
from .repo_facts import DECLARATION, GLOBAL, enclosing
|
from .repo_facts import DECLARATION, GLOBAL, enclosing
|
||||||
|
|
||||||
ROLES = ("vulnerable", "fixed")
|
ROLES = ("vulnerable", "fixed")
|
||||||
@@ -64,8 +65,9 @@ def quick_languages(directory: Path = DEFAULT_RULESET_DIR) -> frozenset[str]:
|
|||||||
|
|
||||||
|
|
||||||
def engine_languages(directory: Path = DEFAULT_RULESET_DIR) -> frozenset[str]:
|
def engine_languages(directory: Path = DEFAULT_RULESET_DIR) -> frozenset[str]:
|
||||||
"""The languages the deterministic engine has a semantic model for: one ``semantic/<language>.toml`` each."""
|
"""Languages with a semantic fact table, including aliases of available tables."""
|
||||||
return frozenset(path.stem for path in (directory / "semantic").glob("*.toml"))
|
languages = {path.stem for path in (directory / "semantic").glob("*.toml")}
|
||||||
|
return frozenset(languages | {alias for alias, target in TAINT_LANGUAGE_ALIAS.items() if target in languages})
|
||||||
|
|
||||||
|
|
||||||
def covers(covered: Iterable[str], language: str) -> bool:
|
def covers(covered: Iterable[str], language: str) -> bool:
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ LANGUAGE_BY_EXTENSION = {
|
|||||||
".cjs": "javascript",
|
".cjs": "javascript",
|
||||||
".ts": "typescript",
|
".ts": "typescript",
|
||||||
".tsx": "typescript",
|
".tsx": "typescript",
|
||||||
|
".mts": "typescript",
|
||||||
|
".cts": "typescript",
|
||||||
".go": "go",
|
".go": "go",
|
||||||
".rs": "rust",
|
".rs": "rust",
|
||||||
".java": "java",
|
".java": "java",
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ from ..ruleset.frameworks import Priors, kept, normalize_priors, read_tag, tag_o
|
|||||||
|
|
||||||
DEFAULT_FACTS_DIR = Path(__file__).resolve().parents[1] / "ruleset" / "semantic"
|
DEFAULT_FACTS_DIR = Path(__file__).resolve().parents[1] / "ruleset" / "semantic"
|
||||||
|
|
||||||
|
TAINT_LANGUAGE_ALIAS = {"typescript": "javascript"}
|
||||||
|
|
||||||
_LANGUAGE_ALIASES = {
|
_LANGUAGE_ALIASES = {
|
||||||
"c": "c",
|
"c": "c",
|
||||||
"cpp": "c",
|
"cpp": "c",
|
||||||
"python": "python",
|
"python": "python",
|
||||||
"javascript": "javascript",
|
"javascript": "javascript",
|
||||||
"typescript": "javascript",
|
**TAINT_LANGUAGE_ALIAS,
|
||||||
"java": "java",
|
"java": "java",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,8 +147,18 @@ class SemanticFacts:
|
|||||||
dispatches: tuple[DispatchFact, ...] = ()
|
dispatches: tuple[DispatchFact, ...] = ()
|
||||||
layouts: tuple[LayoutFact, ...] = ()
|
layouts: tuple[LayoutFact, ...] = ()
|
||||||
|
|
||||||
|
def taint_language(self, language: str) -> str:
|
||||||
|
"""Prefer native facts when supplied; otherwise use the shared language aliases."""
|
||||||
|
if language in TAINT_LANGUAGE_ALIAS and any(
|
||||||
|
item.language == language
|
||||||
|
for items in (self.sources, self.sinks, self.sanitizers, self.dispatches, self.layouts)
|
||||||
|
for item in items
|
||||||
|
):
|
||||||
|
return language
|
||||||
|
return _LANGUAGE_ALIASES.get(language, language)
|
||||||
|
|
||||||
def for_language(self, language: str) -> SemanticFacts:
|
def for_language(self, language: str) -> SemanticFacts:
|
||||||
key = _LANGUAGE_ALIASES.get(language, language)
|
key = self.taint_language(language)
|
||||||
return SemanticFacts(
|
return SemanticFacts(
|
||||||
version=self.version,
|
version=self.version,
|
||||||
sources=tuple(item for item in self.sources if item.language == key),
|
sources=tuple(item for item in self.sources if item.language == key),
|
||||||
|
|||||||
@@ -1368,3 +1368,33 @@ def test_requests_held_for_arbitration_are_reported_as_unasked() -> None:
|
|||||||
merged = scan._batched(once, "taint", {rid: {} for rid in weights}, weights=weights, within=lambda: next(budget, False), unasked=held)
|
merged = scan._batched(once, "taint", {rid: {} for rid in weights}, weights=weights, within=lambda: next(budget, False), unasked=held)
|
||||||
assert merged is not None and len(calls) == 1
|
assert merged is not None and len(calls) == 1
|
||||||
assert sorted(held + calls[0]) == sorted(weights), (held, calls)
|
assert sorted(held + calls[0]) == sorted(weights), (held, calls)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("extension", [".ts", ".tsx", ".mts", ".cts"])
|
||||||
|
def test_typescript_uses_javascript_frontend(tmp_path, monkeypatch, extension):
|
||||||
|
from openultrasast.cpg.backend import JoernBackend
|
||||||
|
|
||||||
|
(tmp_path / "tests").mkdir()
|
||||||
|
(tmp_path / f"tests/probe{extension}").write_bytes(b"function first_party_test(x) { return x; }")
|
||||||
|
from openultrasast.preprocess import detect_language
|
||||||
|
|
||||||
|
assert detect_language(Path(f"probe{extension}")) == "typescript"
|
||||||
|
commands = []
|
||||||
|
|
||||||
|
def runner(command, **kwargs):
|
||||||
|
if _is_overlay(command):
|
||||||
|
return _overlaid(command, kwargs.get("cwd"))
|
||||||
|
commands.append(command)
|
||||||
|
assert Path(command[0]).name == "jssrc2cpg"
|
||||||
|
Path(command[command.index("-o") + 1]).write_bytes(b"graph")
|
||||||
|
return subprocess.CompletedProcess(command, 0, "", "")
|
||||||
|
|
||||||
|
monkeypatch.setattr("shutil.which", lambda name: f"/opt/joern/{name}")
|
||||||
|
monkeypatch.setenv("OPENULTRASAST_JOERN_PROBE", "on")
|
||||||
|
result = JoernBackend(runner=runner).build(tmp_path, language="typescript")
|
||||||
|
try:
|
||||||
|
assert result is not None
|
||||||
|
assert len(commands) == 1
|
||||||
|
finally:
|
||||||
|
if result is not None:
|
||||||
|
result.cleanup()
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ def test_feature_typescript_repository_witness_reaches_static_record(tmp_path: P
|
|||||||
assert parts["engine"].state == "ran"
|
assert parts["engine"].state == "ran"
|
||||||
assert parts["engine"].values["eng.witness_steps_min"] == 5
|
assert parts["engine"].values["eng.witness_steps_min"] == 5
|
||||||
assert parts["engine"].values["eng.findings"] == 1
|
assert parts["engine"].values["eng.findings"] == 1
|
||||||
assert builder.recorded_engine("typescript", [], None, True).state == "none"
|
assert builder.recorded_engine("typescript", [], None, True).state == "failed"
|
||||||
assert builder.recorded_engine("typescript", [], result, False).state == "none"
|
assert builder.recorded_engine("typescript", [], result, False).state == "ran"
|
||||||
assert builder.recorded_engine("typescript", [], result, True).values["eng.findings"] == 0
|
assert builder.recorded_engine("typescript", [], result, True).values["eng.findings"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -344,3 +344,37 @@ def test_unnamed_sharded_census_remains_explicitly_incomplete(tmp_path):
|
|||||||
assert "partition_file_census_unavailable" in graph.execution_diagnostics()
|
assert "partition_file_census_unavailable" in graph.execution_diagnostics()
|
||||||
finally:
|
finally:
|
||||||
graph.cleanup()
|
graph.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
def test_mixed_javascript_typescript_pin_builds_and_queries_one_graph(tmp_path):
|
||||||
|
from openultrasast.model.partitions import build_partitions
|
||||||
|
|
||||||
|
sources = {"app.js": "function run(x) { return x; }", "handler.ts": "const x: number = 1;"}
|
||||||
|
for path, body in sources.items():
|
||||||
|
(tmp_path / path).write_text(body)
|
||||||
|
builds, queries = [], []
|
||||||
|
|
||||||
|
class Graph:
|
||||||
|
def run_batch(self, kind, requests):
|
||||||
|
queries.append(requests)
|
||||||
|
return {**{key: [] for key in requests}, "__census__": [{"file_names": list(sources)}]}
|
||||||
|
|
||||||
|
def build(root, language):
|
||||||
|
assert {p.name: p.read_text() for p in root.iterdir()} == sources
|
||||||
|
builds.append(language)
|
||||||
|
return Graph()
|
||||||
|
|
||||||
|
graph = build_partitions(tmp_path, build, None)
|
||||||
|
try:
|
||||||
|
assert builds == ["javascript"]
|
||||||
|
assert {p.language for p in graph.partitions} == {"javascript", "typescript"}
|
||||||
|
assert sum(p.source_bytes for p in graph.partitions) == sum(len(body.encode()) for body in sources.values())
|
||||||
|
assert "cross_partition_semantics_unresolved" not in graph.boundaries
|
||||||
|
assert graph.boundary_languages["typescript_property_support_unvalidated"] == ("typescript",)
|
||||||
|
requests = {path: {"file": path} for path in sources}
|
||||||
|
answers = graph.run_batch("taint", requests)
|
||||||
|
assert all(answers[path] == [] for path in sources)
|
||||||
|
assert queries == [requests]
|
||||||
|
assert not graph.execution_diagnostics()
|
||||||
|
finally:
|
||||||
|
graph.cleanup()
|
||||||
|
|||||||
@@ -322,3 +322,26 @@ def test_a_sanitizer_that_does_not_stop_sql_injection_is_not_listed() -> None:
|
|||||||
assert "esc_html" not in sanitizers
|
assert "esc_html" not in sanitizers
|
||||||
assert "$wpdb->prepare" in sanitizers, "the parameterized form does stop it"
|
assert "$wpdb->prepare" in sanitizers, "the parameterized form does stop it"
|
||||||
assert "esc_sql" in sanitizers
|
assert "esc_sql" in sanitizers
|
||||||
|
|
||||||
|
|
||||||
|
def test_typescript_taint_specs_share_javascript_facts():
|
||||||
|
from openultrasast.model.specs import taint_specs
|
||||||
|
|
||||||
|
javascript = taint_specs(language="javascript")
|
||||||
|
assert javascript
|
||||||
|
assert taint_specs(language="typescript") == javascript
|
||||||
|
|
||||||
|
|
||||||
|
def test_typescript_native_facts_take_precedence_over_alias():
|
||||||
|
from dataclasses import replace
|
||||||
|
|
||||||
|
from openultrasast.model.specs import taint_specs
|
||||||
|
from openultrasast.semantic.facts import load_facts
|
||||||
|
|
||||||
|
facts = load_facts()
|
||||||
|
native = replace(next(sink for sink in facts.sinks if sink.language == "javascript"), language="typescript", calls=("tsOnly",))
|
||||||
|
facts = replace(facts, sinks=(*facts.sinks, native))
|
||||||
|
specs = taint_specs(language="typescript", facts=facts)
|
||||||
|
assert specs
|
||||||
|
assert {spec.language for spec in specs.values()} == {"typescript"}
|
||||||
|
assert {sink for spec in specs.values() for sink in spec.sinks} == {"tsOnly"}
|
||||||
|
|||||||
@@ -311,3 +311,15 @@ def test_the_loop_runs_as_one_plane_run_and_the_gate_rejects_a_demotion_that_cos
|
|||||||
assert set(features) == {"lmdeploy/vl/media/connection.py::_load_http_url"}, "no fixed-pin alert became a candidate"
|
assert set(features) == {"lmdeploy/vl/media/connection.py::_load_http_url"}, "no fixed-pin alert became a candidate"
|
||||||
assert all(r["pin"] == VULNERABLE and "in_fix_range" not in json.dumps(r["x"]) for r in features.values())
|
assert all(r["pin"] == VULNERABLE and "in_fix_range" not in json.dumps(r["x"]) for r in features.values())
|
||||||
assert fixed_alert.row["in_fix_range"] is True and fixed_alert.row["function"] == "load"
|
assert fixed_alert.row["in_fix_range"] is True and fixed_alert.row["function"] == "load"
|
||||||
|
|
||||||
|
|
||||||
|
def test_engine_languages_include_aliases_only_for_available_tables(tmp_path):
|
||||||
|
from openultrasast.plane.tasks.alerts import engine_languages
|
||||||
|
|
||||||
|
assert "typescript" in engine_languages()
|
||||||
|
semantic = tmp_path / "semantic"
|
||||||
|
semantic.mkdir()
|
||||||
|
(semantic / "python.toml").write_text("")
|
||||||
|
assert engine_languages(tmp_path) == {"python"}
|
||||||
|
(semantic / "javascript.toml").write_text("")
|
||||||
|
assert engine_languages(tmp_path) == {"python", "javascript", "typescript"}
|
||||||
|
|||||||
Reference in New Issue
Block a user