Skip to content

Commit 4b2df61

Browse files
committed
Add SPDX license policy gate
The SBOM recorded each component's license name but never judged it, so a disallowed or copyleft license could ship unnoticed. Add a policy gate that normalizes license strings to SPDX ids and evaluates them against allow/deny lists (with a built-in strong-copyleft set), understanding SPDX OR/AND expressions, then bridges violations into the SARIF exporter. Pure stdlib and fully offline; wired through the facade, AC_check_licenses executor command, ac_check_licenses MCP tool and Script Builder.
1 parent 5947144 commit 4b2df61

15 files changed

Lines changed: 414 additions & 1 deletion

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
## Table of Contents
1515

16+
- [What's new (2026-06-21) — License Policy Gate](#whats-new-2026-06-21--license-policy-gate)
1617
- [What's new (2026-06-21) — OpenVEX Vulnerability Triage](#whats-new-2026-06-21--openvex-vulnerability-triage)
1718
- [What's new (2026-06-21) — Dependency Vulnerability Scanning (OSV)](#whats-new-2026-06-21--dependency-vulnerability-scanning-osv)
1819
- [What's new (2026-06-21) — JSON Schema Validation](#whats-new-2026-06-21--json-schema-validation)
@@ -112,6 +113,12 @@
112113

113114
---
114115

116+
## What's new (2026-06-21) — License Policy Gate
117+
118+
Flag disallowed dependency licenses. Full reference: [`docs/source/Eng/doc/new_features/v60_features_doc.rst`](docs/source/Eng/doc/new_features/v60_features_doc.rst).
119+
120+
- **`evaluate_sbom` / `evaluate_license` / `normalize_spdx` / `license_findings_to_sarif`** (`AC_check_licenses`, `ac_check_licenses`): the SBOM recorded each dependency's license name but never *judged* it. This normalizes license strings to SPDX ids and evaluates them against an allowlist/denylist (with a built-in `DEFAULT_COPYLEFT` set), understanding SPDX expressions (`OR` = choice, `AND` = all), then bridges violations into SARIF (`denied`→error, `unknown`→warning). Pure-stdlib, fully offline — the license-compliance lane beside the OSV vulnerability lane.
121+
115122
## What's new (2026-06-21) — OpenVEX Vulnerability Triage
116123

117124
Suppress the vulns that don't affect you. Full reference: [`docs/source/Eng/doc/new_features/v59_features_doc.rst`](docs/source/Eng/doc/new_features/v59_features_doc.rst).

README/README_zh-CN.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目录
1414

15+
- [本次更新 (2026-06-21) — 许可证策略闸门](#本次更新-2026-06-21--许可证策略闸门)
1516
- [本次更新 (2026-06-21) — OpenVEX 漏洞分级](#本次更新-2026-06-21--openvex-漏洞分级)
1617
- [本次更新 (2026-06-21) — 依赖项漏洞扫描(OSV)](#本次更新-2026-06-21--依赖项漏洞扫描osv)
1718
- [本次更新 (2026-06-21) — JSON Schema 验证](#本次更新-2026-06-21--json-schema-验证)
@@ -111,6 +112,12 @@
111112

112113
---
113114

115+
## 本次更新 (2026-06-21) — 许可证策略闸门
116+
117+
标记不被允许的依赖项许可证。完整参考:[`docs/source/Zh/doc/new_features/v60_features_doc.rst`](../docs/source/Zh/doc/new_features/v60_features_doc.rst)
118+
119+
- **`evaluate_sbom` / `evaluate_license` / `normalize_spdx` / `license_findings_to_sarif`**(`AC_check_licenses``ac_check_licenses`):SBOM 记录了每个依赖项的许可证名称却从未*判断*它。本功能把许可证字符串规范化为 SPDX id,以允许列表/拒绝列表(内建 `DEFAULT_COPYLEFT` 集合)评估,理解 SPDX 表达式(`OR` = 择一、`AND` = 全部),再把违规桥接到 SARIF(`denied`→error、`unknown`→warning)。纯标准库、完全离线 —— 与 OSV 漏洞通道并列的许可证合规通道。
120+
114121
## 本次更新 (2026-06-21) — OpenVEX 漏洞分级
115122

116123
抑制不影响你的漏洞。完整参考:[`docs/source/Zh/doc/new_features/v59_features_doc.rst`](../docs/source/Zh/doc/new_features/v59_features_doc.rst)

README/README_zh-TW.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目錄
1414

15+
- [本次更新 (2026-06-21) — 授權政策閘門](#本次更新-2026-06-21--授權政策閘門)
1516
- [本次更新 (2026-06-21) — OpenVEX 漏洞分級](#本次更新-2026-06-21--openvex-漏洞分級)
1617
- [本次更新 (2026-06-21) — 相依套件漏洞掃描(OSV)](#本次更新-2026-06-21--相依套件漏洞掃描osv)
1718
- [本次更新 (2026-06-21) — JSON Schema 驗證](#本次更新-2026-06-21--json-schema-驗證)
@@ -111,6 +112,12 @@
111112

112113
---
113114

115+
## 本次更新 (2026-06-21) — 授權政策閘門
116+
117+
標記不被允許的相依套件授權。完整參考:[`docs/source/Zh/doc/new_features/v60_features_doc.rst`](../docs/source/Zh/doc/new_features/v60_features_doc.rst)
118+
119+
- **`evaluate_sbom` / `evaluate_license` / `normalize_spdx` / `license_findings_to_sarif`**(`AC_check_licenses``ac_check_licenses`):SBOM 記錄了每個相依套件的授權名稱卻從未*判斷*它。本功能把授權字串正規化為 SPDX id,以允許清單/拒絕清單(內建 `DEFAULT_COPYLEFT` 集合)評估,理解 SPDX 運算式(`OR` = 擇一、`AND` = 全部),再把違規橋接到 SARIF(`denied`→error、`unknown`→warning)。純標準函式庫、完全離線 —— 與 OSV 漏洞通道並列的授權合規通道。
120+
114121
## 本次更新 (2026-06-21) — OpenVEX 漏洞分級
115122

116123
抑制不影響你的漏洞。完整參考:[`docs/source/Zh/doc/new_features/v59_features_doc.rst`](../docs/source/Zh/doc/new_features/v59_features_doc.rst)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
License Policy Gate
2+
===================
3+
4+
``build_sbom`` records each component's license *name*, but nothing ever judged
5+
it — a copyleft or otherwise-disallowed license could ship unnoticed. This adds
6+
the policy gate: normalize the SBOM's license strings to SPDX ids, evaluate them
7+
against an allowlist / denylist (with a built-in strong-copyleft set), and emit
8+
violations that bridge into the existing SARIF exporter — the license-compliance
9+
lane beside the OSV vulnerability lane.
10+
11+
Pure standard library (``re``); fully offline; imports no ``PySide6``.
12+
13+
Headless API
14+
------------
15+
16+
.. code-block:: python
17+
18+
from je_auto_control import (
19+
build_sbom, evaluate_sbom, evaluate_license,
20+
license_findings_to_sarif, write_sarif, DEFAULT_COPYLEFT)
21+
22+
sbom = build_sbom("je_auto_control")
23+
24+
# Allowlist mode: anything outside the list is a violation.
25+
violations = evaluate_sbom(sbom["components"],
26+
allow=["MIT", "Apache-2.0", "BSD-3-Clause"])
27+
28+
# Or denylist mode using the built-in strong-copyleft set.
29+
violations = evaluate_sbom(sbom["components"], deny=DEFAULT_COPYLEFT)
30+
31+
write_sarif(license_findings_to_sarif(violations), "licenses.sarif",
32+
tool_name="AutoControl-License")
33+
34+
``normalize_spdx`` maps loose names (``"MIT License"`` → ``MIT``, ``"Apache
35+
2.0"`` → ``Apache-2.0``) to SPDX ids. ``evaluate_license`` returns ``allowed`` /
36+
``denied`` / ``unknown``: ``deny`` takes precedence; an empty ``allow`` means
37+
"not constrained"; a missing license is ``unknown``. SPDX **expressions** are
38+
understood — ``"MIT OR GPL-3.0-only"`` is a *choice* (allowed if any operand is
39+
allowed), while ``"MIT AND GPL-3.0-only"`` requires every operand. Each
40+
violation is ``{name, version, license, status}``; ``denied`` maps to a SARIF
41+
``error`` and ``unknown`` to a ``warning``.
42+
43+
Executor command
44+
----------------
45+
46+
``AC_check_licenses`` takes ``components`` (a component list, a full SBOM dict,
47+
or a JSON string) and optional ``allow`` / ``deny`` lists; it returns
48+
``{violations, count}``. The same operation is exposed as the MCP tool
49+
``ac_check_licenses`` and as a Script Builder command under **Security**.

docs/source/Eng/eng_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ Comprehensive guides for all AutoControl features.
8282
doc/new_features/v57_features_doc
8383
doc/new_features/v58_features_doc
8484
doc/new_features/v59_features_doc
85+
doc/new_features/v60_features_doc
8586
doc/ocr_backends/ocr_backends_doc
8687
doc/observability/observability_doc
8788
doc/operations_layer/operations_layer_doc
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
授權政策閘門
2+
===========
3+
4+
``build_sbom`` 會記錄每個元件的授權*名稱*,但從未對它做判斷 —— copyleft 或其他不被允許的授權
5+
可能在無人察覺下隨產品出貨。本功能補上政策閘門:把 SBOM 的授權字串正規化為 SPDX id,以
6+
允許清單 / 拒絕清單(內建強 copyleft 集合)評估,並產生可橋接到既有 SARIF 匯出器的違規項目
7+
—— 與 OSV 漏洞通道並列的授權合規通道。
8+
9+
純標準函式庫(``re``);完全離線;不匯入 ``PySide6``。
10+
11+
無頭 API
12+
--------
13+
14+
.. code-block:: python
15+
16+
from je_auto_control import (
17+
build_sbom, evaluate_sbom, evaluate_license,
18+
license_findings_to_sarif, write_sarif, DEFAULT_COPYLEFT)
19+
20+
sbom = build_sbom("je_auto_control")
21+
22+
# 允許清單模式:清單之外的一律視為違規。
23+
violations = evaluate_sbom(sbom["components"],
24+
allow=["MIT", "Apache-2.0", "BSD-3-Clause"])
25+
26+
# 或以內建強 copyleft 集合做拒絕清單模式。
27+
violations = evaluate_sbom(sbom["components"], deny=DEFAULT_COPYLEFT)
28+
29+
write_sarif(license_findings_to_sarif(violations), "licenses.sarif",
30+
tool_name="AutoControl-License")
31+
32+
``normalize_spdx`` 把寬鬆的名稱(``"MIT License"`` → ``MIT``、``"Apache 2.0"`` →
33+
``Apache-2.0``)對應到 SPDX id。``evaluate_license`` 回傳 ``allowed`` / ``denied`` /
34+
``unknown``:``deny`` 優先;空的 ``allow`` 代表「不受限制」;缺少授權則為 ``unknown``。亦理解
35+
SPDX **運算式** —— ``"MIT OR GPL-3.0-only"`` 是*擇一*(任一運算元被允許即允許),而
36+
``"MIT AND GPL-3.0-only"`` 需要每個運算元都被允許。每個違規為
37+
``{name, version, license, status}``;``denied`` 對應 SARIF ``error``,``unknown`` 對應
38+
``warning``。
39+
40+
執行器命令
41+
----------
42+
43+
``AC_check_licenses`` 接受 ``components``(元件清單、完整 SBOM dict 或 JSON 字串)與選用的
44+
``allow`` / ``deny`` 清單;回傳 ``{violations, count}``。同一操作亦以 MCP 工具
45+
``ac_check_licenses`` 以及 Script Builder 中 **Security** 分類下的命令提供。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ AutoControl 所有功能的完整使用指南。
8282
doc/new_features/v57_features_doc
8383
doc/new_features/v58_features_doc
8484
doc/new_features/v59_features_doc
85+
doc/new_features/v60_features_doc
8586
doc/ocr_backends/ocr_backends_doc
8687
doc/observability/observability_doc
8788
doc/operations_layer/operations_layer_doc

je_auto_control/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,11 @@
310310
from je_auto_control.utils.vex import (
311311
VEX_JUSTIFICATIONS, VEX_STATUSES, apply_vex, build_vex, vex_statement,
312312
)
313+
# SPDX license allow/deny policy over SBOM components
314+
from je_auto_control.utils.license_policy import (
315+
DEFAULT_COPYLEFT, evaluate_license, evaluate_sbom,
316+
license_findings_to_sarif, normalize_spdx,
317+
)
313318
# Background popup/interrupt watchdog (unattended automation)
314319
from je_auto_control.utils.watchdog import (
315320
PopupWatchdog, WatchdogRule, default_popup_watchdog,
@@ -784,6 +789,8 @@ def start_autocontrol_gui(*args, **kwargs):
784789
"version_key",
785790
"VEX_JUSTIFICATIONS", "VEX_STATUSES", "apply_vex", "build_vex",
786791
"vex_statement",
792+
"DEFAULT_COPYLEFT", "evaluate_license", "evaluate_sbom",
793+
"license_findings_to_sarif", "normalize_spdx",
787794
# MCP server
788795
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
789796
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,19 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
11791179
),
11801180
description="Suppress not_affected/fixed vulns via an OpenVEX document.",
11811181
))
1182+
specs.append(CommandSpec(
1183+
"AC_check_licenses", "Security", "Check Dependency Licenses",
1184+
fields=(
1185+
FieldSpec("components", FieldType.STRING,
1186+
placeholder='{"components": [{"name": "x", '
1187+
'"licenses": [{"license": {"name": "MIT"}}]}]}'),
1188+
FieldSpec("allow", FieldType.STRING, optional=True,
1189+
placeholder='["MIT", "Apache-2.0", "BSD-3-Clause"]'),
1190+
FieldSpec("deny", FieldType.STRING, optional=True,
1191+
placeholder='["GPL-3.0-only", "AGPL-3.0-only"]'),
1192+
),
1193+
description="Evaluate SBOM licenses against allow/deny SPDX lists.",
1194+
))
11821195
specs.append(CommandSpec(
11831196
"AC_run_saga", "Flow", "Run Saga (Compensating Rollback)",
11841197
fields=(

je_auto_control/utils/executor/action_executor.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2897,6 +2897,23 @@ def _apply_vex(findings: Any, vex: Any) -> Dict[str, Any]:
28972897
return {"findings": kept, "count": len(kept)}
28982898

28992899

2900+
def _check_licenses(components: Any, allow: Any = None,
2901+
deny: Any = None) -> Dict[str, Any]:
2902+
"""Adapter: evaluate SBOM component licenses against allow/deny lists."""
2903+
import json
2904+
from je_auto_control.utils.license_policy import evaluate_sbom
2905+
if isinstance(components, str):
2906+
components = json.loads(components)
2907+
if isinstance(components, dict):
2908+
components = components.get("components", [])
2909+
if isinstance(allow, str):
2910+
allow = json.loads(allow)
2911+
if isinstance(deny, str):
2912+
deny = json.loads(deny)
2913+
violations = evaluate_sbom(components, allow=allow, deny=deny)
2914+
return {"violations": violations, "count": len(violations)}
2915+
2916+
29002917
def _generate_sop(actions: List[Any], title: str = "Automation Procedure",
29012918
path: Optional[str] = None) -> Dict[str, Any]:
29022919
"""Adapter: build (or write) a step-by-step SOP from an action list."""
@@ -3678,6 +3695,7 @@ def __init__(self):
36783695
"AC_scan_secrets": _scan_secrets,
36793696
"AC_scan_vulns": _scan_vulns,
36803697
"AC_apply_vex": _apply_vex,
3698+
"AC_check_licenses": _check_licenses,
36813699
"AC_generate_sop": _generate_sop,
36823700
"AC_tween_drag": _tween_drag,
36833701
"AC_list_plugins": _list_plugins,

0 commit comments

Comments
 (0)