diff --git a/tests/test_api_network_map.py b/tests/test_api_network_map.py new file mode 100644 index 00000000..8083e5e5 --- /dev/null +++ b/tests/test_api_network_map.py @@ -0,0 +1,80 @@ +import json + +# Updated import path to match the network_auditing folder +from gitgalaxy.tools.network_auditing.full_api_network_map import run_api_audit + +def test_shadow_and_ghost_api_detection(tmp_path): + """ + End-to-End test for the API Network Map. + Verifies the multi-language regex traps, Swagger auto-discovery (and test exclusion), + and the strict Shadow/Ghost API set-difference math. + """ + # 1. Construct the Mock Repository Space + repo_dir = tmp_path / "mock_api_repo" + repo_dir.mkdir() + + # ========================================================================== + # A. Mock Source Code (The Physical Endpoints) + # ========================================================================== + # Python FastAPI + (repo_dir / "main.py").write_text('@app.get("/api/health")', encoding="utf-8") + + # Node Express + (repo_dir / "server.js").write_text('router.post("/api/users")', encoding="utf-8") + + # Java Spring Boot + (repo_dir / "UserController.java").write_text('@DeleteMapping("/api/users/{id}")', encoding="utf-8") + + # Ruby Rails (THE SHADOW API - Intentionally omitted from Swagger docs) + (repo_dir / "admin.rb").write_text('get "/api/secret_debug"', encoding="utf-8") + + # ========================================================================== + # B. Mock Official Documentation (The Approved Endpoints) + # ========================================================================== + official_swagger = { + "openapi": "3.0.0", + "paths": { + "/api/health": {"get": {}}, + "/api/users": {"post": {}}, + "/api/users/{id}": {"delete": {}}, + "/api/legacy_v1_sync": {"put": {}} # THE GHOST API (In docs, but deleted from code) + } + } + (repo_dir / "openapi.json").write_text(json.dumps(official_swagger), encoding="utf-8") + + # ========================================================================== + # C. Mock Test Swagger (Must be ignored by auto-discovery!) + # ========================================================================== + # We name this exactly "test" to trigger the programmatic filter inside run_api_audit + test_dir = repo_dir / "test" + test_dir.mkdir() + test_swagger = {"openapi": "3.0.0", "paths": {"/test/mock": {"get": {}}}} + (test_dir / "swagger.json").write_text(json.dumps(test_swagger), encoding="utf-8") + + # ========================================================================== + # 2. Execute the Engine + # ========================================================================== + result = run_api_audit(repo_dir) + + # ========================================================================== + # 3. The Invariant Assertions + # ========================================================================== + assert result["status"] == "success", f"Failed to audit: status was {result['status']}" + + # A) Prove the Multi-Language Regex Traps worked + frameworks = result["frameworks"] + assert "Python (FastAPI/Flask/Django)" in frameworks + assert "Node.js (Express/Fastify/Koa)" in frameworks + assert "Java (Spring Boot)" in frameworks + assert "Ruby (Rails/Sinatra)" in frameworks + + # B) Prove Shadow API detection (Physical code without Documentation) + assert result["shadow_count"] == 1 + assert "GET /api/secret_debug" in result["shadow_apis"], "Engine failed to flag the undocumented Ruby Shadow API!" + + # C) Prove Ghost API detection (Documentation without Physical code) + assert result["ghost_count"] == 1 + + # D) Prove Auto-Discovery segregation (It ignored the test directory) + # If it read the test swagger, the ghost count would be 2 (because /test/mock isn't in the code). + assert "GET /test/mock" not in result["shadow_apis"] \ No newline at end of file diff --git a/tests/test_binary_anomaly_detector.py b/tests/test_binary_anomaly_detector.py new file mode 100644 index 00000000..751c3372 --- /dev/null +++ b/tests/test_binary_anomaly_detector.py @@ -0,0 +1,102 @@ +import os +from unittest.mock import patch, MagicMock + +import gitgalaxy.tools.supply_chain_security.binary_anomaly_detector as xray_module + +# ============================================================================== +# TEST 1: The Routing Matrix (Denylist vs Allowlist vs Test Folders) +# ============================================================================== +@patch("gitgalaxy.tools.supply_chain_security.binary_anomaly_detector.SecurityLens") +@patch("gitgalaxy.tools.supply_chain_security.binary_anomaly_detector.ApertureFilter") +def test_xray_routing_matrix(mock_aperture_class, mock_security_class, tmp_path, monkeypatch): + monkeypatch.setattr(xray_module, "DENYLIST_PATTERNS", ["*.key", "*.pem", "id_rsa*"]) + monkeypatch.setattr(xray_module, "XRAY_BYPASS_EXTENSIONS", [".gz", ".zip"]) + monkeypatch.setattr(xray_module, "ALLOWLIST_PATHS", ["approved_keys/"]) + + mock_aperture = mock_aperture_class.return_value + mock_aperture._check_solar_shield.return_value = True + + mock_security = mock_security_class.return_value + mock_security.scan_content.return_value = {"counts": {"entropy": 6.5, "bitwise_hits": 0}} + mock_security.scan_binary.return_value = {} + + repo_dir = tmp_path / "routing_repo" + repo_dir.mkdir() + + # File A (Anomaly) + (repo_dir / "private.key").write_text("FAKE_PRIVATE_KEY", encoding="utf-8") + + # File B (Bypass Allowlist) + approved_dir = repo_dir / "approved_keys" + approved_dir.mkdir() + (approved_dir / "service.pem").write_text("FAKE_CERT", encoding="utf-8") + + # File C (Bypass Extension) + (repo_dir / "compressed.zip").write_text("FAKE_ZIP_DATA", encoding="utf-8") + + # File D (Bypass Test Folder) + # Nested inside 'src' so the string matching for "/tests/" perfectly aligns + src_dir = repo_dir / "src" + src_dir.mkdir() + test_dir = src_dir / "tests" + test_dir.mkdir() + (test_dir / "mock_payload.dat").write_text("FAKE_HIGH_ENTROPY_DATA", encoding="utf-8") + + result = xray_module.run_xray_audit(repo_dir) + assert result["anomalies_found"] == 1, "The routing matrix failed! Check Denylist/Allowlist math." + +# ============================================================================== +# TEST 2: The Deep Scan Threat Identification +# ============================================================================== +@patch("gitgalaxy.tools.supply_chain_security.binary_anomaly_detector.SecurityLens") +@patch("gitgalaxy.tools.supply_chain_security.binary_anomaly_detector.ApertureFilter") +def test_xray_deep_scan_threats(mock_aperture_class, mock_security_class, tmp_path): + mock_aperture = mock_aperture_class.return_value + mock_aperture._check_solar_shield.return_value = True + + repo_dir = tmp_path / "deep_scan_repo" + repo_dir.mkdir() + + clean_file = repo_dir / "clean.txt" + clean_file.write_text("Hello world", encoding="utf-8") + + spoofed_file = repo_dir / "hidden_exe.jpg" + spoofed_file.write_text("MZ\x90\x00...", encoding="utf-8") + + mock_security = mock_security_class.return_value + + def mock_scan_binary(head_bytes, ext): + if ext == ".jpg": return {"threat_snippet": "Magic Byte Mismatch: Expected JPEG, got PE32 Executable"} + return {} + + def mock_scan_content(content, limit): + if "MZ" in content: return {"counts": {"entropy": 6.8, "bitwise_hits": 2}} + return {"counts": {"entropy": 1.2, "bitwise_hits": 0}} + + mock_security.scan_binary.side_effect = mock_scan_binary + mock_security.scan_content.side_effect = mock_scan_content + + result = xray_module.run_xray_audit(repo_dir) + assert result["anomalies_found"] == 1, "Failed to flag magic byte mismatch or high entropy!" + +# ============================================================================== +# TEST 3: The Shebang Shield +# ============================================================================== +@patch("gitgalaxy.tools.supply_chain_security.binary_anomaly_detector.SecurityLens") +@patch("gitgalaxy.tools.supply_chain_security.binary_anomaly_detector.ApertureFilter") +def test_xray_shebang_shield(mock_aperture_class, mock_security_class, tmp_path): + mock_aperture = mock_aperture_class.return_value + mock_aperture._check_solar_shield.return_value = True + + repo_dir = tmp_path / "shebang_repo" + repo_dir.mkdir() + + sh_file = repo_dir / "deploy.sh" + sh_file.write_text("#!/bin/bash\necho 'Deploying...'", encoding="utf-8") + + mock_security = mock_security_class.return_value + mock_security.scan_content.return_value = {"counts": {"entropy": 0, "bitwise_hits": 0}} + mock_security.scan_binary.return_value = {"threat_snippet": "Suspicious execution header: #!/bin/bash"} + + result = xray_module.run_xray_audit(repo_dir) + assert result["anomalies_found"] == 0, "The Shebang Shield failed!" \ No newline at end of file diff --git a/tests/test_sbom_generator.py b/tests/test_sbom_generator.py new file mode 100644 index 00000000..d4597665 --- /dev/null +++ b/tests/test_sbom_generator.py @@ -0,0 +1,101 @@ +import pytest +import json +import sys +from unittest.mock import patch, MagicMock + +# IMPORTANT: Adjust this import path depending on where sbom_generator.py lives +from gitgalaxy.tools.compliance.sbom_generator import UniversalManifestSlicer, main + +# ============================================================================== +# TEST 1: The Multi-Ecosystem Slicer Guard +# ============================================================================== +def test_universal_manifest_slicer(tmp_path): + """ + Proves that the regex and JSON parsing can perfectly extract dependencies + across diverse language ecosystems without dropping packages. + """ + slicer = UniversalManifestSlicer() + + # 1. Test NPM (JSON Parsing) + pkg_json = tmp_path / "package.json" + pkg_json.write_text('{"dependencies": {"express": "^4.17.1"}, "devDependencies": {"jest": "27.0.0"}}', encoding='utf-8') + eco1, deps1 = slicer.slice_manifest(pkg_json) + + assert eco1 == "npm" + assert deps1["express"] == "^4.17.1" + assert deps1["jest"] == "27.0.0" + + # 2. Test PyPI (Text / Operator Regex) + req_txt = tmp_path / "requirements.txt" + req_txt.write_text("requests==2.26.0\n# comment ignored\nurllib3>=1.26.0", encoding='utf-8') + eco2, deps2 = slicer.slice_manifest(req_txt) + + assert eco2 == "pypi" + assert deps2["requests"] == "2.26.0" + assert deps2["urllib3"] == "latest" # The slicer defaults to 'latest' for non '==' operators + + # 3. Test Rust / Cargo (Toml Block Regex) + cargo_toml = tmp_path / "Cargo.toml" + cargo_toml.write_text("[package]\nname=\"test\"\n\n[dependencies]\ntokio = \"1.0\"\nserde = \"1.0\"", encoding='utf-8') + eco3, deps3 = slicer.slice_manifest(cargo_toml) + + assert eco3 == "cargo" + assert deps3["tokio"] == "latest" + assert deps3["serde"] == "latest" + +# ============================================================================== +# TEST 2: The Zero-Trust CycloneDX Matrix +# ============================================================================== +@patch("gitgalaxy.tools.compliance.sbom_generator.SecurityLens") +@patch("gitgalaxy.tools.compliance.sbom_generator.LanguageDetector") +def test_zero_trust_sbom_generation(mock_detector_class, mock_security_class, tmp_path): + """ + Proves that the physical audit matrix correctly translates missing packages + and spoofed files into strict CycloneDX JSON properties. + """ + # 1. Setup Mock Workspace + project_dir = tmp_path / "target_project" + project_dir.mkdir() + + # Create a package.json with two dependencies + pkg_json = project_dir / "package.json" + pkg_json.write_text('{"dependencies": {"safe-lib": "1.0", "ghost-lib": "2.0"}}', encoding='utf-8') + + # Simulate "safe-lib" existing physically on disk, but "ghost-lib" is missing + safe_lib_dir = project_dir / "node_modules" / "safe-lib" + safe_lib_dir.mkdir(parents=True) + (safe_lib_dir / "index.js").write_text("console.log('hello');", encoding='utf-8') + + # 2. Configure the Mocks to simulate a Spoof Detection + mock_sec_instance = mock_security_class.return_value + mock_det_instance = mock_detector_class.return_value + + # We will pretend the SecurityLens found high entropy malware in safe-lib! + mock_sec_instance.scan_content.return_value = {"counts": {"entropy": 5.2}} + mock_det_instance.inspect.return_value = {"anomaly_flags": ["Disguised Executable"]} + + # 3. Execute the Generator + test_args = ["sbom_generator.py", str(project_dir), "--out", "test_bom.json"] + with patch.object(sys, 'argv', test_args): + main() + + # 4. Verify the CycloneDX Output + bom_file = tmp_path / "target_project_test_bom.json" + assert bom_file.exists(), "SBOM generator failed to create the output JSON." + + bom_data = json.loads(bom_file.read_text(encoding='utf-8')) + + assert bom_data["bomFormat"] == "CycloneDX" + assert len(bom_data["components"]) == 2 + + # Extract components by name for easy assertion + components = {c["name"]: c for c in bom_data["components"]} + + # Assert Ghost-Lib (Missing on Disk) + ghost_props = {p["name"]: p["value"] for p in components["ghost-lib"]["properties"]} + assert ghost_props["gitgalaxy:trust_status"] == "UNVERIFIED_MISSING_ON_DISK" + + # Assert Safe-Lib (Malware Spoof Detected) + safe_props = {p["name"]: p["value"] for p in components["safe-lib"]["properties"]} + assert safe_props["gitgalaxy:trust_status"] == "SPOOF_DETECTED" + assert "High Entropy" in safe_props["gitgalaxy:anomaly_notes"] \ No newline at end of file diff --git a/tests/test_supply_chain_firewall.py b/tests/test_supply_chain_firewall.py new file mode 100644 index 00000000..7c0623f8 --- /dev/null +++ b/tests/test_supply_chain_firewall.py @@ -0,0 +1,93 @@ +import pytest +import sys +from unittest.mock import patch + +import gitgalaxy.tools.supply_chain_security.supply_chain_firewall as firewall_module + +# ============================================================================== +# TEST 1: Zero-Trust Import Slicer (Regex & Bins) +# ============================================================================== +def test_zero_trust_import_slicer(tmp_path, monkeypatch): + monkeypatch.setattr(firewall_module, "APPROVED_IMPORTS", ["react", "express"]) + monkeypatch.setattr(firewall_module, "BLACKLISTED_IMPORTS", ["event-stream-malware"]) + + repo_dir = tmp_path / "imports_repo" + repo_dir.mkdir() + (repo_dir / "app.js").write_text("import 'react'; require('event-stream-malware');", encoding="utf-8") + (repo_dir / "main.py").write_text("from 'django' import models", encoding="utf-8") + + result = firewall_module.run_firewall_audit(repo_dir) + assert result["imports_blacklisted"] == 1, "Failed to identify blacklisted package!" + assert result["imports_unknown"] == 1, "Failed to identify unknown package!" + +# ============================================================================== +# TEST 2: Strict Mode Enforcement +# ============================================================================== +@patch("gitgalaxy.tools.supply_chain_security.supply_chain_firewall.SecurityLens") +@patch("gitgalaxy.tools.supply_chain_security.supply_chain_firewall.ApertureFilter") +def test_strict_mode_enforcement(mock_aperture_class, mock_security_class, tmp_path, monkeypatch): + monkeypatch.setattr(firewall_module, "APPROVED_IMPORTS", ["react"]) + monkeypatch.setattr(firewall_module, "BLACKLISTED_IMPORTS", []) + monkeypatch.setattr(firewall_module, "STRICT_IMPORT_MODE", True) + + mock_aperture = mock_aperture_class.return_value + mock_aperture._check_solar_shield.return_value = True + mock_aperture.evaluate_path_integrity.return_value = (True, 100, "OK") + + mock_security = mock_security_class.return_value + mock_security.scan_content.return_value = {"counts": {}, "snippets": {}} + mock_security.evaluate_risk.return_value = {} + + repo_dir = tmp_path / "strict_repo" + repo_dir.mkdir() + (repo_dir / "server.js").write_text("import 'shadow-library';", encoding="utf-8") + + test_args = ["supply_chain_firewall.py", str(repo_dir)] + with patch.object(sys, 'argv', test_args): + with pytest.raises(SystemExit) as exc: + firewall_module.main() + assert exc.value.code == 1, "STRICT_IMPORT_MODE failed!" + +# ============================================================================== +# TEST 3: The Inert Data Shield (Minified File Bypass) +# ============================================================================== +@patch("gitgalaxy.tools.supply_chain_security.supply_chain_firewall.SecurityLens") +@patch("gitgalaxy.tools.supply_chain_security.supply_chain_firewall.ApertureFilter") +def test_inert_data_shield_minified_bypass(mock_aperture_class, mock_security_class, tmp_path, monkeypatch): + monkeypatch.setattr(firewall_module, "STRICT_IMPORT_MODE", False) + monkeypatch.setattr(firewall_module, "BLACKLISTED_IMPORTS", []) + + mock_aperture = mock_aperture_class.return_value + mock_aperture._check_solar_shield.return_value = True + mock_aperture.evaluate_path_integrity.return_value = (True, 100, "OK") + + mock_security = mock_security_class.return_value + mock_security.scan_content.return_value = {"counts": {"homoglyphs": 500, "danger": 50}, "snippets": {}} + + def mock_eval_risk(counts, loc): + if counts.get("homoglyphs", 0) > 10: return {"Hidden Malware Risk": 0.99} + return {} + + mock_security.evaluate_risk.side_effect = mock_eval_risk + + repo_dir = tmp_path / "inert_repo" + repo_dir.mkdir() + test_args = ["supply_chain_firewall.py", str(repo_dir)] + + normal_file = repo_dir / "logic.js" + normal_file.write_text("var a = 'fake malware';", encoding="utf-8") + + with patch.object(sys, 'argv', test_args): + with pytest.raises(SystemExit) as exc: + firewall_module.main() + assert exc.value.code == 1 + + normal_file.unlink() + min_file = repo_dir / "logic.min.js" + min_file.write_text("var a = 'fake malware';", encoding="utf-8") + + try: + with patch.object(sys, 'argv', test_args): + firewall_module.main() + except SystemExit: + pytest.fail("The Inert Data Shield failed!") \ No newline at end of file diff --git a/tests/test_vault_sentinel.py b/tests/test_vault_sentinel.py new file mode 100644 index 00000000..6e71faf9 --- /dev/null +++ b/tests/test_vault_sentinel.py @@ -0,0 +1,101 @@ +import pytest +import sys +from pathlib import Path +from unittest.mock import patch + +import gitgalaxy.tools.supply_chain_security.vault_sentinel as sentinel_module + +# ============================================================================== +# TEST 1: The Denylist Wall (Immediate Path Blocking) +# ============================================================================== +@patch("gitgalaxy.tools.supply_chain_security.vault_sentinel.SecurityLens") +@patch("gitgalaxy.tools.supply_chain_security.vault_sentinel.ApertureFilter") +def test_sentinel_denylist_blocking(mock_aperture_class, mock_security_class, tmp_path, monkeypatch): + """ + Proves that files matching the DENYLIST_PATTERNS are instantly blocked + and trigger a fatal exit without needing a deep content scan. + """ + monkeypatch.setattr(sentinel_module, "DENYLIST_PATTERNS", ["*.pem", "id_rsa*"]) + monkeypatch.setattr(sentinel_module, "ALLOWLIST_PATHS", []) + + mock_aperture = mock_aperture_class.return_value + mock_aperture._check_solar_shield.return_value = True + + repo_dir = tmp_path / "denylist_repo" + repo_dir.mkdir() + (repo_dir / "production.pem").write_text("FAKE_CERT_DATA", encoding="utf-8") + + test_args = ["vault_sentinel.py", str(repo_dir)] + with patch.object(sys, 'argv', test_args): + with pytest.raises(SystemExit) as exc: + sentinel_module.main() + assert exc.value.code == 1, "Sentinel failed to block a DENYLIST file pattern!" + +# ============================================================================== +# TEST 2: The Deep Scan Trap (Hardcoded Content Leaks) +# ============================================================================== +@patch("gitgalaxy.tools.supply_chain_security.vault_sentinel.SecurityLens") +@patch("gitgalaxy.tools.supply_chain_security.vault_sentinel.ApertureFilter") +def test_sentinel_content_breach(mock_aperture_class, mock_security_class, tmp_path, monkeypatch): + """ + Proves that seemingly benign files are deeply scanned, and if the SecurityLens + detects private_info, it successfully crashes the build. + """ + monkeypatch.setattr(sentinel_module, "DENYLIST_PATTERNS", []) + monkeypatch.setattr(sentinel_module, "ALLOWLIST_PATHS", []) + + mock_aperture = mock_aperture_class.return_value + mock_aperture._check_solar_shield.return_value = True + mock_aperture.evaluate_path_integrity.return_value = (True, 100, None) + + mock_security = mock_security_class.return_value + mock_security.scan_content.return_value = { + "counts": {"private_info": 1}, + "snippets": {"private_info": ["AKIAIOSFODNN7EXAMPLE"]} + } + + repo_dir = tmp_path / "deepscan_repo" + repo_dir.mkdir() + (repo_dir / "database_config.py").write_text("AWS_KEY = 'AKIAIOSFODNN7EXAMPLE'", encoding="utf-8") + + test_args = ["vault_sentinel.py", str(repo_dir)] + with patch.object(sys, 'argv', test_args): + with pytest.raises(SystemExit) as exc: + sentinel_module.main() + + assert exc.value.code == 1, "Sentinel failed to crash the build on a hardcoded secret!" + +# ============================================================================== +# TEST 3: The Allowlist Bypass (Mock/Test Key Suppression) +# ============================================================================== +@patch("gitgalaxy.tools.supply_chain_security.vault_sentinel.SecurityLens") +@patch("gitgalaxy.tools.supply_chain_security.vault_sentinel.ApertureFilter") +def test_sentinel_allowlist_bypass(mock_aperture_class, mock_security_class, tmp_path, monkeypatch): + """ + Proves that if a file is explicitly inside an ALLOWLIST_PATH, it completely + bypasses both Denylist crashes and Content Scan crashes. + """ + monkeypatch.setattr(sentinel_module, "DENYLIST_PATTERNS", ["*.pem"]) + monkeypatch.setattr(sentinel_module, "ALLOWLIST_PATHS", ["mock_keys/"]) + + mock_aperture = mock_aperture_class.return_value + mock_aperture._check_solar_shield.return_value = True + mock_aperture.evaluate_path_integrity.return_value = (False, 100, "CRITICAL LEAK: Private Key") + + mock_security = mock_security_class.return_value + mock_security.scan_content.return_value = {"counts": {"private_info": 5}} + + repo_dir = tmp_path / "allowlist_repo" + repo_dir.mkdir() + + mock_dir = repo_dir / "mock_keys" + mock_dir.mkdir() + (mock_dir / "dummy_test.pem").write_text("FAKE_KEY_DATA", encoding="utf-8") + + test_args = ["vault_sentinel.py", str(repo_dir)] + + try: + with patch.object(sys, 'argv', test_args): + sentinel_module.main() + except SystemExit: + pytest.fail("The Allowlist Bypass failed! A whitelisted test key crashed the build.") \ No newline at end of file