更新: 359 个文件 - 2026-03-16 23:30:01

这个提交包含在:
hao
2026-03-16 23:30:01 -07:00
父节点 527990f535
当前提交 2974cd9ad9
修改 359 个文件,包含 6332 行新增673 行删除

1
scripts/lab/__init__.py 普通文件
查看文件

@@ -0,0 +1 @@
"""Local authorized lab orchestration package."""

66
scripts/lab/attack.py 普通文件
查看文件

@@ -0,0 +1,66 @@
from __future__ import annotations
import subprocess
from pathlib import Path
from typing import Any, Dict, List
from lab.utils import write_json
TOOL_COMMANDS = {
"xss-fuzzer": ["python3", "/Users/x/websafe/02-xss/tools/xss-fuzzer.py"],
"xss-scanner": ["go", "run", "/Users/x/websafe/02-xss/tools/xss-scanner.go"],
"sqli-scanner": ["python3", "/Users/x/websafe/01-sql-injection/tools/sqli-scanner.py"],
"blind-sqli": ["python3", "/Users/x/websafe/01-sql-injection/tools/blind-sqli.py"],
"session-lab": ["python3", "/Users/x/websafe/03-authentication/session/tools/session-lab.py"],
"misconfig-lab": ["python3", "/Users/x/websafe/04-server-security/misconfiguration/tools/misconfig-lab.py"],
"tls-scanner": ["python3", "/Users/x/websafe/04-server-security/tls/tools/tls-scanner.py"],
"site-scope-mapper": ["python3", "/Users/x/websafe/04-server-security/infrastructure/tools/site-scope-mapper.py"],
}
def _render_args(step: Dict[str, Any], profile: Dict[str, Any], advisory: Dict[str, Any], run_dir: Path) -> List[str]:
target = (profile.get("baseline_urls") or [""])[0]
mapping = {
"{target_url}": target,
"{run_id}": run_dir.name,
"{case_id}": advisory["canonical_id"],
"{evidence_dir}": str(run_dir / "logs"),
}
args: List[str] = []
for item in step.get("args", []):
rendered = item
for key, value in mapping.items():
rendered = rendered.replace(key, value)
args.append(rendered)
return args
def run_attack(profile: Dict[str, Any], advisory: Dict[str, Any], run_dir: Path, dry_run: bool = False) -> Dict[str, Any]:
steps: List[Dict[str, Any]] = []
for step in profile.get("attack_actions", []):
tool_name = step.get("tool")
args = _render_args(step, profile, advisory, run_dir)
record = {
"kind": step.get("kind", "tool"),
"tool": tool_name,
"args": args,
"status": "planned" if dry_run else "skipped",
}
if step.get("kind") == "tool" and tool_name in TOOL_COMMANDS and not dry_run:
output_path = run_dir / "logs" / f"{tool_name}.json"
cmd = TOOL_COMMANDS[tool_name] + args + ["--ack-authorized", "--format", "json", "--output", str(output_path)]
completed = subprocess.run(cmd, text=True, capture_output=True)
record.update(
{
"status": "completed" if completed.returncode == 0 else "failed",
"returncode": completed.returncode,
"stdout_excerpt": completed.stdout[-400:],
"stderr_excerpt": completed.stderr[-400:],
"result_path": str(output_path),
}
)
steps.append(record)
payload = {"steps": steps}
write_json(run_dir / "logs" / "attack.json", payload)
return payload

28
scripts/lab/baseline.py 普通文件
查看文件

@@ -0,0 +1,28 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, List
import requests
from lab.utils import write_json
def collect(profile: Dict[str, Any], run_dir: Path, timeout: float = 8.0) -> Dict[str, Any]:
observations: List[Dict[str, Any]] = []
for url in profile.get("baseline_urls", []):
try:
response = requests.get(url, timeout=timeout, verify=False)
observations.append(
{
"url": url,
"status_code": response.status_code,
"headers": dict(response.headers),
"body_excerpt": response.text[:400],
}
)
except Exception as exc:
observations.append({"url": url, "error": str(exc)})
payload = {"observations": observations}
write_json(run_dir / "logs" / "baseline.json", payload)
return payload

47
scripts/lab/browser.py 普通文件
查看文件

@@ -0,0 +1,47 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, List
from lab.utils import ensure_dir, write_json
def capture(url: str, run_dir: Path, prefix: str = "baseline") -> Dict[str, Any]:
payload: Dict[str, Any] = {
"required": True,
"present": False,
"refs": [],
"reason": "playwright runtime unavailable",
}
try:
from playwright.sync_api import sync_playwright # type: ignore
except Exception:
write_json(run_dir / "logs" / f"{prefix}-browser.json", payload)
return payload
assets_dir = run_dir / "assets"
ensure_dir(assets_dir)
screenshot_path = assets_dir / f"{prefix}.png"
dom_path = assets_dir / f"{prefix}-dom.html"
console_path = run_dir / "logs" / f"{prefix}-console.json"
network_path = run_dir / "logs" / f"{prefix}-network.json"
console_messages: List[Dict[str, Any]] = []
requests_seen: List[Dict[str, Any]] = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.on("console", lambda msg: console_messages.append({"type": msg.type, "text": msg.text}))
page.on("request", lambda req: requests_seen.append({"method": req.method, "url": req.url}))
page.goto(url, wait_until="networkidle", timeout=20000)
page.screenshot(path=str(screenshot_path), full_page=True)
dom_path.write_text(page.content(), encoding="utf-8")
browser.close()
write_json(console_path, console_messages)
write_json(network_path, requests_seen)
payload = {
"required": True,
"present": True,
"refs": [str(screenshot_path), str(dom_path), str(console_path), str(network_path)],
}
write_json(run_dir / "logs" / f"{prefix}-browser.json", payload)
return payload

254
scripts/lab/catalog.py 普通文件
查看文件

