import asyncio import json from pathlib import Path from unittest.mock import patch import httpx import pytest from app import client_modules as tools, config, main, nuclei_catalog, pipeline, web_modules from test_worker_pipeline import run_job def test_script_inventory_keeps_same_origin_and_javascript_inline_only(): parser = tools.ScriptInventory("https://example.org/") parser.feed('' '' '') assert parser.urls == ["https://example.org/app.js"] assert len(parser.inline) == 1 def test_retire_no_javascript_skips_without_starting_node(): async def fetch(*args): return b"No scripts", "text/html", "https://example.org/" with patch.object(tools, "fetch_sample", fetch), patch.object(tools, "execute") as execute: result = asyncio.run(tools.retirejs("https://example.org/", "example.org", "Test", 1.5, [])) assert result["status"] == "skipped" assert not execute.called def test_retire_fetch_failure_does_not_claim_a_clean_scan(): async def fetch(*args): raise httpx.ReadTimeout("mock") with patch.object(tools, "fetch_sample", fetch), patch.object(tools, "execute") as execute: result = asyncio.run(tools.retirejs("https://example.org/", "example.org", "Test", 1.5, [])) assert result["status"] == "skipped" assert "ReadTimeout" in result["reason"] assert not execute.called def test_retire_samples_bounded_files_uses_bundled_database_and_maps_findings(): fetches = [] async def fetch(url, *args): fetches.append(url) if url == "https://example.org/": scripts = "".join(f'' for i in range(8)) return scripts.encode(), "text/html", url return b"/* sample */", "application/javascript", url async def execute(args, timeout): assert "--includeOsv" not in args assert args[args.index("--jsrepo") + 1] == "/opt/retire-db/jsrepository-v5.json" root = Path(args[args.index("--path") + 1]) files = sorted(root.rglob("*.js")) assert len(files) == config.RETIRE_MAX_FILES assert files[0].name == "jquery-0.js" report = {"data": [{"file": str(files[0]), "results": [ {"component": "jquery", "version": "1.6.1", "vulnerabilities": [ {"severity": "high", "identifiers": {"CVE": ["CVE-TEST"], "summary": "Mock candidate"}}]}]}], "errors": []} Path(args[args.index("--outputpath") + 1]).write_text(json.dumps(report)) return ["[diagnostic] Process exit code: 13"], "failed" async def sleep(*args): pass with patch.object(tools, "fetch_sample", fetch), patch.object(tools, "execute", execute), \ patch.object(tools.asyncio, "sleep", sleep): result = asyncio.run(tools.retirejs("https://example.org/", "example.org", "Test", 1.5, ["https://outside.org/out.js"])) assert result["status"] == "findings" and result["early_stop"] assert result["coverage_complete"] is False # Deliberate sample cap, findings preserved. assert result["sampled_files"] == 4 assert result["alerts"][0]["url"] == "https://example.org/jquery-0.js" assert result["alerts"][0]["risk"] == "High" assert len(fetches) == 5 assert "Process exit code: 13" not in result["reason"] def test_retire_unknown_report_path_is_never_assigned_to_the_target(tmp_path): known = str((tmp_path / "known.js").resolve()) report = {"data": [{"file": str(tmp_path / "outside.js"), "results": [ {"component": "x", "version": "1", "vulnerabilities": [{"severity": "high"}]}]}]} assert tools.parse_retire(report, {known: "https://example.org/a.js"}) == [] @pytest.mark.parametrize("guard_stats,expected", [ ({"requests": 2}, "completed"), ({"requests": 12, "limit_reached": True}, "partial"), ({"requests": 1, "blocked": 1}, "partial"), ]) def test_wapiti_cli_profile_and_coverage(guard_stats, expected): async def execute(args, timeout): assert args[args.index("-m") + 1] == "csp,http_headers,cookieflags" assert args[args.index("--scope") + 1] == "url" assert args[args.index("--headless") + 1] == "no" assert "--no-bugreport" in args Path(args[args.index("-o") + 1]).write_text(json.dumps({ "vulnerabilities": {"Header": [{"method": "GET", "path": "/", "level": 1, "info": "missing", "module": "http_headers"}]}})) Path(args[args.index("--asfb-stats") + 1]).write_text(json.dumps(guard_stats)) return [], "completed" with patch.object(tools, "execute", execute): result = asyncio.run(tools.wapiti("https://example.org/", "example.org", "Test", 1.5)) assert result["status"] == expected assert result["alerts"][0]["source"] == "wapiti" def test_wapiti_missing_report_is_failed(): async def execute(*args): return [], "completed" with patch.object(tools, "execute", execute): result = asyncio.run(tools.wapiti("https://example.org/", "example.org", "Test", 1.5)) assert result["status"] == "failed" def test_wapiti_parser_filters_other_origins_and_methods(): report = {"vulnerabilities": {"Mock": [ {"method": "GET", "path": "https://outside.org/", "info": "outside"}, {"method": "POST", "path": "/", "info": "wrong method"}, {"method": "GET", "path": "/", "info": "kept", "level": 1}]}} findings = tools.parse_wapiti(report, "https://example.org/") assert len(findings) == 1 and findings[0]["description"] == "kept" def test_new_module_dispatch_receives_discovery_inventory(): seen = [] async def retire(*args): seen.extend(args[-1]) return {"status": "skipped", "alerts": [], "reason": "No JS"} with patch.object(tools, "retirejs", retire): result = asyncio.run(web_modules.run_web_module( "retirejs", "https://example.org/", "example.org", "Test", discovered=["https://example.org/a.js"])) assert result["status"] == "skipped" assert seen == ["https://example.org/a.js"] def test_worker_new_modules_and_skip_are_isolated(): run, job = run_job(["retirejs", "wapiti", "httpx"]) async def module(name, *args): return {"status": "skipped" if name == "retirejs" else "completed", "alerts": [], "output": name} async def probe(*args): return {"status": "completed", "probes": []} with patch.object(main, "public_host"), patch.object(main, "run_web_module", module), \ patch.object(main, "probe", probe): asyncio.run(run()) assert job["status"] == "completed" assert job["pipeline"]["retirejs"] == "skipped" assert job["pipeline"]["httpx"] == "completed" def test_optional_nuclei_templates_validate_and_do_not_change_defaults(): for name in ("directory-listing", "openapi-json"): assert name in nuclei_catalog.builtins() assert name not in config.NUCLEI_DEFAULT_IDS content = nuclei_catalog.bundled_content(name) nuclei_catalog.validate_template(content) assert nuclei_catalog.estimate_requests(content) <= 2 def test_openapi_fixed_paths_run_only_at_origin(): async def execute(args, deadline): targets = Path(args[args.index("-l") + 1]).read_text().splitlines() assert targets == ["https://example.org/"] return [], "completed" with patch.object(pipeline, "execute", execute): result = asyncio.run(pipeline.nuclei("https://example.org/", "example.org", "Test", ["https://example.org/app.js"], default_ids=["openapi-json"])) assert result["targets_checked"] == 1