Download tests/test_final_modules.py from wuhp/bbscanner: direct link, hf CLI and curl.
- Browser
- Download file 16.8 kB
-
https://huggingface.co/spaces/wuhp/bbscanner/resolve/main/tests/test_final_modules.py
- Command line
-
hf download hf://spaces/wuhp/bbscanner/tests/test_final_modules.py
-
curl -L -o test_final_modules.py https://huggingface.co/spaces/wuhp/bbscanner/resolve/main/tests/test_final_modules.py
16.8 kB
| import asyncio | |
| import json | |
| from pathlib import Path | |
| from unittest.mock import patch | |
| import pytest | |
| from fastapi.testclient import TestClient | |
| from pydantic import ValidationError | |
| from app import final_modules as modules, main, template_builder | |
| from app.module_inputs import ModuleInputs, clean_sbom | |
| from test_worker_pipeline import run_job | |
| BOM = json.dumps({"bomFormat": "CycloneDX", "components": [ | |
| {"type": "library", "name": "jquery", "version": "1.6.1", "purl": "pkg:npm/jquery@1.6.1", | |
| "externalReferences": [{"url": "http://127.0.0.1/secret"}]}]}) | |
| def test_missing_required_context_skips_without_process_or_database(name): | |
| with patch.object(modules, "execute") as execute, patch.object(modules, "prepare_database") as prepare: | |
| result = asyncio.run(modules.run(name, "https://example.org/", "example.org", "Test", 1.5)) | |
| assert result["status"] == "skipped" | |
| assert not execute.called and not prepare.called | |
| def test_sbom_is_sanitized_and_external_urls_are_removed(): | |
| result = clean_sbom(BOM) | |
| assert "externalReferences" not in result and "127.0.0.1" not in result | |
| assert json.loads(result)["components"][0]["purl"] == "pkg:npm/jquery@1.6.1" | |
| def test_incompatible_sboms_are_rejected(text): | |
| with pytest.raises(ValueError): | |
| clean_sbom(text) | |
| def test_llm_endpoint_cannot_change_origin_or_embed_credentials(path): | |
| with pytest.raises(ValidationError): | |
| ModuleInputs(llm_path=path) | |
| def test_no_source_or_sbom_content_in_public_options(): | |
| inputs = ModuleInputs(source_text="my-private-source-value", sbom_text=BOM) | |
| summary = json.dumps(inputs.public_summary()) | |
| assert "my-private-source-value" not in summary | |
| assert "jquery" not in summary | |
| assert inputs.public_summary()["sbom_components"] == 1 | |
| def test_trufflehog_redacts_secret_and_context_everywhere(): | |
| secret = "super-secret-material-do-not-return" | |
| rows = [json.dumps({"DetectorName": "MockDetector", "Raw": secret, "RawV2": secret, | |
| "ExtraData": {"credential": secret}, "Redacted": secret})] | |
| async def execute(args, timeout): | |
| assert "--no-verification" in args and "--no-update" in args | |
| assert "--force-skip-archives" in args | |
| root = Path(args[args.index("filesystem") + 1]) | |
| assert (root / "source.txt").read_text() == secret | |
| return rows, "completed" | |
| with patch.object(modules, "execute", execute): | |
| result = asyncio.run(modules.run("trufflehog", "https://example.org/", "example.org", "Test", 1.5, | |
| ModuleInputs(source_text=secret))) | |
| assert result["status"] == "findings" | |
| assert secret not in json.dumps(result) | |
| assert len(result["alerts"]) == 1 | |
| def test_database_failure_is_unavailable_not_clean(name): | |
| async def prepare(module): return False, "Database download timeout" | |
| with patch.object(modules, "prepare_database", prepare), patch.object(modules, "execute") as execute: | |
| result = asyncio.run(modules.run(name, "https://example.org/", "example.org", "Test", 1.5, | |
| ModuleInputs(sbom_text=BOM))) | |
| assert result["status"] == "unavailable" | |
| assert not execute.called | |
| def test_amass_inventory_filters_unrelated_domains_without_expanding_scope(): | |
| async def execute(args, timeout): | |
| assert "-passive" in args and "-active" not in args | |
| Path(args[args.index("-o") + 1]).write_text("a.example.org\nexample.org.evil.org\nexample.org\n") | |
| return [], "completed" | |
| with patch.object(modules, "execute", execute): | |
| result = asyncio.run(modules.run("amass", "https://example.org/", "example.org", "Test", 1.5)) | |
| assert result["hosts"] == ["a.example.org", "example.org"] | |
| assert result["endpoints"] == [] | |
| def test_kiterunner_uses_only_small_get_dictionary_and_parses_upstream_hit_shape(): | |
| async def execute(args, timeout): | |
| assert args[args.index("--max-redirects") + 1] == "0" | |
| assert args[args.index("--force-method") + 1] == "GET" | |
| assert args[args.index("--delay") + 1] == "1.5s" | |
| assert "127.0.0.1" not in " ".join(args) | |
| assert len(Path(args[args.index("-w") + 1]).read_text().splitlines()) == 8 | |
| return [json.dumps({"method": "GET", "target": "https://example.org", "path": "/api/v1", "responses": [{"sc": 200}]}), | |
| json.dumps({"method": "GET", "target": "https://outside.org", "path": "/api"})], "completed" | |
| with patch.object(modules, "execute", execute): | |
| result = asyncio.run(modules.run("kiterunner", "https://example.org/", "example.org", "Test", 1.5, | |
| ModuleInputs(api_target=True))) | |
| assert result["endpoints"] == ["https://example.org/api/v1"] | |
| def test_dependency_json_is_normalized_and_raw_artifacts_not_returned(name, payload): | |
| async def prepare(*args): return True, "" | |
| async def execute(args, timeout): | |
| if name == "depscan": | |
| directory = Path(args[args.index("--reports-dir") + 1]) | |
| directory.mkdir() | |
| (directory / "depscan-universal.vdr.json").write_text(json.dumps(payload)) | |
| else: | |
| Path(args[args.index("--output") + 1]).write_text(json.dumps(payload)) | |
| return [], "completed" | |
| with patch.object(modules, "prepare_database", prepare), patch.object(modules, "execute", execute): | |
| result = asyncio.run(modules.run(name, "https://example.org/", "example.org", "Test", 1.5, | |
| ModuleInputs(sbom_text=BOM))) | |
| assert result["status"] == "findings" | |
| assert len(result["alerts"]) == 1 | |
| assert "externalReferences" not in json.dumps(result) | |
| def test_invalid_report_cannot_claim_clean_scan(): | |
| async def execute(args, timeout): | |
| Path(args[args.index("--output") + 1]).write_text("{}") | |
| return [], "completed" | |
| with patch.object(modules, "execute", execute): | |
| result = asyncio.run(modules.run("osv-scanner", "https://example.org/", "example.org", "Test", 1.5, | |
| ModuleInputs(sbom_text=BOM))) | |
| assert result["status"] == "failed" | |
| def test_worker_keeps_amass_names_in_inventory_only(): | |
| run, job = run_job(["amass", "httpx"]) | |
| async def module(*args): | |
| return {"status": "completed", "alerts": [], "hosts": ["a.example.com"], "output": "inventory"} | |
| async def probe(urls, *args): | |
| assert all("a.example.com" not in url for url in urls) | |
| 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["discovered_hosts"] == ["a.example.com"] | |
| assert job["status"] == "completed" | |
| def test_artifact_input_is_not_returned_in_job_status_and_does_not_reuse_stale_job(): | |
| main.assets.clear() | |
| main.jobs.clear() | |
| main.queue = asyncio.Queue() | |
| async def no_scan(*args): | |
| return None | |
| with TestClient(main.app) as client, patch.object(main, "public_host"), patch.object(main, "run_scan_job", no_scan): | |
| owner = client.get("/api/health").headers["x-asfb-session"] | |
| main.assets["a"] = {"id": "a", "owner": owner, "name": "https://example.org/", "compatibility": "web"} | |
| body = {"asset_id": "a", "policy_confirmed": True, "active_confirmed": True, | |
| "modules": ["trufflehog"], "options": {"inputs": {"source_text": "secret-first"}}} | |
| response = client.post("/api/scans", json=body) | |
| assert response.status_code == 200 | |
| first = response.json()["id"] | |
| body["options"]["inputs"]["source_text"] = "secret-other" | |
| second = client.post("/api/scans", json=body).json()["id"] | |
| assert first != second | |
| assert "secret-first" not in json.dumps(main.jobs) | |
| assert "secret-other" not in json.dumps(main.jobs) | |
| def test_keyless_builder_cannot_create_payloads_or_external_hosts(): | |
| from app.nuclei_catalog import validate_template | |
| draft = template_builder.build_draft("Test observation", "/status", "healthy") | |
| validate_template(draft) | |
| assert "{{BaseURL}}/status" in draft | |
| for path in ("//outside.org", "/../secret", "/x?cmd=1"): | |
| with pytest.raises(ValueError): | |
| template_builder.build_draft("Test observation", path, "ok") | |
| def test_optional_gemini_without_credentials_is_unavailable_not_required(monkeypatch): | |
| monkeypatch.delenv("GEMINI_API_KEY", raising=False) | |
| monkeypatch.delenv("GEMINI_MODEL", raising=False) | |
| with pytest.raises(RuntimeError): | |
| asyncio.run(template_builder.gemini_draft("Observation")) | |
| def test_draft_endpoint_validates_but_never_launches_scan(): | |
| async def validate(content): | |
| return content | |
| with TestClient(main.app) as client, patch.object(template_builder, "validate_draft", validate): | |
| response = client.post("/api/nuclei/draft", json={"name": "Observation", "path": "/", "word": "hello"}) | |
| assert response.status_code == 200 | |
| assert response.json()["executed"] is False | |
| # Native process output is mocked. No test authenticates a real credential. | |
| def native_secret(name="Github", raw="fixture-private-key", **fields): | |
| return json.dumps({"DetectorName": name, "Raw": raw, "Redacted": raw, | |
| "ExtraData": {"account": "fixture-private-identity"}, | |
| "SourceMetadata": {"source": "fixture-private-source"}, | |
| "SecretParts": {"key": raw}, **fields}) | |
| def run_secret_fixture(rows, enabled=True, native_status="completed"): | |
| calls, paths = [], [] | |
| source = "database=https://fixture-private-host/\npassword=fixture-private-key\noriginal context" | |
| async def execute(args, timeout): | |
| calls.append((args, timeout)) | |
| assert "--no-update" in args and "--force-skip-archives" in args | |
| assert "--force-skip-binaries" in args | |
| assert args[args.index("--results") + 1] == "verified,unverified,unknown" | |
| assert args[args.index("--concurrency") + 1] == "1" | |
| # No allowlist, source reconstruction or replacement of built-in policy. | |
| assert not {"--include-detectors", "--exclude-detectors", "--verifier", "--config", | |
| "--allow-verification-overlap", "--no-verification-cache"} & set(args) | |
| assert ("--no-verification" in args) is (not enabled) | |
| assert timeout == (120 if enabled else 40) | |
| root = Path(args[args.index("filesystem") + 1]) | |
| path = root / "source.txt" | |
| assert path.read_text() == source | |
| assert (path.stat().st_mode & 0o777) == 0o600 | |
| assert sorted(p.name for p in root.iterdir()) == ["source.txt"] | |
| assert "fixture-private" not in " ".join(args) | |
| paths.append(path) | |
| return rows, native_status | |
| with patch.object(modules, "execute", execute): | |
| result = asyncio.run(modules.run("trufflehog", "https://example.org/", "example.org", "Test", 1.5, | |
| ModuleInputs(source_text=source, trufflehog_verify=enabled))) | |
| assert all(not path.exists() for path in paths) | |
| assert len(calls) == 1 | |
| assert "fixture-private" not in json.dumps(result) | |
| assert "attempted" not in result["verification"] | |
| assert "limit" not in result["verification"] | |
| return result | |
| def test_native_verification_labels_and_redaction(fields, status, expected): | |
| result = run_secret_fixture([native_secret(**fields)], native_status=status) | |
| finding = result["alerts"][0] | |
| assert finding["verification_status"] == expected | |
| assert finding["confidence"] == ("High" if expected == "verified" else "Low") | |
| assert finding["risk"] == ("High" if expected == "verified" else "Medium") | |
| assert result["verification"][expected] == 1 | |
| assert result["verification"]["mode"] == "native" | |
| assert result["verification"]["detectors"] == "all" | |
| assert result["status"] == (status if status != "completed" else | |
| "partial" if expected == "unknown" else "findings") | |
| def test_detection_default_disables_native_verification_even_if_output_claims_verified(): | |
| result = run_secret_fixture([native_secret(Verified=True)], enabled=False) | |
| assert result["alerts"][0]["verification_status"] == "not_attempted" | |
| assert result["verification"]["enabled"] is False | |
| def test_native_verification_accepts_all_detector_results_without_application_allowlist(name): | |
| result = run_secret_fixture([native_secret(name, Verified=True)]) | |
| assert result["verification"]["verified"] == 1 | |
| assert result["alerts"][0]["name"] == "Verified " + name + " secret" | |
| def test_more_than_ten_credentials_remain_in_single_native_scan(): | |
| rows = [native_secret(raw="fixture-private-" + str(i), Verified=True) for i in range(15)] | |
| result = run_secret_fixture(rows) | |
| assert result["verification"]["verified"] == 15 | |
| assert result["verification"]["candidates_reported"] == 15 | |
| assert result["alerts"][0]["candidate_count"] == 15 | |
| def test_partial_native_output_retains_findings_and_timeout_status(): | |
| result = run_secret_fixture([native_secret(Verified=True), | |
| "[diagnostic] Deadline exceeded; fixture-private-error"], | |
| native_status="timeout") | |
| assert result["status"] == "timeout" | |
| assert result["verification"]["verified"] == 1 | |
| assert result["alerts"][0]["verification_status"] == "verified" | |
| def test_empty_timeout_cannot_claim_clean_scan_or_completed_verification(): | |
| result = run_secret_fixture(["[diagnostic] Deadline exceeded"], native_status="timeout") | |
| assert result["status"] == "timeout" and result["alerts"] == [] | |
| assert result["verification"]["candidates_reported"] == 0 | |
| def test_unverified_does_not_claim_invalid_or_known_verification_attempt(): | |
| result = run_secret_fixture([native_secret("UnsupportedExample", Verified=False)]) | |
| assert result["alerts"][0]["verification_status"] == "unverified" | |
| assert "may not support" in result["alerts"][0]["description"] | |
| assert "does not prove" in result["alerts"][0]["description"] | |
| def test_native_verification_deduplicates_candidates_and_keeps_strongest_evidence(): | |
| rows = [native_secret(raw="fixture-private-good", Verified=False), | |
| native_secret(raw="fixture-private-good", Verified=True), | |
| native_secret(raw="fixture-private-bad", Verified=False)] | |
| result = run_secret_fixture(rows) | |
| assert result["verification"]["verified"] == result["verification"]["unverified"] == 1 | |
| assert result["verification"]["candidates_reported"] == 2 | |
| assert {a["verification_status"] for a in result["alerts"]} == {"verified", "unverified"} | |
| def test_native_verification_summary_counts_all_candidates_beyond_alert_group_cap(): | |
| rows = [native_secret("Detector" + str(i), Verified=True) for i in range(101)] | |
| result = run_secret_fixture(rows) | |
| assert result["verification"]["verified"] == 101 | |
| assert len(result["alerts"]) == 100 | |
| def test_verification_flag_is_visible_but_source_stays_private_in_job_options(): | |
| summary = ModuleInputs(source_text="fixture-private-source", trufflehog_verify=True).public_summary() | |
| assert summary["trufflehog_verify"] is True | |
| assert "fixture-private" not in json.dumps(summary) | |