更新: 421 个文件 - 2026-03-17 18:30:02
这个提交包含在:
@@ -4,6 +4,7 @@ import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from lab.runners.dispatcher import run_attack as run_runner_attack
|
||||
from lab.utils import write_json
|
||||
|
||||
|
||||
@@ -37,6 +38,9 @@ def _render_args(step: Dict[str, Any], profile: Dict[str, Any], advisory: Dict[s
|
||||
|
||||
|
||||
def run_attack(profile: Dict[str, Any], advisory: Dict[str, Any], run_dir: Path, dry_run: bool = False) -> Dict[str, Any]:
|
||||
if profile.get("runner_id") and not dry_run:
|
||||
return run_runner_attack(profile, advisory, run_dir)
|
||||
|
||||
steps: List[Dict[str, Any]] = []
|
||||
for step in profile.get("attack_actions", []):
|
||||
tool_name = step.get("tool")
|
||||
@@ -60,6 +64,8 @@ def run_attack(profile: Dict[str, Any], advisory: Dict[str, Any], run_dir: Path,
|
||||
"result_path": str(output_path),
|
||||
}
|
||||
)
|
||||
elif step.get("kind") == "note" and not dry_run:
|
||||
record["status"] = "completed"
|
||||
steps.append(record)
|
||||
payload = {"steps": steps}
|
||||
write_json(run_dir / "logs" / "attack.json", payload)
|
||||
|
||||
@@ -10,6 +10,7 @@ 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]] = []
|
||||
steps: List[Dict[str, Any]] = []
|
||||
for url in profile.get("baseline_urls", []):
|
||||
try:
|
||||
response = requests.get(url, timeout=timeout, verify=False)
|
||||
@@ -23,6 +24,41 @@ def collect(profile: Dict[str, Any], run_dir: Path, timeout: float = 8.0) -> Dic
|
||||
)
|
||||
except Exception as exc:
|
||||
observations.append({"url": url, "error": str(exc)})
|
||||
payload = {"observations": observations}
|
||||
base_url = str((profile.get("baseline_urls") or [""])[0]).rstrip("/")
|
||||
for action in profile.get("baseline_actions", []) or []:
|
||||
kind = action.get("kind", "note")
|
||||
if kind == "note":
|
||||
steps.append({"kind": kind, "status": "recorded", "message": action.get("message", "")})
|
||||
continue
|
||||
try:
|
||||
if kind == "http-get":
|
||||
path = action.get("path", "/")
|
||||
response = requests.get(f"{base_url}{path}", timeout=timeout)
|
||||
steps.append(
|
||||
{
|
||||
"kind": kind,
|
||||
"status": "completed",
|
||||
"path": path,
|
||||
"status_code": response.status_code,
|
||||
"body_excerpt": response.text[:200],
|
||||
}
|
||||
)
|
||||
elif kind == "http-post":
|
||||
path = action.get("path", "/")
|
||||
response = requests.post(f"{base_url}{path}", json=action.get("json", {}), timeout=timeout)
|
||||
steps.append(
|
||||
{
|
||||
"kind": kind,
|
||||
"status": "completed",
|
||||
"path": path,
|
||||
"status_code": response.status_code,
|
||||
"body_excerpt": response.text[:200],
|
||||
}
|
||||
)
|
||||
else:
|
||||
steps.append({"kind": kind, "status": "skipped", "message": "baseline action type not automated"})
|
||||
except Exception as exc:
|
||||
steps.append({"kind": kind, "status": "failed", "message": str(exc)})
|
||||
payload = {"observations": observations, "steps": steps}
|
||||
write_json(run_dir / "logs" / "baseline.json", payload)
|
||||
return payload
|
||||
|
||||
@@ -12,10 +12,12 @@ def capture(url: str, run_dir: Path, prefix: str = "baseline") -> Dict[str, Any]
|
||||
"present": False,
|
||||
"refs": [],
|
||||
"reason": "playwright runtime unavailable",
|
||||
"error_kind": "import-error",
|
||||
}
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright # type: ignore
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
payload["reason"] = f"playwright import failed: {exc}"
|
||||
write_json(run_dir / "logs" / f"{prefix}-browser.json", payload)
|
||||
return payload
|
||||
|
||||
@@ -33,11 +35,29 @@ def capture(url: str, run_dir: Path, prefix: str = "baseline") -> Dict[str, Any]
|
||||
final_url = url
|
||||
try:
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
try:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
except Exception as exc:
|
||||
payload["reason"] = f"chromium launch failed: {exc}"
|
||||
payload["error_kind"] = "launch-failed"
|
||||
write_json(run_dir / "logs" / f"{prefix}-browser.json", payload)
|
||||
return payload
|
||||
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)
|
||||
response = page.goto(url, wait_until="networkidle", timeout=20000)
|
||||
if response is None:
|
||||
payload["reason"] = "page navigation returned no response"
|
||||
payload["error_kind"] = "navigation-failed"
|
||||
browser.close()
|
||||
write_json(run_dir / "logs" / f"{prefix}-browser.json", payload)
|
||||
return payload
|
||||
if response.status >= 500:
|
||||
payload["reason"] = f"page returned {response.status}"
|
||||
payload["error_kind"] = "target-5xx"
|
||||
browser.close()
|
||||
write_json(run_dir / "logs" / f"{prefix}-browser.json", payload)
|
||||
return payload
|
||||
page.screenshot(path=str(screenshot_path), full_page=True)
|
||||
dom_path.write_text(page.content(), encoding="utf-8")
|
||||
final_url = page.url
|
||||
@@ -46,6 +66,7 @@ def capture(url: str, run_dir: Path, prefix: str = "baseline") -> Dict[str, Any]
|
||||
browser.close()
|
||||
except Exception as exc:
|
||||
payload["reason"] = str(exc)
|
||||
payload["error_kind"] = "target-timeout" if "Timeout" in str(exc) else "navigation-failed"
|
||||
write_json(run_dir / "logs" / f"{prefix}-browser.json", payload)
|
||||
return payload
|
||||
write_json(console_path, console_messages)
|
||||
@@ -63,6 +84,7 @@ def capture(url: str, run_dir: Path, prefix: str = "baseline") -> Dict[str, Any]
|
||||
"present": True,
|
||||
"page_title": page_title,
|
||||
"page_url": final_url,
|
||||
"error_kind": None,
|
||||
"refs": [str(screenshot_path), str(dom_path), str(console_path), str(network_path), str(page_path)],
|
||||
}
|
||||
write_json(run_dir / "logs" / f"{prefix}-browser.json", payload)
|
||||
|
||||
@@ -11,11 +11,22 @@ def compose_payload(profile: Dict[str, Any]) -> Dict[str, Any]:
|
||||
for service_name, service in profile.get("services", {}).items():
|
||||
payload = {
|
||||
"image": service["image"],
|
||||
"networks": ["labnet"],
|
||||
}
|
||||
if service.get("ports"):
|
||||
payload["ports"] = service["ports"]
|
||||
if service.get("environment"):
|
||||
payload["environment"] = service["environment"]
|
||||
if service.get("command"):
|
||||
payload["command"] = service["command"]
|
||||
if service.get("working_dir"):
|
||||
payload["working_dir"] = service["working_dir"]
|
||||
if service.get("volumes"):
|
||||
payload["volumes"] = service["volumes"]
|
||||
if service.get("healthcheck"):
|
||||
payload["healthcheck"] = service["healthcheck"]
|
||||
if service.get("build"):
|
||||
payload["build"] = service["build"]
|
||||
if service.get("depends_on"):
|
||||
payload["depends_on"] = service["depends_on"]
|
||||
services[service_name] = payload
|
||||
|
||||
131
scripts/lab/doctor.py
普通文件
131
scripts/lab/doctor.py
普通文件
@@ -0,0 +1,131 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
from contextlib import closing
|
||||
from typing import Any, Dict, Iterable, List, Tuple
|
||||
|
||||
from lab.utils import command_available, run
|
||||
|
||||
|
||||
def _result(name: str, ok: bool, detail: str, **extra: Any) -> Dict[str, Any]:
|
||||
payload = {"name": name, "ok": ok, "detail": detail}
|
||||
payload.update(extra)
|
||||
return payload
|
||||
|
||||
|
||||
def _parse_host_ports(profiles: Iterable[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
ports: List[Dict[str, Any]] = []
|
||||
for profile in profiles:
|
||||
for service_name, service in (profile.get("services") or {}).items():
|
||||
for binding in service.get("ports", []) or []:
|
||||
host_port = None
|
||||
value = str(binding)
|
||||
parts = value.split(":")
|
||||
if len(parts) == 3 and parts[1].isdigit():
|
||||
host_port = int(parts[1])
|
||||
elif len(parts) >= 2 and parts[0].isdigit():
|
||||
host_port = int(parts[0])
|
||||
elif value.isdigit():
|
||||
host_port = int(value)
|
||||
if host_port is None:
|
||||
continue
|
||||
ports.append(
|
||||
{
|
||||
"profile_id": profile.get("profile_id"),
|
||||
"service": service_name,
|
||||
"binding": value,
|
||||
"port": host_port,
|
||||
}
|
||||
)
|
||||
return ports
|
||||
|
||||
|
||||
def _port_available(port: int) -> bool:
|
||||
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
sock.bind(("127.0.0.1", port))
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _check_docker_cli() -> Dict[str, Any]:
|
||||
ok = command_available("docker")
|
||||
return _result("docker-cli", ok, "docker CLI available" if ok else "docker CLI is not installed")
|
||||
|
||||
|
||||
def _check_docker_daemon() -> Dict[str, Any]:
|
||||
if not command_available("docker"):
|
||||
return _result("docker-daemon", False, "docker CLI unavailable")
|
||||
context = run(["docker", "context", "show"], check=False)
|
||||
info = run(["docker", "info"], check=False)
|
||||
detail = f"context={context.stdout.strip() or 'unknown'}"
|
||||
if info.returncode != 0:
|
||||
detail = info.stderr.strip() or info.stdout.strip() or "docker daemon unavailable"
|
||||
return _result("docker-daemon", False, detail)
|
||||
return _result("docker-daemon", True, detail or "docker daemon reachable")
|
||||
|
||||
|
||||
def _check_playwright_import() -> Dict[str, Any]:
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright # noqa: F401
|
||||
except Exception as exc:
|
||||
return _result("playwright-import", False, f"playwright import failed: {exc}")
|
||||
return _result("playwright-import", True, "playwright Python package import passed")
|
||||
|
||||
|
||||
def _check_chromium_launch() -> Dict[str, Any]:
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
except Exception as exc:
|
||||
return _result("playwright-browser", False, f"playwright import failed: {exc}")
|
||||
try:
|
||||
with sync_playwright() as playwright:
|
||||
browser = playwright.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
page.set_content("<html><body>ok</body></html>")
|
||||
browser.close()
|
||||
except Exception as exc:
|
||||
return _result("playwright-browser", False, f"chromium launch failed: {exc}")
|
||||
return _result("playwright-browser", True, "chromium runtime launch passed")
|
||||
|
||||
|
||||
def _check_ports(profiles: Iterable[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
requested = _parse_host_ports(profiles)
|
||||
if not requested:
|
||||
return _result("ports", True, "no host ports declared")
|
||||
|
||||
conflicts: List[Dict[str, Any]] = []
|
||||
for item in requested:
|
||||
if not _port_available(item["port"]):
|
||||
conflicts.append(item)
|
||||
|
||||
if conflicts:
|
||||
detail = ", ".join(
|
||||
f"{item['port']}({item['profile_id']}::{item['service']})" for item in conflicts
|
||||
)
|
||||
return _result("ports", False, f"host ports already in use: {detail}", conflicts=conflicts)
|
||||
return _result("ports", True, f"checked {len(requested)} host port bindings", bindings=requested)
|
||||
|
||||
|
||||
def run_checks(profiles: Iterable[Dict[str, Any]] | None = None) -> Dict[str, Any]:
|
||||
selected = list(profiles or [])
|
||||
checks = [
|
||||
_check_docker_cli(),
|
||||
_check_docker_daemon(),
|
||||
_check_playwright_import(),
|
||||
_check_chromium_launch(),
|
||||
_check_ports(selected),
|
||||
]
|
||||
ok = all(item["ok"] for item in checks)
|
||||
failures = [item for item in checks if not item["ok"]]
|
||||
return {
|
||||
"status": "passed" if ok else "failed",
|
||||
"ok": ok,
|
||||
"checks": checks,
|
||||
"profile_ids": [item.get("profile_id") for item in selected if item.get("profile_id")],
|
||||
"failure_count": len(failures),
|
||||
"summary": "; ".join(item["detail"] for item in failures) if failures else "all checks passed",
|
||||
}
|
||||
|
||||
100
scripts/lab/evaluate.py
普通文件
100
scripts/lab/evaluate.py
普通文件
@@ -0,0 +1,100 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
def _assertion(name: str, kind: str, passed: bool, detail: str) -> Dict[str, Any]:
|
||||
return {
|
||||
"name": name,
|
||||
"kind": kind,
|
||||
"passed": passed,
|
||||
"detail": detail,
|
||||
}
|
||||
|
||||
|
||||
def _baseline_ok(payload: Dict[str, Any]) -> bool:
|
||||
observations = payload.get("observations", []) or []
|
||||
if not observations:
|
||||
return False
|
||||
for item in observations:
|
||||
if item.get("error"):
|
||||
return False
|
||||
status_code = item.get("status_code")
|
||||
if status_code is None or int(status_code) >= 500:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _attack_steps_ok(payload: Dict[str, Any]) -> bool:
|
||||
steps = payload.get("steps", []) or []
|
||||
if payload.get("success") is True:
|
||||
return True
|
||||
if not steps:
|
||||
return False
|
||||
return not any(step.get("status") == "failed" for step in steps)
|
||||
|
||||
|
||||
def evaluate_run(
|
||||
profile: Dict[str, Any],
|
||||
provision_result: Dict[str, Any],
|
||||
baseline_payload: Dict[str, Any],
|
||||
attack_payload: Dict[str, Any],
|
||||
browser_payload: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
assertions: List[Dict[str, Any]] = []
|
||||
configured = profile.get("success_assertions", []) or []
|
||||
browser_required = bool(profile.get("browser_assertions", {}).get("required"))
|
||||
if not configured:
|
||||
configured = [
|
||||
{"name": "baseline-ok", "type": "baseline-ok"},
|
||||
{"name": "attack-steps", "type": "attack-steps-ok"},
|
||||
]
|
||||
if browser_required:
|
||||
configured.append({"name": "browser-present", "type": "browser-present"})
|
||||
|
||||
for item in configured:
|
||||
assertion_type = item.get("type", "")
|
||||
name = item.get("name") or assertion_type or "assertion"
|
||||
if assertion_type == "runner-success":
|
||||
passed = bool(attack_payload.get("success"))
|
||||
detail = attack_payload.get("detail") or ("runner reported success" if passed else "runner did not confirm success")
|
||||
elif assertion_type == "baseline-ok":
|
||||
passed = _baseline_ok(baseline_payload)
|
||||
detail = "baseline URLs responded without 5xx or transport errors" if passed else "baseline checks were incomplete"
|
||||
elif assertion_type == "attack-steps-ok":
|
||||
passed = _attack_steps_ok(attack_payload)
|
||||
detail = "attack steps completed without failures" if passed else "attack steps failed or produced no usable result"
|
||||
elif assertion_type == "browser-present":
|
||||
passed = bool(browser_payload.get("present"))
|
||||
detail = "browser evidence captured" if passed else (browser_payload.get("reason") or "browser evidence missing")
|
||||
else:
|
||||
passed = False
|
||||
detail = f"unsupported assertion type: {assertion_type}"
|
||||
assertions.append(_assertion(name, assertion_type, passed, detail))
|
||||
|
||||
blocked_reason = provision_result.get("blocked_reason")
|
||||
if browser_required and not browser_payload.get("present"):
|
||||
blocked_reason = blocked_reason or browser_payload.get("reason") or "browser evidence incomplete"
|
||||
|
||||
passed = all(item["passed"] for item in assertions)
|
||||
artifact_mode = profile.get("artifact_mode", profile.get("provisioning_mode", "synthetic"))
|
||||
verification_status = "triage-manual"
|
||||
if provision_result.get("status") == "blocked-artifact":
|
||||
verification_status = "blocked-artifact"
|
||||
elif not passed:
|
||||
verification_status = "triage-manual"
|
||||
failed = next((item for item in assertions if not item["passed"]), None)
|
||||
if failed and not blocked_reason:
|
||||
blocked_reason = failed["detail"]
|
||||
elif artifact_mode == "synthetic":
|
||||
verification_status = "verified-synthetic"
|
||||
else:
|
||||
verification_status = "verified-real"
|
||||
|
||||
return {
|
||||
"passed": passed and verification_status.startswith("verified-"),
|
||||
"verification_status": verification_status,
|
||||
"blocked_reason": blocked_reason,
|
||||
"assertions": assertions,
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ 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 import attack, baseline, browser, catalog, doctor, evaluate, 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
|
||||
|
||||
@@ -99,6 +99,7 @@ def _build_run_bundle(
|
||||
compose_refs: List[str],
|
||||
browser_evidence: Dict[str, Any],
|
||||
timeline: List[Dict[str, Any]],
|
||||
success_evaluation: Dict[str, Any],
|
||||
started_at: str,
|
||||
finished_at: str,
|
||||
blocked_reason: str | None,
|
||||
@@ -121,6 +122,9 @@ def _build_run_bundle(
|
||||
"request_log_refs": request_log_refs,
|
||||
"compose_refs": compose_refs,
|
||||
"timeline": timeline,
|
||||
"success_evaluation": success_evaluation,
|
||||
"historical_status": verification_status,
|
||||
"latest_status": verification_status,
|
||||
"started_at": started_at,
|
||||
"finished_at": finished_at,
|
||||
"blocked_reason": blocked_reason,
|
||||
@@ -141,6 +145,9 @@ def _dry_run_case_plan(advisory: Dict[str, Any], profile: Dict[str, Any], run_id
|
||||
"compose_services": sorted(profile.get("services", {}).keys()),
|
||||
"seed_actions": profile.get("seed_actions", []),
|
||||
"attack_actions": profile.get("attack_actions", []),
|
||||
"runner_id": profile.get("runner_id"),
|
||||
"fixture_path": profile.get("fixture_path"),
|
||||
"success_assertions": profile.get("success_assertions", []),
|
||||
"compose_preview": provision_result.get("compose_preview", {}),
|
||||
"note": "dry-run only; no bundle, report, compose file, or registry update was written",
|
||||
}
|
||||
@@ -159,7 +166,23 @@ def _execute_case(canonical_id: str, run_id: str | None = None, dry_run: bool =
|
||||
_timeline_event(timeline, "resolve-repro-profile", "completed", profile["profile_id"])
|
||||
|
||||
run_dir = _run_dir(resolved_run_id)
|
||||
provision_result = provision.prepare(profile, run_dir, dry_run=False)
|
||||
doctor_result = doctor.run_checks([profile])
|
||||
write_json(run_dir / "logs" / "doctor.json", doctor_result)
|
||||
_timeline_event(
|
||||
timeline,
|
||||
"doctor",
|
||||
"completed" if doctor_result.get("ok") else "failed",
|
||||
doctor_result.get("summary", ""),
|
||||
)
|
||||
|
||||
if doctor_result.get("ok"):
|
||||
provision_result = provision.prepare(profile, run_dir, dry_run=False)
|
||||
else:
|
||||
provision_result = {
|
||||
"compose_path": str(run_dir / "compose" / "compose.yaml"),
|
||||
"status": "blocked-artifact",
|
||||
"blocked_reason": doctor_result.get("summary", "doctor failed"),
|
||||
}
|
||||
_timeline_event(
|
||||
timeline,
|
||||
"provision-compose-environment",
|
||||
@@ -168,11 +191,39 @@ def _execute_case(canonical_id: str, run_id: str | None = None, dry_run: bool =
|
||||
)
|
||||
allow_runtime_steps = provision_result.get("status") not in {"blocked-artifact"}
|
||||
browser_required = bool(profile.get("browser_assertions", {}).get("required"))
|
||||
compose_path = Path(provision_result.get("compose_path", run_dir / "compose" / "compose.yaml"))
|
||||
|
||||
ready_payload = {"status": "skipped", "detail": "provisioning blocked", "observations": []}
|
||||
if allow_runtime_steps:
|
||||
ready_payload = provision.wait_ready(profile, run_dir, compose_path)
|
||||
allow_runtime_steps = ready_payload.get("status") == "completed"
|
||||
_timeline_event(timeline, "wait-ready", ready_payload.get("status", "unknown"), ready_payload.get("detail", ""))
|
||||
else:
|
||||
_timeline_event(timeline, "wait-ready", "skipped", "provisioning blocked")
|
||||
|
||||
seed_payload = {"steps": [], "seeded": False}
|
||||
if allow_runtime_steps:
|
||||
seed_payload = seed.run_seed(profile, advisory, run_dir, dry_run=False)
|
||||
seed_failed = any(step.get("status") == "failed" for step in seed_payload.get("steps", []))
|
||||
_timeline_event(
|
||||
timeline,
|
||||
"seed-environment",
|
||||
"failed" if seed_failed else "completed",
|
||||
f"steps={len(seed_payload.get('steps', []))}",
|
||||
)
|
||||
else:
|
||||
_timeline_event(timeline, "seed-environment", "skipped", "runtime steps unavailable")
|
||||
|
||||
baseline_payload = {"observations": []}
|
||||
if profile.get("baseline_urls") and allow_runtime_steps:
|
||||
baseline_payload = baseline.collect(profile, run_dir)
|
||||
_timeline_event(timeline, "baseline-snapshot", "completed", f"urls={len(profile.get('baseline_urls', []))}")
|
||||
baseline_failed = any(item.get("error") for item in baseline_payload.get("observations", []))
|
||||
_timeline_event(
|
||||
timeline,
|
||||
"baseline-snapshot",
|
||||
"failed" if baseline_failed else "completed",
|
||||
f"urls={len(profile.get('baseline_urls', []))}",
|
||||
)
|
||||
else:
|
||||
_timeline_event(timeline, "baseline-snapshot", "skipped", "no baseline urls or provisioning blocked")
|
||||
|
||||
@@ -213,7 +264,6 @@ def _execute_case(canonical_id: str, run_id: str | None = None, dry_run: bool =
|
||||
elif browser_required:
|
||||
_timeline_event(timeline, "browser-replay-after-attack", "skipped", "proof browser capture unavailable")
|
||||
|
||||
compose_path = Path(provision_result["compose_path"])
|
||||
container_logs = evidence.collect_container_logs(run_dir, compose_path) if compose_path.exists() and allow_runtime_steps else []
|
||||
_timeline_event(
|
||||
timeline,
|
||||
@@ -231,25 +281,31 @@ def _execute_case(canonical_id: str, run_id: str | None = None, dry_run: bool =
|
||||
"proof_refs": proof_browser.get("refs", []),
|
||||
"baseline_title": baseline_browser.get("page_title"),
|
||||
"proof_title": proof_browser.get("page_title"),
|
||||
"error_kind": proof_browser.get("error_kind") or baseline_browser.get("error_kind"),
|
||||
"reason": proof_browser.get("reason") or baseline_browser.get("reason"),
|
||||
}
|
||||
|
||||
blocked_reason = provision_result.get("blocked_reason")
|
||||
if browser_required and not browser_present:
|
||||
blocked_reason = blocked_reason or baseline_browser.get("reason") or proof_browser.get("reason") or "browser evidence incomplete"
|
||||
|
||||
verification_mode = profile.get("verification_mode", "synthetic")
|
||||
artifact_mode = profile.get("artifact_mode", profile.get("provisioning_mode", "synthetic"))
|
||||
verification_status = "triage-manual"
|
||||
if provision_result.get("status") == "blocked-artifact":
|
||||
verification_status = "blocked-artifact"
|
||||
elif browser_required and not browser_present:
|
||||
verification_status = "triage-manual"
|
||||
elif any(step.get("status") == "failed" for step in attack_payload.get("steps", [])):
|
||||
verification_status = "triage-manual"
|
||||
elif artifact_mode == "synthetic":
|
||||
verification_status = "verified-synthetic"
|
||||
else:
|
||||
verification_status = "verified-real"
|
||||
success_evaluation = evaluate.evaluate_run(
|
||||
profile=profile,
|
||||
provision_result=provision_result,
|
||||
baseline_payload=baseline_payload,
|
||||
attack_payload=attack_payload,
|
||||
browser_payload=browser_payload,
|
||||
)
|
||||
verification_status = success_evaluation["verification_status"]
|
||||
blocked_reason = success_evaluation.get("blocked_reason")
|
||||
|
||||
cleanup_payload = {"status": "skipped", "detail": "cleanup_policy not destroy"}
|
||||
if compose_path.exists() and profile.get("cleanup_policy") == "destroy":
|
||||
cleanup_payload = provision.teardown(run_dir, compose_path)
|
||||
_timeline_event(
|
||||
timeline,
|
||||
"cleanup-compose-environment",
|
||||
cleanup_payload.get("status", "unknown"),
|
||||
cleanup_payload.get("detail", ""),
|
||||
)
|
||||
|
||||
finished_at = isoformat(now_utc())
|
||||
bundle = _build_run_bundle(
|
||||
@@ -267,6 +323,7 @@ def _execute_case(canonical_id: str, run_id: str | None = None, dry_run: bool =
|
||||
compose_refs=[str(compose_path)] if compose_path.exists() else [],
|
||||
browser_evidence=browser_payload,
|
||||
timeline=timeline,
|
||||
success_evaluation=success_evaluation,
|
||||
started_at=started_at,
|
||||
finished_at=finished_at,
|
||||
blocked_reason=blocked_reason,
|
||||
@@ -311,7 +368,8 @@ def cmd_provision(args) -> int:
|
||||
def cmd_seed(args) -> int:
|
||||
advisory = _load_advisory(args.case)
|
||||
profile = _resolve_profile(advisory)
|
||||
print({"steps": seed.run_seed(profile)})
|
||||
run_dir = _run_dir(args.run_id or _compose_run_id(advisory))
|
||||
print(seed.run_seed(profile, advisory, run_dir, dry_run=args.dry_run))
|
||||
return 0
|
||||
|
||||
|
||||
@@ -355,7 +413,8 @@ def cmd_run_case(args) -> int:
|
||||
|
||||
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]
|
||||
advisories = sorted(advisories, key=lambda item: item.get("canonical_id", ""))
|
||||
selected = advisories if not args.limit or args.limit <= 0 else advisories[: args.limit]
|
||||
for advisory in selected:
|
||||
_execute_case(advisory["canonical_id"], run_id=None, dry_run=args.dry_run, sync_outputs=False)
|
||||
if selected and not args.dry_run:
|
||||
@@ -442,6 +501,20 @@ def cmd_validate(args) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_doctor(args) -> int:
|
||||
profiles: List[Dict[str, Any]] = []
|
||||
if getattr(args, "case", None):
|
||||
advisory = _load_advisory(args.case)
|
||||
profiles.append(_resolve_profile(advisory))
|
||||
elif getattr(args, "system", None):
|
||||
advisories = [item for item in load_json_dir(ADVISORIES_DIR) if item.get("system_id") == args.system]
|
||||
if advisories:
|
||||
profiles.append(_resolve_profile(advisories[0]))
|
||||
result = doctor.run_checks(profiles)
|
||||
print(result)
|
||||
return 0 if result.get("ok") else 1
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="Websafe local lab orchestrator")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
@@ -479,7 +552,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
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("--limit", type=int, default=0)
|
||||
run_system.add_argument("--dry-run", action="store_true")
|
||||
run_system.set_defaults(func=cmd_run_system)
|
||||
|
||||
@@ -508,6 +581,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
validate = subparsers.add_parser("validate", help="validate lab assets")
|
||||
validate.set_defaults(func=cmd_validate)
|
||||
|
||||
doctor_cmd = subparsers.add_parser("doctor", help="run environment preflight checks")
|
||||
doctor_cmd.add_argument("--case")
|
||||
doctor_cmd.add_argument("--system")
|
||||
doctor_cmd.set_defaults(func=cmd_doctor)
|
||||
return parser
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
import requests
|
||||
|
||||
from lab.compose import compose_payload, generate_compose
|
||||
from lab.utils import command_available, run
|
||||
from lab.utils import command_available, run, write_json
|
||||
|
||||
|
||||
def prepare(profile: Dict[str, Any], run_dir: Path, dry_run: bool = False) -> Dict[str, Any]:
|
||||
@@ -34,10 +37,58 @@ def prepare(profile: Dict[str, Any], run_dir: Path, dry_run: bool = False) -> Di
|
||||
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)
|
||||
up = run(["docker", "compose", "-f", str(compose_path), "up", "-d", "--wait"], 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"
|
||||
result["blocked_reason"] = up.stderr.strip() or up.stdout.strip() or "docker compose up failed"
|
||||
return result
|
||||
return result
|
||||
|
||||
|
||||
def wait_ready(profile: Dict[str, Any], run_dir: Path, compose_path: Path) -> Dict[str, Any]:
|
||||
timeout_seconds = int(profile.get("ready_timeout_seconds") or 45)
|
||||
baseline_urls = profile.get("baseline_urls", []) or []
|
||||
started = time.monotonic()
|
||||
observations = []
|
||||
status = "completed"
|
||||
detail = f"baseline urls ready ({len(baseline_urls)})"
|
||||
|
||||
while True:
|
||||
observations = []
|
||||
ready = True
|
||||
for url in baseline_urls:
|
||||
try:
|
||||
response = requests.get(url, timeout=4)
|
||||
observations.append({"url": url, "status_code": response.status_code})
|
||||
if response.status_code >= 500:
|
||||
ready = False
|
||||
except Exception as exc:
|
||||
observations.append({"url": url, "error": str(exc)})
|
||||
ready = False
|
||||
if ready:
|
||||
break
|
||||
if time.monotonic() - started >= timeout_seconds:
|
||||
status = "failed"
|
||||
detail = f"services not ready within {timeout_seconds}s"
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
payload = {
|
||||
"status": status,
|
||||
"detail": detail,
|
||||
"elapsed_seconds": round(time.monotonic() - started, 1),
|
||||
"observations": observations,
|
||||
"compose_path": str(compose_path),
|
||||
}
|
||||
write_json(run_dir / "logs" / "ready.json", payload)
|
||||
return payload
|
||||
|
||||
|
||||
def teardown(run_dir: Path, compose_path: Path) -> Dict[str, Any]:
|
||||
if not command_available("docker") or not compose_path.exists():
|
||||
return {"status": "skipped", "detail": "docker unavailable or compose file missing"}
|
||||
down = run(["docker", "compose", "-f", str(compose_path), "down", "-v", "--remove-orphans"], cwd=run_dir)
|
||||
if down.returncode != 0:
|
||||
return {"status": "failed", "detail": down.stderr.strip() or down.stdout.strip() or "docker compose down failed"}
|
||||
return {"status": "completed", "detail": "docker compose down completed"}
|
||||
|
||||
@@ -88,6 +88,11 @@ def resolve_profile(advisory_id: str, advisory: Optional[Dict[str, Any]] = None)
|
||||
return direct_profile
|
||||
|
||||
family = resolve_repro_family(advisory, system_map)
|
||||
system_family_profile = profiles.get(f"{advisory.get('system_id', '')}-{family.replace('-generic', '')}")
|
||||
if system_family_profile:
|
||||
resolved = dict(system_family_profile)
|
||||
resolved.setdefault("resolved_via", "system-family")
|
||||
return resolved
|
||||
profile = profiles.get(family)
|
||||
if profile:
|
||||
resolved = dict(profile)
|
||||
@@ -103,6 +108,7 @@ def resolve_profile(advisory_id: str, advisory: Optional[Dict[str, Any]] = None)
|
||||
"attack_actions": [],
|
||||
"baseline_actions": [],
|
||||
"success_criteria": ["manual triage required"],
|
||||
"success_assertions": [],
|
||||
"cleanup_policy": "destroy",
|
||||
"destructive_risk": "medium",
|
||||
"allowed_target_types": ["lab-local", "lab-public", "authorized-third-party"],
|
||||
@@ -152,6 +158,8 @@ def annotate_with_latest_run(advisory: Dict[str, Any], run: Optional[Dict[str, A
|
||||
"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"),
|
||||
"historical_status": run.get("verification_status", merged["verification_status"]),
|
||||
"latest_status": run.get("verification_status", merged["verification_status"]),
|
||||
}
|
||||
)
|
||||
return merged
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from lab.runners.dispatcher import run_attack, run_seed
|
||||
|
||||
__all__ = ["run_seed", "run_attack"]
|
||||
|
||||
169
scripts/lab/runners/common.py
普通文件
169
scripts/lab/runners/common.py
普通文件
@@ -0,0 +1,169 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import requests
|
||||
|
||||
from lab.utils import ensure_dir, write_json
|
||||
|
||||
|
||||
@dataclass
|
||||
class RunnerContext:
|
||||
profile: Dict[str, Any]
|
||||
advisory: Dict[str, Any]
|
||||
run_dir: Path
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return str((self.profile.get("baseline_urls") or [""])[0]).rstrip("/")
|
||||
|
||||
@property
|
||||
def family(self) -> str:
|
||||
return str(self.profile.get("vuln_family") or "").strip()
|
||||
|
||||
@property
|
||||
def runner_id(self) -> str:
|
||||
return str(self.profile.get("runner_id") or "").strip()
|
||||
|
||||
@property
|
||||
def logs_dir(self) -> Path:
|
||||
path = self.run_dir / "logs"
|
||||
ensure_dir(path)
|
||||
return path
|
||||
|
||||
|
||||
def _request(
|
||||
context: RunnerContext,
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
json_payload: Dict[str, Any] | None = None,
|
||||
timeout: float = 15.0,
|
||||
) -> Dict[str, Any]:
|
||||
response = requests.request(
|
||||
method=method,
|
||||
url=f"{context.base_url}{path}",
|
||||
json=json_payload,
|
||||
timeout=timeout,
|
||||
)
|
||||
body: Dict[str, Any]
|
||||
try:
|
||||
parsed = response.json()
|
||||
body = parsed if isinstance(parsed, dict) else {"data": parsed}
|
||||
except Exception:
|
||||
body = {"text": response.text[:1000]}
|
||||
return {
|
||||
"status_code": response.status_code,
|
||||
"ok": response.ok,
|
||||
"body": body,
|
||||
}
|
||||
|
||||
|
||||
def _seed_payload(context: RunnerContext) -> Dict[str, Any]:
|
||||
return {
|
||||
"system_id": context.advisory["system_id"],
|
||||
"family": context.family,
|
||||
"case_id": context.advisory["canonical_id"],
|
||||
"title": context.advisory.get("title"),
|
||||
}
|
||||
|
||||
|
||||
def _attack_payload(context: RunnerContext, family: str) -> Dict[str, Any]:
|
||||
case_id = context.advisory["canonical_id"]
|
||||
payloads = {
|
||||
"proxy-boundary": {
|
||||
"family": family,
|
||||
"case_id": case_id,
|
||||
"headers": {"x-forwarded-user": "admin", "x-forwarded-host": "internal.lab"},
|
||||
},
|
||||
"authz-bypass": {
|
||||
"family": family,
|
||||
"case_id": case_id,
|
||||
"actor": "guest",
|
||||
"target": "/admin",
|
||||
"override": "bypass",
|
||||
},
|
||||
"ssrf": {
|
||||
"family": family,
|
||||
"case_id": case_id,
|
||||
"target_url": "http://127.0.0.1/sink",
|
||||
},
|
||||
"xss": {
|
||||
"family": family,
|
||||
"case_id": case_id,
|
||||
"payload": f"<script>document.body.dataset.case='{case_id}'</script>",
|
||||
},
|
||||
"file-upload": {
|
||||
"family": family,
|
||||
"case_id": case_id,
|
||||
"filename": f"{case_id}.txt",
|
||||
"content": f"fixture upload marker for {case_id}",
|
||||
},
|
||||
"deserialization": {
|
||||
"family": family,
|
||||
"case_id": case_id,
|
||||
"payload": {"marker": case_id, "mode": "inert-object"},
|
||||
},
|
||||
}
|
||||
return payloads[family]
|
||||
|
||||
|
||||
def run_fixture_seed(context: RunnerContext, family: str) -> Dict[str, Any]:
|
||||
result = _request(context, "POST", "/seed", json_payload=_seed_payload(context))
|
||||
payload = {
|
||||
"steps": [
|
||||
{
|
||||
"kind": "runner",
|
||||
"tool": context.runner_id or f"{context.advisory['system_id']}.{family}",
|
||||
"status": "completed" if result["ok"] else "failed",
|
||||
"status_code": result["status_code"],
|
||||
"detail": result["body"].get("detail") or "seed request completed",
|
||||
}
|
||||
],
|
||||
"seeded": bool(result["ok"]),
|
||||
"result": result,
|
||||
}
|
||||
write_json(context.logs_dir / "seed.json", payload)
|
||||
return payload
|
||||
|
||||
|
||||
def run_fixture_attack(context: RunnerContext, family: str) -> Dict[str, Any]:
|
||||
before: Dict[str, Any] = {}
|
||||
if family in {"proxy-boundary", "authz-bypass"}:
|
||||
before = _request(context, "GET", "/admin")
|
||||
attack = _request(context, "POST", "/attack", json_payload=_attack_payload(context, family))
|
||||
proof = _request(context, "GET", "/proof")
|
||||
after: Dict[str, Any] = {}
|
||||
if family in {"proxy-boundary", "authz-bypass"}:
|
||||
after = _request(context, "GET", "/admin")
|
||||
|
||||
success = bool(attack["ok"] and proof["ok"] and proof["body"].get("success"))
|
||||
step = {
|
||||
"kind": "runner",
|
||||
"tool": context.runner_id or f"{context.advisory['system_id']}.{family}",
|
||||
"status": "completed" if success else "failed",
|
||||
"status_code": attack["status_code"],
|
||||
"result_path": str(context.logs_dir / "attack.json"),
|
||||
}
|
||||
payload = {
|
||||
"steps": [step],
|
||||
"success": success,
|
||||
"detail": proof["body"].get("detail") or attack["body"].get("detail") or "runner attack finished",
|
||||
"before": before,
|
||||
"attack": attack,
|
||||
"after": after,
|
||||
"proof": proof,
|
||||
"assertions": [
|
||||
{
|
||||
"name": "proof-success",
|
||||
"kind": "runner-proof",
|
||||
"passed": success,
|
||||
"detail": proof["body"].get("detail") or "runner proof endpoint returned success",
|
||||
}
|
||||
],
|
||||
}
|
||||
write_json(context.logs_dir / "attack.json", payload)
|
||||
return payload
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from lab.runners.common import RunnerContext
|
||||
|
||||
|
||||
def _module_name(profile: Dict[str, Any]) -> str:
|
||||
runner_id = str(profile.get("runner_id") or "").strip()
|
||||
if runner_id:
|
||||
system_name, family_name = runner_id.split(".", 1)
|
||||
else:
|
||||
system_name = str(profile.get("system_id") or "").strip()
|
||||
family_name = str(profile.get("vuln_family") or "").strip()
|
||||
system_name = system_name.replace("-", "_")
|
||||
family_name = family_name.replace("-", "_")
|
||||
return f"lab.runners.{system_name}.{family_name}"
|
||||
|
||||
|
||||
def _load_runner(profile: Dict[str, Any]):
|
||||
return importlib.import_module(_module_name(profile))
|
||||
|
||||
|
||||
def run_seed(profile: Dict[str, Any], advisory: Dict[str, Any], run_dir: Path) -> Dict[str, Any]:
|
||||
module = _load_runner(profile)
|
||||
context = RunnerContext(profile=profile, advisory=advisory, run_dir=run_dir)
|
||||
return module.run_seed(context)
|
||||
|
||||
|
||||
def run_attack(profile: Dict[str, Any], advisory: Dict[str, Any], run_dir: Path) -> Dict[str, Any]:
|
||||
module = _load_runner(profile)
|
||||
context = RunnerContext(profile=profile, advisory=advisory, run_dir=run_dir)
|
||||
return module.run_attack(context)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Gitea family runners."""
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from lab.runners.common import RunnerContext, run_fixture_attack, run_fixture_seed
|
||||
|
||||
|
||||
def run_seed(context: RunnerContext):
|
||||
return run_fixture_seed(context, "authz-bypass")
|
||||
|
||||
|
||||
def run_attack(context: RunnerContext):
|
||||
return run_fixture_attack(context, "authz-bypass")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from lab.runners.common import RunnerContext, run_fixture_attack, run_fixture_seed
|
||||
|
||||
|
||||
def run_seed(context: RunnerContext):
|
||||
return run_fixture_seed(context, "file-upload")
|
||||
|
||||
|
||||
def run_attack(context: RunnerContext):
|
||||
return run_fixture_attack(context, "file-upload")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from lab.runners.common import RunnerContext, run_fixture_attack, run_fixture_seed
|
||||
|
||||
|
||||
def run_seed(context: RunnerContext):
|
||||
return run_fixture_seed(context, "proxy-boundary")
|
||||
|
||||
|
||||
def run_attack(context: RunnerContext):
|
||||
return run_fixture_attack(context, "proxy-boundary")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from lab.runners.common import RunnerContext, run_fixture_attack, run_fixture_seed
|
||||
|
||||
|
||||
def run_seed(context: RunnerContext):
|
||||
return run_fixture_seed(context, "ssrf")
|
||||
|
||||
|
||||
def run_attack(context: RunnerContext):
|
||||
return run_fixture_attack(context, "ssrf")
|
||||
|
||||
10
scripts/lab/runners/gitea/xss.py
普通文件
10
scripts/lab/runners/gitea/xss.py
普通文件
@@ -0,0 +1,10 @@
|
||||
from lab.runners.common import RunnerContext, run_fixture_attack, run_fixture_seed
|
||||
|
||||
|
||||
def run_seed(context: RunnerContext):
|
||||
return run_fixture_seed(context, "xss")
|
||||
|
||||
|
||||
def run_attack(context: RunnerContext):
|
||||
return run_fixture_attack(context, "xss")
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Next.js family runners."""
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from lab.runners.common import RunnerContext, run_fixture_attack, run_fixture_seed
|
||||
|
||||
|
||||
def run_seed(context: RunnerContext):
|
||||
return run_fixture_seed(context, "authz-bypass")
|
||||
|
||||
|
||||
def run_attack(context: RunnerContext):
|
||||
return run_fixture_attack(context, "authz-bypass")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from lab.runners.common import RunnerContext, run_fixture_attack, run_fixture_seed
|
||||
|
||||
|
||||
def run_seed(context: RunnerContext):
|
||||
return run_fixture_seed(context, "deserialization")
|
||||
|
||||
|
||||
def run_attack(context: RunnerContext):
|
||||
return run_fixture_attack(context, "deserialization")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from lab.runners.common import RunnerContext, run_fixture_attack, run_fixture_seed
|
||||
|
||||
|
||||
def run_seed(context: RunnerContext):
|
||||
return run_fixture_seed(context, "proxy-boundary")
|
||||
|
||||
|
||||
def run_attack(context: RunnerContext):
|
||||
return run_fixture_attack(context, "proxy-boundary")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from lab.runners.common import RunnerContext, run_fixture_attack, run_fixture_seed
|
||||
|
||||
|
||||
def run_seed(context: RunnerContext):
|
||||
return run_fixture_seed(context, "ssrf")
|
||||
|
||||
|
||||
def run_attack(context: RunnerContext):
|
||||
return run_fixture_attack(context, "ssrf")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from lab.runners.common import RunnerContext, run_fixture_attack, run_fixture_seed
|
||||
|
||||
|
||||
def run_seed(context: RunnerContext):
|
||||
return run_fixture_seed(context, "xss")
|
||||
|
||||
|
||||
def run_attack(context: RunnerContext):
|
||||
return run_fixture_attack(context, "xss")
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Undici family runners."""
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from lab.runners.common import RunnerContext, run_fixture_attack, run_fixture_seed
|
||||
|
||||
|
||||
def run_seed(context: RunnerContext):
|
||||
return run_fixture_seed(context, "ssrf")
|
||||
|
||||
|
||||
def run_attack(context: RunnerContext):
|
||||
return run_fixture_attack(context, "ssrf")
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Vite family runners."""
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from lab.runners.common import RunnerContext, run_fixture_attack, run_fixture_seed
|
||||
|
||||
|
||||
def run_seed(context: RunnerContext):
|
||||
return run_fixture_seed(context, "file-upload")
|
||||
|
||||
|
||||
def run_attack(context: RunnerContext):
|
||||
return run_fixture_attack(context, "file-upload")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from lab.runners.common import RunnerContext, run_fixture_attack, run_fixture_seed
|
||||
|
||||
|
||||
def run_seed(context: RunnerContext):
|
||||
return run_fixture_seed(context, "proxy-boundary")
|
||||
|
||||
|
||||
def run_attack(context: RunnerContext):
|
||||
return run_fixture_attack(context, "proxy-boundary")
|
||||
|
||||
10
scripts/lab/runners/vite/xss.py
普通文件
10
scripts/lab/runners/vite/xss.py
普通文件
@@ -0,0 +1,10 @@
|
||||
from lab.runners.common import RunnerContext, run_fixture_attack, run_fixture_seed
|
||||
|
||||
|
||||
def run_seed(context: RunnerContext):
|
||||
return run_fixture_seed(context, "xss")
|
||||
|
||||
|
||||
def run_attack(context: RunnerContext):
|
||||
return run_fixture_attack(context, "xss")
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from lab.runners.dispatcher import run_seed as run_runner_seed
|
||||
from lab.utils import write_json
|
||||
|
||||
def run_seed(profile: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
steps = []
|
||||
|
||||
def run_seed(profile: Dict[str, Any], advisory: Dict[str, Any], run_dir: Path, dry_run: bool = False) -> Dict[str, Any]:
|
||||
if profile.get("runner_id") and not dry_run:
|
||||
return run_runner_seed(profile, advisory, run_dir)
|
||||
|
||||
steps: List[Dict[str, Any]] = []
|
||||
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
|
||||
steps.append(
|
||||
{
|
||||
"kind": kind,
|
||||
"status": "planned" if dry_run else "skipped",
|
||||
"message": "Seed action type not yet automated",
|
||||
}
|
||||
)
|
||||
payload = {"steps": steps, "seeded": not any(item["status"] == "skipped" for item in steps)}
|
||||
write_json(run_dir / "logs" / "seed.json", payload)
|
||||
return payload
|
||||
|
||||
@@ -34,6 +34,9 @@ def validate_assets() -> List[str]:
|
||||
]:
|
||||
if field not in content:
|
||||
errors.append(f"repro profile missing {field}: {path}")
|
||||
fixture_path = content.get("fixture_path")
|
||||
if fixture_path and not Path(fixture_path).exists():
|
||||
errors.append(f"fixture path missing for {path}: {fixture_path}")
|
||||
docker_available = command_available("docker")
|
||||
profile_roots = sorted((ENV_CATALOG_DIR.parent.parent / "profiles").rglob("*.yaml"))
|
||||
for path in profile_roots:
|
||||
|
||||
在新工单中引用
屏蔽一个用户