@@ -0,0 +1,254 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, List, Tuple
import yaml
from intel.config import load_source_map
from lab.config import ENV_CATALOG_DIR, ENV_PROFILES_DIR, REPRO_MAP_PATH
from lab.utils import ensure_dir, read_yaml, slugify, write_yaml
IMAGE_HINTS: Dict[str, Dict[str, Any]] = {
"wordpress": {
"artifact_mode": "official-image",
"services": {
"app": {"image": "wordpress:php8.2-apache", "ports": ["18080:80"]},
"db": {
"image": "mariadb:10.11",
"environment": {
"MARIADB_DATABASE": "wordpress",
"MARIADB_USER": "wordpress",
"MARIADB_PASSWORD": "wordpress",
"MARIADB_ROOT_PASSWORD": "root",
},
},
},
"browser_required": True,
},
"drupal": {
"artifact_mode": "official-image",
"services": {
"app": {"image": "drupal:10-apache", "ports": ["18081:80"]},
"db": {
"image": "postgres:15",
"environment": {
"POSTGRES_DB": "drupal",
"POSTGRES_USER": "drupal",
"POSTGRES_PASSWORD": "drupal",
},
},
},
"browser_required": True,
},
"joomla": {
"artifact_mode": "official-image",
"services": {
"app": {"image": "joomla:latest", "ports": ["18082:80"]},
"db": {
"image": "mariadb:10.11",
"environment": {
"MARIADB_DATABASE": "joomla",
"MARIADB_USER": "joomla",
"MARIADB_PASSWORD": "joomla",
"MARIADB_ROOT_PASSWORD": "root",
},
},
},
"browser_required": True,
},
"prestashop": {
"artifact_mode": "official-image",
"services": {
"app": {"image": "prestashop/prestashop:latest", "ports": ["18083:80"]},
"db": {
"image": "mariadb:10.11",
"environment": {
"MARIADB_DATABASE": "prestashop",
"MARIADB_USER": "prestashop",
"MARIADB_PASSWORD": "prestashop",
"MARIADB_ROOT_PASSWORD": "root",
},
},
},
"browser_required": True,
},
"opencart": {
"artifact_mode": "official-image",
"services": {
"app": {"image": "bitnami/opencart:latest", "ports": ["18084:8080"]},
"db": {
"image": "mariadb:10.11",
"environment": {
"MARIADB_DATABASE": "opencart",
"MARIADB_USER": "opencart",
"MARIADB_PASSWORD": "opencart",
"MARIADB_ROOT_PASSWORD": "root",
},
},
},
"browser_required": True,
},
"gitea": {
"artifact_mode": "official-image",
"services": {
"app": {"image": "gitea/gitea:1.22.6", "ports": ["18085:3000"]},
},
"browser_required": True,
},
"nginx": {
"artifact_mode": "official-image",
"services": {"app": {"image": "nginx:1.27-alpine", "ports": ["18086:80"]}},
"browser_required": False,
},
"apache-httpd": {
"artifact_mode": "official-image",
"services": {"app": {"image": "httpd:2.4", "ports": ["18087:80"]}},
"browser_required": False,
},
"apache-tomcat": {
"artifact_mode": "official-image",
"services": {"app": {"image": "tomcat:10.1", "ports": ["18088:8080"]}},
"browser_required": False,
},
"nodejs": {
"artifact_mode": "official-source",
"services": {"app": {"image": "node:22-alpine", "ports": ["18089:3000"]}},
"browser_required": False,
},
"nextjs": {
"artifact_mode": "official-source",
"services": {"app": {"image": "node:22-alpine", "ports": ["18090:3000"]}},
"browser_required": True,
},
"vue": {
"artifact_mode": "official-source",
"services": {"app": {"image": "node:22-alpine", "ports": ["18091:5173"]}},
"browser_required": True,
},
"nuxt": {
"artifact_mode": "official-source",
"services": {"app": {"image": "node:22-alpine", "ports": ["18092:3000"]}},
"browser_required": True,
},
"vite": {
"artifact_mode": "official-source",
"services": {"app": {"image": "node:22-alpine", "ports": ["18093:5173"]}},
"browser_required": True,
},
}
def _default_repro_family(system: Dict[str, Any]) -> str:
topics = set(system.get("secure_code_topics", []))
text = " ".join([system["display_name"], system["system_id"], *topics]).lower()
if "xss" in text or "trusted types" in text:
return "xss-generic"
if "proxy" in text or "middleware" in text:
return "proxy-boundary-generic"
if "upload" in text:
return "file-upload-generic"
if "ssrf" in text:
return "ssrf-generic"
if "deserialization" in text:
return "deserialization-generic"
if "template" in text:
return "template-injection-generic"
if "token" in text or "cookie" in text or "session" in text:
return "session-token-generic"
if any(mode in {"plugin", "module", "extension"} for mode in system.get("advisory_modes", [])):
return "plugin-extension-generic"
return "authz-bypass-generic"
def _fallback_port(system_id: str) -> str:
base = 18100 + (sum(ord(ch) for ch in system_id) % 700)
return f"{base}:80"
def _system_catalog(system: Dict[str, Any]) -> Dict[str, Any]:
hint = IMAGE_HINTS.get(system["system_id"], {})
artifact_mode = hint.get("artifact_mode", "synthetic")
services = hint.get(
"services",
{"app": {"image": "nginxdemos/hello:latest", "ports": [_fallback_port(system["system_id"])]}},
)
return {
"system_id": system["system_id"],
"display_name": system["display_name"],
"category": system["category"],
"tier": system["tier"],
"artifact_mode_preference": [
artifact_mode,
"official-source" if artifact_mode != "official-source" else "synthetic",
"synthetic",
],
"default_repro_family": _default_repro_family(system),
"browser_required_default": bool(hint.get("browser_required", system["category"] in {"cms", "ecommerce", "frameworks", "platforms"})),
"log_collectors": ["docker-logs", "http-snapshot"],
"report_template": "default-lab-report",
"services": services,
"source_reference": system.get("official_sources", [])[:2],
}
def _profile_for_system(system: Dict[str, Any], catalog: Dict[str, Any]) -> Dict[str, Any]:
ports = []
for service in catalog["services"].values():
ports.extend(service.get("ports", []))
baseline_url = None
if ports:
first = str(ports[0]).split(":")[0]
baseline_url = f"http://127.0.0.1:{first}/"
return {
"profile_id": f"{system['system_id']}-core-current",
"system_id": system["system_id"],
"version": "current",
"artifact_mode": catalog["artifact_mode_preference"][0],
"verification_mode": "real" if catalog["artifact_mode_preference"][0] != "synthetic" else "synthetic",
"browser_required": catalog["browser_required_default"],
"services": catalog["services"],
"baseline_urls": [baseline_url] if baseline_url else [],
"seed_actions": [{"kind": "note", "message": "Use default seed strategy derived from repro profile."}],
"cleanup_policy": "destroy",
}
def _build_repro_map_entry(system: Dict[str, Any], catalog: Dict[str, Any]) -> Dict[str, Any]:
return {
"system_id": system["system_id"],
"default_repro_family": catalog["default_repro_family"],
"provisioning_mode_preference": catalog["artifact_mode_preference"],
"browser_required_default": catalog["browser_required_default"],
"seed_strategy": "default-seed" if catalog["browser_required_default"] else "minimal-seed",
"log_collectors": catalog["log_collectors"],
"report_template": catalog["report_template"],
}
def sync_catalog(write_profiles: bool = True, write_repro_map: bool = True) -> Dict[str, Any]:
source_map = load_source_map()
ensure_dir(ENV_CATALOG_DIR)
ensure_dir(ENV_PROFILES_DIR / "core")
written_catalogs = 0
written_profiles = 0
repro_entries: List[Dict[str, Any]] = []
for system in source_map["systems"]:
catalog = _system_catalog(system)
catalog_path = ENV_CATALOG_DIR / f"{system['system_id']}.yaml"
write_yaml(catalog_path, catalog)
written_catalogs += 1
if write_profiles:
profile_dir = ENV_PROFILES_DIR / "core" / system["system_id"]
write_yaml(profile_dir / "current.yaml", _profile_for_system(system, catalog))
written_profiles += 1
repro_entries.append(_build_repro_map_entry(system, catalog))
if write_repro_map:
write_yaml(REPRO_MAP_PATH, {"systems": repro_entries})
return {
"systems": len(source_map["systems"]),
"catalogs_written": written_catalogs,
"profiles_written": written_profiles,
"repro_map_written": write_repro_map,
}

