diff --git a/src/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py b/src/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py index e49ff42a..450d227e 100644 --- a/src/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py +++ b/src/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py @@ -58,9 +58,21 @@ ), ] 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["KEY"] / os.environ['SECRET'] — whitespace-tolerant + ( + r"os\s*\.\s*environ\s*\[\s*['\"][^'\"]*(?:KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)[^'\"]*['\"]\s*\]", + 0.8, + ), + # Python: os.environ.get("KEY") — whitespace-tolerant + (r"os\s*\.\s*environ\s*\.\s*get\s*\([^)]*(?:KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)", 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..72c02493 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 os.environ access is still detected.""" + state = { + "components": ["script.py"], + "file_cache": { + "script.py": "import os\nx = os . environ [ 'API_KEY' ]\ny = os.environ.get('SECRET')", + }, + } + findings = static_runner.run_static_patterns(state, [data_exfiltration_module]) + e2 = [f for f in findings if f.rule_id == "E2"] + assert len(e2) >= 2 + + 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 = {