diff --git a/tests/test_rules.py b/tests/test_rules.py new file mode 100644 index 0000000..4dc64af --- /dev/null +++ b/tests/test_rules.py @@ -0,0 +1,278 @@ +"""Tests for rule analyzers.""" + +import pytest +from pathlib import Path +import tempfile +import os + +from src.analyzers.base import SeverityLevel, FindingCategory +from src.rules.security import SQLInjectionAnalyzer, EvalUsageAnalyzer, PathTraversalAnalyzer +from src.rules.antipatterns import ( + ExceptionSwallowAnalyzer, + MagicNumberAnalyzer, + DeepNestingAnalyzer, + LongFunctionAnalyzer, +) +from src.rules.secrets import HardcodedSecretAnalyzer +from src.rules.performance import InefficientLoopAnalyzer, RedundantOperationAnalyzer, UnnecessaryCopyAnalyzer + + +class TestSQLInjectionAnalyzer: + """Tests for SQL injection detection.""" + + def setup_method(self): + self.analyzer = SQLInjectionAnalyzer() + + def test_no_false_positive_with_safe_query(self): + code = ''' +user_input = "test" +result = safe_query(user_input) +''' + tree = self._parse_code(code) + findings = self.analyzer.analyze(code, Path("test.py"), tree) + assert len(findings) == 0 + + def test_no_false_positive_with_parameterized_query(self): + code = """ +cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,)) +""" + tree = self._parse_code(code) + findings = self.analyzer.analyze(code, Path("test.py"), tree) + assert len(findings) == 0 + + def _parse_code(self, code): + from src.analyzers import PythonParser + parser = PythonParser() + return parser.parse(code) + + +class TestEvalUsageAnalyzer: + """Tests for eval/exec detection.""" + + def setup_method(self): + self.analyzer = EvalUsageAnalyzer() + + def test_detects_eval_usage(self): + code = """ +user_input = "os.system('rm -rf /')" +result = eval(user_input) +""" + tree = self._parse_code(code) + findings = self.analyzer.analyze(code, Path("test.py"), tree) + assert len(findings) > 0 + assert findings[0].severity == SeverityLevel.CRITICAL + + def test_detects_exec_usage(self): + code = """ +code = "print('hello')" +exec(code) +""" + tree = self._parse_code(code) + findings = self.analyzer.analyze(code, Path("test.py"), tree) + assert len(findings) > 0 + + def _parse_code(self, code): + from src.analyzers import PythonParser + parser = PythonParser() + return parser.parse(code) + + +class TestExceptionSwallowAnalyzer: + """Tests for exception swallowing detection.""" + + def setup_method(self): + self.analyzer = ExceptionSwallowAnalyzer() + + def test_detects_empty_except_clause(self): + code = """ +try: + dangerous_operation() +except: + pass +""" + tree = self._parse_code(code) + findings = self.analyzer.analyze(code, Path("test.py"), tree) + assert len(findings) > 0 + + def _parse_code(self, code): + from src.analyzers import PythonParser + parser = PythonParser() + return parser.parse(code) + + +class TestMagicNumberAnalyzer: + """Tests for magic number detection.""" + + def setup_method(self): + self.analyzer = MagicNumberAnalyzer() + + def test_detects_magic_number(self): + code = """ +result = 42 * multiplier +""" + tree = self._parse_code(code) + findings = self.analyzer.analyze(code, Path("test.py"), tree) + assert len(findings) > 0 + + def test_no_false_positive_for_small_numbers(self): + code = """ +for i in range(3): + process(i) +""" + tree = self._parse_code(code) + findings = self.analyzer.analyze(code, Path("test.py"), tree) + assert len(findings) == 0 + + def _parse_code(self, code): + from src.analyzers import PythonParser + parser = PythonParser() + return parser.parse(code) + + +class TestHardcodedSecretAnalyzer: + """Tests for hardcoded secret detection.""" + + def setup_method(self): + self.analyzer = HardcodedSecretAnalyzer() + + def test_detects_aws_access_key(self): + code = """ +AWS_KEY = "AKIAIOSFODNN7EXAMPLE" +""" + tree = self._parse_code(code) + findings = self.analyzer.analyze(code, Path("test.py"), tree) + assert len(findings) > 0 + assert findings[0].severity == SeverityLevel.CRITICAL + + def test_detects_github_token(self): + code = """ +GITHUB_TOKEN = "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +""" + tree = self._parse_code(code) + findings = self.analyzer.analyze(code, Path("test.py"), tree) + assert len(findings) > 0 + + def _parse_code(self, code): + from src.analyzers import PythonParser + parser = PythonParser() + return parser.parse(code) + + +class TestPathTraversalAnalyzer: + """Tests for path traversal detection.""" + + def setup_method(self): + self.analyzer = PathTraversalAnalyzer() + + def test_no_false_positive_with_safe_path(self): + code = ''' +user_input = "safe/path" +result = safe_open(user_input) +''' + tree = self._parse_code(code) + findings = self.analyzer.analyze(code, Path("test.py"), tree) + assert len(findings) == 0 + + def _parse_code(self, code): + from src.analyzers import PythonParser + parser = PythonParser() + return parser.parse(code) + + +class TestInefficientLoopAnalyzer: + """Tests for inefficient loop detection.""" + + def setup_method(self): + self.analyzer = InefficientLoopAnalyzer() + + def test_detects_inefficient_loop(self): + code = """ +items = [1, 2, 3, 4, 5] +for i in range(len(items)): + print(items[i]) +""" + tree = self._parse_code(code) + findings = self.analyzer.analyze(code, Path("test.py"), tree) + assert len(findings) > 0 + + def _parse_code(self, code): + from src.analyzers import PythonParser + parser = PythonParser() + return parser.parse(code) + + +class TestRedundantOperationAnalyzer: + """Tests for redundant operation detection.""" + + def setup_method(self): + self.analyzer = RedundantOperationAnalyzer() + + def test_detects_redundant_list_call(self): + code = """ +items = [1, 2, 3] +result = list(list(items)) +""" + tree = self._parse_code(code) + findings = self.analyzer.analyze(code, Path("test.py"), tree) + assert len(findings) >= 0 + + def _parse_code(self, code): + from src.analyzers import PythonParser + parser = PythonParser() + return parser.parse(code) + + +class TestRuleMetadata: + """Tests for rule metadata.""" + + def test_all_rules_have_unique_ids(self): + analyzers = [ + SQLInjectionAnalyzer(), + EvalUsageAnalyzer(), + PathTraversalAnalyzer(), + ExceptionSwallowAnalyzer(), + MagicNumberAnalyzer(), + DeepNestingAnalyzer(), + LongFunctionAnalyzer(), + HardcodedSecretAnalyzer(), + InefficientLoopAnalyzer(), + RedundantOperationAnalyzer(), + UnnecessaryCopyAnalyzer(), + ] + rule_ids = [a.rule_id() for a in analyzers] + assert len(rule_ids) == len(set(rule_ids)), "Rule IDs must be unique" + + def test_all_rules_have_valid_severity(self): + analyzers = [ + SQLInjectionAnalyzer(), + EvalUsageAnalyzer(), + PathTraversalAnalyzer(), + ExceptionSwallowAnalyzer(), + MagicNumberAnalyzer(), + DeepNestingAnalyzer(), + LongFunctionAnalyzer(), + HardcodedSecretAnalyzer(), + InefficientLoopAnalyzer(), + RedundantOperationAnalyzer(), + UnnecessaryCopyAnalyzer(), + ] + for analyzer in analyzers: + assert analyzer.severity() in list(SeverityLevel) + + def test_all_rules_have_valid_category(self): + from src.analyzers.base import FindingCategory + analyzers = [ + SQLInjectionAnalyzer(), + EvalUsageAnalyzer(), + PathTraversalAnalyzer(), + ExceptionSwallowAnalyzer(), + MagicNumberAnalyzer(), + DeepNestingAnalyzer(), + LongFunctionAnalyzer(), + HardcodedSecretAnalyzer(), + InefficientLoopAnalyzer(), + RedundantOperationAnalyzer(), + UnnecessaryCopyAnalyzer(), + ] + for analyzer in analyzers: + assert analyzer.category() in list(FindingCategory)