mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
security: harden supply-chain CI gates
This commit is contained in:
@@ -30,6 +30,9 @@ jobs:
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: Supply-chain hardening check
|
||||
run: |
|
||||
python scripts/check_supply_chain_hardening.py
|
||||
- name: Install import deps
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
@@ -60,6 +63,9 @@ jobs:
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: Supply-chain hardening check
|
||||
run: |
|
||||
python scripts/check_supply_chain_hardening.py
|
||||
- name: Install preflight deps
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
@@ -69,7 +75,7 @@ jobs:
|
||||
python scripts/preflight_check.py --strict
|
||||
- name: Install Node deps
|
||||
run: |
|
||||
npm install
|
||||
npm ci
|
||||
- name: Install Playwright browsers
|
||||
run: |
|
||||
npx playwright install chromium
|
||||
@@ -89,6 +95,9 @@ jobs:
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: Supply-chain hardening check
|
||||
run: |
|
||||
python scripts/check_supply_chain_hardening.py
|
||||
- name: Install test deps
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
@@ -131,6 +140,9 @@ jobs:
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: Supply-chain hardening check
|
||||
run: |
|
||||
python scripts/check_supply_chain_hardening.py
|
||||
- name: Install test deps
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
@@ -163,6 +175,9 @@ jobs:
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: Supply-chain hardening check
|
||||
run: |
|
||||
python scripts/check_supply_chain_hardening.py
|
||||
- name: Install test deps
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
@@ -183,14 +198,16 @@ jobs:
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '20'
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Supply-chain hardening check
|
||||
run: |
|
||||
python scripts/check_supply_chain_hardening.py
|
||||
- name: Frontend Audit (npm)
|
||||
run: |
|
||||
# Audit only production dependencies, ignore dev
|
||||
npm audit --production
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install backend deps
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
@@ -214,6 +231,9 @@ jobs:
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Supply-chain hardening check
|
||||
run: |
|
||||
python scripts/check_supply_chain_hardening.py
|
||||
- name: Install test deps
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
@@ -246,6 +266,9 @@ jobs:
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Supply-chain hardening check
|
||||
run: |
|
||||
python scripts/check_supply_chain_hardening.py
|
||||
- name: Install test deps
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Dependency Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
- "requirements.txt"
|
||||
- "pyproject.toml"
|
||||
- ".github/workflows/dependency-review.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
name: Dependency Review
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: high
|
||||
comment-summary-in-pr: always
|
||||
@@ -9,6 +9,7 @@ on:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
@@ -32,6 +33,6 @@ jobs:
|
||||
echo "Skipping registry publish because pyproject version is unchanged."
|
||||
- name: Publish Custom Node
|
||||
if: steps.publish_guard.outputs.should_publish == 'true'
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
uses: Comfy-Org/publish-node-action@d2366e7abb6ab16f3bb03e3520ae25c8cf749bc9
|
||||
with:
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Repo-local supply-chain hardening checks.
|
||||
|
||||
This checker is intentionally stdlib-only and read-only so it can run before
|
||||
package installation. It detects known Mini Shai-Hulud package-family and
|
||||
persistence indicators without executing code from dependencies.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
AFFECTED_NPM_PREFIXES = (
|
||||
"@tanstack/",
|
||||
"@uipath/",
|
||||
"@mistralai/",
|
||||
"@opensearch-project/",
|
||||
"@squawk/",
|
||||
"@tallyui/",
|
||||
"@draftauth/",
|
||||
"@draftlab/",
|
||||
"@taskflow-corp/",
|
||||
"@tolka/",
|
||||
"@beproduct/",
|
||||
"@dirigible-ai/",
|
||||
"@ml-toolkit-ts/",
|
||||
"@supersurkhet/",
|
||||
"@mesadev/",
|
||||
)
|
||||
|
||||
AFFECTED_NPM_NAMES = {
|
||||
"safe-action",
|
||||
"agentwork-cli",
|
||||
"cmux-agent-mcp",
|
||||
"cross-stitch",
|
||||
"git-branch-selector",
|
||||
"git-git-git",
|
||||
"ml-toolkit-ts",
|
||||
"nextmove-mcp",
|
||||
"ts-dna",
|
||||
"wot-api",
|
||||
"intercom-client",
|
||||
}
|
||||
|
||||
AFFECTED_PYPI_NAMES = {
|
||||
"mistralai",
|
||||
"guardrails-ai",
|
||||
"lightning",
|
||||
"pytorch-lightning",
|
||||
"intercom-client",
|
||||
}
|
||||
|
||||
IOC_FILENAMES = {
|
||||
"router_init.js",
|
||||
"tanstack_runner.js",
|
||||
"opensearch_init.js",
|
||||
"setup.mjs",
|
||||
"setup_bun.js",
|
||||
"transformers.pyz",
|
||||
"shai-hulud-workflow.yml",
|
||||
"shai-hulud-workflow.yaml",
|
||||
}
|
||||
|
||||
IOC_STRINGS = {
|
||||
"@tanstack/setup",
|
||||
"git-tanstack",
|
||||
"83.142.209.194",
|
||||
"IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner",
|
||||
"Shai-Hulud",
|
||||
"shai-hulud",
|
||||
"Session Protocol",
|
||||
"transformers.pyz",
|
||||
}
|
||||
|
||||
# Current repo baseline. A new package lifecycle script must be reviewed.
|
||||
ALLOWED_INSTALL_SCRIPT_PACKAGES = {
|
||||
"esbuild",
|
||||
"fsevents",
|
||||
"vite/node_modules/fsevents",
|
||||
}
|
||||
|
||||
TEXT_SCAN_PATHS = (
|
||||
".github",
|
||||
".vscode",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"requirements.txt",
|
||||
"pyproject.toml",
|
||||
)
|
||||
|
||||
SKIP_DIR_NAMES = {
|
||||
".git",
|
||||
".planning",
|
||||
".pytest_cache",
|
||||
".tmp",
|
||||
".venv",
|
||||
".venv-wsl",
|
||||
"reference",
|
||||
"REFERENCE",
|
||||
"__pycache__",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Finding:
|
||||
code: str
|
||||
path: str
|
||||
detail: str
|
||||
|
||||
|
||||
def _normalize_package_name(name: str) -> str:
|
||||
return name.strip().lower().replace("_", "-")
|
||||
|
||||
|
||||
def _load_json(path: Path) -> Any:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def _iter_lock_packages(lock_path: Path) -> Iterable[tuple[str, str, dict[str, Any]]]:
|
||||
lock = _load_json(lock_path)
|
||||
packages = lock.get("packages") or {}
|
||||
for package_path, metadata in packages.items():
|
||||
if not package_path.startswith("node_modules/"):
|
||||
continue
|
||||
name = package_path.removeprefix("node_modules/")
|
||||
version = str((metadata or {}).get("version") or "")
|
||||
yield name, version, metadata or {}
|
||||
|
||||
|
||||
def check_npm_lock(lock_path: Path) -> list[Finding]:
|
||||
findings: list[Finding] = []
|
||||
if not lock_path.exists():
|
||||
return findings
|
||||
|
||||
for name, version, metadata in _iter_lock_packages(lock_path):
|
||||
normalized = _normalize_package_name(name)
|
||||
if normalized in AFFECTED_NPM_NAMES or normalized.startswith(
|
||||
AFFECTED_NPM_PREFIXES
|
||||
):
|
||||
findings.append(
|
||||
Finding(
|
||||
"mini-shai-hulud-npm-package",
|
||||
str(lock_path),
|
||||
f"{name}@{version} matches a known affected package family",
|
||||
)
|
||||
)
|
||||
if metadata.get("hasInstallScript") is True:
|
||||
if name not in ALLOWED_INSTALL_SCRIPT_PACKAGES:
|
||||
findings.append(
|
||||
Finding(
|
||||
"unexpected-npm-install-script",
|
||||
str(lock_path),
|
||||
f"{name}@{version} declares hasInstallScript=true and is not allowlisted",
|
||||
)
|
||||
)
|
||||
return findings
|
||||
|
||||
|
||||
_REQ_NAME_RE = re.compile(r"^\s*([A-Za-z0-9_.-]+)\s*(?:\[.*?\])?\s*(?:[<>=!~]=|==|~=|>|<|$)")
|
||||
_TOML_DEP_RE = re.compile(r"""["']([A-Za-z0-9_.-]+)(?:\[.*?\])?\s*(?:[<>=!~]=|==|~=|>|<|["'])""")
|
||||
|
||||
|
||||
def check_python_manifest(path: Path) -> list[Finding]:
|
||||
findings: list[Finding] = []
|
||||
if not path.exists():
|
||||
return findings
|
||||
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
candidates: set[str] = set()
|
||||
if path.name == "requirements.txt":
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#") or stripped.startswith("-"):
|
||||
continue
|
||||
match = _REQ_NAME_RE.match(stripped)
|
||||
if match:
|
||||
candidates.add(_normalize_package_name(match.group(1)))
|
||||
else:
|
||||
for match in _TOML_DEP_RE.finditer(text):
|
||||
candidates.add(_normalize_package_name(match.group(1)))
|
||||
|
||||
for name in sorted(candidates & AFFECTED_PYPI_NAMES):
|
||||
findings.append(
|
||||
Finding(
|
||||
"mini-shai-hulud-pypi-package",
|
||||
str(path),
|
||||
f"{name} matches a known affected PyPI package family",
|
||||
)
|
||||
)
|
||||
return findings
|
||||
|
||||
|
||||
def _is_skipped_dir(path: Path, root: Path) -> bool:
|
||||
try:
|
||||
rel_parts = path.relative_to(root).parts
|
||||
except ValueError:
|
||||
return True
|
||||
return any(part in SKIP_DIR_NAMES for part in rel_parts)
|
||||
|
||||
|
||||
def check_ioc_filenames(root: Path) -> list[Finding]:
|
||||
findings: list[Finding] = []
|
||||
# IMPORTANT: prune skipped dirs before stat; Windows cannot stat WSL venv links reliably.
|
||||
for current_dir, dir_names, file_names in os.walk(root):
|
||||
current_path = Path(current_dir)
|
||||
dir_names[:] = [
|
||||
name for name in dir_names if not _is_skipped_dir(current_path / name, root)
|
||||
]
|
||||
for file_name in file_names:
|
||||
if file_name in IOC_FILENAMES:
|
||||
path = current_path / file_name
|
||||
findings.append(
|
||||
Finding(
|
||||
"mini-shai-hulud-ioc-file",
|
||||
str(path.relative_to(root)),
|
||||
f"matched suspicious filename {file_name}",
|
||||
)
|
||||
)
|
||||
return findings
|
||||
|
||||
|
||||
def _iter_text_scan_files(root: Path) -> Iterable[Path]:
|
||||
for rel in TEXT_SCAN_PATHS:
|
||||
path = root / rel
|
||||
if path.is_file():
|
||||
yield path
|
||||
elif path.is_dir():
|
||||
for child in path.rglob("*"):
|
||||
if child.is_file() and not _is_skipped_dir(child, root):
|
||||
yield child
|
||||
|
||||
|
||||
def check_ioc_strings(root: Path) -> list[Finding]:
|
||||
findings: list[Finding] = []
|
||||
for path in _iter_text_scan_files(root):
|
||||
try:
|
||||
if path.stat().st_size > 2_000_000:
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
continue
|
||||
for needle in sorted(IOC_STRINGS):
|
||||
if needle in text:
|
||||
findings.append(
|
||||
Finding(
|
||||
"mini-shai-hulud-ioc-string",
|
||||
str(path.relative_to(root)),
|
||||
f"matched suspicious string {needle!r}",
|
||||
)
|
||||
)
|
||||
return findings
|
||||
|
||||
|
||||
def run_checks(root: Path) -> list[Finding]:
|
||||
root = root.resolve()
|
||||
findings: list[Finding] = []
|
||||
findings.extend(check_npm_lock(root / "package-lock.json"))
|
||||
findings.extend(check_npm_lock(root / "node_modules" / ".package-lock.json"))
|
||||
findings.extend(check_python_manifest(root / "requirements.txt"))
|
||||
findings.extend(check_python_manifest(root / "pyproject.toml"))
|
||||
findings.extend(check_ioc_filenames(root))
|
||||
findings.extend(check_ioc_strings(root))
|
||||
return findings
|
||||
|
||||
|
||||
def _print_findings(findings: Iterable[Finding]) -> None:
|
||||
for finding in findings:
|
||||
print(f"{finding.code}: {finding.path}: {finding.detail}")
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--root", default=".", help="Repository root to scan")
|
||||
parser.add_argument("--json", action="store_true", help="Emit JSON output")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
findings = run_checks(Path(args.root))
|
||||
if args.json:
|
||||
print(json.dumps([finding.__dict__ for finding in findings], indent=2))
|
||||
elif findings:
|
||||
_print_findings(findings)
|
||||
else:
|
||||
print("supply-chain hardening check passed")
|
||||
return 1 if findings else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -357,6 +357,8 @@ else
|
||||
fi
|
||||
|
||||
echo "[pre-push] Node version: $(node -v)"
|
||||
echo "[pre-push] 0/10 supply-chain hardening check"
|
||||
"$VENV_PY" scripts/check_supply_chain_hardening.py
|
||||
echo "[pre-push] 0/7 R120 dependency preflight"
|
||||
"$VENV_PY" scripts/preflight_check.py --strict
|
||||
echo "[pre-push] 1/7 detect-secrets"
|
||||
|
||||
@@ -38,7 +38,7 @@ function runPlaywright(args, { label }) {
|
||||
const cli = resolvePlaywrightCli();
|
||||
if (!cli) {
|
||||
console.error(
|
||||
`[OpenClaw] Failed to run ${label}: Playwright CLI not found. Did you run 'npm install'?`,
|
||||
`[OpenClaw] Failed to run ${label}: Playwright CLI not found. Did you run 'npm ci'?`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -90,8 +90,8 @@ ensure_npm_deps() {
|
||||
if [ -f "$ROOT_DIR/node_modules/@playwright/test/package.json" ]; then
|
||||
return 0
|
||||
fi
|
||||
echo "[tests] Installing frontend dependencies via npm install ..."
|
||||
npm install
|
||||
echo "[tests] Installing frontend dependencies via npm ci ..."
|
||||
npm ci
|
||||
}
|
||||
|
||||
# Always use project-local venv to avoid global interpreter / tool drift.
|
||||
@@ -189,6 +189,9 @@ fi
|
||||
|
||||
echo "[tests] Node version: $(node -v)"
|
||||
|
||||
echo "[tests] 0/11 supply-chain hardening check"
|
||||
"$VENV_PY" scripts/check_supply_chain_hardening.py
|
||||
|
||||
ensure_npm_deps
|
||||
|
||||
echo "[tests] 0/9 R120 dependency preflight"
|
||||
|
||||
@@ -63,8 +63,8 @@ function Ensure-NpmDeps {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "[tests] Installing frontend dependencies via npm install ..."
|
||||
Invoke-Checked "npm install" { npm install }
|
||||
Write-Host "[tests] Installing frontend dependencies via npm ci ..."
|
||||
Invoke-Checked "npm ci" { npm ci }
|
||||
}
|
||||
|
||||
# Prefer project-local virtualenv to avoid global PATH / cache conflicts on Windows.
|
||||
@@ -260,6 +260,11 @@ else {
|
||||
}
|
||||
|
||||
Write-Host "[tests] Node version: $(node -v)"
|
||||
|
||||
Write-Host "[tests] 0/11 supply-chain hardening check"
|
||||
Invoke-Checked "supply-chain hardening check" {
|
||||
& $venvPython scripts\check_supply_chain_hardening.py
|
||||
}
|
||||
Ensure-NpmDeps
|
||||
|
||||
Write-Host "[tests] 0/8 R120 dependency preflight"
|
||||
|
||||
@@ -34,7 +34,7 @@ node -v
|
||||
npm -v
|
||||
python --version
|
||||
|
||||
npm install
|
||||
npm ci
|
||||
npx playwright install chromium
|
||||
|
||||
npm test
|
||||
@@ -59,7 +59,7 @@ python3 --version
|
||||
mkdir -p .tmp/bin
|
||||
ln -sf "$(command -v python3)" .tmp/bin/python
|
||||
|
||||
npm install
|
||||
npm ci
|
||||
npx playwright install chromium
|
||||
|
||||
# Run with safe temp directory (WSL /mnt/*)
|
||||
@@ -145,7 +145,7 @@ node -v
|
||||
npm -v
|
||||
python --version
|
||||
|
||||
npm install
|
||||
npm ci
|
||||
npx playwright install chromium
|
||||
npm test
|
||||
```
|
||||
@@ -161,7 +161,7 @@ python3 --version
|
||||
mkdir -p .tmp/bin
|
||||
ln -sf "$(command -v python3)" .tmp/bin/python
|
||||
|
||||
npm install
|
||||
npm ci
|
||||
npx playwright install chromium
|
||||
|
||||
mkdir -p .tmp/playwright
|
||||
@@ -174,7 +174,7 @@ TMPDIR=.tmp/playwright TMP=.tmp/playwright TEMP=.tmp/playwright \
|
||||
- `python: command not found` on WSL: create `.tmp/bin/python` as a shim to `python3`.
|
||||
- Port bind failure: use the repo-documented E2E port override or stop the conflicting process.
|
||||
- Browser missing: run `npx playwright install chromium`.
|
||||
- Dependency drift: remove `node_modules` and rerun `npm install`.
|
||||
- Dependency drift: remove `node_modules` and rerun `npm ci`.
|
||||
|
||||
### Non-applicable E2E
|
||||
|
||||
|
||||
+2
-2
@@ -59,7 +59,7 @@ Required guardrails:
|
||||
- Node.js 18+ (CI uses 20)
|
||||
- `pre-commit` installed: `python -m pip install pre-commit`
|
||||
- Backend test deps available in the same interpreter (`numpy`, `pillow`, `aiohttp`)
|
||||
- Frontend deps installed: `npm install`
|
||||
- Frontend deps installed: `npm ci`
|
||||
|
||||
## Environment Sanity (Required Guardrails)
|
||||
|
||||
@@ -390,7 +390,7 @@ node -v
|
||||
# Then re-check:
|
||||
# node -v
|
||||
#
|
||||
# IMPORTANT: run `npm install` with the same Node version you use for `npm test`.
|
||||
# IMPORTANT: run `npm ci` with the same Node version you use for `npm test`.
|
||||
|
||||
# One-time browser install (recommended)
|
||||
npx playwright install chromium
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ Playwright-based end-to-end tests for the OpenClaw sidebar UI.
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm ci
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@ EXPECTED_PERMISSION_BLOCKS = {
|
||||
" contents: read\n"
|
||||
" security-events: write\n"
|
||||
),
|
||||
"dependency-review.yml": (
|
||||
"permissions:\n" " contents: read\n" " pull-requests: read\n"
|
||||
),
|
||||
"pre-commit.yml": "permissions:\n contents: read\n",
|
||||
"publish.yml": "permissions:\n contents: read\n issues: write\n",
|
||||
"secret-scan.yml": "permissions:\n contents: read\n",
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
WORKFLOW_ROOT = REPO_ROOT / ".github" / "workflows"
|
||||
|
||||
FULL_SHA_RE = re.compile(r"^[a-f0-9]{40}$")
|
||||
USES_RE = re.compile(r"^\s*uses:\s*([^@\s]+)@([^\s#]+)", re.MULTILINE)
|
||||
|
||||
VERSION_TAG_ACTIONS = {
|
||||
"actions/checkout",
|
||||
"actions/dependency-review-action",
|
||||
"actions/setup-node",
|
||||
"actions/setup-python",
|
||||
"actions/upload-artifact",
|
||||
"github/codeql-action/init",
|
||||
"github/codeql-action/analyze",
|
||||
}
|
||||
|
||||
|
||||
class TestSupplyChainWorkflowPolicy(unittest.TestCase):
|
||||
def test_workflows_do_not_use_privileged_pr_or_oidc_boundaries(self):
|
||||
for workflow_path in sorted(WORKFLOW_ROOT.glob("*.yml")):
|
||||
text = workflow_path.read_text(encoding="utf-8")
|
||||
with self.subTest(workflow=workflow_path.name):
|
||||
self.assertNotIn("pull_request_target:", text)
|
||||
self.assertNotIn("id-token: write", text)
|
||||
self.assertNotIn("uses: actions/cache@", text)
|
||||
|
||||
def test_third_party_actions_use_immutable_sha_refs(self):
|
||||
for workflow_path in sorted(WORKFLOW_ROOT.glob("*.yml")):
|
||||
text = workflow_path.read_text(encoding="utf-8")
|
||||
for action, ref in USES_RE.findall(text):
|
||||
if action.startswith("./"):
|
||||
continue
|
||||
with self.subTest(workflow=workflow_path.name, action=action):
|
||||
if action in VERSION_TAG_ACTIONS:
|
||||
self.assertRegex(ref, r"^v\d+(?:\.\d+){0,2}$")
|
||||
else:
|
||||
self.assertRegex(ref, FULL_SHA_RE)
|
||||
|
||||
def test_publish_workflow_keeps_narrow_release_boundary(self):
|
||||
text = (WORKFLOW_ROOT / "publish.yml").read_text(encoding="utf-8")
|
||||
self.assertIn("workflow_dispatch:", text)
|
||||
self.assertIn("push:", text)
|
||||
self.assertNotIn("pull_request:", text)
|
||||
self.assertNotIn("pull_request_target:", text)
|
||||
self.assertNotIn("id-token: write", text)
|
||||
self.assertNotIn("uses: actions/cache@", text)
|
||||
self.assertRegex(
|
||||
text,
|
||||
re.compile(r"uses: Comfy-Org/publish-node-action@[a-f0-9]{40}\b"),
|
||||
)
|
||||
self.assertIn("personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}", text)
|
||||
|
||||
def test_dependency_review_gate_is_present_for_dependency_diffs(self):
|
||||
text = (WORKFLOW_ROOT / "dependency-review.yml").read_text(encoding="utf-8")
|
||||
self.assertIn("pull_request:", text)
|
||||
self.assertIn("package-lock.json", text)
|
||||
self.assertIn("requirements.txt", text)
|
||||
self.assertIn("pyproject.toml", text)
|
||||
self.assertIn("permissions:\n contents: read\n pull-requests: read\n", text)
|
||||
self.assertIn("uses: actions/dependency-review-action@v4", text)
|
||||
self.assertIn("fail-on-severity: high", text)
|
||||
self.assertNotIn("secrets.", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -10,14 +10,14 @@ class TestFullTestScriptPlaywrightBootstrap(unittest.TestCase):
|
||||
def test_linux_full_gate_bootstraps_frontend_deps_and_browsers(self):
|
||||
content = LINUX_SCRIPT.read_text(encoding="utf-8")
|
||||
self.assertIn("ensure_npm_deps", content)
|
||||
self.assertIn("npm install", content)
|
||||
self.assertIn("npm ci", content)
|
||||
self.assertIn("OPENCLAW_PLAYWRIGHT_INSTALL=1", content)
|
||||
self.assertIn("OPENCLAW_PLAYWRIGHT_BROWSERS=chromium", content)
|
||||
|
||||
def test_windows_full_gate_bootstraps_frontend_deps_and_browsers(self):
|
||||
content = WINDOWS_SCRIPT.read_text(encoding="utf-8")
|
||||
self.assertIn("Ensure-NpmDeps", content)
|
||||
self.assertIn("npm install", content)
|
||||
self.assertIn("npm ci", content)
|
||||
self.assertIn('$env:OPENCLAW_PLAYWRIGHT_INSTALL = "1"', content)
|
||||
self.assertIn('$env:OPENCLAW_PLAYWRIGHT_BROWSERS = "chromium"', content)
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ DEPRECATED_ACTION_PATTERNS = {
|
||||
"Comfy-Org/publish-node-action@v1": re.compile(
|
||||
r"\bComfy-Org/publish-node-action@v1\b"
|
||||
),
|
||||
"Comfy-Org/publish-node-action@main": re.compile(
|
||||
r"\bComfy-Org/publish-node-action@main\b"
|
||||
),
|
||||
}
|
||||
|
||||
EXPECTED_ACTION_PATTERNS = {
|
||||
@@ -65,9 +68,9 @@ class GitHubActionsRuntimeContractTests(unittest.TestCase):
|
||||
|
||||
def test_publish_workflow_uses_official_publish_action(self):
|
||||
text = (WORKFLOW_DIR / "publish.yml").read_text(encoding="utf-8")
|
||||
self.assertIn(
|
||||
"uses: Comfy-Org/publish-node-action@main",
|
||||
self.assertRegex(
|
||||
text,
|
||||
re.compile(r"uses: Comfy-Org/publish-node-action@[a-f0-9]{40}\b"),
|
||||
)
|
||||
self.assertIn(
|
||||
"personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}",
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from scripts import check_supply_chain_hardening as hardening
|
||||
|
||||
|
||||
def write_lock(path: Path, packages: dict[str, dict[str, object]]) -> None:
|
||||
path.write_text(
|
||||
json.dumps({"lockfileVersion": 3, "packages": packages}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
class TestSupplyChainHardening(unittest.TestCase):
|
||||
def test_clean_lockfile_has_no_findings(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
write_lock(
|
||||
root / "package-lock.json",
|
||||
{
|
||||
"": {"name": "example"},
|
||||
"node_modules/@playwright/test": {"version": "1.0.0"},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.0",
|
||||
"hasInstallScript": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(hardening.run_checks(root), [])
|
||||
|
||||
def test_affected_npm_package_family_is_reported(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
write_lock(
|
||||
root / "package-lock.json",
|
||||
{
|
||||
"node_modules/@tanstack/router-core": {"version": "9.9.9"},
|
||||
},
|
||||
)
|
||||
|
||||
findings = hardening.run_checks(root)
|
||||
|
||||
self.assertEqual(findings[0].code, "mini-shai-hulud-npm-package")
|
||||
|
||||
def test_unexpected_npm_install_script_is_reported(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
write_lock(
|
||||
root / "package-lock.json",
|
||||
{
|
||||
"node_modules/unreviewed-package": {
|
||||
"version": "1.2.3",
|
||||
"hasInstallScript": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
findings = hardening.run_checks(root)
|
||||
|
||||
self.assertEqual(findings[0].code, "unexpected-npm-install-script")
|
||||
|
||||
def test_affected_python_requirement_is_reported(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
(root / "requirements.txt").write_text("mistralai==2.4.6\n", encoding="utf-8")
|
||||
|
||||
findings = hardening.run_checks(root)
|
||||
|
||||
self.assertEqual(findings[0].code, "mini-shai-hulud-pypi-package")
|
||||
|
||||
def test_ioc_filename_is_reported_without_executing_dependency_code(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
package_dir = root / "node_modules" / "package"
|
||||
package_dir.mkdir(parents=True)
|
||||
(package_dir / "router_init.js").write_text("void 0;\n", encoding="utf-8")
|
||||
|
||||
findings = hardening.run_checks(root)
|
||||
|
||||
self.assertEqual(findings[0].code, "mini-shai-hulud-ioc-file")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user