32
scripts/lab/compose.py 普通文件
查看文件

@@ -0,0 +1,32 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, Tuple
from lab.utils import write_yaml
def compose_payload(profile: Dict[str, Any]) -> Dict[str, Any]:
services = {}
for service_name, service in profile.get("services", {}).items():
payload = {
"image": service["image"],
}
if service.get("ports"):
payload["ports"] = service["ports"]
if service.get("environment"):
payload["environment"] = service["environment"]
if service.get("depends_on"):
payload["depends_on"] = service["depends_on"]
services[service_name] = payload
return {
"services": services,
"networks": {"labnet": {"driver": "bridge"}},
}
def generate_compose(profile: Dict[str, Any], run_dir: Path) -> Tuple[Path, Dict[str, Any]]:
payload = compose_payload(profile)
compose_path = run_dir / "compose" / "compose.yaml"
write_yaml(compose_path, payload)
return compose_path, payload

25
scripts/lab/config.py 普通文件
查看文件

@@ -0,0 +1,25 @@
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SCRIPTS_ROOT = ROOT / "scripts"
INTEL_ROOT = ROOT / "08-threat-intel"
REGISTRY_ROOT = INTEL_ROOT / "registry"
ADVISORIES_DIR = REGISTRY_ROOT / "advisories"
SYSTEMS_DIR = REGISTRY_ROOT / "systems"
RUNS_DIR = REGISTRY_ROOT / "runs"
TRIAGE_DIR = REGISTRY_ROOT / "triage"
GENERATED_DIR = INTEL_ROOT / "generated"
DASHBOARD_DIR = GENERATED_DIR / "dashboard"
QUEUE_DIR = INTEL_ROOT / "queue"
QUEUE_PATH = QUEUE_DIR / "repro-queue.json"
SOURCE_MAP_PATH = INTEL_ROOT / "source-map.yaml"
REPRO_MAP_PATH = INTEL_ROOT / "repro-map.yaml"
REPRO_PROFILES_DIR = INTEL_ROOT / "repro-profiles"
CASE_RUNS_DIR = ROOT / "06-case-studies" / "generated-runs"
ENV_ROOT = ROOT / "00-environments"
ENV_CATALOG_DIR = ENV_ROOT / "catalog" / "systems"
ENV_PROFILES_DIR = ENV_ROOT / "profiles"
ENV_TEMPLATES_DIR = ENV_ROOT / "templates" / "synthetic"

32
scripts/lab/evidence.py 普通文件
查看文件

@@ -0,0 +1,32 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, List
from lab.utils import command_available, run, write_json
def collect_container_logs(run_dir: Path, compose_path: Path) -> List[str]:
if not command_available("docker"):
return []
log_dir = run_dir / "logs" / "docker"
log_dir.mkdir(parents=True, exist_ok=True)
ps = run(["docker", "compose", "-f", str(compose_path), "ps", "--services"], cwd=run_dir)
refs: List[str] = []
if ps.returncode != 0:
return refs
for service in ps.stdout.splitlines():
service = service.strip()
if not service:
continue
result = run(["docker", "compose", "-f", str(compose_path), "logs", "--no-color", service], cwd=run_dir)
path = log_dir / f"{service}.log"
path.write_text(result.stdout or result.stderr or "", encoding="utf-8")
refs.append(str(path))
return refs
def write_run_bundle(run_dir: Path, bundle: Dict[str, Any]) -> Path:
path = run_dir / "run.json"
write_json(path, bundle)
return path

380
scripts/lab/main.py 普通文件
查看文件

