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
335 lines
12 KiB
Python
335 lines
12 KiB
Python
"""Regression tests for the offline `scripts/lockfile_supply_chain_audit.py`."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
SCRIPT = REPO_ROOT / "scripts" / "lockfile_supply_chain_audit.py"
|
|
FIXTURES = Path(__file__).resolve().parent / "fixtures"
|
|
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
from scripts import lockfile_supply_chain_audit as lsa # noqa: E402
|
|
|
|
|
|
def _run_auditor(
|
|
*,
|
|
root: Path,
|
|
npm_lockfiles: list[Path] | None = None,
|
|
cargo_lockfiles: list[Path] | None = None,
|
|
strict: bool = False,
|
|
timeout: int = 30,
|
|
) -> subprocess.CompletedProcess:
|
|
cmd = [sys.executable, str(SCRIPT), "--root", str(root)]
|
|
if strict:
|
|
cmd.append("--strict")
|
|
for p in npm_lockfiles or []:
|
|
cmd.extend(["--npm-lockfile", str(p)])
|
|
for p in cargo_lockfiles or []:
|
|
cmd.extend(["--cargo-lockfile", str(p)])
|
|
return subprocess.run(
|
|
cmd,
|
|
capture_output = True,
|
|
text = True,
|
|
timeout = timeout,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# npm lockfile audit.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_malicious_lockfile_exits_1(tmp_path):
|
|
"""Non-registry URL + IOC substring + missing integrity hash -> auditor exits 1."""
|
|
fixture = FIXTURES / "malicious_lockfile.json"
|
|
assert fixture.is_file()
|
|
proc = _run_auditor(root = tmp_path, npm_lockfiles = [fixture])
|
|
assert proc.returncode == 1, (
|
|
f"expected exit 1, got {proc.returncode}\n"
|
|
f"--- stdout ---\n{proc.stdout}\n--- stderr ---\n{proc.stderr}"
|
|
)
|
|
combined = proc.stdout + proc.stderr
|
|
assert "non-registry-resolved-url" in combined
|
|
assert "missing-integrity-hash" in combined
|
|
assert "known-ioc-string" in combined
|
|
# IOC literal built at runtime so CodeQL's
|
|
# py/incomplete-url-substring-sanitization rule doesn't false-positive on
|
|
# the source-literal + `in` (the operand is the scanner's own output).
|
|
_ioc_host = "filev2." + "getsession.org"
|
|
assert _ioc_host in combined
|
|
|
|
|
|
def test_clean_lockfile_exits_0(tmp_path):
|
|
fixture = FIXTURES / "clean_lockfile.json"
|
|
proc = _run_auditor(root = tmp_path, npm_lockfiles = [fixture])
|
|
assert proc.returncode == 0, (
|
|
f"expected exit 0, got {proc.returncode}\n"
|
|
f"--- stdout ---\n{proc.stdout}\n--- stderr ---\n{proc.stderr}"
|
|
)
|
|
assert "0 findings" in proc.stdout
|
|
|
|
|
|
def test_audit_npm_lockfile_direct_call_findings():
|
|
"""In-process audit_npm_lockfile() returns the same findings as the subprocess."""
|
|
findings = lsa.audit_npm_lockfile(FIXTURES / "malicious_lockfile.json")
|
|
kinds = {f.kind for f in findings}
|
|
assert "non-registry-resolved-url" in kinds
|
|
assert "missing-integrity-hash" in kinds
|
|
assert "known-ioc-string" in kinds
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# IOC string table -- gated on Fork 1's NPM_IOC_STRINGS additions.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
_MAY12_IOCS = (
|
|
"git-tanstack.com",
|
|
"transformers.pyz",
|
|
"/tmp/transformers.pyz",
|
|
"With Love TeamPCP",
|
|
)
|
|
|
|
|
|
def test_npm_ioc_strings_contains_may11_baseline():
|
|
"""May-11 wave IOCs must remain in NPM_IOC_STRINGS (baseline)."""
|
|
iocs = set(lsa.NPM_IOC_STRINGS)
|
|
for needle in (
|
|
"router_init.js",
|
|
"tanstack_runner.js",
|
|
"router_runtime.js",
|
|
"filev2.getsession.org",
|
|
):
|
|
assert needle in iocs, f"baseline IOC {needle!r} disappeared"
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not all(s in lsa.NPM_IOC_STRINGS for s in _MAY12_IOCS),
|
|
reason = "Fork 1 (May-12 IOC additions) not merged yet",
|
|
)
|
|
def test_npm_ioc_strings_contains_may12_additions():
|
|
iocs = set(lsa.NPM_IOC_STRINGS)
|
|
for needle in _MAY12_IOCS:
|
|
assert needle in iocs
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not hasattr(lsa, "BLOCKED_NPM_VERSIONS"),
|
|
reason = "Fork 1 (BLOCKED_NPM_VERSIONS in auditor) not merged yet",
|
|
)
|
|
def test_lockfile_auditor_blocked_versions_match_scanner():
|
|
"""Auditor's BLOCKED_NPM_VERSIONS must mirror the scanner's table verbatim."""
|
|
from scripts import scan_npm_packages as snp
|
|
assert (
|
|
lsa.BLOCKED_NPM_VERSIONS == snp.BLOCKED_NPM_VERSIONS
|
|
), "auditor and scanner BLOCKED_NPM_VERSIONS tables drifted"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Cargo.lock audit.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
_MALICIOUS_CARGO_LOCK = """\
|
|
version = 3
|
|
|
|
[[package]]
|
|
name = "fix-path-env"
|
|
version = "0.0.1"
|
|
source = "git+https://example.com/foo#deadbeef"
|
|
|
|
[[package]]
|
|
name = "honest-crate"
|
|
version = "1.0.0"
|
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
checksum = "0000000000000000000000000000000000000000000000000000000000000000"
|
|
"""
|
|
|
|
|
|
def test_malicious_cargo_lockfile_refused(tmp_path):
|
|
"""git+https:// Cargo source trips non-registry-cargo-source; --strict makes it blocking."""
|
|
lockfile = tmp_path / "Cargo.lock"
|
|
lockfile.write_text(_MALICIOUS_CARGO_LOCK)
|
|
proc = _run_auditor(
|
|
root = tmp_path,
|
|
npm_lockfiles = [FIXTURES / "clean_lockfile.json"],
|
|
cargo_lockfiles = [lockfile],
|
|
strict = True,
|
|
)
|
|
assert proc.returncode == 1
|
|
combined = proc.stdout + proc.stderr
|
|
assert "non-registry-cargo-source" in combined
|
|
assert "git+https://example.com" in combined
|
|
|
|
|
|
def test_malicious_cargo_lockfile_default_mode_advisory(tmp_path):
|
|
"""Default mode emits non-registry-cargo-source as advisory ::warning:: but exits 0."""
|
|
lockfile = tmp_path / "Cargo.lock"
|
|
lockfile.write_text(_MALICIOUS_CARGO_LOCK)
|
|
proc = _run_auditor(
|
|
root = tmp_path,
|
|
npm_lockfiles = [FIXTURES / "clean_lockfile.json"],
|
|
cargo_lockfiles = [lockfile],
|
|
)
|
|
assert proc.returncode == 0, (
|
|
f"expected exit 0 (advisory), got {proc.returncode}\n"
|
|
f"--- stdout ---\n{proc.stdout}\n--- stderr ---\n{proc.stderr}"
|
|
)
|
|
combined = proc.stdout + proc.stderr
|
|
assert "non-registry-cargo-source" in combined
|
|
assert "advisory finding" in combined
|
|
|
|
|
|
def test_audit_cargo_lockfile_direct_call(tmp_path):
|
|
lockfile = tmp_path / "Cargo.lock"
|
|
lockfile.write_text(_MALICIOUS_CARGO_LOCK)
|
|
findings = lsa.audit_cargo_lockfile(lockfile)
|
|
kinds = {f.kind for f in findings}
|
|
assert "non-registry-cargo-source" in kinds
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GitHub Actions annotation escape: ::warning:: / ::error:: messages
|
|
# are truncated at the first newline unless escaped, so the multi-line
|
|
# Finding must be collapsed via the spec'd %0A / %0D / %25 encoding.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_gha_escape_collapses_finding_to_one_line():
|
|
"""_gha_escape() encodes \\n/\\r/% so GHA annotations aren't truncated; % must escape first."""
|
|
assert lsa._gha_escape("a\nb\nc") == "a%0Ab%0Ac"
|
|
assert lsa._gha_escape("a\rb") == "a%0Db"
|
|
assert lsa._gha_escape("100%") == "100%25"
|
|
# Order regression: `%` must escape before `\n`, else escapes double-encode.
|
|
assert lsa._gha_escape("a%b\nc") == "a%25b%0Ac"
|
|
|
|
f = lsa.Finding(
|
|
path = "/x/lock.json",
|
|
package = "node_modules/foo",
|
|
kind = "missing-integrity-hash",
|
|
detail = "bad stuff",
|
|
)
|
|
escaped = lsa._gha_escape(str(f))
|
|
assert "\n" not in escaped
|
|
assert "%0A" in escaped
|
|
assert "missing-integrity-hash" in escaped
|
|
assert "node_modules/foo" in escaped
|
|
assert "bad stuff" in escaped
|
|
|
|
|
|
def test_advisory_finding_emitted_as_single_line_annotation(tmp_path):
|
|
"""Advisory ::warning:: must be one physical line (%0A-escaped). Regression for PR #5604."""
|
|
lockfile = tmp_path / "Cargo.lock"
|
|
lockfile.write_text(_MALICIOUS_CARGO_LOCK)
|
|
proc = _run_auditor(
|
|
root = tmp_path,
|
|
npm_lockfiles = [FIXTURES / "clean_lockfile.json"],
|
|
cargo_lockfiles = [lockfile],
|
|
)
|
|
warning_lines = [line for line in proc.stderr.splitlines() if line.startswith("::warning::")]
|
|
assert warning_lines, (
|
|
"expected at least one ::warning:: annotation; " f"stderr was:\n{proc.stderr}"
|
|
)
|
|
for line in warning_lines:
|
|
# One physical line: kind/package/detail joined via %0A, not split.
|
|
assert "%0A" in line, (
|
|
f"::warning:: line has no %0A escape; multi-line text "
|
|
f"would be truncated by GH Actions:\n{line}"
|
|
)
|
|
assert "non-registry-cargo-source" in line
|
|
assert "package:" in line
|
|
assert "detail:" in line
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SF4: skip env var requires a justification value.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_skip_env_var_with_short_value_rejected(tmp_path):
|
|
"""SF4: a short/boolean UNSLOTH_LOCKFILE_AUDIT_SKIP is rejected; a real justification is honored."""
|
|
fixture = FIXTURES / "clean_lockfile.json"
|
|
|
|
# Case 1 -- "1" rejected, audit RUNS.
|
|
env_bad = {**os.environ, "UNSLOTH_LOCKFILE_AUDIT_SKIP": "1"}
|
|
proc_bad = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--root",
|
|
str(tmp_path),
|
|
"--npm-lockfile",
|
|
str(fixture),
|
|
],
|
|
capture_output = True,
|
|
text = True,
|
|
timeout = 30,
|
|
env = env_bad,
|
|
)
|
|
combined_bad = proc_bad.stdout + proc_bad.stderr
|
|
assert "::warning::" in combined_bad, combined_bad
|
|
assert "REQUIRES a justification" in combined_bad, combined_bad
|
|
# Per-file banner proves the audit ran.
|
|
assert "[lockfile-audit] npm:" in combined_bad, combined_bad
|
|
# Clean fixture -> exit 0, but the audit was performed.
|
|
assert proc_bad.returncode == 0, (
|
|
f"expected rc 0 on clean fixture, got {proc_bad.returncode}\n"
|
|
f"--- stdout ---\n{proc_bad.stdout}\n"
|
|
f"--- stderr ---\n{proc_bad.stderr}"
|
|
)
|
|
|
|
# Case 2 -- a real-looking justification accepted, audit skipped.
|
|
env_ok = {**os.environ, "UNSLOTH_LOCKFILE_AUDIT_SKIP": "ticket-5397"}
|
|
proc_ok = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--root",
|
|
str(tmp_path),
|
|
"--npm-lockfile",
|
|
str(fixture),
|
|
],
|
|
capture_output = True,
|
|
text = True,
|
|
timeout = 30,
|
|
env = env_ok,
|
|
)
|
|
combined_ok = proc_ok.stdout + proc_ok.stderr
|
|
assert proc_ok.returncode == 0
|
|
assert "::warning::" in combined_ok
|
|
assert "skipped" in combined_ok.lower()
|
|
assert "ticket-5397" in combined_ok
|
|
# Skip path: no "npm:" banner means the audit body never ran.
|
|
assert "[lockfile-audit] npm:" not in combined_ok, combined_ok
|
|
|
|
# Case 3 -- the booleanish tokens are ALL rejected.
|
|
for bad_val in ("true", "yes", "on", "0", ""):
|
|
env_b = {**os.environ, "UNSLOTH_LOCKFILE_AUDIT_SKIP": bad_val}
|
|
p = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--root",
|
|
str(tmp_path),
|
|
"--npm-lockfile",
|
|
str(fixture),
|
|
],
|
|
capture_output = True,
|
|
text = True,
|
|
timeout = 30,
|
|
env = env_b,
|
|
)
|
|
c = p.stdout + p.stderr
|
|
assert (
|
|
"::warning::" in c and "REQUIRES" in c
|
|
), f"value {bad_val!r} should have been rejected; got:\n{c}"
|
|
assert "[lockfile-audit] npm:" in c, (
|
|
f"value {bad_val!r} should have fallen through to run audit; " f"got:\n{c}"
|
|
)
|