HARvestGym / scripts /inspect_har_endpoints.py
kdcyberdude's picture
Upload folder using huggingface_hub
9eebce3 verified
Raw
History Blame
8.1 kB
#!/usr/bin/env python3
"""
inspect_har_endpoints.py
Runs extract_openapi_spec() on every HAR file in hars/ and prints a full
summary of discovered endpoints β€” method, path, status code, auth, and a
snippet of the request/response body where available.
Usage:
python scripts/inspect_har_endpoints.py [--json]
Flags:
--json Emit machine-readable JSON instead of the human-readable table
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
# ---------------------------------------------------------------------------
# Path setup β€” make the package importable without installing
# ---------------------------------------------------------------------------
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT))
from server.tools.browser_agent import extract_openapi_spec # noqa: E402
# ---------------------------------------------------------------------------
# HAR files to inspect
# ---------------------------------------------------------------------------
HARS_DIR = REPO_ROOT / "hars"
HAR_FILES = {
"shopping": HARS_DIR / "shopping.har",
"shopping_admin": HARS_DIR / "shopping_admin.har",
"forum": HARS_DIR / "forum.har",
"wikipedia": HARS_DIR / "wikipedia.har",
}
# Fake base URLs β€” only used for pass-through in extract_openapi_spec
APP_BASE_URLS = {
"shopping": "http://localhost:7770",
"shopping_admin": "http://localhost:7780",
"forum": "http://localhost:9999",
"wikipedia": "http://localhost:8888",
}
# ---------------------------------------------------------------------------
# Pretty-print helpers
# ---------------------------------------------------------------------------
_COL_W = 80
def _hr(char: str = "─") -> None:
print(char * _COL_W)
def _body_snippet(value) -> str | None:
if value is None:
return None
if isinstance(value, str):
snippet = value[:120]
else:
snippet = json.dumps(value)[:120]
return snippet + ("…" if len(str(snippet)) >= 120 else "")
def _print_entry(idx: int, entry: dict) -> None:
auth_flag = "πŸ” AUTH" if entry["auth_observed"] else "open"
print(f" [{idx:>3}] {entry['method']:<7} {entry['path']}")
print(f" status={entry['status_code']} ct={entry['response_content_type'] or 'β€”'} {auth_flag}")
if entry.get("query_params"):
print(f" query: {entry['query_params'][:100]}")
req_snippet = _body_snippet(entry.get("request_body"))
if req_snippet:
print(f" req_body: {req_snippet}")
resp_snippet = _body_snippet(entry.get("response_body_sample"))
if resp_snippet:
print(f" resp_sample: {resp_snippet}")
def _method_counts(entries: list[dict]) -> dict[str, int]:
counts: dict[str, int] = {}
for e in entries:
counts[e["method"]] = counts.get(e["method"], 0) + 1
return dict(sorted(counts.items()))
def print_app_summary(app_name: str, entries: list[dict], raw_total: int | None = None) -> None:
_hr("═")
header = f" APP: {app_name.upper()} ({len(entries)} unique API endpoints"
if raw_total is not None:
header += f" extracted from {raw_total} raw HAR entries"
header += ")"
print(header)
counts = _method_counts(entries)
print(f" Methods: {counts}")
auth_count = sum(1 for e in entries if e["auth_observed"])
print(f" Auth-required endpoints: {auth_count}/{len(entries)}")
_hr()
if not entries:
print(" (no API-like entries survived filtering)")
for i, entry in enumerate(entries, 1):
_print_entry(i, entry)
print()
# ---------------------------------------------------------------------------
# JSON mode
# ---------------------------------------------------------------------------
def emit_json(results: dict) -> None:
# Convert to a JSON-safe structure
output = {}
for app_name, entries in results.items():
output[app_name] = {
"total": len(entries),
"method_counts": _method_counts(entries),
"endpoints": entries,
}
print(json.dumps(output, indent=2))
# ---------------------------------------------------------------------------
# Verification / assertion checks
# ---------------------------------------------------------------------------
# NOTE: These HAR files are sparse β€” each was recorded for a narrow task
# scenario, not as a full API crawl. The vast majority of HAR entries are
# static assets (/static/ prefix) that the extractor correctly filters out.
# Thresholds below reflect the actual usable API surface in each file.
SANITY_CHECKS: dict[str, dict] = {
"shopping": {
"min_endpoints": 1,
"expected_methods": {"GET"},
"note": "Sparse HAR β€” only checkout success page recorded; "
"213 total entries but 212 are /static/ assets.",
},
"shopping_admin": {
"min_endpoints": 2,
"expected_methods": {"GET", "POST"},
"note": "Sparse HAR β€” product save/edit + MUI JSON endpoint; "
"353 total entries but 350 are /static/ assets.",
},
"forum": {
"min_endpoints": 2,
"expected_methods": {"GET", "POST"},
"note": "Sparse HAR β€” one POST submission + one forum thread GET; "
"24 total entries but 22 are .js build files.",
},
"wikipedia": {
"min_endpoints": 0,
"expected_methods": set(),
"note": "Sparse HAR β€” only an article HTML page + /-/mw/ style/JS assets; "
"no XHR/REST traffic recorded.",
},
}
def run_checks(results: dict) -> bool:
print("\n" + "─" * _COL_W)
print("SANITY CHECKS (thresholds calibrated to actual HAR content)")
print("─" * _COL_W)
all_passed = True
for app_name, checks in SANITY_CHECKS.items():
entries = results.get(app_name, [])
methods_found = set(e["method"] for e in entries)
n = len(entries)
min_ok = n >= checks["min_endpoints"]
exp = checks["expected_methods"]
methods_ok = exp.issubset(methods_found) if exp else True
status = "PASS" if (min_ok and methods_ok) else "FAIL"
if status == "FAIL":
all_passed = False
print(f" {status} {app_name}")
print(f" endpoints : {n} (min={checks['min_endpoints']}) {'βœ“' if min_ok else 'βœ—'}")
if exp:
print(f" methods : {sorted(methods_found)} "
f"(expected βŠ‡ {sorted(exp)}) {'βœ“' if methods_ok else 'βœ—'}")
print(f" note : {checks['note']}")
print("─" * _COL_W)
print("Overall:", "ALL PASSED βœ“" if all_passed else "SOME FAILED βœ—")
return all_passed
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> int:
emit_json_mode = "--json" in sys.argv
results: dict[str, list[dict]] = {}
raw_totals: dict[str, int] = {}
missing: list[str] = []
for app_name, har_path in HAR_FILES.items():
if not har_path.exists():
print(f"[WARN] HAR not found: {har_path}", file=sys.stderr)
missing.append(app_name)
results[app_name] = []
raw_totals[app_name] = 0
continue
with open(har_path) as f:
har_data = json.load(f)
raw_totals[app_name] = len(har_data.get("log", {}).get("entries", []))
entries = extract_openapi_spec(har_data, APP_BASE_URLS[app_name])
results[app_name] = entries
if emit_json_mode:
emit_json(results)
return 0
# Human-readable output
for app_name, entries in results.items():
print_app_summary(app_name, entries, raw_totals.get(app_name))
passed = run_checks(results)
if missing:
print(f"\n[WARN] Missing HAR files for: {', '.join(missing)}")
return 0 if passed else 1
if __name__ == "__main__":
sys.exit(main())