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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

## Table of Contents

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

---

## What's new (2026-06-21) — License Policy Gate

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).

- **`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.

## What's new (2026-06-21) — OpenVEX Vulnerability Triage

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).
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目录

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

---

## 本次更新 (2026-06-21) — 许可证策略闸门

标记不被允许的依赖项许可证。完整参考:[`docs/source/Zh/doc/new_features/v60_features_doc.rst`](../docs/source/Zh/doc/new_features/v60_features_doc.rst)。

- **`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 漏洞通道并列的许可证合规通道。

## 本次更新 (2026-06-21) — OpenVEX 漏洞分级

抑制不影响你的漏洞。完整参考:[`docs/source/Zh/doc/new_features/v59_features_doc.rst`](../docs/source/Zh/doc/new_features/v59_features_doc.rst)。
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目錄

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

---

## 本次更新 (2026-06-21) — 授權政策閘門

標記不被允許的相依套件授權。完整參考:[`docs/source/Zh/doc/new_features/v60_features_doc.rst`](../docs/source/Zh/doc/new_features/v60_features_doc.rst)。

- **`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 漏洞通道並列的授權合規通道。

## 本次更新 (2026-06-21) — OpenVEX 漏洞分級

抑制不影響你的漏洞。完整參考:[`docs/source/Zh/doc/new_features/v59_features_doc.rst`](../docs/source/Zh/doc/new_features/v59_features_doc.rst)。
Expand Down
49 changes: 49 additions & 0 deletions docs/source/Eng/doc/new_features/v60_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
License Policy Gate
===================

``build_sbom`` records each component's license *name*, but nothing ever judged
it — a copyleft or otherwise-disallowed license could ship unnoticed. This adds
the policy gate: normalize the SBOM's license strings to SPDX ids, evaluate them
against an allowlist / denylist (with a built-in strong-copyleft set), and emit
violations that bridge into the existing SARIF exporter — the license-compliance
lane beside the OSV vulnerability lane.

Pure standard library (``re``); fully offline; imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import (
build_sbom, evaluate_sbom, evaluate_license,
license_findings_to_sarif, write_sarif, DEFAULT_COPYLEFT)

sbom = build_sbom("je_auto_control")

# Allowlist mode: anything outside the list is a violation.
violations = evaluate_sbom(sbom["components"],
allow=["MIT", "Apache-2.0", "BSD-3-Clause"])

# Or denylist mode using the built-in strong-copyleft set.
violations = evaluate_sbom(sbom["components"], deny=DEFAULT_COPYLEFT)

write_sarif(license_findings_to_sarif(violations), "licenses.sarif",
tool_name="AutoControl-License")

``normalize_spdx`` maps loose names (``"MIT License"`` → ``MIT``, ``"Apache
2.0"`` → ``Apache-2.0``) to SPDX ids. ``evaluate_license`` returns ``allowed`` /
``denied`` / ``unknown``: ``deny`` takes precedence; an empty ``allow`` means
"not constrained"; a missing license is ``unknown``. SPDX **expressions** are
understood — ``"MIT OR GPL-3.0-only"`` is a *choice* (allowed if any operand is
allowed), while ``"MIT AND GPL-3.0-only"`` requires every operand. Each
violation is ``{name, version, license, status}``; ``denied`` maps to a SARIF
``error`` and ``unknown`` to a ``warning``.

Executor command
----------------

``AC_check_licenses`` takes ``components`` (a component list, a full SBOM dict,
or a JSON string) and optional ``allow`` / ``deny`` lists; it returns
``{violations, count}``. The same operation is exposed as the MCP tool
``ac_check_licenses`` and as a Script Builder command under **Security**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v57_features_doc
doc/new_features/v58_features_doc
doc/new_features/v59_features_doc
doc/new_features/v60_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
45 changes: 45 additions & 0 deletions docs/source/Zh/doc/new_features/v60_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
授權政策閘門
===========

``build_sbom`` 會記錄每個元件的授權*名稱*,但從未對它做判斷 —— copyleft 或其他不被允許的授權
可能在無人察覺下隨產品出貨。本功能補上政策閘門:把 SBOM 的授權字串正規化為 SPDX id,以
允許清單 / 拒絕清單(內建強 copyleft 集合)評估,並產生可橋接到既有 SARIF 匯出器的違規項目
—— 與 OSV 漏洞通道並列的授權合規通道。

純標準函式庫(``re``);完全離線;不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import (
build_sbom, evaluate_sbom, evaluate_license,
license_findings_to_sarif, write_sarif, DEFAULT_COPYLEFT)

sbom = build_sbom("je_auto_control")

# 允許清單模式:清單之外的一律視為違規。
violations = evaluate_sbom(sbom["components"],
allow=["MIT", "Apache-2.0", "BSD-3-Clause"])

# 或以內建強 copyleft 集合做拒絕清單模式。
violations = evaluate_sbom(sbom["components"], deny=DEFAULT_COPYLEFT)

write_sarif(license_findings_to_sarif(violations), "licenses.sarif",
tool_name="AutoControl-License")

``normalize_spdx`` 把寬鬆的名稱(``"MIT License"`` → ``MIT``、``"Apache 2.0"`` →
``Apache-2.0``)對應到 SPDX id。``evaluate_license`` 回傳 ``allowed`` / ``denied`` /
``unknown``:``deny`` 優先;空的 ``allow`` 代表「不受限制」;缺少授權則為 ``unknown``。亦理解
SPDX **運算式** —— ``"MIT OR GPL-3.0-only"`` 是*擇一*(任一運算元被允許即允許),而
``"MIT AND GPL-3.0-only"`` 需要每個運算元都被允許。每個違規為
``{name, version, license, status}``;``denied`` 對應 SARIF ``error``,``unknown`` 對應
``warning``。

執行器命令
----------

``AC_check_licenses`` 接受 ``components``(元件清單、完整 SBOM dict 或 JSON 字串)與選用的
``allow`` / ``deny`` 清單;回傳 ``{violations, count}``。同一操作亦以 MCP 工具
``ac_check_licenses`` 以及 Script Builder 中 **Security** 分類下的命令提供。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v57_features_doc
doc/new_features/v58_features_doc
doc/new_features/v59_features_doc
doc/new_features/v60_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
7 changes: 7 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@
from je_auto_control.utils.vex import (
VEX_JUSTIFICATIONS, VEX_STATUSES, apply_vex, build_vex, vex_statement,
)
# SPDX license allow/deny policy over SBOM components
from je_auto_control.utils.license_policy import (
DEFAULT_COPYLEFT, evaluate_license, evaluate_sbom,
license_findings_to_sarif, normalize_spdx,
)
# Background popup/interrupt watchdog (unattended automation)
from je_auto_control.utils.watchdog import (
PopupWatchdog, WatchdogRule, default_popup_watchdog,
Expand Down Expand Up @@ -784,6 +789,8 @@ def start_autocontrol_gui(*args, **kwargs):
"version_key",
"VEX_JUSTIFICATIONS", "VEX_STATUSES", "apply_vex", "build_vex",
"vex_statement",
"DEFAULT_COPYLEFT", "evaluate_license", "evaluate_sbom",
"license_findings_to_sarif", "normalize_spdx",
# MCP server
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",
Expand Down
13 changes: 13 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,19 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
),
description="Suppress not_affected/fixed vulns via an OpenVEX document.",
))
specs.append(CommandSpec(
"AC_check_licenses", "Security", "Check Dependency Licenses",
fields=(
FieldSpec("components", FieldType.STRING,
placeholder='{"components": [{"name": "x", '
'"licenses": [{"license": {"name": "MIT"}}]}]}'),
FieldSpec("allow", FieldType.STRING, optional=True,
placeholder='["MIT", "Apache-2.0", "BSD-3-Clause"]'),
FieldSpec("deny", FieldType.STRING, optional=True,
placeholder='["GPL-3.0-only", "AGPL-3.0-only"]'),
),
description="Evaluate SBOM licenses against allow/deny SPDX lists.",
))
specs.append(CommandSpec(
"AC_run_saga", "Flow", "Run Saga (Compensating Rollback)",
fields=(
Expand Down
18 changes: 18 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2897,6 +2897,23 @@ def _apply_vex(findings: Any, vex: Any) -> Dict[str, Any]:
return {"findings": kept, "count": len(kept)}


def _check_licenses(components: Any, allow: Any = None,
deny: Any = None) -> Dict[str, Any]:
"""Adapter: evaluate SBOM component licenses against allow/deny lists."""
import json
from je_auto_control.utils.license_policy import evaluate_sbom
if isinstance(components, str):
components = json.loads(components)
if isinstance(components, dict):
components = components.get("components", [])
if isinstance(allow, str):
allow = json.loads(allow)
if isinstance(deny, str):
deny = json.loads(deny)
violations = evaluate_sbom(components, allow=allow, deny=deny)
return {"violations": violations, "count": len(violations)}


def _generate_sop(actions: List[Any], title: str = "Automation Procedure",
path: Optional[str] = None) -> Dict[str, Any]:
"""Adapter: build (or write) a step-by-step SOP from an action list."""
Expand Down Expand Up @@ -3678,6 +3695,7 @@ def __init__(self):
"AC_scan_secrets": _scan_secrets,
"AC_scan_vulns": _scan_vulns,
"AC_apply_vex": _apply_vex,
"AC_check_licenses": _check_licenses,
"AC_generate_sop": _generate_sop,
"AC_tween_drag": _tween_drag,
"AC_list_plugins": _list_plugins,
Expand Down
10 changes: 10 additions & 0 deletions je_auto_control/utils/license_policy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""SPDX license allow/deny policy evaluation over SBOM components."""
from je_auto_control.utils.license_policy.license_policy import (
DEFAULT_COPYLEFT, evaluate_license, evaluate_sbom,
license_findings_to_sarif, normalize_spdx,
)

__all__ = [
"DEFAULT_COPYLEFT", "evaluate_license", "evaluate_sbom",
"license_findings_to_sarif", "normalize_spdx",
]
129 changes: 129 additions & 0 deletions je_auto_control/utils/license_policy/license_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""Evaluate dependency licenses against an allow / deny policy.

``utils/sbom`` records each component's license *name* but never judges it, so
a copyleft or otherwise-disallowed license could ship unnoticed. This adds the
policy gate: normalize the SBOM's license strings to SPDX ids, evaluate them
against an allowlist / denylist (with a built-in strong-copyleft set), and emit
violations that bridge into the existing SARIF exporter — the license-compliance
lane beside the OSV vulnerability lane.

Pure standard library (``re``); imports no ``PySide6``.
"""
import re
from typing import Any, Dict, List, Mapping, Optional, Sequence, Set

from je_auto_control.utils.sarif import make_finding

# Strong/network copyleft SPDX ids most policies want to flag.
DEFAULT_COPYLEFT = frozenset({
"GPL-2.0-only", "GPL-2.0-or-later", "GPL-3.0-only", "GPL-3.0-or-later",
"AGPL-3.0-only", "AGPL-3.0-or-later", "LGPL-2.1-only", "LGPL-3.0-only",
"LGPL-3.0-or-later", "MPL-2.0", "EPL-2.0", "CDDL-1.0",
})

# Canonical SPDX id -> the loose names that should normalize to it. Inverted
# below so each SPDX id literal appears exactly once (no duplicated literals).
_ALIAS_GROUPS = {
"MIT": ("mit license", "the mit license", "mit"),
"Apache-2.0": ("apache 2.0", "apache-2", "apache 2", "apache2",
"apache software license", "apache license 2.0"),
"BSD-3-Clause": ("bsd", "bsd license", "new bsd license"),
"GPL-2.0-only": ("gplv2",),
"GPL-3.0-only": ("gplv3", "gnu gplv3"),
"LGPL-3.0-only": ("lgplv3",),
"MPL-2.0": ("mpl 2.0",),
"ISC": ("isc license",),
}
_ALIASES = {alias: spdx for spdx, names in _ALIAS_GROUPS.items()
for alias in names}

_OPERATOR_RE = re.compile(r"\b(?:OR|AND|WITH)\b|[()]", re.IGNORECASE)


def normalize_spdx(raw: str) -> str:
"""Normalize a single license token to a canonical SPDX id (best effort)."""
text = str(raw).strip()
if not text:
return ""
alias = _ALIASES.get(text.lower())
if alias:
return alias
lowered = text.lower()
for suffix in (" license", " licence"):
if lowered.endswith(suffix):
return text[:-len(suffix)].strip()
return text


def _extract_ids(license_str: str) -> List[str]:
parts = _OPERATOR_RE.split(str(license_str))
return [spdx for spdx in (normalize_spdx(part) for part in parts) if spdx]


def _norm_set(values: Optional[Sequence[str]]) -> Set[str]:
return {normalize_spdx(value) for value in values} if values else set()


def _allow_status(ids: Sequence[str], allow: Sequence[str],
license_str: str) -> str:
allow_set = _norm_set(allow)
matcher = any if " or " in f" {str(license_str).lower()} " else all
return "allowed" if matcher(spdx in allow_set for spdx in ids) else "denied"


def evaluate_license(license_str: str, *,
allow: Optional[Sequence[str]] = None,
deny: Optional[Sequence[str]] = None) -> str:
"""Return ``allowed`` / ``denied`` / ``unknown`` for a license string."""
ids = _extract_ids(license_str)
if not ids:
return "unknown"
deny_set = _norm_set(deny)
if deny_set and any(spdx in deny_set for spdx in ids):
return "denied"
if allow is None:
return "allowed"
return _allow_status(ids, allow, license_str)


def _component_license(component: Mapping[str, Any]) -> str:
for entry in component.get("licenses", []):
if "expression" in entry:
return str(entry["expression"])
license_obj = entry.get("license", {})
name = license_obj.get("id") or license_obj.get("name")
if name:
return str(name)
return ""


def evaluate_sbom(components: Sequence[Mapping[str, Any]], *,
allow: Optional[Sequence[str]] = None,
deny: Optional[Sequence[str]] = None) -> List[Dict[str, Any]]:
"""Return a violation per component whose license is not ``allowed``."""
violations: List[Dict[str, Any]] = []
for component in components:
license_str = _component_license(component)
status = evaluate_license(license_str, allow=allow, deny=deny)
if status != "allowed":
violations.append({
"name": str(component.get("name", "")),
"version": str(component.get("version", "")),
"license": license_str,
"status": status,
})
return violations


def license_findings_to_sarif(violations: Sequence[Mapping[str, Any]]
) -> List[Dict[str, Any]]:
"""Convert license violations into SARIF-ready normalized findings."""
findings = []
for violation in violations:
level = "error" if violation["status"] == "denied" else "warning"
shown = violation["license"] or "unknown"
message = (f"{violation['name']} {violation['version']}: license "
f"'{shown}' is {violation['status']}")
findings.append(make_finding(
f"license/{violation['name']}", message, level=level))
return findings
Loading
Loading