mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
301 lines
8.4 KiB
Python
301 lines
8.4 KiB
Python
#!/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())
|