@@ -0,0 +1,380 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from typing import Any, Dict, List
CURRENT_DIR = Path(__file__).resolve().parent
SCRIPTS_DIR = CURRENT_DIR.parent
if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))
from lab import attack, baseline, browser, catalog, evidence, provision, render, repro, seed, task_queue, validators # noqa: E402
from lab.config import ADVISORIES_DIR, CASE_RUNS_DIR, ENV_PROFILES_DIR, RUNS_DIR # noqa: E402
from lab.utils import command_available, ensure_dir, isoformat, load_json_dir, now_utc, read_json, read_yaml, write_json # noqa: E402
def _load_advisory(canonical_id: str) -> Dict[str, Any]:
advisory = read_json(ADVISORIES_DIR / f"{canonical_id}.json", default=None)
if not advisory:
raise ValueError(f"Unknown advisory: {canonical_id}")
return advisory
def _run_dir(run_id: str) -> Path:
path = CASE_RUNS_DIR / run_id
ensure_dir(path)
ensure_dir(path / "logs")
ensure_dir(path / "assets")
return path
def _compose_run_id(advisory: Dict[str, Any]) -> str:
return f"{advisory['system_id']}-{advisory['canonical_id']}-{now_utc().strftime('%Y%m%d%H%M%S')}"
def _resolve_profile(advisory: Dict[str, Any]) -> Dict[str, Any]:
profile = repro.resolve_profile(advisory["canonical_id"], advisory)
current_profile = read_yaml(ENV_PROFILES_DIR / "core" / advisory["system_id"] / "current.yaml", default={}) or {}
merged = dict(current_profile)
merged.update(profile)
if current_profile.get("services") and not merged.get("services"):
merged["services"] = current_profile["services"]
if current_profile.get("baseline_urls") and not merged.get("baseline_urls"):
merged["baseline_urls"] = current_profile["baseline_urls"]
if current_profile.get("artifact_mode") and not merged.get("artifact_mode"):
merged["artifact_mode"] = current_profile["artifact_mode"]
if current_profile.get("verification_mode") and not merged.get("verification_mode"):
merged["verification_mode"] = current_profile["verification_mode"]
if current_profile.get("browser_required"):
merged.setdefault("browser_assertions", {})
merged["browser_assertions"].setdefault("required", current_profile["browser_required"])
if not profile.get("system_id"):
merged["system_id"] = advisory["system_id"]
if not profile.get("profile_id"):
merged["profile_id"] = advisory["canonical_id"]
return merged
def _build_run_bundle(
advisory: Dict[str, Any],
profile: Dict[str, Any],
run_id: str,
verification_status: str,
verification_mode: str,
artifact_mode: str,
baseline_refs: List[str],
attack_steps: List[Dict[str, Any]],
browser_refs: List[str],
container_log_refs: List[str],
request_log_refs: List[str],
blocked_reason: str | None,
) -> Dict[str, Any]:
return {
"run_id": run_id,
"system_id": advisory["system_id"],
"advisory_id": advisory["canonical_id"],
"repro_profile_id": profile["profile_id"],
"verification_status": verification_status,
"verification_mode": verification_mode,
"artifact_mode": artifact_mode,
"target_env": "local-docker",
"compose_services": sorted(profile.get("services", {}).keys()),
"baseline_refs": baseline_refs,
"attack_steps": attack_steps,
"browser_refs": browser_refs,
"container_log_refs": container_log_refs,
"request_log_refs": request_log_refs,
"timeline": [],
"started_at": isoformat(now_utc()),
"finished_at": isoformat(now_utc()),
"blocked_reason": blocked_reason,
}
def cmd_catalog_sync(args) -> int:
summary = catalog.sync_catalog(write_profiles=True, write_repro_map=True)
print(summary)
return 0
def cmd_compose_generate(args) -> int:
advisory = _load_advisory(args.case)
profile = _resolve_profile(advisory)
run_dir = _run_dir(args.run_id or f"compose-{advisory['canonical_id']}")
compose_result = provision.prepare(profile, run_dir, dry_run=True)
print(compose_result)
return 0
def cmd_provision(args) -> int:
advisory = _load_advisory(args.case)
profile = _resolve_profile(advisory)
run_dir = _run_dir(args.run_id or _compose_run_id(advisory))
result = provision.prepare(profile, run_dir, dry_run=args.dry_run)
print(result)
return 0
def cmd_seed(args) -> int:
advisory = _load_advisory(args.case)
profile = _resolve_profile(advisory)
print({"steps": seed.run_seed(profile)})
return 0
def cmd_baseline(args) -> int:
advisory = _load_advisory(args.case)
profile = _resolve_profile(advisory)
run_dir = _run_dir(args.run_id or _compose_run_id(advisory))
result = baseline.collect(profile, run_dir)
print(result)
return 0
def cmd_attack(args) -> int:
advisory = _load_advisory(args.case)
profile = _resolve_profile(advisory)
run_dir = _run_dir(args.run_id or _compose_run_id(advisory))
result = attack.run_attack(profile, advisory, run_dir, dry_run=args.dry_run)
print(result)
return 0
def cmd_verify(args) -> int:
advisory = _load_advisory(args.case)
profile = _resolve_profile(advisory)
browser_required = bool(profile.get("browser_assertions", {}).get("required"))
payload = {
"advisory": advisory["canonical_id"],
"profile_id": profile["profile_id"],
"browser_required": browser_required,
"result": "ready-for-run",
}
print(payload)
return 0
def cmd_run_case(args) -> int:
advisory = _load_advisory(args.case)
profile = _resolve_profile(advisory)
run_id = args.run_id or _compose_run_id(advisory)
run_dir = _run_dir(run_id)
provision_result = provision.prepare(profile, run_dir, dry_run=args.dry_run)
baseline_payload = baseline.collect(profile, run_dir) if profile.get("baseline_urls") else {"observations": []}
attack_payload = attack.run_attack(profile, advisory, run_dir, dry_run=args.dry_run)
browser_payload = {"required": bool(profile.get("browser_assertions", {}).get("required")), "present": False, "refs": []}
blocked_reason = provision_result.get("blocked_reason")
if browser_payload["required"] and not args.dry_run and profile.get("baseline_urls"):
browser_payload = browser.capture(profile["baseline_urls"][0], run_dir, prefix="proof")
if not browser_payload.get("present"):
blocked_reason = blocked_reason or browser_payload.get("reason")
compose_path = Path(provision_result["compose_path"])
container_logs = evidence.collect_container_logs(run_dir, compose_path) if compose_path.exists() else []
verification_status = "triage-manual"
verification_mode = profile.get("verification_mode", "synthetic")
artifact_mode = profile.get("artifact_mode", profile.get("provisioning_mode", "synthetic"))
if args.dry_run:
verification_status = "triage-manual"
blocked_reason = blocked_reason or "dry-run only"
elif provision_result.get("status") == "blocked-artifact":
verification_status = "blocked-artifact"
elif browser_payload.get("required") and not browser_payload.get("present"):
verification_status = "triage-manual"
elif artifact_mode == "synthetic":
verification_status = "verified-synthetic"
else:
verification_status = "verified-real"
bundle = _build_run_bundle(
advisory=advisory,
profile=profile,
run_id=run_id,
verification_status=verification_status,
verification_mode=verification_mode,
artifact_mode=artifact_mode,
baseline_refs=[str(run_dir / "logs" / "baseline.json")] if baseline_payload.get("observations") else [],
attack_steps=attack_payload.get("steps", []),
browser_refs=browser_payload.get("refs", []),
container_log_refs=container_logs,
request_log_refs=[str(run_dir / "logs" / "attack.json"), str(run_dir / "logs" / "baseline.json")],
blocked_reason=blocked_reason,
)
report_refs = render.render_run(bundle)
bundle["report_refs"] = report_refs
evidence.write_run_bundle(run_dir, bundle)
ensure_dir(RUNS_DIR)
write_json(RUNS_DIR / f"{run_id}.json", bundle)
render.render_dashboard()
print(bundle)
return 0
def cmd_run_system(args) -> int:
advisories = [item for item in load_json_dir(ADVISORIES_DIR) if item.get("system_id") == args.system]
selected = advisories[: args.limit]
for advisory in selected:
cmd_run_case(argparse.Namespace(case=advisory["canonical_id"], run_id=None, dry_run=args.dry_run))
print({"system": args.system, "count": len(selected)})
return 0
def cmd_run_batch(args) -> int:
if args.from_queue:
items = task_queue.dequeue(limit=args.limit)
else:
task_queue.enqueue_from_registry(only_hotlane=args.only_hotlane, limit=args.limit)
items = task_queue.dequeue(limit=args.limit)
for item in items:
cmd_run_case(argparse.Namespace(case=item["advisory_id"], run_id=None, dry_run=args.dry_run))
print({"processed": len(items)})
return 0
def cmd_render_run(args) -> int:
run = read_json(RUNS_DIR / f"{args.run_id}.json", default=None)
if not run:
raise ValueError(f"Unknown run: {args.run_id}")
print(render.render_run(run))
return 0
def cmd_serve_dashboard(args) -> int:
render.render_dashboard()
import http.server
import socketserver
os_dir = str(render.DASHBOARD_DIR if hasattr(render, "DASHBOARD_DIR") else "")
if not os_dir:
from lab.config import DASHBOARD_DIR
os_dir = str(DASHBOARD_DIR)
handler = http.server.SimpleHTTPRequestHandler
with socketserver.TCPServer(("127.0.0.1", args.port), handler) as httpd:
print(f"serving dashboard at http://127.0.0.1:{args.port}/")
import os
os.chdir(os_dir)
httpd.serve_forever()
def cmd_cleanup(args) -> int:
run = read_json(RUNS_DIR / f"{args.run_id}.json", default=None)
if not run:
raise ValueError(f"Unknown run: {args.run_id}")
compose_path = Path(run["report_refs"]["bundle_dir"]) / "compose" / "compose.yaml"
if command_available("docker") and compose_path.exists():
from lab.utils import run as shell_run
shell_run(["docker", "compose", "-f", str(compose_path), "down", "-v"], cwd=compose_path.parent.parent)
print({"cleaned": args.run_id})
return 0
def cmd_retry_failures(args) -> int:
failed = [
item
for item in load_json_dir(RUNS_DIR)
if item.get("verification_status") in {"blocked-artifact", "triage-manual"}
]
task_queue.enqueue_items(
[{"advisory_id": item["advisory_id"], "system_id": item["system_id"], "priority": "retry"} for item in failed[: args.limit]]
)
print({"requeued": min(len(failed), args.limit)})
return 0
def cmd_validate(args) -> int:
errors = validators.validate_assets()
if errors:
print("Validation failed:")
for error in errors:
print(f"- {error}")
return 1
print("Validation passed.")
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Websafe local lab orchestrator")
subparsers = parser.add_subparsers(dest="command", required=True)
catalog_sync = subparsers.add_parser("catalog", help="catalog operations")
catalog_sub = catalog_sync.add_subparsers(dest="catalog_command", required=True)
catalog_sync_cmd = catalog_sub.add_parser("sync", help="sync environment catalog and repro map")
catalog_sync_cmd.set_defaults(func=cmd_catalog_sync)
compose_generate = subparsers.add_parser("compose", help="compose operations")
compose_sub = compose_generate.add_subparsers(dest="compose_command", required=True)
compose_generate_cmd = compose_sub.add_parser("generate", help="generate compose file for a case")
compose_generate_cmd.add_argument("--case", required=True)
compose_generate_cmd.add_argument("--run-id")
compose_generate_cmd.set_defaults(func=cmd_compose_generate)
for name, func in [
("provision", cmd_provision),
("seed", cmd_seed),
("baseline", cmd_baseline),
("attack", cmd_attack),
("verify", cmd_verify),
]:
sub = subparsers.add_parser(name)
sub.add_argument("--case", required=True)
sub.add_argument("--run-id")
sub.add_argument("--dry-run", action="store_true")
sub.set_defaults(func=func)
run_case = subparsers.add_parser("run-case", help="run a single advisory through the lab pipeline")
run_case.add_argument("--case", required=True)
run_case.add_argument("--run-id")
run_case.add_argument("--dry-run", action="store_true")
run_case.set_defaults(func=cmd_run_case)
run_system = subparsers.add_parser("run-system", help="run the first N advisories for a system")
run_system.add_argument("--system", required=True)
run_system.add_argument("--limit", type=int, default=5)
run_system.add_argument("--dry-run", action="store_true")
run_system.set_defaults(func=cmd_run_system)
run_batch = subparsers.add_parser("run-batch", help="process repro queue or enqueue from registry")
run_batch.add_argument("--limit", type=int, default=10)
run_batch.add_argument("--only-hotlane", action="store_true")
run_batch.add_argument("--from-queue", action="store_true")
run_batch.add_argument("--dry-run", action="store_true")
run_batch.set_defaults(func=cmd_run_batch)
render_run = subparsers.add_parser("render-run", help="re-render a stored run")
render_run.add_argument("--run-id", required=True)
render_run.set_defaults(func=cmd_render_run)
serve = subparsers.add_parser("serve-dashboard", help="serve the static dashboard locally")
serve.add_argument("--port", type=int, default=8734)
serve.set_defaults(func=cmd_serve_dashboard)
cleanup = subparsers.add_parser("cleanup", help="tear down a stored run compose environment")
cleanup.add_argument("--run-id", required=True)
cleanup.set_defaults(func=cmd_cleanup)
retry = subparsers.add_parser("retry-failures", help="requeue blocked or manual runs")
retry.add_argument("--limit", type=int, default=50)
retry.set_defaults(func=cmd_retry_failures)
validate = subparsers.add_parser("validate", help="validate lab assets")
validate.set_defaults(func=cmd_validate)
return parser
def main() -> int:
parser = build_parser()
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())

