Files
yao-meta-skill/scripts/trust_check.py
T
2026-06-13 21:01:27 +08:00

715 lines
31 KiB
Python

#!/usr/bin/env python3
import argparse
import ast
import hashlib
import json
import subprocess
import sys
import re
from datetime import date
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
try:
import yaml
except ImportError: # pragma: no cover
yaml = None
ROOT = Path(__file__).resolve().parent.parent
SCAN_DIRS = ["agents", "docs", "evals", "references", "runtime", "scripts", "security", "skill-ir", "templates"]
ROOT_FILES = ["SKILL.md", "README.md", "manifest.json", "requirements-ci.txt", "Makefile"]
TEXT_SUFFIXES = {".md", ".json", ".jsonl", ".yaml", ".yml", ".py", ".sh", ".txt", ".toml"}
PACKAGE_HASH_SCOPE = "source-contract-without-generated-reports"
INTERNAL_SCRIPT_INTERFACE = "internal-module"
NETWORK_POLICY_REL_PATH = "security/network_policy.json"
PERMISSION_POLICY_REL_PATH = "security/permission_policy.json"
HELP_SMOKE_TIMEOUT_SECONDS = 5.0
PERMISSION_CAPABILITIES = ("network", "file_write", "subprocess", "interactive")
PERMISSION_TARGETS = ("openai", "claude", "generic", "vscode")
SECRET_PATTERNS = [
("private_key", re.compile(r"-----BEGIN (?:RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----")),
("github_token", re.compile(r"\bgh[pousr]_[A-Za-z0-9_]{20,}\b")),
("slack_token", re.compile(r"\bxox[baprs]-[A-Za-z0-9-]{20,}\b")),
("aws_access_key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
("openai_api_key", re.compile(r"\bsk-[A-Za-z0-9]{32,}\b")),
]
def display_path(path: Path) -> str:
try:
return str(path.resolve().relative_to(ROOT.resolve()))
except ValueError:
return str(path.resolve())
def load_json(path: Path) -> dict[str, Any]:
if not path.exists():
return {}
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError:
return {}
return payload if isinstance(payload, dict) else {}
def load_yaml(path: Path) -> dict[str, Any]:
if not path.exists() or yaml is None:
return {}
payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
return payload if isinstance(payload, dict) else {}
def iter_scan_files(skill_dir: Path) -> list[Path]:
files = []
for rel in ROOT_FILES:
path = skill_dir / rel
if path.exists() and path.is_file():
files.append(path)
for rel in SCAN_DIRS:
folder = skill_dir / rel
if not folder.exists():
continue
for path in sorted(folder.rglob("*")):
if not path.is_file() or path.is_symlink():
continue
if path.suffix in TEXT_SUFFIXES and path.stat().st_size <= 1_000_000:
files.append(path)
return sorted(set(files))
def relpath(skill_dir: Path, path: Path) -> str:
return str(path.relative_to(skill_dir))
def scan_secrets(skill_dir: Path, files: list[Path]) -> list[dict[str, Any]]:
findings = []
for path in files:
text = path.read_text(encoding="utf-8", errors="replace")
for name, pattern in SECRET_PATTERNS:
for match in pattern.finditer(text):
line = text.count("\n", 0, match.start()) + 1
findings.append({"type": name, "path": relpath(skill_dir, path), "line": line})
return findings
def script_inventory(skill_dir: Path) -> list[dict[str, Any]]:
scripts_dir = skill_dir / "scripts"
if not scripts_dir.exists():
return []
inventory = []
for path in sorted(scripts_dir.glob("*.py")):
text = path.read_text(encoding="utf-8", errors="replace")
flags = script_flags(text)
interface = script_interface(text)
urls = extract_url_literals(text)
inventory.append(
{
"path": relpath(skill_dir, path),
"interface": interface["name"],
"interface_declared": interface["declared"],
"interface_reason": interface["reason"],
"has_argparse": "argparse" in text,
"has_main_guard": 'if __name__ == "__main__"' in text,
"uses_input": flags["uses_input"],
"uses_network": flags["uses_network"],
"uses_file_write": flags["uses_file_write"],
"uses_subprocess": flags["uses_subprocess"],
"network_urls": urls,
"network_hosts": sorted({urlparse(url).hostname or "" for url in urls if urlparse(url).hostname}),
}
)
return inventory
def string_assignment(tree: ast.Module, variable_name: str) -> str:
for node in tree.body:
value_node = None
if isinstance(node, ast.Assign):
if any(isinstance(target, ast.Name) and target.id == variable_name for target in node.targets):
value_node = node.value
elif isinstance(node, ast.AnnAssign):
target = node.target
if isinstance(target, ast.Name) and target.id == variable_name:
value_node = node.value
if isinstance(value_node, ast.Constant) and isinstance(value_node.value, str):
return value_node.value
return ""
def script_interface(text: str) -> dict[str, Any]:
try:
tree = ast.parse(text)
except SyntaxError:
match = re.search(r"SCRIPT_INTERFACE\s*=\s*['\"]([^'\"]+)['\"]", text)
reason_match = re.search(r"SCRIPT_INTERFACE_REASON\s*=\s*['\"]([^'\"]+)['\"]", text)
name = match.group(1) if match else "cli"
reason = reason_match.group(1) if reason_match else ""
return {"name": name, "declared": bool(match), "reason": reason}
name = string_assignment(tree, "SCRIPT_INTERFACE")
reason = string_assignment(tree, "SCRIPT_INTERFACE_REASON")
if name:
return {"name": name, "declared": True, "reason": reason}
return {"name": "cli", "declared": False, "reason": "Default CLI classification; add SCRIPT_INTERFACE for internal modules."}
def extract_url_literals(text: str) -> list[str]:
values: list[str] = []
try:
tree = ast.parse(text)
for node in ast.walk(tree):
if isinstance(node, ast.Constant) and isinstance(node.value, str):
values.append(node.value)
except SyntaxError:
values = re.findall(r"['\"]([^'\"]+)['\"]", text)
urls = []
seen = set()
for value in values:
for match in re.finditer(r"https?://[^\s'\"<>]+", value):
url = match.group(0).rstrip(").,]")
if url not in seen:
seen.add(url)
urls.append(url)
return urls
def call_name(node: ast.AST) -> str:
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
base = call_name(node.value)
return f"{base}.{node.attr}" if base else node.attr
return ""
def script_flags(text: str) -> dict[str, bool]:
try:
tree = ast.parse(text)
except SyntaxError:
return {
"uses_input": bool(re.search(r"\b(?:input|getpass)\s*\(", text)),
"uses_network": bool(re.search(r"\b(?:urlopen|Request)\s*\(|\brequests\.", text)),
"uses_file_write": bool(
re.search(r"\.(?:write_text|write_bytes|mkdir|unlink)\s*\(", text)
or re.search(r"\b(?:open)\s*\([^)]*['\"][wa+x]", text)
or "shutil.rmtree" in text
),
"uses_subprocess": "subprocess." in text,
}
call_nodes = [node for node in ast.walk(tree) if isinstance(node, ast.Call)]
calls = [call_name(node.func) for node in call_nodes]
return {
"uses_input": any(name in {"input", "getpass.getpass"} or name.endswith(".getpass") for name in calls),
"uses_network": any(name in {"urlopen", "Request"} or name.startswith("requests.") for name in calls),
"uses_file_write": any(is_file_write_call(node, call_name(node.func)) for node in call_nodes),
"uses_subprocess": any(name.startswith("subprocess.") for name in calls),
}
def is_file_write_call(node: ast.Call, name: str) -> bool:
if name.endswith((".write_text", ".write_bytes", ".mkdir", ".unlink", ".rmdir")):
return True
if name in {"shutil.copy", "shutil.copy2", "shutil.copytree", "shutil.move", "shutil.rmtree"}:
return True
if name == "zipfile.ZipFile":
return True
if name in {"open", "Path.open"} or name.endswith(".open"):
args = list(node.args)
keywords = {keyword.arg: keyword.value for keyword in node.keywords if keyword.arg}
mode_node = args[1] if len(args) > 1 else keywords.get("mode")
if isinstance(mode_node, ast.Constant) and isinstance(mode_node.value, str):
return any(flag in mode_node.value for flag in ("w", "a", "x", "+"))
return False
def dependency_status(skill_dir: Path) -> dict[str, Any]:
candidates = ["requirements-ci.txt", "requirements.txt", "pyproject.toml", "package-lock.json", "uv.lock", "poetry.lock"]
present = [name for name in candidates if (skill_dir / name).exists()]
pinned = []
unpinned = []
requirements = skill_dir / "requirements-ci.txt"
if requirements.exists():
for line in requirements.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
if "==" in stripped or " @ " in stripped:
pinned.append(stripped)
else:
unpinned.append(stripped)
return {"present": present, "pinned": pinned, "unpinned": unpinned}
def script_capability_paths(scripts: list[dict[str, Any]]) -> dict[str, list[str]]:
return {
"network": [item["path"] for item in scripts if item["uses_network"]],
"file_write": [item["path"] for item in scripts if item.get("uses_file_write", False)],
"subprocess": [item["path"] for item in scripts if item["uses_subprocess"]],
"interactive": [item["path"] for item in scripts if item["uses_input"]],
}
def load_network_policy(skill_dir: Path) -> dict[str, Any]:
path = skill_dir / NETWORK_POLICY_REL_PATH
if not path.exists():
return {"present": False, "path": NETWORK_POLICY_REL_PATH, "scripts": {}}
payload = load_json(path)
scripts = payload.get("scripts", {})
return {
"present": True,
"path": NETWORK_POLICY_REL_PATH,
"schema_version": payload.get("schema_version", ""),
"default_policy": payload.get("default_policy", {}),
"scripts": scripts if isinstance(scripts, dict) else {},
}
def load_permission_policy(skill_dir: Path) -> dict[str, Any]:
path = skill_dir / PERMISSION_POLICY_REL_PATH
if not path.exists():
return {"present": False, "path": PERMISSION_POLICY_REL_PATH, "capabilities": {}}
payload = load_json(path)
capabilities = payload.get("capabilities", {})
return {
"present": True,
"path": PERMISSION_POLICY_REL_PATH,
"schema_version": payload.get("schema_version", ""),
"reviewed_at": payload.get("reviewed_at", ""),
"capabilities": capabilities if isinstance(capabilities, dict) else {},
}
def parse_iso_date(value: Any) -> date | None:
try:
return date.fromisoformat(str(value)[:10])
except (TypeError, ValueError):
return None
def permission_governance_status(skill_dir: Path, scripts: list[dict[str, Any]], today: date | None = None) -> dict[str, Any]:
policy = load_permission_policy(skill_dir)
today = today or date.today()
capability_paths = script_capability_paths(scripts)
required = {name: paths for name, paths in capability_paths.items() if paths}
approvals = []
missing = []
invalid = []
expired = []
approved = []
for name, paths in required.items():
entry = policy["capabilities"].get(name, {}) if policy["present"] else {}
if not isinstance(entry, dict) or not entry:
missing.append(name)
approvals.append({"capability": name, "status": "missing", "scripts": paths, "validation": ["approval entry is missing"]})
continue
validation = []
if entry.get("decision") != "approved":
validation.append("decision must be approved")
if not entry.get("reviewer"):
validation.append("reviewer is required")
if len(str(entry.get("reason", ""))) < 30:
validation.append("reason must be at least 30 characters")
if not entry.get("scope"):
validation.append("scope is required")
expires_at = parse_iso_date(entry.get("expires_at"))
if expires_at is None:
validation.append("expires_at must be ISO date")
target_enforcement = entry.get("target_enforcement", {})
if not isinstance(target_enforcement, dict):
validation.append("target_enforcement must be an object")
target_enforcement = {}
missing_targets = [target for target in PERMISSION_TARGETS if not target_enforcement.get(target)]
if missing_targets:
validation.append(f"target_enforcement missing: {', '.join(missing_targets)}")
if validation:
invalid.append(name)
status = "invalid"
elif expires_at is not None and expires_at < today:
expired.append(name)
status = "expired"
else:
approved.append(name)
status = "approved"
approvals.append(
{
"capability": name,
"status": status,
"scripts": paths,
"reviewer": str(entry.get("reviewer", "")),
"scope": str(entry.get("scope", "")),
"reason": str(entry.get("reason", "")),
"expires_at": str(entry.get("expires_at", "")),
"evidence": entry.get("evidence", []) if isinstance(entry.get("evidence", []), list) else [],
"target_enforcement": target_enforcement,
"validation": validation,
}
)
return {
"present": policy["present"],
"path": policy["path"],
"schema_version": policy.get("schema_version", ""),
"reviewed_at": policy.get("reviewed_at", ""),
"required_capabilities": sorted(required),
"approved_capabilities": sorted(approved),
"missing_capabilities": sorted(missing),
"invalid_capabilities": sorted(invalid),
"expired_capabilities": sorted(expired),
"approval_count": len(approved),
"required_count": len(required),
"missing_count": len(missing),
"invalid_count": len(invalid),
"expired_count": len(expired),
"approvals": approvals,
}
def network_policy_status(skill_dir: Path, scripts: list[dict[str, Any]]) -> dict[str, Any]:
policy = load_network_policy(skill_dir)
network_scripts = [item for item in scripts if item["uses_network"]]
covered = []
missing = []
mismatches = []
for item in network_scripts:
entry = policy["scripts"].get(item["path"], {}) if policy["present"] else {}
if not isinstance(entry, dict) or not entry:
missing.append(item["path"])
continue
allowed_hosts = set(entry.get("allowed_hosts") or [])
if not allowed_hosts:
mismatches.append(
{
"path": item["path"],
"reason": "allowed_hosts is empty",
"observed_hosts": item["network_hosts"],
"allowed_hosts": [],
}
)
continue
observed_hosts = set(item.get("network_hosts") or [])
unexpected_hosts = sorted(observed_hosts - allowed_hosts)
if unexpected_hosts:
mismatches.append(
{
"path": item["path"],
"reason": "observed HTTPS hosts are not declared in policy",
"observed_hosts": sorted(observed_hosts),
"allowed_hosts": sorted(allowed_hosts),
"unexpected_hosts": unexpected_hosts,
}
)
continue
covered.append(item["path"])
return {
"present": policy["present"],
"path": policy["path"],
"schema_version": policy.get("schema_version", ""),
"network_script_count": len(network_scripts),
"covered_scripts": covered,
"missing_scripts": missing,
"mismatches": mismatches,
"default_policy": policy.get("default_policy", {}),
}
def interface_trust(skill_dir: Path) -> dict[str, Any]:
interface = load_yaml(skill_dir / "agents" / "interface.yaml")
trust = interface.get("compatibility", {}).get("trust", {})
return {
"source_tier": trust.get("source_tier", ""),
"remote_inline_execution": trust.get("remote_inline_execution", ""),
"remote_metadata_policy": trust.get("remote_metadata_policy", ""),
}
def package_digest(files: list[Path], skill_dir: Path) -> str:
digest = hashlib.sha256()
for path in files:
rel = relpath(skill_dir, path)
digest.update(rel.encode("utf-8"))
digest.update(b"\0")
digest.update(hashlib.sha256(path.read_bytes()).hexdigest().encode("ascii"))
digest.update(b"\0")
return digest.hexdigest()
def help_smoke_candidates(scripts: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [
item
for item in scripts
if item["interface"] != INTERNAL_SCRIPT_INTERFACE and item["has_argparse"]
]
def run_help_smoke_checks(skill_dir: Path, scripts: list[dict[str, Any]], timeout: float) -> dict[str, Any]:
candidates = help_smoke_candidates(scripts)
skipped = [
{
"path": item["path"],
"reason": "internal module" if item["interface"] == INTERNAL_SCRIPT_INTERFACE else "missing argparse/help surface",
}
for item in scripts
if item not in candidates
]
results = []
for item in candidates:
command = [sys.executable, item["path"], "--help"]
try:
proc = subprocess.run(
command,
cwd=skill_dir,
capture_output=True,
text=True,
timeout=timeout,
)
output = f"{proc.stdout}\n{proc.stderr}".lower()
has_help_text = "usage:" in output or "--help" in output
results.append(
{
"path": item["path"],
"command": f"python3 {item['path']} --help",
"returncode": proc.returncode,
"timed_out": False,
"passed": proc.returncode == 0 and has_help_text,
"has_help_text": has_help_text,
"stdout_excerpt": proc.stdout.strip()[:240],
"stderr_excerpt": proc.stderr.strip()[:240],
}
)
except subprocess.TimeoutExpired as exc:
results.append(
{
"path": item["path"],
"command": f"python3 {item['path']} --help",
"returncode": None,
"timed_out": True,
"passed": False,
"has_help_text": False,
"stdout_excerpt": (exc.stdout or "").strip()[:240] if isinstance(exc.stdout, str) else "",
"stderr_excerpt": (exc.stderr or "").strip()[:240] if isinstance(exc.stderr, str) else "",
}
)
failed = [item for item in results if not item["passed"]]
return {
"enabled": True,
"timeout_seconds": timeout,
"candidate_count": len(candidates),
"checked_count": len(results),
"passed_count": len(results) - len(failed),
"failed_count": len(failed),
"skipped_count": len(skipped),
"failed_scripts": [item["path"] for item in failed],
"results": results,
"skipped": skipped,
}
def disabled_help_smoke_status(scripts: list[dict[str, Any]], timeout: float) -> dict[str, Any]:
candidates = help_smoke_candidates(scripts)
return {
"enabled": False,
"timeout_seconds": timeout,
"candidate_count": len(candidates),
"checked_count": 0,
"passed_count": 0,
"failed_count": 0,
"skipped_count": len(scripts),
"failed_scripts": [],
"results": [],
"skipped": [{"path": item["path"], "reason": "help smoke disabled"} for item in scripts],
}
def build_trust_report(skill_dir: Path, run_help_smoke: bool = True, help_smoke_timeout: float = HELP_SMOKE_TIMEOUT_SECONDS) -> dict[str, Any]:
skill_dir = skill_dir.resolve()
files = iter_scan_files(skill_dir)
secrets = scan_secrets(skill_dir, files)
scripts = script_inventory(skill_dir)
deps = dependency_status(skill_dir)
network_policy = network_policy_status(skill_dir, scripts)
help_smoke = (
run_help_smoke_checks(skill_dir, scripts, help_smoke_timeout)
if run_help_smoke
else disabled_help_smoke_status(scripts, help_smoke_timeout)
)
permission_governance = permission_governance_status(skill_dir, scripts)
trust = interface_trust(skill_dir)
failures = []
warnings = []
if secrets:
failures.append(f"High-risk secret patterns found: {len(secrets)}")
if trust.get("remote_inline_execution") not in {"forbid", "deny", "false"}:
failures.append("remote_inline_execution must be forbid for governed release")
if deps["unpinned"]:
warnings.append(f"Unpinned dependency entries: {', '.join(deps['unpinned'])}")
if not deps["present"]:
warnings.append("No dependency or lock file detected")
internal_modules = [item for item in scripts if item["interface"] == INTERNAL_SCRIPT_INTERFACE]
missing_help = [
item["path"] for item in scripts if item["interface"] != INTERNAL_SCRIPT_INTERFACE and not item["has_argparse"]
]
if missing_help:
warnings.append(f"CLI scripts without argparse/help surface: {', '.join(missing_help[:8])}")
interactive = [item["path"] for item in scripts if item["uses_input"]]
if interactive:
warnings.append(f"Interactive scripts require reviewer awareness: {', '.join(interactive[:8])}")
network = [item["path"] for item in scripts if item["uses_network"]]
file_write = [item["path"] for item in scripts if item["uses_file_write"]]
if network_policy["missing_scripts"]:
warnings.append(f"Network-capable scripts require bounded host policy: {', '.join(network_policy['missing_scripts'][:8])}")
if network_policy["mismatches"]:
warning_paths = [item["path"] for item in network_policy["mismatches"]]
warnings.append(f"Network host policy mismatch: {', '.join(warning_paths[:8])}")
if help_smoke["failed_scripts"]:
warnings.append(f"CLI help smoke failed: {', '.join(help_smoke['failed_scripts'][:8])}")
if permission_governance["missing_capabilities"]:
warnings.append(f"Permission approvals missing: {', '.join(permission_governance['missing_capabilities'][:8])}")
if permission_governance["invalid_capabilities"]:
warnings.append(f"Permission approvals invalid: {', '.join(permission_governance['invalid_capabilities'][:8])}")
if permission_governance["expired_capabilities"]:
warnings.append(f"Permission approvals expired: {', '.join(permission_governance['expired_capabilities'][:8])}")
summary = {
"scanned_files": len(files),
"script_count": len(scripts),
"internal_module_count": len(internal_modules),
"secret_findings": len(secrets),
"dependency_files": deps["present"],
"network_script_count": len(network),
"network_policy_covered_count": len(network_policy["covered_scripts"]),
"network_policy_missing_count": len(network_policy["missing_scripts"]),
"file_write_script_count": len(file_write),
"permission_required_count": permission_governance["required_count"],
"permission_approved_count": permission_governance["approval_count"],
"permission_missing_count": permission_governance["missing_count"],
"permission_invalid_count": permission_governance["invalid_count"],
"permission_expired_count": permission_governance["expired_count"],
"help_smoke_checked_count": help_smoke["checked_count"],
"help_smoke_failed_count": help_smoke["failed_count"],
"interactive_script_count": len(interactive),
"package_hash_scope": PACKAGE_HASH_SCOPE,
"package_hash_file_count": len(files),
"package_sha256": package_digest(files, skill_dir),
}
return {
"ok": not failures,
"skill_dir": display_path(skill_dir),
"summary": summary,
"failures": failures,
"warnings": warnings,
"secrets": secrets,
"scripts": scripts,
"dependencies": deps,
"network_policy": network_policy,
"help_smoke": help_smoke,
"permission_governance": permission_governance,
"trust_metadata": trust,
}
def render_markdown(payload: dict[str, Any]) -> str:
summary = payload["summary"]
lines = [
"# Security Trust Report",
"",
f"- OK: `{payload['ok']}`",
f"- Scanned files: `{summary['scanned_files']}`",
f"- Scripts: `{summary['script_count']}`",
f"- Internal script modules: `{summary.get('internal_module_count', 0)}`",
f"- Secret findings: `{summary['secret_findings']}`",
f"- Network-capable scripts: `{summary['network_script_count']}`",
f"- Network policy covered scripts: `{summary.get('network_policy_covered_count', 0)}`",
f"- Network policy missing scripts: `{summary.get('network_policy_missing_count', 0)}`",
f"- File-write scripts: `{summary.get('file_write_script_count', 0)}`",
f"- Permission approvals: `{summary.get('permission_approved_count', 0)} / {summary.get('permission_required_count', 0)}`",
f"- Permission approval gaps: `{summary.get('permission_missing_count', 0) + summary.get('permission_invalid_count', 0) + summary.get('permission_expired_count', 0)}`",
f"- CLI help smoke checked: `{summary.get('help_smoke_checked_count', 0)}`",
f"- CLI help smoke failures: `{summary.get('help_smoke_failed_count', 0)}`",
f"- Interactive scripts: `{summary['interactive_script_count']}`",
f"- Package hash scope: `{summary['package_hash_scope']}`",
f"- Package hash files: `{summary['package_hash_file_count']}`",
f"- Package SHA256: `{summary['package_sha256']}`",
"",
"## Failures",
"",
]
lines.extend([f"- {item}" for item in payload["failures"]] or ["- None"])
lines.extend(["", "## Warnings", ""])
lines.extend([f"- {item}" for item in payload["warnings"]] or ["- None"])
lines.extend(["", "## Dependency Evidence", ""])
lines.append(f"- Files: `{', '.join(payload['dependencies']['present']) or 'none'}`")
lines.append(f"- Pinned entries: `{len(payload['dependencies']['pinned'])}`")
lines.append(f"- Unpinned entries: `{len(payload['dependencies']['unpinned'])}`")
network_policy = payload.get("network_policy", {})
lines.extend(["", "## Network Policy", ""])
lines.append(f"- Policy file: `{network_policy.get('path', NETWORK_POLICY_REL_PATH)}`")
lines.append(f"- Present: `{network_policy.get('present', False)}`")
lines.append(f"- Covered scripts: `{len(network_policy.get('covered_scripts', []))}`")
lines.append(f"- Missing scripts: `{', '.join(network_policy.get('missing_scripts', [])) or 'none'}`")
lines.append(f"- Mismatches: `{len(network_policy.get('mismatches', []))}`")
permission_governance = payload.get("permission_governance", {})
lines.extend(["", "## Permission Governance", ""])
lines.append(f"- Policy file: `{permission_governance.get('path', PERMISSION_POLICY_REL_PATH)}`")
lines.append(f"- Present: `{permission_governance.get('present', False)}`")
lines.append(f"- Required capabilities: `{', '.join(permission_governance.get('required_capabilities', [])) or 'none'}`")
lines.append(f"- Approved capabilities: `{', '.join(permission_governance.get('approved_capabilities', [])) or 'none'}`")
lines.append(f"- Missing approvals: `{', '.join(permission_governance.get('missing_capabilities', [])) or 'none'}`")
lines.append(f"- Invalid approvals: `{', '.join(permission_governance.get('invalid_capabilities', [])) or 'none'}`")
lines.append(f"- Expired approvals: `{', '.join(permission_governance.get('expired_capabilities', [])) or 'none'}`")
help_smoke = payload.get("help_smoke", {})
lines.extend(["", "## CLI Help Smoke", ""])
lines.append(f"- Enabled: `{help_smoke.get('enabled', False)}`")
lines.append(f"- Timeout seconds: `{help_smoke.get('timeout_seconds', HELP_SMOKE_TIMEOUT_SECONDS)}`")
lines.append(f"- Checked scripts: `{help_smoke.get('checked_count', 0)}`")
lines.append(f"- Passed scripts: `{help_smoke.get('passed_count', 0)}`")
lines.append(f"- Failed scripts: `{', '.join(help_smoke.get('failed_scripts', [])) or 'none'}`")
lines.extend(
[
"",
"## Script Surface",
"",
"| Script | Interface | Declared | Argparse | Main Guard | Input | Network | File Write | Subprocess | Reason |",
"| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
]
)
for item in payload["scripts"]:
lines.append(
f"| {item['path']} | {item['interface']} | {item['interface_declared']} | {item['has_argparse']} | {item['has_main_guard']} | {item['uses_input']} | {item['uses_network']} | {item.get('uses_file_write', False)} | {item['uses_subprocess']} | {item['interface_reason']} |"
)
return "\n".join(lines).strip() + "\n"
def main() -> None:
parser = argparse.ArgumentParser(description="Run Skill OS trust and security checks.")
parser.add_argument("skill_dir", nargs="?", default=".")
parser.add_argument("--output-json")
parser.add_argument("--output-md")
parser.add_argument("--skip-help-smoke", action="store_true")
parser.add_argument("--help-smoke-timeout", type=float, default=HELP_SMOKE_TIMEOUT_SECONDS)
args = parser.parse_args()
skill_dir = Path(args.skill_dir).resolve()
output_json = Path(args.output_json).resolve() if args.output_json else skill_dir / "reports" / "security_trust_report.json"
output_md = Path(args.output_md).resolve() if args.output_md else skill_dir / "reports" / "security_trust_report.md"
payload = build_trust_report(
skill_dir,
run_help_smoke=not args.skip_help_smoke,
help_smoke_timeout=args.help_smoke_timeout,
)
payload["artifacts"] = {"json": display_path(output_json), "markdown": display_path(output_md)}
output_json.parent.mkdir(parents=True, exist_ok=True)
output_md.parent.mkdir(parents=True, exist_ok=True)
output_json.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
output_md.write_text(render_markdown(payload), encoding="utf-8")
print(json.dumps(payload, ensure_ascii=False, indent=2))
raise SystemExit(0 if payload["ok"] else 2)
if __name__ == "__main__":
main()