e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
151 lines
5.0 KiB
Python
151 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
|
|
|
"""Refuse dangerous GitHub Actions trigger patterns at PR time.
|
|
|
|
Bans patterns behind the TanStack GHSA-g7cv-rxg3-hmpx compromise:
|
|
|
|
1. `pull_request_target` -- runs a fork's workflow against the base
|
|
repo's secrets/permissions; use `pull_request` instead.
|
|
2. `workflow_run` chained to a PR-triggered workflow -- same trust
|
|
boundary problem one hop later (poisoned artifacts/caches run with
|
|
elevated permissions).
|
|
3. Cache keys shared between PR-triggered and publish/release/push
|
|
workflows -- a fork PR could poison a cache the publish workflow
|
|
restores. Partition the key namespaces.
|
|
|
|
Exit codes: 0 = no findings, 1 = findings (listed on stderr).
|
|
Run from repo root: python3 scripts/lint_workflow_triggers.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
try:
|
|
import yaml
|
|
except ImportError:
|
|
print("ERROR: PyYAML is required. Install with 'pip install pyyaml'", file = sys.stderr)
|
|
sys.exit(2)
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
DEFAULT_WORKFLOWS_DIR = REPO_ROOT / ".github" / "workflows"
|
|
|
|
BANNED_TRIGGERS: tuple[str, ...] = ("pull_request_target",)
|
|
RESTRICTED_TRIGGERS: tuple[str, ...] = ("workflow_run",)
|
|
PUBLISH_WORKFLOW_NAMES: tuple[str, ...] = ("release-desktop.yml",)
|
|
|
|
|
|
def _normalise_on(on_field):
|
|
if isinstance(on_field, str):
|
|
return {on_field}
|
|
if isinstance(on_field, list):
|
|
return set(on_field)
|
|
if isinstance(on_field, dict):
|
|
return set(on_field.keys())
|
|
return set()
|
|
|
|
|
|
def _load_workflow(path: Path):
|
|
try:
|
|
return yaml.safe_load(path.read_text())
|
|
except Exception as exc:
|
|
print(f"ERROR: failed to parse {path}: {exc}", file = sys.stderr)
|
|
sys.exit(2)
|
|
|
|
|
|
def _extract_cache_keys(path: Path) -> list[str]:
|
|
text = path.read_text()
|
|
keys: list[str] = []
|
|
for m in re.finditer(r"(?:^|\n)\s*key:\s*([^\n]+)", text):
|
|
keys.append(m.group(1).strip())
|
|
return keys
|
|
|
|
|
|
def _trigger_set(yaml_doc) -> set[str]:
|
|
on = yaml_doc.get(True)
|
|
if on is None:
|
|
on = yaml_doc.get("on")
|
|
return _normalise_on(on)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description = __doc__)
|
|
parser.add_argument(
|
|
"--workflows-dir",
|
|
type = Path,
|
|
default = DEFAULT_WORKFLOWS_DIR,
|
|
help = "Override the workflows directory (used by tests).",
|
|
)
|
|
args = parser.parse_args()
|
|
workflows_dir = args.workflows_dir
|
|
|
|
findings: list[str] = []
|
|
workflows = sorted(workflows_dir.glob("*.yml"))
|
|
pr_triggered: list[tuple[Path, list[str]]] = []
|
|
publish_triggered: list[tuple[Path, list[str]]] = []
|
|
|
|
for path in workflows:
|
|
doc = _load_workflow(path)
|
|
triggers = _trigger_set(doc)
|
|
|
|
for t in BANNED_TRIGGERS:
|
|
if t in triggers:
|
|
findings.append(
|
|
f"{path.name}: BANNED trigger '{t}' (GHSA-g7cv-rxg3-hmpx "
|
|
"pattern: fork PRs run in base-repo context). Switch to "
|
|
"'pull_request' and use a deploy-on-merge workflow for "
|
|
"any privileged step."
|
|
)
|
|
|
|
for t in RESTRICTED_TRIGGERS:
|
|
if t in triggers:
|
|
text = path.read_text()
|
|
if "lint:workflow_triggers-allow-workflow_run" not in text:
|
|
findings.append(
|
|
f"{path.name}: RESTRICTED trigger '{t}' requires an "
|
|
"explicit `# lint:workflow_triggers-allow-workflow_run` "
|
|
"comment somewhere in the file, with a justification."
|
|
)
|
|
|
|
if "pull_request" in triggers:
|
|
pr_triggered.append((path, _extract_cache_keys(path)))
|
|
is_dispatch_only = "workflow_dispatch" in triggers and not (
|
|
"push" in triggers or "pull_request" in triggers
|
|
)
|
|
if path.name in PUBLISH_WORKFLOW_NAMES or is_dispatch_only:
|
|
publish_triggered.append((path, _extract_cache_keys(path)))
|
|
|
|
pr_keys = {key for _, keys in pr_triggered for key in keys}
|
|
for pub_path, pub_keys in publish_triggered:
|
|
for k in pub_keys:
|
|
if k in pr_keys:
|
|
findings.append(
|
|
f"{pub_path.name}: cache key {k!r} is also declared in a "
|
|
"PR-triggered workflow. A fork PR could poison this cache "
|
|
"and the publish workflow would restore it on next run. "
|
|
"Add a unique suffix (e.g. '-publish-only') to partition "
|
|
"the namespaces."
|
|
)
|
|
|
|
if findings:
|
|
print("Workflow trigger lint failed with the following issues:", file = sys.stderr)
|
|
for f in findings:
|
|
print(f" - {f}", file = sys.stderr)
|
|
return 1
|
|
|
|
print(
|
|
f"OK: scanned {len(workflows)} workflow file(s); "
|
|
f"no pull_request_target, no unjustified workflow_run, "
|
|
f"no PR/publish cache-key collision."
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|