40
scripts/lab/provision.py 普通文件
查看文件

@@ -0,0 +1,40 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict
from lab.compose import generate_compose
from lab.utils import command_available, run
def prepare(profile: Dict[str, Any], run_dir: Path, dry_run: bool = False) -> Dict[str, Any]:
compose_path, payload = generate_compose(profile, run_dir)
result = {
"compose_path": str(compose_path),
"service_count": len(payload.get("services", {})),
"docker_available": command_available("docker"),
"status": "ready",
}
if dry_run:
result["status"] = "planned"
return result
if not result["docker_available"]:
result["status"] = "blocked-artifact"
result["blocked_reason"] = "docker unavailable on this machine"
return result
config = run(["docker", "compose", "-f", str(compose_path), "config"], cwd=run_dir)
result["compose_config_rc"] = config.returncode
if config.returncode != 0:
result["status"] = "blocked-artifact"
result["blocked_reason"] = config.stderr.strip() or "docker compose config failed"
return result
up = run(["docker", "compose", "-f", str(compose_path), "up", "-d"], cwd=run_dir)
result["compose_up_rc"] = up.returncode
if up.returncode != 0:
result["status"] = "blocked-artifact"
result["blocked_reason"] = up.stderr.strip() or "docker compose up failed"
return result
return result

166
scripts/lab/render.py 普通文件
查看文件

