From a92654b5410a98066a4f70ac847d12f19781404c Mon Sep 17 00:00:00 2001 From: weed33834 Date: Thu, 6 Aug 2026 14:29:53 +0000 Subject: [PATCH] fix(analyzer): resolve merge conflict and adopt upstream variable naming The upstream refactored the E2 regex fallback list to use E2_PYTHON_FALLBACK_PATTERNS (instead of E2_PATTERNS) and added a comprehensive AST-based environment-read analysis. Our PR's improved regex patterns (whitespace-tolerant, os.environ["KEY"], {**os.environ} spread) are merged into the upstream variable name, and the upstream's more thorough test suite is retained. All 19 E2-specific tests and 303 unit tests pass. --- .../static_patterns_data_exfiltration.py | 19 ++++++++++++++++ tests/unit/test_patterns.py | 22 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py b/src/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py index e49ff42a..d36be8c8 100644 --- a/src/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py +++ b/src/skillspector/nodes/analyzers/static_patterns_data_exfiltration.py @@ -58,18 +58,37 @@ ), ] 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'] + ( + r"os\s*\.\s*environ\s*\[\s*['\"][^'\"]*(?:KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)[^'\"]*['\"]\s*\]", + 0.8, + ), + # Python: os.environ.get("KEY") + (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 spread (mapping-unpack boundaries + # only: bare `**os.environ` is also exponentiation and must not be flagged) (r"\{\s*\*\*\s*os\s*\.\s*environ\s*\}", 0.6), + # Generic keyword-in-var check ] E2_OTHER_PATTERNS = [ (r"(?:API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)\s+in\s+(?:key|name|var)", 0.8), + # Node.js: process.env["KEY"] (r"process\.env\s*\[\s*['\"][^'\"]*(?:KEY|SECRET|TOKEN|PASSWORD)[^'\"]*['\"]\s*\]", 0.7), + # Node.js: Object.keys(process.env) (r"Object\.keys\s*\(\s*process\.env\s*\)", 0.6), + # Shell: env | grep key (r"env\s*\|\s*grep\s+(?:-i\s+)?(?:key|secret|token|password)", 0.8), + # Shell: printenv KEY (r"printenv\s+(?:\w*(?:KEY|SECRET|TOKEN|PASSWORD)\w*)", 0.7), + # Natural language: "collect all environment variables" (r"collect\s+(?:all\s+)?(?:environment\s+variables?|env\s+vars?)", 0.7), + # Natural language: "extract/harvest/gather API keys from environment" (r"(?:extract|harvest|gather)\s+(?:api\s+)?keys?\s+from\s+environment", 0.8), ] E2_PATTERNS = E2_PYTHON_FALLBACK_PATTERNS + E2_OTHER_PATTERNS diff --git a/tests/unit/test_patterns.py b/tests/unit/test_patterns.py index 11dba52e..99f5d32e 100644 --- a/tests/unit/test_patterns.py +++ b/tests/unit/test_patterns.py @@ -185,6 +185,28 @@ def test_e2_unparseable_python_uses_regex_fallback(self) -> None: assert any(finding.rule_id == "E2" for finding in findings) + def test_e2_malformed_python_exponentiation_not_flagged(self) -> None: + """Malformed-Python fallback must not flag bare `** os.environ` exponentiation. + + `2 ** os.environ` is an exponentiation expression, not a mapping unpack; + only `{**os.environ}`-style spreads are harvesting signals. The fallback + regex runs because the file has an unrelated syntax error, so this + exercises the fallback path specifically (AST is unavailable). + """ + content = "import os\nresult = 2 ** os.environ\ndef broken(\n" + + findings = data_exfiltration_module.analyze(content, "script.py", "python") + + assert not any(finding.rule_id == "E2" for finding in findings), findings + + def test_e2_malformed_python_mapping_spread_still_flagged(self) -> None: + """Malformed-Python fallback still detects real `{**os.environ}` spread.""" + content = "import os\npayload = {**os.environ}\ndef broken(\n" + + findings = data_exfiltration_module.analyze(content, "script.py", "python") + + assert any(finding.rule_id == "E2" for finding in findings), findings + @pytest.mark.parametrize( "expression", [