diff --git a/src/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py b/src/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py index e49ff42a..6d91b5c9 100644 --- a/src/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py +++ b/src/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py @@ -58,9 +58,14 @@ ), ] E2_PYTHON_FALLBACK_PATTERNS = [ + # Python: for k, v in os.environ.items() — whitespace-tolerant (r"for\s+\w+\s*,\s*\w+\s+in\s+os\s*\.\s*environ\s*\.\s*items\s*\(\s*\)", 0.7), + # Python: os.environ.copy() — full environ read (r"os\s*\.\s*environ\s*\.\s*copy\s*\(\s*\)", 0.6), + # Python: dict(os.environ) — full environ read via dict() (r"dict\s*\(\s*os\s*\.\s*environ\s*\)", 0.6), + # Python: {**os.environ} — full environ read via dict-spread. + # Require braces so bare ``2 ** os.environ`` (exponentiation) is not flagged. (r"\{\s*\*\*\s*os\s*\.\s*environ\s*\}", 0.6), ] E2_OTHER_PATTERNS = [ diff --git a/tests/nodes/analyzers/test_static_patterns.py b/tests/nodes/analyzers/test_static_patterns.py index 05fe19fc..e05e1ce4 100644 --- a/tests/nodes/analyzers/test_static_patterns.py +++ b/tests/nodes/analyzers/test_static_patterns.py @@ -333,6 +333,44 @@ def test_e2_env_harvesting_produces_finding(self): e2 = next(f for f in findings if f.rule_id == "E2") assert e2.severity == "HIGH" + def test_e2_whitespace_tolerant_environ_access(self): + """Whitespace-obfuscated full-environ reads are still detected.""" + state = { + "components": ["script.py"], + "file_cache": { + "script.py": "import os\nx = os . environ . copy ()\ny = dict ( os.environ )\nz = { ** os.environ }", + }, + } + findings = static_runner.run_static_patterns(state, [data_exfiltration_module]) + e2 = [f for f in findings if f.rule_id == "E2"] + assert len(e2) >= 3 + + def test_e2_exponentiation_not_flagged(self): + """Bare ``2 ** os.environ`` (exponentiation) must not be flagged as E2.""" + # Malformed Python (triggers regex fallback) with exponentiation + state = { + "components": ["script.py"], + "file_cache": { + "script.py": "import os\nresult = 2 ** os.environ\n def broken(", + }, + } + findings = static_runner.run_static_patterns(state, [data_exfiltration_module]) + e2 = [f for f in findings if f.rule_id == "E2"] + # Should NOT flag the exponentiation as env harvesting + assert not any("**" in f.matched_text for f in e2) + + def test_e2_dict_spread_environ_flagged(self): + """``{**os.environ}`` (dict spread) is flagged as full environ read.""" + state = { + "components": ["script.py"], + "file_cache": { + "script.py": "import os\nenv_copy = {**os.environ}", + }, + } + findings = static_runner.run_static_patterns(state, [data_exfiltration_module]) + e2 = [f for f in findings if f.rule_id == "E2"] + assert len(e2) >= 1 + def test_e5_boto3_put_object_produces_finding(self): """boto3 put_object yields E5, MEDIUM severity.""" state = {