@@ -0,0 +1,166 @@
from __future__ import annotations
import html
from pathlib import Path
from typing import Any, Dict, List
from lab.config import CASE_RUNS_DIR, DASHBOARD_DIR, RUNS_DIR
from lab.utils import ensure_dir, load_json_dir, read_json, write_json, write_text
def mermaid_from_steps(run: Dict[str, Any]) -> str:
lines = [
"flowchart LR",
'A["Select Advisory"] --> B["Resolve Repro Profile"]',
'B --> C["Provision Compose Environment"]',
'C --> D["Baseline Snapshot"]',
'D --> E["Controlled Attack Steps"]',
'E --> F["Browser Replay"]',
'F --> G["Collect Logs and Evidence"]',
'G --> H["Update Registry and Reports"]',
]
if run.get("blocked_reason"):
lines.append(f'H --> I["Blocked: {run["blocked_reason"][:60]}"]')
return "\n".join(lines)
def render_run(run: Dict[str, Any]) -> Dict[str, str]:
run_dir = CASE_RUNS_DIR / run["run_id"]
ensure_dir(run_dir / "assets")
timeline_path = run_dir / "timeline.mmd"
write_text(timeline_path, mermaid_from_steps(run))
md_lines = [
f"# Run {run['run_id']}",
"",
"> `LAB ONLY` | `AUTHORIZED TARGETS ONLY` | 自动生成 run bundle",
"",
f"- Advisory: `{run['advisory_id']}`",
f"- 系统: `{run['system_id']}`",
f"- Repro Profile: `{run['repro_profile_id']}`",
f"- 实证状态: `{run['verification_status']}`",
f"- 实证方式: `{run['verification_mode']}`",
f"- Artifact 模式: `{run['artifact_mode']}`",
f"- 启动时间: `{run['started_at']}`",
f"- 完成时间: `{run['finished_at']}`",
f"- 阻塞原因: `{run.get('blocked_reason') or '-'}`",
"",
"## 运行时间线",
"",
f"- Mermaid: [{timeline_path.name}]({timeline_path})",
"",
"## 证据摘要",
"",
f"- Baseline: `{len(run.get('baseline_refs', []))}`",
f"- 攻击步骤: `{len(run.get('attack_steps', []))}`",
f"- 浏览器证据: `{len(run.get('browser_refs', []))}`",
f"- 容器日志: `{len(run.get('container_log_refs', []))}`",
f"- 请求日志: `{len(run.get('request_log_refs', []))}`",
"",
"## 最小化验证说明",
"",
"- 仅限自有资产、本地靶场或已授权实验目标。",
"- 默认执行 minimal-proof;不会把破坏性或不可回滚动作作为默认路径。",
"",
]
if run.get("browser_refs"):
md_lines.extend(["## 浏览器证据", ""])
for ref in run["browser_refs"]:
md_lines.append(f"- {ref}")
md_lines.append("")
if run.get("container_log_refs"):
md_lines.extend(["## 容器日志", ""])
for ref in run["container_log_refs"]:
md_lines.append(f"- {ref}")
md_lines.append("")
report_md = run_dir / "report.md"
write_text(report_md, "\n".join(md_lines))
html_body = [
"<!doctype html>",
"<html><head><meta charset='utf-8'><title>websafe run report</title>",
"<style>body{font-family:ui-monospace,Menlo,monospace;margin:2rem;line-height:1.5;} code,pre{background:#f5f5f5;padding:.2rem .4rem;} .grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem;} .card{border:1px solid #ddd;padding:1rem;border-radius:.5rem;}</style>",
"</head><body>",
f"<h1>Run {html.escape(run['run_id'])}</h1>",
"<div class='grid'>",
f"<div class='card'><strong>Advisory</strong><br><code>{html.escape(run['advisory_id'])}</code></div>",
f"<div class='card'><strong>Status</strong><br><code>{html.escape(run['verification_status'])}</code></div>",
f"<div class='card'><strong>Profile</strong><br><code>{html.escape(run['repro_profile_id'])}</code></div>",
f"<div class='card'><strong>Artifact Mode</strong><br><code>{html.escape(run['artifact_mode'])}</code></div>",
"</div>",
"<h2>Mermaid Timeline</h2>",
f"<pre>{html.escape(mermaid_from_steps(run))}</pre>",
"<h2>Evidence</h2><ul>",
]
for ref in run.get("browser_refs", []) + run.get("container_log_refs", []) + run.get("request_log_refs", []):
html_body.append(f"<li><code>{html.escape(ref)}</code></li>")
html_body.extend(["</ul>", "</body></html>"])
report_html = run_dir / "report.html"
write_text(report_html, "\n".join(html_body))
return {"bundle_dir": str(run_dir), "report_md": str(report_md), "report_html": str(report_html), "timeline": str(timeline_path)}
def render_dashboard() -> Dict[str, str]:
ensure_dir(DASHBOARD_DIR)
runs = load_json_dir(RUNS_DIR)
summary = {
"run_count": len(runs),
"statuses": {},
"recent_runs": sorted(runs, key=lambda item: item.get("finished_at") or "", reverse=True)[:50],
}
for item in runs:
status = item.get("verification_status", "triage-manual")
summary["statuses"][status] = summary["statuses"].get(status, 0) + 1
write_json(DASHBOARD_DIR / "summary.json", summary)
write_json(DASHBOARD_DIR / "runs.json", summary["recent_runs"])
html_page = """<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>websafe dashboard</title>
<style>
body { font-family: ui-sans-serif, system-ui, sans-serif; margin: 2rem; background: #f8fafc; color: #0f172a; }
h1, h2 { margin-bottom: .5rem; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin: 1rem 0 2rem; }
.card { background: white; border: 1px solid #cbd5e1; border-radius: 14px; padding: 1rem; box-shadow: 0 4px 18px rgba(15,23,42,.06); }
table { width: 100%%; border-collapse: collapse; background: white; border-radius: 12px; overflow: hidden; }
th, td { padding: .75rem; border-bottom: 1px solid #e2e8f0; text-align: left; font-size: .92rem; }
code { background: #e2e8f0; padding: .1rem .35rem; border-radius: 6px; }
</style>
</head>
<body>
<h1>websafe Local Lab Dashboard</h1>
<p>LAB ONLY | AUTHORIZED TARGETS ONLY | 本地静态看板</p>
<div id="summary" class="cards"></div>
<h2>Recent Runs</h2>
<table>
<thead><tr><th>Run</th><th>Advisory</th><th>Status</th><th>Mode</th><th>Finished</th><th>Report</th></tr></thead>
<tbody id="rows"></tbody>
</table>
<script>
async function main() {
const summary = await fetch('./summary.json').then(r => r.json());
const runs = await fetch('./runs.json').then(r => r.json());
const summaryRoot = document.getElementById('summary');
const cards = [{label: 'Run Count', value: summary.run_count}];
for (const [key, value] of Object.entries(summary.statuses)) {
cards.push({label: key, value});
}
summaryRoot.innerHTML = cards.map(item => `<div class="card"><strong>${item.label}</strong><div style="font-size:2rem;margin-top:.5rem;">${item.value}</div></div>`).join('');
const rows = document.getElementById('rows');
rows.innerHTML = runs.map(item => {
const report = item.report_refs && item.report_refs.report_html ? item.report_refs.report_html : '';
return `<tr><td><code>${item.run_id}</code></td><td><code>${item.advisory_id}</code></td><td>${item.verification_status}</td><td>${item.verification_mode}</td><td>${item.finished_at || ''}</td><td>${report ? `<a href="../../../../${report.replace('/Users/x/websafe/', '')}">open</a>` : '-'}</td></tr>`;
}).join('');
}
main();
</script>
</body>
</html>
"""
write_text(DASHBOARD_DIR / "index.html", html_page)
return {
"dashboard_dir": str(DASHBOARD_DIR),
"index_html": str(DASHBOARD_DIR / "index.html"),
}

