Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ jobs:
python audit/aggregate/score.py --selftest
python audit/aggregate/report.py --selftest
python audit/static/tools/xaml_check.py --selftest
python audit/static/tools/xaml_facts.py --selftest
python audit/static/run_static.py --selftest
python audit/runtime/ingest.py --selftest

Expand Down
8 changes: 7 additions & 1 deletion audit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ audit/
owncheck.py # build-free runner: own-check.sh --format sarif (needs dotnet)
codeql.sh # build-free runner: CodeQL build-mode=none, security-and-quality
xaml_check.py # build-free runner: markup-only XAML perf/lifetime pass (stdlib XML, no SDK)
xaml_facts.py # XAML facts extractor (resource graph + binding facts) -> xaml-facts.json (Phase-2 seam)
roslyn_pack.ps1 # build-required runner (local Windows): NetAnalyzers/Roslynator/...
infersharp.sh # build-required runner: Infer# over built binaries
inject/ # OwnAudit.Directory.Build.props/.targets (analyzer injection, gated)
Expand Down Expand Up @@ -110,6 +111,7 @@ python audit/aggregate/normalize.py --selftest
python audit/aggregate/score.py --selftest
python audit/aggregate/report.py --selftest
python audit/static/tools/xaml_check.py --selftest # XAML rules + line preservation + SARIF round-trip
python audit/static/tools/xaml_facts.py --selftest # XAML facts: binding parser + resource graph
python audit/static/run_static.py --selftest # full pipeline end-to-end on fixtures
```

Expand All @@ -133,7 +135,11 @@ python audit/static/run_static.py --selftest # full pipeline end-to-end on fix
Freezable duplication). This makes category 8 (broken virtualization) statically
covered, not NO-TOOL. Design + the full rule catalogue, phasing, and the Phase-2
binding-path join: [`../docs/notes/xaml-analyzer-design.md`](../docs/notes/xaml-analyzer-design.md).
Phase 2 (Roslyn-linked XAML2xx) and Phase 3 (runtime correlation) are deferred.
- **XAML Phase-2 seam — done:** `static/tools/xaml_facts.py` emits `xaml-facts.json` (resource graph
+ binding facts: parsed binding paths / converters / handlers + the file's `x:Class`) from the same
parsed tree, in an OwnIR-parallel envelope. This is the structured input the binding-path join reads
next to the `OwnSharp.Extractor` OwnIR facts. The join itself (Roslyn-linked XAML2xx) and Phase 3
(runtime correlation) remain deferred.
- **Runtime (Phase 2) — started:** the runtime→pipeline bridge (`runtime/ingest.py`,
CI-gated), the leak-harness scenario schema + one scenario, runtime rule mappings
in the taxonomy (categories 2/3/4/11), and the C# leak-harness skeleton. See
Expand Down
55 changes: 43 additions & 12 deletions audit/static/tools/xaml_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,17 +643,24 @@ def _rule_layout_transform(root: Node, avalonia: bool) -> list[XamlFinding]:
]


def analyze_root(root: Node) -> list[XamlFinding]:
"""All Phase-1 rules over an already-parsed tree. Split out from ``analyze_text``
so a caller (e.g. ``run_xaml_check``) can parse a file once and feed the same
tree to both the rules and the facts extractor (``xaml_facts``)."""
avalonia = _is_avalonia(root)
out: list[XamlFinding] = []
for rule in RULES:
out.extend(rule(root, avalonia))
return out


def analyze_text(text: str | bytes) -> list[XamlFinding]:
"""All Phase-1 rules over one markup document (``str`` or raw ``bytes``).
Malformed markup -> no findings."""
root = parse_xaml(text)
if root is None:
return []
avalonia = _is_avalonia(root)
out: list[XamlFinding] = []
for rule in RULES:
out.extend(rule(root, avalonia))
return out
return analyze_root(root)


def _to_sarif(results: list[tuple[str, XamlFinding]]) -> dict[str, Any]:
Expand All @@ -679,11 +686,19 @@ def _to_sarif(results: list[tuple[str, XamlFinding]]) -> dict[str, Any]:


def run_xaml_check(target: str, out_dir: Path) -> dict[str, Any]:
"""Scan every ``.xaml`` / ``.axaml`` under ``target`` and write SARIF to
``out_dir/xaml-check.sarif``. Always best-effort: a missing target or zero
markup files yields ``available=False`` with a reason, never a crash."""
"""Scan every ``.xaml`` / ``.axaml`` under ``target`` and write two artifacts to
``out_dir``: ``xaml-check.sarif`` (the Phase-1 rule findings, into the audit
pipeline) and ``xaml-facts.json`` (the structured resource-graph + binding facts
for the Phase-2 binding-path join — see ``xaml_facts``). Each file is parsed once
and the same tree feeds both the rules and the facts extractor. Always
best-effort: a missing target or zero markup files yields ``available=False`` with
a reason, never a crash."""
# Local import keeps the module pair decoupled (xaml_facts imports from here).
from xaml_facts import document_facts, module_facts

out_dir.mkdir(parents=True, exist_ok=True)
sarif_path = out_dir / "xaml-check.sarif"
facts_path = out_dir / "xaml-facts.json"
status: dict[str, Any] = {"tool": "xaml", "tier": "build-free",
"available": False, "sarif": None, "reason": ""}

Expand All @@ -698,6 +713,7 @@ def run_xaml_check(target: str, out_dir: Path) -> dict[str, Any]:
return status

results: list[tuple[str, XamlFinding]] = []
documents: list[dict[str, Any]] = []
scanned = 0
for fp in files:
try:
Expand All @@ -709,12 +725,27 @@ def run_xaml_check(target: str, out_dir: Path) -> dict[str, Any]:
continue
scanned += 1
rel = fp.relative_to(root).as_posix()
for f in analyze_text(data):
tree = parse_xaml(data)
if tree is None:
continue # malformed markup: skipped (no findings, no facts)
for f in analyze_root(tree):
results.append((rel, f))
documents.append(document_facts(tree, rel))

sarif_path.write_text(json.dumps(_to_sarif(results), indent=2), encoding="utf-8")
status.update(available=True, sarif=str(sarif_path),
findings=len(results), files_scanned=scanned)
facts = module_facts(documents, module=Path(target).name or "target")
try:
# Best-effort to the end: a permission/disk error on the artifact writes is
# recorded as a tier reason, not raised — the same never-crash contract as a
# missing target or absent SDK.
sarif_path.write_text(json.dumps(_to_sarif(results), indent=2), encoding="utf-8")
facts_path.write_text(json.dumps(facts, indent=2), encoding="utf-8")
except OSError as exc:
status["reason"] = f"failed to write XAML artifacts: {exc}"
return status
status.update(available=True, sarif=str(sarif_path), facts=str(facts_path),
findings=len(results), files_scanned=scanned,
bindings=sum(len(d["bindings"]) for d in documents),
resources=sum(len(d["resources"]) for d in documents))
return status


Expand Down
Loading
Loading