diff --git a/README.md b/README.md index b3b60d70..df75980d 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ ## Table of Contents +- [What's new (2026-06-20) — SARIF 2.1.0 Findings Export](#whats-new-2026-06-20--sarif-210-findings-export) - [What's new (2026-06-20) — Text PII Detection & Redaction](#whats-new-2026-06-20--text-pii-detection--redaction) - [What's new (2026-06-20) — Self-Healing Locator Write-Back](#whats-new-2026-06-20--self-healing-locator-write-back) - [What's new (2026-06-20) — DMN-Style Decision Tables](#whats-new-2026-06-20--dmn-style-decision-tables) @@ -108,6 +109,12 @@ --- +## What's new (2026-06-20) — SARIF 2.1.0 Findings Export + +Unify scanner findings for GitHub code scanning. Full reference: [`docs/source/Eng/doc/new_features/v56_features_doc.rst`](docs/source/Eng/doc/new_features/v56_features_doc.rst). + +- **`to_sarif` / `write_sarif` / `make_finding` / `from_lint_issues` / `from_audit_findings`** (`AC_export_sarif`, `ac_export_sarif`): the framework's findings producers (action-lint, secrets scan, WCAG audit, guardrail) had no common export. This builds a SARIF 2.1.0 document — with auto rule catalog and stable `partialFingerprints` for cross-run dedupe — that GitHub/Azure DevOps code scanning ingests as line-anchored alerts. Pure-stdlib `json`+`hashlib`; adapters normalize the existing lint/audit shapes. + ## What's new (2026-06-20) — Text PII Detection & Redaction Mask PII in text before it leaks. Full reference: [`docs/source/Eng/doc/new_features/v55_features_doc.rst`](docs/source/Eng/doc/new_features/v55_features_doc.rst). diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 82e764f4..7b34e713 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -12,6 +12,7 @@ ## 目录 +- [本次更新 (2026-06-20) — SARIF 2.1.0 发现项目导出](#本次更新-2026-06-20--sarif-210-发现项目导出) - [本次更新 (2026-06-20) — 文本 PII 检测与遮蔽](#本次更新-2026-06-20--文本-pii-检测与遮蔽) - [本次更新 (2026-06-20) — 自我修复定位器回写](#本次更新-2026-06-20--自我修复定位器回写) - [本次更新 (2026-06-20) — DMN 式决策表](#本次更新-2026-06-20--dmn-式决策表) @@ -107,6 +108,12 @@ --- +## 本次更新 (2026-06-20) — SARIF 2.1.0 发现项目导出 + +统一扫描结果供 GitHub 代码扫描。完整参考:[`docs/source/Zh/doc/new_features/v56_features_doc.rst`](../docs/source/Zh/doc/new_features/v56_features_doc.rst)。 + +- **`to_sarif` / `write_sarif` / `make_finding` / `from_lint_issues` / `from_audit_findings`**(`AC_export_sarif`、`ac_export_sarif`):框架的发现项目产生器(action-lint、密钥扫描、WCAG 审计、guardrail)缺乏共通导出。本功能建立 SARIF 2.1.0 文件(自动规则目录 + 稳定 `partialFingerprints` 跨运行去重),供 GitHub/Azure DevOps 代码扫描以定位到行的警示导入。纯标准库 `json`+`hashlib`;转接器规范化既有 lint/audit 形状。 + ## 本次更新 (2026-06-20) — 文本 PII 检测与遮蔽 在文本泄漏前遮蔽 PII。完整参考:[`docs/source/Zh/doc/new_features/v55_features_doc.rst`](../docs/source/Zh/doc/new_features/v55_features_doc.rst)。 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index eb29c13d..58aa8143 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -12,6 +12,7 @@ ## 目錄 +- [本次更新 (2026-06-20) — SARIF 2.1.0 發現項目匯出](#本次更新-2026-06-20--sarif-210-發現項目匯出) - [本次更新 (2026-06-20) — 文字 PII 偵測與遮蔽](#本次更新-2026-06-20--文字-pii-偵測與遮蔽) - [本次更新 (2026-06-20) — 自我修復定位器回寫](#本次更新-2026-06-20--自我修復定位器回寫) - [本次更新 (2026-06-20) — DMN 式決策表](#本次更新-2026-06-20--dmn-式決策表) @@ -107,6 +108,12 @@ --- +## 本次更新 (2026-06-20) — SARIF 2.1.0 發現項目匯出 + +統一掃描結果供 GitHub 程式碼掃描。完整參考:[`docs/source/Zh/doc/new_features/v56_features_doc.rst`](../docs/source/Zh/doc/new_features/v56_features_doc.rst)。 + +- **`to_sarif` / `write_sarif` / `make_finding` / `from_lint_issues` / `from_audit_findings`**(`AC_export_sarif`、`ac_export_sarif`):框架的發現項目產生器(action-lint、密鑰掃描、WCAG 稽核、guardrail)缺乏共通匯出。本功能建立 SARIF 2.1.0 文件(自動規則目錄 + 穩定 `partialFingerprints` 跨執行去重),供 GitHub/Azure DevOps 程式碼掃描以定位到行的警示匯入。純標準函式庫 `json`+`hashlib`;轉接器正規化既有 lint/audit 形狀。 + ## 本次更新 (2026-06-20) — 文字 PII 偵測與遮蔽 在文字洩漏前遮蔽 PII。完整參考:[`docs/source/Zh/doc/new_features/v55_features_doc.rst`](../docs/source/Zh/doc/new_features/v55_features_doc.rst)。 diff --git a/docs/source/Eng/doc/new_features/v56_features_doc.rst b/docs/source/Eng/doc/new_features/v56_features_doc.rst new file mode 100644 index 00000000..6bf8a402 --- /dev/null +++ b/docs/source/Eng/doc/new_features/v56_features_doc.rst @@ -0,0 +1,46 @@ +SARIF 2.1.0 Findings Export +=========================== + +The framework has several findings producers — action-lint, the secrets scan, +the WCAG/a11y audit, the guardrail — but no common export, so results couldn't +land in GitHub / Azure DevOps **code scanning** (the durable, deduplicated, +line-anchored alert store). SARIF 2.1.0 (OASIS) is that interchange format. + +``to_sarif`` builds a SARIF document from a list of normalized *findings* +(``{rule_id, level, message, file?, line?}``), with adapters for the existing +lint / audit shapes and stable ``partialFingerprints`` so the same issue +deduplicates across runs. Pure standard library (``json`` + ``hashlib``); +imports no ``PySide6``. + +Headless API +------------ + +.. code-block:: python + + from je_auto_control import ( + make_finding, to_sarif, write_sarif, from_lint_issues, + from_audit_findings) + + findings = [ + make_finding("AC_SHELL", "shell command not allow-listed", + level="error", file="flow.json", line=12), + *from_lint_issues(lint_issues, file="flow.json"), + *from_audit_findings(wcag_report["findings"]), + ] + write_sarif(findings, "results.sarif", tool_name="AutoControl") + # upload results.sarif via the GitHub "upload-sarif" action + +``make_finding`` builds one normalized finding; ``from_lint_issues`` (maps +``index/severity/code/message``) and ``from_audit_findings`` (maps +``sc/criterion/kind/severity``) adapt the existing producers; ``to_sarif`` +auto-derives the rule catalog and attaches a stable fingerprint per result; +``write_sarif`` serializes to a file. Severity strings map to SARIF +``error`` / ``warning`` / ``note``. + +Executor command +---------------- + +``AC_export_sarif`` takes ``findings`` (a list, or JSON string) plus optional +``path`` and ``tool_name`` and returns ``{sarif, path?}``. The same operation is +exposed as the MCP tool ``ac_export_sarif`` and as a Script Builder command under +**Report**. diff --git a/docs/source/Eng/eng_index.rst b/docs/source/Eng/eng_index.rst index 6f390ec1..5cb65505 100644 --- a/docs/source/Eng/eng_index.rst +++ b/docs/source/Eng/eng_index.rst @@ -78,6 +78,7 @@ Comprehensive guides for all AutoControl features. doc/new_features/v53_features_doc doc/new_features/v54_features_doc doc/new_features/v55_features_doc + doc/new_features/v56_features_doc doc/ocr_backends/ocr_backends_doc doc/observability/observability_doc doc/operations_layer/operations_layer_doc diff --git a/docs/source/Zh/doc/new_features/v56_features_doc.rst b/docs/source/Zh/doc/new_features/v56_features_doc.rst new file mode 100644 index 00000000..baf6cc14 --- /dev/null +++ b/docs/source/Zh/doc/new_features/v56_features_doc.rst @@ -0,0 +1,41 @@ +SARIF 2.1.0 發現項目匯出 +======================== + +框架有多個發現項目產生器 —— action-lint、密鑰掃描、WCAG/a11y 稽核、guardrail —— 但缺乏 +共通匯出,因此結果無法進入 GitHub / Azure DevOps 的**程式碼掃描**(持久、去重、定位到行 +的警示儲存)。SARIF 2.1.0(OASIS)正是該交換格式。 + +``to_sarif`` 由一份正規化*發現項目*清單(``{rule_id, level, message, file?, line?}``)建 +立 SARIF 文件,並為既有 lint / audit 形狀提供轉接器,加上穩定的 ``partialFingerprints`` +讓同一問題能跨執行去重。純標準函式庫(``json`` + ``hashlib``);不匯入 ``PySide6``。 + +無頭 API +-------- + +.. code-block:: python + + from je_auto_control import ( + make_finding, to_sarif, write_sarif, from_lint_issues, + from_audit_findings) + + findings = [ + make_finding("AC_SHELL", "shell command not allow-listed", + level="error", file="flow.json", line=12), + *from_lint_issues(lint_issues, file="flow.json"), + *from_audit_findings(wcag_report["findings"]), + ] + write_sarif(findings, "results.sarif", tool_name="AutoControl") + # 以 GitHub「upload-sarif」action 上傳 results.sarif + +``make_finding`` 建立單一正規化發現項目;``from_lint_issues``(對映 +``index/severity/code/message``)與 ``from_audit_findings``(對映 +``sc/criterion/kind/severity``)轉接既有產生器;``to_sarif`` 自動衍生規則目錄並為每個結 +果附上穩定指紋;``write_sarif`` 序列化成檔案。嚴重度字串對映為 SARIF 的 ``error`` / +``warning`` / ``note``。 + +執行器指令 +---------- + +``AC_export_sarif`` 接受 ``findings``(清單或 JSON 字串)以及選用的 ``path`` 與 +``tool_name``,回傳 ``{sarif, path?}``。相同操作亦提供為 MCP 工具 ``ac_export_sarif``,以 +及 Script Builder 中 **Report** 分類下的指令。 diff --git a/docs/source/Zh/zh_index.rst b/docs/source/Zh/zh_index.rst index bd44736f..cca026f2 100644 --- a/docs/source/Zh/zh_index.rst +++ b/docs/source/Zh/zh_index.rst @@ -78,6 +78,7 @@ AutoControl 所有功能的完整使用指南。 doc/new_features/v53_features_doc doc/new_features/v54_features_doc doc/new_features/v55_features_doc + doc/new_features/v56_features_doc doc/ocr_backends/ocr_backends_doc doc/observability/observability_doc doc/operations_layer/operations_layer_doc diff --git a/je_auto_control/__init__.py b/je_auto_control/__init__.py index 5771f98c..5c3adaca 100644 --- a/je_auto_control/__init__.py +++ b/je_auto_control/__init__.py @@ -294,6 +294,10 @@ from je_auto_control.utils.pii_text import ( PIIFinding, detect_pii, redact_pii_text, ) +# SARIF 2.1.0 export (unify findings for code-scanning) +from je_auto_control.utils.sarif import ( + from_audit_findings, from_lint_issues, make_finding, to_sarif, write_sarif, +) # Background popup/interrupt watchdog (unattended automation) from je_auto_control.utils.watchdog import ( PopupWatchdog, WatchdogRule, default_popup_watchdog, @@ -761,6 +765,8 @@ def start_autocontrol_gui(*args, **kwargs): "DecisionTable", "Rule", "evaluate_table", "RepairStore", "RepairSuggestion", "repair_from_heal", "PIIFinding", "detect_pii", "redact_pii_text", + "from_audit_findings", "from_lint_issues", "make_finding", "to_sarif", + "write_sarif", # MCP server "AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt", "MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool", diff --git a/je_auto_control/gui/script_builder/command_schema.py b/je_auto_control/gui/script_builder/command_schema.py index daf1a8e6..5fc1bd43 100644 --- a/je_auto_control/gui/script_builder/command_schema.py +++ b/je_auto_control/gui/script_builder/command_schema.py @@ -1224,6 +1224,19 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None: ), description="Redact PII in text (label/mask/partial/hash).", )) + specs.append(CommandSpec( + "AC_export_sarif", "Report", "Export SARIF (Code Scanning)", + fields=( + FieldSpec("findings", FieldType.STRING, + placeholder='[{"rule_id": "AC1", "message": "...", ' + '"level": "error"}]'), + FieldSpec("path", FieldType.STRING, optional=True, + placeholder="results.sarif"), + FieldSpec("tool_name", FieldType.STRING, optional=True, + default="AutoControl"), + ), + description="Unify findings into a SARIF 2.1.0 document.", + )) specs.append(CommandSpec( "AC_generate_sop", "Report", "Generate SOP Document", fields=( diff --git a/je_auto_control/utils/executor/action_executor.py b/je_auto_control/utils/executor/action_executor.py index 483c5f29..65f2e984 100644 --- a/je_auto_control/utils/executor/action_executor.py +++ b/je_auto_control/utils/executor/action_executor.py @@ -3390,6 +3390,20 @@ def _redact_pii(text: str, kinds: Any = None, mode: str = "label", mask_char=mask_char)} +def _export_sarif(findings: Any, path: Optional[str] = None, + tool_name: str = "AutoControl") -> Dict[str, Any]: + """Adapter: build (and optionally write) a SARIF 2.1.0 document.""" + import json + from je_auto_control.utils.sarif import to_sarif, write_sarif + if isinstance(findings, str): + findings = json.loads(findings) + document = to_sarif(findings, tool_name=tool_name) + result: Dict[str, Any] = {"sarif": document} + if path: + result["path"] = write_sarif(findings, path, tool_name=tool_name) + return result + + class Executor: """ Executor @@ -3680,6 +3694,7 @@ def __init__(self): "AC_repair_approve": _repair_approve, "AC_detect_pii": _detect_pii, "AC_redact_pii": _redact_pii, + "AC_export_sarif": _export_sarif, "AC_a11y_record_start": _a11y_record_start, "AC_a11y_record_stop": _a11y_record_stop, "AC_a11y_record_events": _a11y_record_events, diff --git a/je_auto_control/utils/mcp_server/tools/_factories.py b/je_auto_control/utils/mcp_server/tools/_factories.py index 4ccc2085..64bdd3b9 100644 --- a/je_auto_control/utils/mcp_server/tools/_factories.py +++ b/je_auto_control/utils/mcp_server/tools/_factories.py @@ -3338,6 +3338,25 @@ def pii_text_tools() -> List[MCPTool]: ] +def sarif_tools() -> List[MCPTool]: + return [ + MCPTool( + name="ac_export_sarif", + description=("Build a SARIF 2.1.0 document from normalized " + "'findings' ([{rule_id, level, message, file?, " + "line?}]) for GitHub/Azure code-scanning. Optional " + "'path' writes it; 'tool_name' labels the driver. " + "Returns {sarif, path?}."), + input_schema=schema( + {"findings": {"type": "array", "items": {"type": "object"}}, + "path": {"type": "string"}, + "tool_name": {"type": "string"}}, ["findings"]), + handler=h.export_sarif, + annotations=READ_ONLY, + ), + ] + + def unattended_tools() -> List[MCPTool]: return [ MCPTool( @@ -4402,7 +4421,7 @@ def media_assert_tools() -> List[MCPTool]: locale_tools, voice_tools, coordinate_space_tools, loop_guard_tools, process_mining_tools, asset_tools, events_tools, notify_channel_tools, jsonpath_tools, saga_tools, decision_table_tools, locator_repair_tools, - pii_text_tools, + pii_text_tools, sarif_tools, screen_record_tools, process_and_shell_tools, remote_desktop_tools, gamepad_tools, usb_passthrough_tools, assertion_tools, data_source_tools, diff --git a/je_auto_control/utils/mcp_server/tools/_handlers.py b/je_auto_control/utils/mcp_server/tools/_handlers.py index a4db9f6a..6d067b23 100644 --- a/je_auto_control/utils/mcp_server/tools/_handlers.py +++ b/je_auto_control/utils/mcp_server/tools/_handlers.py @@ -1606,6 +1606,14 @@ def redact_pii(text, kinds=None, mode="label", mask_char="*"): mask_char=mask_char)} +def export_sarif(findings, path=None, tool_name="AutoControl"): + from je_auto_control.utils.sarif import to_sarif, write_sarif + result = {"sarif": to_sarif(findings, tool_name=tool_name)} + if path: + result["path"] = write_sarif(findings, path, tool_name=tool_name) + return result + + def vlm_locate(description: str, screen_region: Optional[List[int]] = None, model: Optional[str] = None) -> Optional[List[int]]: diff --git a/je_auto_control/utils/sarif/__init__.py b/je_auto_control/utils/sarif/__init__.py new file mode 100644 index 00000000..1307455f --- /dev/null +++ b/je_auto_control/utils/sarif/__init__.py @@ -0,0 +1,10 @@ +"""Export findings as SARIF 2.1.0 for GitHub/Azure code-scanning.""" +from je_auto_control.utils.sarif.sarif import ( + from_audit_findings, from_lint_issues, make_finding, result_fingerprint, + to_sarif, write_sarif, +) + +__all__ = [ + "from_audit_findings", "from_lint_issues", "make_finding", + "result_fingerprint", "to_sarif", "write_sarif", +] diff --git a/je_auto_control/utils/sarif/sarif.py b/je_auto_control/utils/sarif/sarif.py new file mode 100644 index 00000000..42f8b809 --- /dev/null +++ b/je_auto_control/utils/sarif/sarif.py @@ -0,0 +1,124 @@ +"""Unify AutoControl findings into a SARIF 2.1.0 document. + +The framework has several findings producers — action-lint, the secrets scan, +the WCAG/a11y audit, the guardrail — but no common export, so results couldn't +land in GitHub/Azure DevOps "code scanning" (the durable, deduplicated, +line-anchored alert store). SARIF 2.1.0 (OASIS) is that interchange format. + +This builds a SARIF document from a list of normalized *findings* +(``{rule_id, level, message, file?, line?}``), with adapters for the existing +lint / audit shapes and stable ``partialFingerprints`` so the same issue +deduplicates across runs. Pure standard library (``json`` + ``hashlib``); +imports no ``PySide6``. +""" +import hashlib +from typing import Any, Dict, List, Mapping, Optional, Sequence + +SARIF_VERSION = "2.1.0" +_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json" +_LEVELS = { + "error": "error", "critical": "error", "serious": "error", + "high": "error", "warning": "warning", "moderate": "warning", + "medium": "warning", "info": "note", "note": "note", "minor": "note", + "low": "note", +} + + +def _level(severity: Any) -> str: + return _LEVELS.get(str(severity).lower(), "warning") + + +def _get(item: Any, key: str, default: Any = None) -> Any: + if isinstance(item, Mapping): + return item.get(key, default) + return getattr(item, key, default) + + +def make_finding(rule_id: str, message: str, *, level: str = "warning", + file: Optional[str] = None, + line: Optional[int] = None) -> Dict[str, Any]: + """Build one normalized finding for :func:`to_sarif`.""" + finding: Dict[str, Any] = {"rule_id": str(rule_id), "level": level, + "message": str(message)} + if file is not None: + finding["file"] = file + if line is not None: + finding["line"] = int(line) + return finding + + +def result_fingerprint(finding: Mapping[str, Any]) -> str: + """Stable short hash of a finding (for SARIF partialFingerprints/dedupe).""" + basis = "|".join(str(finding.get(k, "")) for k in + ("rule_id", "message", "file", "line")) + return hashlib.sha256(basis.encode("utf-8")).hexdigest()[:16] + + +def _result(finding: Mapping[str, Any]) -> Dict[str, Any]: + result: Dict[str, Any] = { + "ruleId": finding.get("rule_id", "AC0000"), + "level": finding.get("level", "warning"), + "message": {"text": finding.get("message", "")}, + "partialFingerprints": {"primaryLocationLineHash": + result_fingerprint(finding)}, + } + if finding.get("file"): + region = {"startLine": int(finding["line"])} if finding.get("line") \ + else {} + result["locations"] = [{"physicalLocation": { + "artifactLocation": {"uri": finding["file"]}, + **({"region": region} if region else {})}}] + return result + + +def to_sarif(findings: Sequence[Mapping[str, Any]], *, + tool_name: str = "AutoControl", + rules: Optional[Sequence[Mapping[str, Any]]] = None + ) -> Dict[str, Any]: + """Build a SARIF 2.1.0 document from normalized findings.""" + if rules is None: + rule_ids = sorted({str(f.get("rule_id", "AC0000")) for f in findings}) + rules = [{"id": rule_id} for rule_id in rule_ids] + return { + "version": SARIF_VERSION, "$schema": _SCHEMA, + "runs": [{ + "tool": {"driver": {"name": tool_name, "rules": list(rules)}}, + "results": [_result(f) for f in findings], + }], + } + + +def write_sarif(findings: Sequence[Mapping[str, Any]], path: str, + **kwargs: Any) -> str: + """Write a SARIF document for ``findings`` to ``path``; return the path.""" + import json + from pathlib import Path + output = Path(path) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text( + json.dumps(to_sarif(findings, **kwargs), ensure_ascii=False, indent=2), + encoding="utf-8") + return str(output) + + +def from_lint_issues(issues: Sequence[Any], *, + file: Optional[str] = None) -> List[Dict[str, Any]]: + """Normalize action-lint issues (``index/severity/code/message``).""" + return [ + make_finding(str(_get(i, "code") or "lint"), _get(i, "message", ""), + level=_level(_get(i, "severity")), file=file, + line=(_get(i, "index") if (_get(i, "index") or 0) >= 0 + else None)) + for i in issues + ] + + +def from_audit_findings(findings: Sequence[Mapping[str, Any]] + ) -> List[Dict[str, Any]]: + """Normalize WCAG audit findings (``sc/criterion/kind/severity``).""" + return [ + make_finding(str(f.get("sc") or f.get("criterion") or "wcag"), + f"{f.get('criterion', '')}: {f.get('kind', '')}".strip(), + level=_level(f.get("severity"))) + for f in findings + ] diff --git a/test/unit_test/headless/test_sarif_batch.py b/test/unit_test/headless/test_sarif_batch.py new file mode 100644 index 00000000..04e9d9cc --- /dev/null +++ b/test/unit_test/headless/test_sarif_batch.py @@ -0,0 +1,102 @@ +"""Headless tests for SARIF 2.1.0 export. Pure stdlib, no Qt imports.""" +import json + +import je_auto_control as ac +from je_auto_control.utils.sarif import ( + from_audit_findings, from_lint_issues, make_finding, result_fingerprint, + to_sarif, write_sarif) + + +def test_document_shape(): + doc = to_sarif([make_finding("AC101", "boom", level="error", + file="flow.json", line=3)], + tool_name="AutoControl") + assert doc["version"] == "2.1.0" and "$schema" in doc + run = doc["runs"][0] + assert run["tool"]["driver"]["name"] == "AutoControl" + assert [r["id"] for r in run["tool"]["driver"]["rules"]] == ["AC101"] + result = run["results"][0] + assert result["ruleId"] == "AC101" and result["level"] == "error" + assert result["message"]["text"] == "boom" + loc = result["locations"][0]["physicalLocation"] + assert loc["artifactLocation"]["uri"] == "flow.json" + assert loc["region"]["startLine"] == 3 + + +def test_finding_without_location(): + doc = to_sarif([make_finding("AC1", "msg")]) + assert "locations" not in doc["runs"][0]["results"][0] + + +def test_fingerprint_stable_and_distinct(): + a = make_finding("R", "same", file="f", line=1) + assert result_fingerprint(a) == result_fingerprint(dict(a)) + assert result_fingerprint(a) != result_fingerprint(make_finding("R", "x")) + + +def test_explicit_rules_passthrough(): + doc = to_sarif([make_finding("R1", "m")], + rules=[{"id": "R1", "name": "Custom"}]) + assert doc["runs"][0]["tool"]["driver"]["rules"][0]["name"] == "Custom" + + +def test_from_lint_issues(): + findings = from_lint_issues( + [{"index": 4, "severity": "error", "code": "E1", "message": "bad"}], + file="flow.json") + assert findings[0] == {"rule_id": "E1", "level": "error", "message": "bad", + "file": "flow.json", "line": 4} + + +def test_from_lint_negative_index_has_no_line(): + findings = from_lint_issues( + [{"index": -1, "severity": "warning", "code": "E2", "message": "x"}]) + assert "line" not in findings[0] and findings[0]["level"] == "warning" + + +def test_from_audit_findings(): + findings = from_audit_findings( + [{"sc": "1.4.3", "criterion": "Contrast", "kind": "low-contrast", + "severity": "serious"}]) + assert findings[0]["rule_id"] == "1.4.3" + assert findings[0]["level"] == "error" + assert "Contrast" in findings[0]["message"] + + +def test_write_sarif(tmp_path): + from pathlib import Path + path = write_sarif([make_finding("R", "m")], str(tmp_path / "out.sarif")) + doc = json.loads(Path(path).read_text(encoding="utf-8")) + assert doc["version"] == "2.1.0" + + +# --- wiring --------------------------------------------------------------- + +def test_executor_round_trip(tmp_path): + out = str(tmp_path / "r.sarif") + rec = ac.execute_action([[ + "AC_export_sarif", + {"findings": [{"rule_id": "AC1", "message": "m", "level": "warning"}], + "path": out}, + ]]) + res = next(v for v in rec.values() if isinstance(v, dict) and "sarif" in v) + assert res["sarif"]["version"] == "2.1.0" + assert res["path"] == out + + +def test_wiring(): + assert "AC_export_sarif" in ac.executor.known_commands() + from je_auto_control.utils.mcp_server.tools import ( + build_default_tool_registry) + names = {t.name for t in build_default_tool_registry()} + assert "ac_export_sarif" in names + from je_auto_control.gui.script_builder.command_schema import _build_specs + cmds = {s.command for s in _build_specs()} + assert "AC_export_sarif" in cmds + + +def test_facade_exports(): + for attr in ("to_sarif", "write_sarif", "make_finding", "from_lint_issues", + "from_audit_findings"): + assert hasattr(ac, attr) + assert attr in ac.__all__