reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Four-layer artifact validator.
|
||||
|
||||
Layer 1: Schema validation (analysis_result + candidates against JSON schemas)
|
||||
Layer 2: Static validation (Python syntax, JS syntax via Node)
|
||||
Layer 3: Candidate invariant checks (verified=true requires verification records)
|
||||
Layer 4: Cross-file consistency (actions present in both jsrpc_inject.js and flask_proxy.py)
|
||||
|
||||
Usage:
|
||||
python3 scripts/validate_artifacts.py \
|
||||
--analysis analysis_result.json \
|
||||
--candidates artifacts/encryption_candidates.json \
|
||||
--generated generated/ \
|
||||
--report artifacts/validation_report.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import importlib.util
|
||||
import json
|
||||
import py_compile
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qsl
|
||||
|
||||
from common import dump_json, load_json
|
||||
|
||||
|
||||
def check_schema(name: str, data: dict, schema_path: Path) -> dict:
|
||||
"""Validate data against a JSON schema."""
|
||||
try:
|
||||
from jsonschema import Draft202012Validator
|
||||
schema = load_json(schema_path)
|
||||
errors = [e.message for e in Draft202012Validator(schema).iter_errors(data)]
|
||||
return {"name": name, "ok": not errors, "errors": errors}
|
||||
except ImportError:
|
||||
return {"name": name, "ok": False, "errors": ["jsonschema not installed"]}
|
||||
except FileNotFoundError:
|
||||
return {"name": name, "ok": True, "skipped": f"schema not found: {schema_path}"}
|
||||
|
||||
|
||||
def check_python(path: Path) -> dict:
|
||||
"""Check Python syntax."""
|
||||
try:
|
||||
py_compile.compile(str(path), doraise=True)
|
||||
ast.parse(path.read_text(encoding="utf-8"))
|
||||
return {"name": f"python:{path.name}", "ok": True}
|
||||
except Exception as error:
|
||||
return {"name": f"python:{path.name}", "ok": False, "errors": [str(error)]}
|
||||
|
||||
|
||||
def check_javascript(path: Path) -> dict:
|
||||
"""Check JavaScript syntax via Node.js."""
|
||||
if not path.exists():
|
||||
return {"name": f"javascript:{path.name}", "ok": False, "errors": ["missing"]}
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
return {"name": f"javascript:{path.name}", "ok": True, "skipped": "node unavailable"}
|
||||
proc = subprocess.run([node, "--check", str(path)], text=True, capture_output=True)
|
||||
return {
|
||||
"name": f"javascript:{path.name}",
|
||||
"ok": proc.returncode == 0,
|
||||
"errors": [proc.stderr.strip()] if proc.returncode else []
|
||||
}
|
||||
|
||||
|
||||
def check_dependency(name: str) -> dict:
|
||||
"""Require packages used by generated services and schema validation."""
|
||||
available = importlib.util.find_spec(name) is not None
|
||||
return {
|
||||
"name": f"dependency:{name}",
|
||||
"ok": available,
|
||||
"errors": [] if available else [f"{name} is not installed"],
|
||||
}
|
||||
|
||||
|
||||
def check_jsrpc_evidence_gate(path: Path) -> dict:
|
||||
"""Fail validation when generated JSRPC code is still quarantined."""
|
||||
if not path.exists():
|
||||
return {"name": "jsrpc_evidence_gate", "ok": False, "errors": ["missing jsrpc_inject.js"]}
|
||||
content = path.read_text(encoding="utf-8")
|
||||
match = re.search(r"const EVIDENCE_ERRORS = (\{.*?\});", content, re.DOTALL)
|
||||
if not match:
|
||||
return {"name": "jsrpc_evidence_gate", "ok": False, "errors": ["missing evidence gate"]}
|
||||
try:
|
||||
errors = json.loads(match.group(1))
|
||||
except json.JSONDecodeError as error:
|
||||
return {"name": "jsrpc_evidence_gate", "ok": False, "errors": [str(error)]}
|
||||
if errors:
|
||||
return {
|
||||
"name": "jsrpc_evidence_gate",
|
||||
"ok": False,
|
||||
"errors": [f"quarantined parameters: {', '.join(sorted(errors))}"],
|
||||
}
|
||||
return {"name": "jsrpc_evidence_gate", "ok": True, "errors": []}
|
||||
|
||||
|
||||
def check_candidate_invariants(candidates: dict) -> dict:
|
||||
"""Check candidate verification invariants."""
|
||||
errors = []
|
||||
for c in candidates.get("candidates", []):
|
||||
evidence_types = [e.get("type") for e in c.get("evidence", []) if isinstance(e, dict)]
|
||||
verification = c.get("verification")
|
||||
provenance = c.get("provenance")
|
||||
if c.get("confidence") == "high" and not c.get("verified"):
|
||||
errors.append(f"{c.get('id')}: high confidence without verification")
|
||||
if c.get("verified"):
|
||||
if not isinstance(verification, list) or not verification:
|
||||
errors.append(f"{c.get('id')}: verified without verification records")
|
||||
if c.get("source") == "hint" or c.get("type") == "hint":
|
||||
errors.append(f"{c.get('id')}: manual hint cannot be verified")
|
||||
matched = any(
|
||||
isinstance(record, dict) and (
|
||||
record.get("matched") is True or record.get("verified") is True or
|
||||
any(isinstance(test, dict) and test.get("matched") is True
|
||||
for test in record.get("tests", []))
|
||||
)
|
||||
for record in (verification or [])
|
||||
)
|
||||
if not matched:
|
||||
errors.append(f"{c.get('id')}: no successful runtime/request match")
|
||||
return {"name": "candidate_invariants", "ok": not errors, "errors": errors}
|
||||
|
||||
|
||||
def check_cross_file(analysis: dict, generated: Path) -> dict:
|
||||
"""Check cross-file consistency between JSRPC and Flask."""
|
||||
errors = []
|
||||
jsrpc = (generated / "jsrpc_inject.js").read_text(encoding="utf-8") if (generated / "jsrpc_inject.js").exists() else ""
|
||||
flask = (generated / "flask_proxy.py").read_text(encoding="utf-8") if (generated / "flask_proxy.py").exists() else ""
|
||||
|
||||
# Check action names
|
||||
action_name = analysis.get("jsrpc", {}).get("action_name", "")
|
||||
if action_name and action_name not in jsrpc:
|
||||
errors.append(f"action missing from jsrpc_inject.js: {action_name}")
|
||||
if action_name and action_name not in flask:
|
||||
errors.append(f"action missing from flask_proxy.py: {action_name}")
|
||||
|
||||
# Check entrypoint resolution
|
||||
if "resolveEntrypoint" not in jsrpc and "resolvePath" not in jsrpc:
|
||||
errors.append("jsrpc_inject.js does not contain entrypoint resolution")
|
||||
|
||||
return {"name": "cross_file_consistency", "ok": not errors, "errors": errors}
|
||||
|
||||
|
||||
def mock_semantics() -> dict:
|
||||
"""Check HTTP form semantics."""
|
||||
raw = "a=1&a=2&password=x+y&empty="
|
||||
pairs = parse_qsl(raw, keep_blank_values=True)
|
||||
errors = []
|
||||
if pairs[:2] != [("a", "1"), ("a", "2")]:
|
||||
errors.append("duplicate form keys not preserved")
|
||||
if pairs[-1] != ("empty", ""):
|
||||
errors.append("blank form value not preserved")
|
||||
return {"name": "mock_http_semantics", "ok": not errors, "errors": errors}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Four-layer artifact validator.")
|
||||
parser.add_argument("--analysis", required=True, help="Path to analysis_result.json.")
|
||||
parser.add_argument("--candidates", help="Path to encryption_candidates.json.")
|
||||
parser.add_argument("--generated", required=True, help="Path to generated/ directory.")
|
||||
parser.add_argument("--report", required=True, help="Path to validation report JSON.")
|
||||
parser.add_argument("--e2e-result", help="Path to optional E2E result JSON.")
|
||||
parser.add_argument("--adversarial-trace", help="Optional adversarial runtime trace JSON.")
|
||||
args = parser.parse_args()
|
||||
|
||||
base = Path(__file__).parents[1]
|
||||
generated = Path(args.generated)
|
||||
analysis = load_json(args.analysis)
|
||||
|
||||
checks = []
|
||||
|
||||
# A green report must mean the generated service can start and schema
|
||||
# checks really ran. Missing required packages are therefore failures,
|
||||
# not silently skipped checks.
|
||||
checks.extend(check_dependency(name) for name in ("flask", "requests", "jsonschema"))
|
||||
|
||||
# Layer 1: Schema validation
|
||||
schema_path = base / "schemas" / "analysis_result.schema.json"
|
||||
if schema_path.exists():
|
||||
checks.append(check_schema("analysis_schema", analysis, schema_path))
|
||||
|
||||
if args.candidates:
|
||||
candidates = load_json(args.candidates)
|
||||
candidates_schema = base / "schemas" / "candidates.schema.json"
|
||||
if candidates_schema.exists():
|
||||
checks.append(check_schema("candidate_schema", candidates, candidates_schema))
|
||||
checks.append(check_candidate_invariants(candidates))
|
||||
|
||||
# Layer 2: Static validation
|
||||
for name in ("flask_proxy.py",):
|
||||
path = generated / name
|
||||
checks.append(check_python(path) if path.exists() else {"name": f"python:{name}", "ok": False, "errors": ["missing"]})
|
||||
for name in ("jsrpc_inject.js", "runtime_hook_probe.js", "adversarial_runtime_probe.js", "module_probe.js"):
|
||||
checks.append(check_javascript(generated / name))
|
||||
checks.append(check_jsrpc_evidence_gate(generated / "jsrpc_inject.js"))
|
||||
|
||||
# Layer 3: Cross-file consistency
|
||||
checks.append(check_cross_file(analysis, generated))
|
||||
checks.append(mock_semantics())
|
||||
|
||||
# Layer 4: Optional real E2E
|
||||
if args.e2e_result:
|
||||
e2e = load_json(args.e2e_result, {})
|
||||
checks.append({"name": "real_e2e", "ok": bool(e2e.get("passed")), "details": e2e})
|
||||
|
||||
if args.adversarial_trace:
|
||||
trace = load_json(args.adversarial_trace, {})
|
||||
schema = base / "schemas" / "adversarial_trace.schema.json"
|
||||
checks.append(check_schema("adversarial_trace_schema", trace, schema))
|
||||
|
||||
report = {"passed": all(c.get("ok") for c in checks), "checks": checks}
|
||||
dump_json(args.report, report)
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
return 0 if report["passed"] else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user