From a4a9687eff8f724f182200ad9581e7932af551ed Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Fri, 10 Jul 2026 20:43:03 -0400 Subject: [PATCH] fix(supply-chain): patch critical firewall bypasses and lock down schema regressions Resolves #156, #157, #158, #159, #160, #161, #162 --- .../supply_chain_firewall.py | 37 ++- .../test_supply_chain_firewall.py | 299 +++++++++++++----- 2 files changed, 250 insertions(+), 86 deletions(-) diff --git a/gitgalaxy/tools/supply_chain_security/supply_chain_firewall.py b/gitgalaxy/tools/supply_chain_security/supply_chain_firewall.py index af738174d..517a5e604 100644 --- a/gitgalaxy/tools/supply_chain_security/supply_chain_firewall.py +++ b/gitgalaxy/tools/supply_chain_security/supply_chain_firewall.py @@ -85,7 +85,10 @@ def run_firewall_audit(parsed_files: list, alias_map: dict = None) -> dict: # ===================================================================== # 1. ZERO-TRUST IMPORT VERIFICATION # ===================================================================== - for raw_pkg in file_node.get("raw_imports", []): + for imp in file_node.get("raw_imports", []): + # JSON serializes Python tuples as lists, so we must check for both + raw_pkg = imp[0] if isinstance(imp, (tuple, list)) else imp + # DEFENSIVE DESIGN (RELATIVE PATH SHIELD): # Ignore native internal routing (e.g., './utils') to focus strictly # on external supply chain dependencies. @@ -137,8 +140,11 @@ def run_firewall_audit(parsed_files: list, alias_map: dict = None) -> dict: equations = file_node.get("equations", {}) loc = file_node.get("coding_loc", 1) - # Clone the dictionary to safely apply the sandbox multiplier without corrupting global RAM - local_counts = dict(equations) + # Clone the dictionary and strip the 'sec_' prefix for the security evaluator + local_counts = {} + for k, v in equations.items(): + clean_key = k[4:] if k.startswith("sec_") else k + local_counts[clean_key] = v # DEFENSIVE DESIGN (BUILD-TIME EXECUTION MULTIPLIER): # Configuration scripts (like setup.py or package.json) are executed by CI/CD @@ -156,8 +162,6 @@ def run_firewall_audit(parsed_files: list, alias_map: dict = None) -> dict: ]: build_time_multiplier = 10.0 - safe_loc = max(loc + 150, 1) - if build_time_multiplier > 1.0: for k in local_counts: if isinstance(local_counts[k], (int, float)): @@ -165,13 +169,14 @@ def run_firewall_audit(parsed_files: list, alias_map: dict = None) -> dict: # Evaluate risk using Phase 1 network topography context (e.g., Downstream Exposure) network_metrics = file_node.get("dependency_network", {}) - exposures = security.evaluate_risk(local_counts, safe_loc, network_metrics) + exposures = security.evaluate_risk(local_counts, loc, network_metrics) if ( "Hidden Malware Risk" in exposures or "Data Injection Risk" in exposures or "Secrets Leak Risk" in exposures or "Logic Bomb Risk" in exposures + or "Memory Corruption Risk" in exposures ): if is_whitelisted: threats_allowed += 1 @@ -221,8 +226,9 @@ def main(): with tempfile.TemporaryDirectory() as tmpdir: # Run the orchestrator to generate the JSON graph in a temp directory + target_out_file = str(Path(tmpdir) / "firewall_temp.json") result = subprocess.run( - ["python", "-m", "gitgalaxy.galaxyscope", str(target_path), "--output", tmpdir], + ["python", "-m", "gitgalaxy.galaxyscope", str(target_path), "--output", target_out_file], capture_output=True, text=True ) @@ -233,7 +239,7 @@ def main(): # Dynamically locate the generated audit file to avoid naming convention bugs tmp_path = Path(tmpdir) - audit_files = list(tmp_path.glob("*_galaxy_audit.json")) + audit_files = list(tmp_path.glob("*_audit.json")) if not audit_files: print("❌ GalaxyScope did not produce an audit JSON in the temp directory.") @@ -241,12 +247,23 @@ def main(): with open(audit_files[0], "r", encoding="utf-8") as f: data = json.load(f) - parsed_files = data.get("artifacts", data.get("stars", [])) + # Extract files from the structured audit directory groups + parsed_files = [] + groups = data.get("6. Parsed Files (Scanned Artifacts)", {}) + for folder_data in groups.values(): + for path, file_info in folder_data.get("Files", {}).items(): + file_info["path"] = path # Ensure path remains attached + parsed_files.append(file_info) else: # Standard file-based load with open(target_path, "r", encoding="utf-8") as f: data = json.load(f) - parsed_files = data.get("artifacts", data.get("stars", [])) + parsed_files = [] + groups = data.get("6. Parsed Files (Scanned Artifacts)", {}) + for folder_data in groups.values(): + for path, file_info in folder_data.get("Files", {}).items(): + file_info["path"] = path + parsed_files.append(file_info) except Exception as e: print(f"❌ Failed to parse RAM graph: {e}") diff --git a/tests/security_auditing/test_supply_chain_firewall.py b/tests/security_auditing/test_supply_chain_firewall.py index fd8d162ae..880f835d4 100644 --- a/tests/security_auditing/test_supply_chain_firewall.py +++ b/tests/security_auditing/test_supply_chain_firewall.py @@ -1,7 +1,8 @@ import pytest import sys import json -from unittest.mock import patch +from pathlib import Path +from unittest.mock import patch, MagicMock import gitgalaxy.tools.supply_chain_security.supply_chain_firewall as firewall_module @@ -96,7 +97,7 @@ def test_alias_spoofing_detection(monkeypatch, capsys): assert "Spoofed alias blocked" in captured.out, "Missing spoofed alias log output." # ============================================================================== -# TEST 4: Strict Policy Enforcement Mode +# TEST 4: Strict Policy Enforcement Mode (Updated Schema) # ============================================================================== def test_strict_mode_enforcement(tmp_path, monkeypatch): """ @@ -108,9 +109,17 @@ def test_strict_mode_enforcement(tmp_path, monkeypatch): monkeypatch.setattr(firewall_module, "STRICT_IMPORT_MODE", True) mock_ram_graph = { - "artifacts": [ - {"path": "server.js", "raw_imports": ["shadow-library"], "equations": {}} - ] + "6. Parsed Files (Scanned Artifacts)": { + "root": { + "Files": { + "server.js": { + "raw_imports": ["shadow-library"], + "equations": {}, + "coding_loc": 50 + } + } + } + } } graph_file = tmp_path / "results.json" @@ -123,7 +132,7 @@ def test_strict_mode_enforcement(tmp_path, monkeypatch): assert exc.value.code == 1, "Strict import policy enforcement failed to block an unknown package." # ============================================================================== -# TEST 5: Behavioral Threat Density Evaluation +# TEST 5: Behavioral Threat Density Evaluation (Updated Schema) # ============================================================================== def test_behavioral_threat_evaluation(tmp_path, monkeypatch): """ @@ -134,14 +143,17 @@ def test_behavioral_threat_evaluation(tmp_path, monkeypatch): monkeypatch.setattr(firewall_module, "BLACKLISTED_IMPORTS", []) mock_ram_graph_threat = { - "artifacts": [ - { - "path": "logic.js", - "raw_imports": [], - "equations": {"homoglyphs": 500, "high_risk_execution": 50}, - "coding_loc": 50, + "6. Parsed Files (Scanned Artifacts)": { + "root": { + "Files": { + "logic.js": { + "raw_imports": [], + "equations": {"sec_homoglyphs": 500, "sec_high_risk_execution": 50}, + "coding_loc": 50, + } + } } - ] + } } graph_file = tmp_path / "results.json" @@ -164,24 +176,17 @@ def test_build_time_execution_multiplier(monkeypatch): """ monkeypatch.setattr(firewall_module, "STRICT_IMPORT_MODE", False) - # MATHEMATICS FIX: - # A file with 1000 LOC gets a safe_loc of 1150. - # To breach the 20% 'paranoid' Logic Bomb threshold, the file needs - # a sabotage density of >= 0.20 (230 effective hits). - # - # standard_app.py: 20 danger hits * 1.5 = 30 hits (2.6% density) -> Safely Passes - # setup.py: (20 danger hits * 10x multiplier) * 1.5 = 300 hits (26.0% density) -> Blocks! mock_ram_graph = [ { "path": "setup.py", "raw_imports": [], - "equations": {"high_risk_execution": 20}, + "equations": {"sec_high_risk_execution": 20}, "coding_loc": 1000, }, { "path": "standard_app.py", "raw_imports": [], - "equations": {"high_risk_execution": 20}, + "equations": {"sec_high_risk_execution": 20}, "coding_loc": 1000, } ] @@ -230,68 +235,27 @@ def test_monorepo_contextual_alias_resolution(monkeypatch, capsys): monkeypatch.setattr(firewall_module, "APPROVED_IMPORTS", []) monkeypatch.setattr(firewall_module, "BLACKLISTED_IMPORTS", ["malicious-core", "rogue-ui"]) - # 1. Simulate a nested alias map generated by the updated manifest_parser.py - # The frontend maps 'lodash' to a rogue UI package. - # The backend maps 'lodash' to a malicious core package. mock_alias_map = { "frontend": {"lodash": "rogue-ui"}, "backend/src": {"lodash": "malicious-core"}, - "backend": {"express": "safe-express"} # Parent directory fallback + "backend": {"express": "safe-express"} } mock_ram_graph = [ - # Scenario A: Exact Directory Match (frontend) - { - "path": "frontend/component.jsx", - "raw_imports": ["lodash"], - "equations": {}, - "coding_loc": 10, - }, - # Scenario B: Exact Directory Match (backend/src) - { - "path": "backend/src/server.js", - "raw_imports": ["lodash"], - "equations": {}, - "coding_loc": 10, - }, - # Scenario C: Upward Traversal Match - # (File is in backend/src/utils, but manifest is in backend/src) - { - "path": "backend/src/utils/helper.js", - "raw_imports": ["lodash"], - "equations": {}, - "coding_loc": 10, - }, - # Scenario D: Deep Upward Traversal Match - # (File is in backend/src/utils, but express manifest is in backend) - { - "path": "backend/src/utils/router.js", - "raw_imports": ["express"], - "equations": {}, - "coding_loc": 10, - }, - # Scenario E: Unmapped Directory (Root) - { - "path": "scripts/deploy.js", - "raw_imports": ["lodash"], # No alias should be applied - "equations": {}, - "coding_loc": 10, - } + {"path": "frontend/component.jsx", "raw_imports": ["lodash"], "equations": {}, "coding_loc": 10}, + {"path": "backend/src/server.js", "raw_imports": ["lodash"], "equations": {}, "coding_loc": 10}, + {"path": "backend/src/utils/helper.js", "raw_imports": ["lodash"], "equations": {}, "coding_loc": 10}, + {"path": "backend/src/utils/router.js", "raw_imports": ["express"], "equations": {}, "coding_loc": 10}, + {"path": "scripts/deploy.js", "raw_imports": ["lodash"], "equations": {}, "coding_loc": 10} ] result = firewall_module.run_firewall_audit(mock_ram_graph, alias_map=mock_alias_map) captured = capsys.readouterr() - # The rogue-ui and malicious-core aliases are both blacklisted. - # We should have exactly 3 threats found (Scenarios A, B, C). - # Scenario D resolves to 'safe-express' (unknown, but not blacklisted). - # Scenario E resolves to 'lodash' (unknown, but not blacklisted). assert result["imports_blacklisted"] == 3, "Failed to resolve contextual aliases correctly!" assert result["threats_found"] == 3, "Failed to increment threats for contextually spoofed packages!" - - # Verify specific log outputs to ensure traversal hit the correct targets assert "'lodash' -> 'rogue-ui'" in captured.out, "Failed to resolve exact directory alias (Frontend)!" - assert "'lodash' -> 'malicious-core'" in captured.out, "Failed to traverse upwards to authoritative manifest (Backend)!" + assert "'lodash' -> 'malicious-core'" in captured.out, "Failed to traverse upwards to authoritative manifest!" # ============================================================================== # TEST 10: THE ALLOWLIST LOOPHOLE GUARD (UNHAPPY PATH) @@ -301,7 +265,6 @@ def test_firewall_allowlist_loophole_guard(monkeypatch): Proves that a file residing in an ALLOWLIST_PATH can bypass strict mode for unknown imports, but is STILL blocked if it imports a known BLACKLISTED package. """ - # Whitelist the 'experiments' folder monkeypatch.setattr(firewall_module, "ALLOWLIST_PATHS", ["experiments/"]) monkeypatch.setattr(firewall_module, "BLACKLISTED_IMPORTS", ["known-malware"]) monkeypatch.setattr(firewall_module, "STRICT_IMPORT_MODE", True) @@ -316,10 +279,194 @@ def test_firewall_allowlist_loophole_guard(monkeypatch): ] result = firewall_module.run_firewall_audit(mock_ram_graph) - - # 'unknown-package' is safely ignored because the path is whitelisted. assert result["imports_unknown"] == 1 - - # 'known-malware' MUST trigger a block despite the whitelist. assert result["imports_blacklisted"] == 1 - assert result["threats_found"] == 1, "Blacklisted import bypassed the firewall via the Allowlist Loophole!" \ No newline at end of file + assert result["threats_found"] == 1, "Blacklisted import bypassed the firewall via the Allowlist Loophole!" + + +# ============================================================================== +# TEST 11: ISSUE #156 - THE 'SEC_' PREFIX STRIPPING BUG +# ============================================================================== +def test_behavioral_threat_evaluation_strips_prefix(tmp_path, monkeypatch): + """Proves the firewall correctly strips the 'sec_' prefix from Phase 1 equations.""" + monkeypatch.setattr(firewall_module, "STRICT_IMPORT_MODE", False) + + mock_ram_graph = { + "6. Parsed Files (Scanned Artifacts)": { + "src": { + "Files": { + "logic.js": { + "raw_imports": [], + "equations": {"sec_logic_bomb": 100, "sec_high_risk_execution": 50}, + "coding_loc": 50, + } + } + } + } + } + + graph_file = tmp_path / "results.json" + graph_file.write_text(json.dumps(mock_ram_graph), encoding="utf-8") + + with patch.object(sys, "argv", ["supply_chain_firewall.py", str(graph_file)]): + with pytest.raises(SystemExit): + firewall_module.main() + +# ============================================================================== +# TEST 12: ISSUE #157 - THE TUPLE CRASH +# ============================================================================== +def test_tuple_import_handling(tmp_path, monkeypatch): + """Proves the firewall safely unpacks Phase 2 entity tuples from the imports array.""" + monkeypatch.setattr(firewall_module, "STRICT_IMPORT_MODE", False) + + mock_ram_graph = { + "6. Parsed Files (Scanned Artifacts)": { + "root": { + "Files": { + "app.py": { + "raw_imports": [("express", "Router"), "normal-string"], + "equations": {}, + "coding_loc": 100 + } + } + } + } + } + + graph_file = tmp_path / "results.json" + graph_file.write_text(json.dumps(mock_ram_graph), encoding="utf-8") + + with patch.object(sys, "argv", ["supply_chain_firewall.py", str(graph_file)]): + try: + firewall_module.main() + except SystemExit: + pytest.fail("Firewall triggered SystemExit on safe tuple imports.") + +# ============================================================================== +# TEST 13: ISSUES #158 & #162 - STANDALONE DIRECTORY CRASH & GLOB MISMATCH +# ============================================================================== +@patch("subprocess.run") +def test_directory_execution_and_globbing(mock_subprocess_run, tmp_path, monkeypatch): + """Proves passing a directory triggers the orchestrator and globs correctly.""" + monkeypatch.setattr(firewall_module, "STRICT_IMPORT_MODE", False) + target_dir = tmp_path / "mock_repo" + target_dir.mkdir() + + def mock_run_side_effect(*args, **kwargs): + # Correctly extract the target output path from the subprocess call arguments + call_args = args[0] + out_target = Path(call_args[call_args.index("--output") + 1]) + audit_file = out_target.parent / "firewall_temp_audit.json" + + mock_audit_content = { + "6. Parsed Files (Scanned Artifacts)": { + "root": { + "Files": {"app.js": {"raw_imports": [], "equations": {}, "coding_loc": 10}} + } + } + } + audit_file.write_text(json.dumps(mock_audit_content)) + return MagicMock(returncode=0) + + mock_subprocess_run.side_effect = mock_run_side_effect + + with patch.object(sys, "argv", ["supply_chain_firewall.py", str(target_dir)]): + try: + firewall_module.main() + except SystemExit: + pytest.fail("Firewall crashed during standalone directory execution.") + + called_args = mock_subprocess_run.call_args[0][0] + assert "--output" in called_args + output_target = called_args[called_args.index("--output") + 1] + assert output_target.endswith("firewall_temp.json") + +# ============================================================================== +# TEST 14: ISSUE #159 - SILENT "0 FILES SCANNED" FAILURE (SCHEMA MISMATCH) +# ============================================================================== +def test_directory_group_schema_parsing(tmp_path, monkeypatch, capsys): + """Proves the firewall iterates nested directory groups to extract files.""" + monkeypatch.setattr(firewall_module, "STRICT_IMPORT_MODE", False) + + mock_ram_graph = { + "6. Parsed Files (Scanned Artifacts)": { + "src/backend": { + "Files": { + "server.py": {"raw_imports": [], "equations": {}, "coding_loc": 10} + } + }, + "src/frontend": { + "Files": { + "ui.jsx": {"raw_imports": [], "equations": {}, "coding_loc": 10} + } + } + } + } + + graph_file = tmp_path / "results.json" + graph_file.write_text(json.dumps(mock_ram_graph), encoding="utf-8") + + with patch.object(sys, "argv", ["supply_chain_firewall.py", str(graph_file)]): + try: + firewall_module.main() + except SystemExit: + pytest.fail("Failed on clean schema test.") + + captured = capsys.readouterr() + assert "Files Evaluated : 2" in captured.out + +# ============================================================================== +# TEST 15: ISSUE #160 - DENSITY DILUTION BUG +# ============================================================================== +def test_density_dilution_fix_for_build_scripts(tmp_path, monkeypatch): + """Proves that small build scripts are NOT diluted by +150 LOC padding.""" + monkeypatch.setattr(firewall_module, "STRICT_IMPORT_MODE", False) + + mock_ram_graph = { + "6. Parsed Files (Scanned Artifacts)": { + "root": { + "Files": { + "postinstall.js": { + "raw_imports": [], + "equations": {"sec_high_risk_execution": 10}, + "coding_loc": 5, + } + } + } + } + } + + graph_file = tmp_path / "results.json" + graph_file.write_text(json.dumps(mock_ram_graph), encoding="utf-8") + + with patch.object(sys, "argv", ["supply_chain_firewall.py", str(graph_file)]): + with pytest.raises(SystemExit): + firewall_module.main() + +# ============================================================================== +# TEST 16: ISSUE #161 - MISSING THREAT VECTORS (MEMORY CORRUPTION) +# ============================================================================== +def test_memory_corruption_detection(tmp_path, monkeypatch): + """Proves 'Memory Corruption Risk' triggers a blocking action when detected.""" + monkeypatch.setattr(firewall_module, "STRICT_IMPORT_MODE", False) + + mock_ram_graph = { + "6. Parsed Files (Scanned Artifacts)": { + "root": { + "Files": { + "payload.c": { + "raw_imports": [], + "equations": {"sec_memory_corruption": 500}, + "coding_loc": 50, + } + } + } + } + } + + graph_file = tmp_path / "results.json" + graph_file.write_text(json.dumps(mock_ram_graph), encoding="utf-8") + + with patch.object(sys, "argv", ["supply_chain_firewall.py", str(graph_file)]): + with pytest.raises(SystemExit): + firewall_module.main() \ No newline at end of file