157
scripts/lab/repro.py 普通文件
查看文件

@@ -0,0 +1,157 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from intel.utils import load_all_json
from lab.config import ADVISORIES_DIR, ENV_CATALOG_DIR, REPRO_MAP_PATH, REPRO_PROFILES_DIR, RUNS_DIR
from lab.utils import read_json, read_yaml, unique
FAMILY_KEYWORDS = {
"xss-generic": ["xss", "cross-site scripting", "dom xss", "trusted types", "content injection"],
"sqli-generic": ["sql injection", "sqli"],
"authz-bypass-generic": ["authorization bypass", "auth bypass", "access control", "permission"],
"ssrf-generic": ["ssrf", "server-side request forgery"],
"file-upload-generic": ["file upload", "attachment", "extension bypass"],
"request-smuggling-generic": ["request smuggling", "http desync"],
"template-injection-generic": ["template injection", "ssti"],
"deserialization-generic": ["deserialization", "serialization"],
"proxy-boundary-generic": ["proxy", "middleware", "header trust"],
"plugin-extension-generic": ["plugin", "module", "extension", "theme"],
"session-token-generic": ["token", "cookie", "session", "jwt", "localstorage"],
"path-traversal-generic": ["path traversal", "directory traversal"],
"misconfiguration-generic": ["misconfiguration", "default credentials", "admin panel", "debug"],
}
def load_repro_map() -> Dict[str, Any]:
return read_yaml(REPRO_MAP_PATH, default={"systems": []}) or {"systems": []}
def load_profiles() -> Dict[str, Dict[str, Any]]:
profiles: Dict[str, Dict[str, Any]] = {}
if not REPRO_PROFILES_DIR.exists():
return profiles
for file_path in sorted(REPRO_PROFILES_DIR.rglob("*.yaml")):
content = read_yaml(file_path, default=None)
if not isinstance(content, dict):
continue
profile_id = content.get("profile_id")
if profile_id:
profiles[profile_id] = content
return profiles
def latest_runs_by_advisory() -> Dict[str, Dict[str, Any]]:
runs: Dict[str, Dict[str, Any]] = {}
for item in load_all_json(RUNS_DIR):
advisory_id = item.get("advisory_id")
if not advisory_id:
continue
previous = runs.get(advisory_id)
if previous is None or (item.get("finished_at") or "") >= (previous.get("finished_at") or ""):
runs[advisory_id] = item
return runs
def resolve_repro_family(advisory: Dict[str, Any], system_map: Dict[str, Any]) -> str:
text = " ".join(
filter(
None,
[
advisory.get("title"),
advisory.get("summary"),
advisory.get("system_id"),
" ".join(advisory.get("aliases", [])),
" ".join(advisory.get("secure_code_topics", [])),
],
)
).lower()
for family, keywords in FAMILY_KEYWORDS.items():
if any(keyword in text for keyword in keywords):
return family
return system_map.get("default_repro_family", "authz-bypass-generic")
def resolve_profile(advisory_id: str, advisory: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
profiles = load_profiles()
if advisory_id in profiles:
return profiles[advisory_id]
advisory = advisory or read_json(ADVISORIES_DIR / f"{advisory_id}.json", default={}) or {}
repro_map = {item["system_id"]: item for item in load_repro_map().get("systems", [])}
system_map = repro_map.get(advisory.get("system_id", ""), {})
direct_profile = profiles.get(f"{advisory.get('system_id', '')}-{advisory_id}")
if direct_profile:
return direct_profile
family = resolve_repro_family(advisory, system_map)
profile = profiles.get(family)
if profile:
resolved = dict(profile)
resolved.setdefault("resolved_via", "family-generic")
resolved.setdefault("profile_id", family)
return resolved
return {
"profile_id": family,
"resolved_via": "implicit-fallback",
"vuln_family": family.replace("-generic", ""),
"provisioning_mode": "synthetic",
"browser_assertions": {"required": bool(system_map.get("browser_required_default"))},
"attack_actions": [],
"baseline_actions": [],
"success_criteria": ["manual triage required"],
"cleanup_policy": "destroy",
"destructive_risk": "medium",
"allowed_target_types": ["lab-local", "lab-public", "authorized-third-party"],
}
def advisory_defaults(advisory: Dict[str, Any]) -> Dict[str, Any]:
profile = resolve_profile(advisory["canonical_id"], advisory)
repro_map = {item["system_id"]: item for item in load_repro_map().get("systems", [])}
system_map = repro_map.get(advisory.get("system_id", ""), {})
mode = "synthetic" if profile.get("provisioning_mode") == "synthetic" else "real"
return {
"verification_status": advisory.get("verification_status") or "triage-manual",
"verification_mode": advisory.get("verification_mode") or mode,
"last_verified_at": advisory.get("last_verified_at"),
"last_run_id": advisory.get("last_run_id"),
"evidence_bundle": advisory.get("evidence_bundle"),
"browser_evidence": advisory.get("browser_evidence")
or {
"required": bool(profile.get("browser_assertions", {}).get("required", system_map.get("browser_required_default", False))),
"present": False,
"refs": [],
},
"repro_profile_id": advisory.get("repro_profile_id") or profile.get("profile_id"),
"artifact_mode": advisory.get("artifact_mode") or system_map.get("provisioning_mode_preference", ["synthetic"])[0],
"blocked_reason": advisory.get("blocked_reason"),
}
def annotate_with_latest_run(advisory: Dict[str, Any], run: Optional[Dict[str, Any]]) -> Dict[str, Any]:
merged = dict(advisory)
merged.update(advisory_defaults(advisory))
if not run:
return merged
merged.update(
{
"verification_status": run.get("verification_status", merged["verification_status"]),
"verification_mode": run.get("verification_mode", merged["verification_mode"]),
"last_verified_at": run.get("finished_at", merged["last_verified_at"]),
"last_run_id": run.get("run_id"),
"evidence_bundle": run.get("report_refs", {}).get("bundle_dir"),
"browser_evidence": {
"required": merged.get("browser_evidence", {}).get("required", False),
"present": bool(run.get("browser_refs")),
"refs": run.get("browser_refs", []),
},
"repro_profile_id": run.get("repro_profile_id", merged["repro_profile_id"]),
"artifact_mode": run.get("artifact_mode", merged["artifact_mode"]),
"blocked_reason": run.get("blocked_reason"),
}
)
return merged

11
scripts/lab/run-queue.sh 普通文件
查看文件

@@ -0,0 +1,11 @@
#!/bin/bash
set -euo pipefail
cd /Users/x/websafe
LOG_DIR="/Users/x/websafe/08-threat-intel/generated/logs"
mkdir -p "$LOG_DIR"
STAMP="$(date '+%Y%m%d-%H%M%S')"
exec >> "$LOG_DIR/lab-queue-$STAMP.log" 2>&1
echo "[lab-queue] $(date -u '+%Y-%m-%dT%H:%M:%SZ') starting"
python3 /Users/x/websafe/scripts/lab/main.py run-batch --from-queue --limit 10

14
scripts/lab/seed.py 普通文件
查看文件

@@ -0,0 +1,14 @@
from __future__ import annotations
from typing import Any, Dict, List
def run_seed(profile: Dict[str, Any]) -> List[Dict[str, Any]]:
steps = []
for action in profile.get("seed_actions", []):
kind = action.get("kind", "note")
if kind == "note":
steps.append({"kind": kind, "status": "recorded", "message": action.get("message", "")})
else:
steps.append({"kind": kind, "status": "skipped", "message": "Seed action type not yet automated"})
return steps

49
scripts/lab/task_queue.py 普通文件
查看文件

@@ -0,0 +1,49 @@
from __future__ import annotations
from typing import Any, Dict, List
from lab.config import ADVISORIES_DIR, QUEUE_PATH
from lab.utils import load_json_dir, read_json, write_json
def load_queue() -> Dict[str, Any]:
return read_json(QUEUE_PATH, default={"items": []}) or {"items": []}
def save_queue(queue: Dict[str, Any]) -> None:
write_json(QUEUE_PATH, queue)
def enqueue_items(items: List[Dict[str, Any]]) -> Dict[str, Any]:
queue = load_queue()
existing = {item["advisory_id"] for item in queue.get("items", [])}
added = 0
for item in items:
if item["advisory_id"] in existing:
continue
queue.setdefault("items", []).append(item)
existing.add(item["advisory_id"])
added += 1
save_queue(queue)
return {"queued": len(queue["items"]), "added": added}
def enqueue_from_registry(only_hotlane: bool = False, limit: int = 50) -> Dict[str, Any]:
advisories = load_json_dir(ADVISORIES_DIR)
items = []
for advisory in advisories:
if only_hotlane:
hot = advisory.get("exploit_status") in {"known_exploited", "active_exploitation", "in_the_wild"}
if not hot and not (advisory.get("cvss_score") or 0) >= 8.8 and advisory.get("severity") != "critical":
continue
items.append({"advisory_id": advisory["canonical_id"], "system_id": advisory["system_id"], "priority": "hotlane" if only_hotlane else "default"})
return enqueue_items(items[:limit])
def dequeue(limit: int = 10) -> List[Dict[str, Any]]:
queue = load_queue()
items = queue.get("items", [])
selected = items[:limit]
queue["items"] = items[limit:]
save_queue(queue)
return selected

103
scripts/lab/utils.py 普通文件
查看文件

@@ -0,0 +1,103 @@
from __future__ import annotations
import json
import shutil
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional
import yaml
UTC = timezone.utc
def now_utc() -> datetime:
return datetime.now(tz=UTC)
def isoformat(dt: datetime) -> str:
return dt.astimezone(UTC).replace(microsecond=0).isoformat()
def ensure_dir(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
def read_json(path: Path, default: Any = None) -> Any:
if not path.exists():
return default
return json.loads(path.read_text(encoding="utf-8"))
def write_json(path: Path, data: Any) -> None:
ensure_dir(path.parent)
path.write_text(json.dumps(data, indent=2, ensure_ascii=True, sort_keys=False) + "\n", encoding="utf-8")
def read_yaml(path: Path, default: Any = None) -> Any:
if not path.exists():
return default
with path.open("r", encoding="utf-8") as handle:
return yaml.safe_load(handle)
def write_yaml(path: Path, data: Any) -> None:
ensure_dir(path.parent)
with path.open("w", encoding="utf-8") as handle:
yaml.safe_dump(data, handle, allow_unicode=False, sort_keys=False)
def write_text(path: Path, content: str) -> None:
ensure_dir(path.parent)
path.write_text(content.rstrip() + "\n", encoding="utf-8")
def load_json_dir(path: Path) -> List[Dict[str, Any]]:
if not path.exists():
return []
values: List[Dict[str, Any]] = []
for file_path in sorted(path.glob("*.json")):
content = read_json(file_path, default=None)
if isinstance(content, dict):
values.append(content)
return values
def run(cmd: List[str], cwd: Optional[Path] = None, check: bool = False) -> subprocess.CompletedProcess:
return subprocess.run(
cmd,
cwd=str(cwd) if cwd else None,
text=True,
capture_output=True,
check=check,
)
def command_available(name: str) -> bool:
return shutil.which(name) is not None
def slugify(value: str) -> str:
safe = []
for ch in value.lower().strip():
if ch.isalnum():
safe.append(ch)
else:
safe.append("-")
result = "".join(safe)
while "--" in result:
result = result.replace("--", "-")
return result.strip("-") or "item"
def unique(values: Iterable[str]) -> List[str]:
seen = set()
result = []
for value in values:
if not value or value in seen:
continue
seen.add(value)
result.append(value)
return result

35
scripts/lab/validators.py 普通文件
查看文件

@@ -0,0 +1,35 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, List
from lab.config import ENV_CATALOG_DIR, REPRO_MAP_PATH, REPRO_PROFILES_DIR
from lab.utils import read_yaml
def validate_assets() -> List[str]:
errors: List[str] = []
if not REPRO_MAP_PATH.exists():
errors.append(f"missing repro map: {REPRO_MAP_PATH}")
if not ENV_CATALOG_DIR.exists():
errors.append(f"missing environment catalog dir: {ENV_CATALOG_DIR}")
for path in sorted(REPRO_PROFILES_DIR.rglob("*.yaml")):
content = read_yaml(path, default=None)
if not isinstance(content, dict):
errors.append(f"invalid repro profile yaml: {path}")
continue
for field in [
"profile_id",
"match_rules",
"vuln_family",
"provisioning_mode",
"attack_actions",
"baseline_actions",
"success_criteria",
"cleanup_policy",
"destructive_risk",
"allowed_target_types",
]:
if field not in content:
errors.append(f"repro profile missing {field}: {path}")
return errors