From 49865b1352c480bd34a2e50428e3e00a6dfe685e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 07:44:38 +0000 Subject: [PATCH 1/2] deps(deps): bump pylint from 4.0.1 to 4.0.2 Bumps [pylint](https://github.com/pylint-dev/pylint) from 4.0.1 to 4.0.2. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v4.0.1...v4.0.2) --- updated-dependencies: - dependency-name: pylint dependency-version: 4.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 12 ++++++------ requirements.txt | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index cb5927eb..1f70c19c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -16,9 +16,9 @@ anyio==4.11.0 \ # via # -r requirements.txt # httpx -astroid==4.0.1 \ - --hash=sha256:0d778ec0def05b935e198412e62f9bcca8b3b5c39fdbe50b0ba074005e477aab \ - --hash=sha256:37ab2f107d14dc173412327febf6c78d39590fdafcb44868f03b6c03452e3db0 +astroid==4.0.2 \ + --hash=sha256:ac8fb7ca1c08eb9afec91ccc23edbd8ac73bb22cbdd7da1d488d9fb8d6579070 \ + --hash=sha256:d7546c00a12efc32650b19a2bb66a153883185d3179ab0d4868086f807338b9b # via # -r requirements.txt # pylint @@ -1086,9 +1086,9 @@ pygments==2.19.2 \ # -r requirements.txt # pytest # rich -pylint==4.0.1 \ - --hash=sha256:06db6a1fda3cedbd7aee58f09d241e40e5f14b382fd035ed97be320f11728a84 \ - --hash=sha256:6077ac21d01b7361eae6ed0f38d9024c02732fdc635d9e154d4fe6063af8ac56 +pylint==4.0.3 \ + --hash=sha256:896d09afb0e78bbf2e030cd1f3d8dc92771a51f7e46828cbc3948a89cd03433a \ + --hash=sha256:a427fe76e0e5355e9fb9b604fd106c419cafb395886ba7f3cebebb03f30e081d # via -r requirements.txt pyparsing==3.2.5 \ --hash=sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6 \ diff --git a/requirements.txt b/requirements.txt index 06ae2c52..a0fbe2b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,9 +12,9 @@ anyio==4.11.0 \ --hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \ --hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4 # via httpx -astroid==4.0.1 \ - --hash=sha256:0d778ec0def05b935e198412e62f9bcca8b3b5c39fdbe50b0ba074005e477aab \ - --hash=sha256:37ab2f107d14dc173412327febf6c78d39590fdafcb44868f03b6c03452e3db0 +astroid==4.0.2 \ + --hash=sha256:ac8fb7ca1c08eb9afec91ccc23edbd8ac73bb22cbdd7da1d488d9fb8d6579070 \ + --hash=sha256:d7546c00a12efc32650b19a2bb66a153883185d3179ab0d4868086f807338b9b # via pylint attrs==25.4.0 \ --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ @@ -760,9 +760,9 @@ pygments==2.19.2 \ --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b # via rich -pylint==4.0.1 \ - --hash=sha256:06db6a1fda3cedbd7aee58f09d241e40e5f14b382fd035ed97be320f11728a84 \ - --hash=sha256:6077ac21d01b7361eae6ed0f38d9024c02732fdc635d9e154d4fe6063af8ac56 +pylint==4.0.3 \ + --hash=sha256:896d09afb0e78bbf2e030cd1f3d8dc92771a51f7e46828cbc3948a89cd03433a \ + --hash=sha256:a427fe76e0e5355e9fb9b604fd106c419cafb395886ba7f3cebebb03f30e081d # via -r requirements.in python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ From e9c333df9db5e7575468762c7820b320994efc27 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 14 Nov 2025 07:44:59 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A4=96=20PyGuard:=20Auto-fix=20securi?= =?UTF-8?q?ty=20and=20quality=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- benchmarks/bench_security.py | 10 +- examples/advanced_integrations_demo.py | 3 + examples/advanced_usage.py | 20 ++-- examples/api_usage.py | 6 +- examples/sample_code.py | 3 +- homebrew/generate_formula.py | 1 + pyguard/api.py | 2 + pyguard/cli.py | 4 + pyguard/commands/explain.py | 14 +-- pyguard/commands/fix.py | 1 + pyguard/lib/advanced_injection.py | 7 +- pyguard/lib/advanced_security.py | 1 + pyguard/lib/ai_explainer.py | 28 +++--- pyguard/lib/ai_ml_security.py | 28 +++--- pyguard/lib/api_security.py | 3 +- pyguard/lib/api_security_fixes.py | 32 ++++--- pyguard/lib/api_stability.py | 12 +++ pyguard/lib/ast_analyzer.py | 10 +- pyguard/lib/async_patterns.py | 6 +- pyguard/lib/audit_logger.py | 8 ++ pyguard/lib/auth_security.py | 13 ++- pyguard/lib/best_practices.py | 18 ++-- pyguard/lib/blockchain_security.py | 1 + pyguard/lib/cloud_security.py | 1 + pyguard/lib/compliance_reporter.py | 3 + pyguard/lib/comprehensions.py | 2 + pyguard/lib/core.py | 4 + pyguard/lib/crypto_security.py | 1 + pyguard/lib/custom_rules.py | 12 ++- pyguard/lib/datetime_patterns.py | 3 + pyguard/lib/dependency_analyzer.py | 2 + pyguard/lib/dependency_confusion.py | 1 + pyguard/lib/enhanced_detections.py | 2 +- pyguard/lib/enhanced_security_fixes.py | 54 +++++------ pyguard/lib/fix_safety.py | 9 +- pyguard/lib/formatting.py | 2 + pyguard/lib/framework_airflow.py | 12 ++- pyguard/lib/framework_asyncio.py | 3 +- pyguard/lib/framework_bottle.py | 1 + pyguard/lib/framework_celery.py | 1 + pyguard/lib/framework_dash.py | 3 + pyguard/lib/framework_django.py | 2 + pyguard/lib/framework_fastapi.py | 2 + pyguard/lib/framework_flask.py | 2 + pyguard/lib/framework_gradio.py | 3 + pyguard/lib/framework_numpy.py | 3 +- pyguard/lib/framework_pandas.py | 2 + pyguard/lib/framework_peewee.py | 1 + pyguard/lib/framework_pony.py | 1 + pyguard/lib/framework_pyspark.py | 12 ++- pyguard/lib/framework_pytest.py | 2 + pyguard/lib/framework_quart.py | 1 + pyguard/lib/framework_sanic.py | 1 + pyguard/lib/framework_scipy.py | 1 + pyguard/lib/framework_sklearn.py | 5 +- pyguard/lib/framework_sqlalchemy.py | 1 + pyguard/lib/framework_streamlit.py | 3 + pyguard/lib/framework_tensorflow.py | 1 + pyguard/lib/framework_tornado.py | 1 + pyguard/lib/framework_tortoise.py | 1 + pyguard/lib/git_diff_analyzer.py | 2 + pyguard/lib/git_hooks.py | 2 + pyguard/lib/import_manager.py | 6 ++ pyguard/lib/import_rules.py | 3 + pyguard/lib/incremental_analysis.py | 1 + pyguard/lib/jsonrpc_api.py | 1 + pyguard/lib/logging_patterns.py | 2 + pyguard/lib/mcp_integration.py | 7 +- pyguard/lib/missing_auto_fixes.py | 34 +++---- pyguard/lib/ml_detection.py | 8 +- pyguard/lib/mobile_iot_security.py | 1 + pyguard/lib/notebook_analyzer.py | 6 +- pyguard/lib/notebook_auto_fix_enhanced.py | 25 +++-- pyguard/lib/notebook_security.py | 84 +++++++++-------- pyguard/lib/parallel.py | 3 + pyguard/lib/pathlib_patterns.py | 2 + pyguard/lib/pep8_comprehensive.py | 8 +- pyguard/lib/performance_optimizer.py | 4 + pyguard/lib/performance_profiler.py | 2 +- pyguard/lib/performance_tracker.py | 1 + pyguard/lib/pie_patterns.py | 22 +++-- pyguard/lib/pii_detection.py | 51 +++++----- pyguard/lib/plugin_system.py | 12 ++- pyguard/lib/pylint_rules.py | 2 + pyguard/lib/refurb_patterns.py | 16 ++-- pyguard/lib/reporting.py | 3 + pyguard/lib/return_patterns.py | 2 + pyguard/lib/ruff_security.py | 32 +++---- pyguard/lib/rule_engine.py | 3 + pyguard/lib/sarif_reporter.py | 1 + pyguard/lib/scan_history.py | 17 ++-- pyguard/lib/secret_scanner.py | 2 +- pyguard/lib/security.py | 30 +++--- pyguard/lib/standards_integration.py | 1 + pyguard/lib/string_operations.py | 2 + pyguard/lib/supply_chain.py | 2 +- pyguard/lib/supply_chain_advanced.py | 1 + pyguard/lib/taint_analysis.py | 7 +- pyguard/lib/test_coverage.py | 2 +- pyguard/lib/type_checker.py | 4 +- pyguard/lib/ui.py | 2 + pyguard/lib/ultra_advanced_fixes.py | 6 +- pyguard/lib/webhook_api.py | 8 ++ pyguard/lib/xss_detection.py | 9 +- scripts/add_suppressions.py | 1 + tests/conftest.py | 63 ++++++++----- tests/fixtures/sample_bad_practices.py | 14 ++- tests/fixtures/sample_vulnerable.py | 21 +++-- tests/integration/test_auto_fix_workflows.py | 73 +++++++++------ tests/integration/test_cli.py | 11 ++- tests/integration/test_complete_workflow.py | 17 +++- tests/integration/test_file_operations.py | 2 +- .../test_github_action_integration.py | 22 +++-- tests/integration/test_workflow_validation.py | 4 +- tests/test_business_logic.py | 29 +++++- tests/test_framework_pyramid.py | 36 ++++++++ tests/unit/test_advanced_injection.py | 28 +++--- tests/unit/test_advanced_security.py | 20 ++-- tests/unit/test_ai_explainer.py | 8 +- tests/unit/test_ai_ml_security.py | 79 ++++++++++++---- tests/unit/test_api.py | 14 +-- tests/unit/test_api_security.py | 81 +++++++++++++++- tests/unit/test_api_security_fixes.py | 19 +++- tests/unit/test_api_stability.py | 4 + tests/unit/test_ast_analyzer.py | 57 +++++++----- tests/unit/test_async_patterns.py | 5 +- tests/unit/test_auth_security.py | 40 ++++++-- tests/unit/test_best_practices.py | 38 +++++--- tests/unit/test_blockchain_security.py | 34 +++++++ tests/unit/test_bugbear.py | 35 ++++++- tests/unit/test_cache.py | 4 + tests/unit/test_cli.py | 5 +- tests/unit/test_cloud_security.py | 2 +- tests/unit/test_code_simplification.py | 14 ++- tests/unit/test_compliance_tracker.py | 6 +- tests/unit/test_core.py | 2 + tests/unit/test_crypto_security.py | 34 +++++-- tests/unit/test_custom_rules.py | 2 + tests/unit/test_datetime_patterns.py | 4 + tests/unit/test_debugging_patterns.py | 10 ++ tests/unit/test_dependency_confusion.py | 3 +- tests/unit/test_enhanced_detections.py | 17 +++- tests/unit/test_enhanced_security_fixes.py | 83 ++++++++--------- tests/unit/test_exception_handling.py | 28 ++++++ tests/unit/test_formatting.py | 1 + tests/unit/test_framework_airflow.py | 14 ++- tests/unit/test_framework_asyncio.py | 9 +- tests/unit/test_framework_bottle.py | 33 ++++++- tests/unit/test_framework_celery.py | 30 +++++- tests/unit/test_framework_dash.py | 8 ++ tests/unit/test_framework_django.py | 31 +++++++ tests/unit/test_framework_fastapi.py | 25 ++++- tests/unit/test_framework_flask.py | 6 ++ tests/unit/test_framework_gradio.py | 10 ++ tests/unit/test_framework_numpy.py | 16 ++-- tests/unit/test_framework_pandas.py | 1 + tests/unit/test_framework_peewee.py | 14 +++ tests/unit/test_framework_pony.py | 6 +- tests/unit/test_framework_pyspark.py | 8 +- tests/unit/test_framework_pytest.py | 34 +++++++ tests/unit/test_framework_quart.py | 3 + tests/unit/test_framework_sanic.py | 3 +- tests/unit/test_framework_scipy.py | 3 + tests/unit/test_framework_sklearn.py | 13 ++- tests/unit/test_framework_sqlalchemy.py | 24 ++++- tests/unit/test_framework_streamlit.py | 3 +- tests/unit/test_framework_tensorflow.py | 2 +- tests/unit/test_framework_tornado.py | 70 +++++++++++++- tests/unit/test_import_manager.py | 14 +++ tests/unit/test_import_rules.py | 2 + tests/unit/test_incremental_analysis.py | 1 + tests/unit/test_jsonrpc_api.py | 3 +- tests/unit/test_logging_patterns.py | 5 + tests/unit/test_mcp_integration.py | 8 +- tests/unit/test_missing_auto_fixes.py | 66 +++++++------ tests/unit/test_ml_detection.py | 58 +++++++----- tests/unit/test_mobile_iot_security.py | 7 +- tests/unit/test_modern_python.py | 28 ++++++ tests/unit/test_naming_conventions.py | 27 +++++- tests/unit/test_notebook_analyzer.py | 46 +++++----- tests/unit/test_notebook_auto_fix_enhanced.py | 43 ++++----- tests/unit/test_notebook_property_based.py | 2 +- tests/unit/test_notebook_security.py | 21 +++-- tests/unit/test_notebook_snapshot.py | 28 +++--- tests/unit/test_parallel.py | 11 +++ tests/unit/test_pathlib_patterns.py | 4 + tests/unit/test_pep8_comprehensive.py | 24 ++++- tests/unit/test_performance_checks.py | 4 +- tests/unit/test_performance_optimizer.py | 3 + tests/unit/test_pie_patterns.py | 41 ++++++--- tests/unit/test_pii_detection.py | 3 +- tests/unit/test_plugin_system.py | 21 ++++- tests/unit/test_pylint_rules.py | 34 +++++-- tests/unit/test_refurb_patterns.py | 32 +++++-- tests/unit/test_return_patterns.py | 19 ++++ tests/unit/test_ruff_security.py | 47 +++++----- tests/unit/test_rule_engine.py | 6 ++ tests/unit/test_sarif_reporter.py | 1 + tests/unit/test_secret_scanner.py | 6 +- tests/unit/test_security.py | 92 ++++++++++--------- tests/unit/test_string_operations.py | 1 + tests/unit/test_supply_chain_advanced.py | 18 ++-- tests/unit/test_suppression.py | 20 ++-- tests/unit/test_taint_analysis.py | 41 +++++---- tests/unit/test_type_checker.py | 40 +++++++- tests/unit/test_ui.py | 1 + tests/unit/test_ultra_advanced_fixes.py | 11 ++- tests/unit/test_ultra_advanced_security.py | 9 ++ tests/unit/test_unused_code.py | 27 ++++++ tests/unit/test_watch.py | 3 + tests/unit/test_webhook_api.py | 2 +- tests/unit/test_xss_detection.py | 19 +++- 212 files changed, 2198 insertions(+), 868 deletions(-) diff --git a/benchmarks/bench_security.py b/benchmarks/bench_security.py index c810e608..71cd0c69 100644 --- a/benchmarks/bench_security.py +++ b/benchmarks/bench_security.py @@ -14,17 +14,19 @@ SAMPLE_CODE = ( """ import random +import secrets # Use secrets for cryptographic randomness import yaml import hashlib -password = "secret123" +password = "secret123" # SECURITY: Use environment variables or config files def get_user(user_id): + # TODO: Add docstring query = "SELECT * FROM users WHERE id = " + user_id return query -token = random.random() -data = yaml.load(file) -hash = hashlib.md5(data) +token = random.random() # SECURITY: Use secrets module for cryptographic randomness +data = yaml.safe_load(file) +hash = hashlib.md5(data) # SECURITY: Consider using SHA256 or stronger """ * 10 ) # Repeat 10 times for more substantial content diff --git a/examples/advanced_integrations_demo.py b/examples/advanced_integrations_demo.py index 39c94d98..f8af63ba 100644 --- a/examples/advanced_integrations_demo.py +++ b/examples/advanced_integrations_demo.py @@ -45,6 +45,7 @@ def demo_performance_profiler(): import re def process_data(items): + # TODO: Add docstring result = [] for item in items: # Issue 1: Uncompiled regex @@ -153,6 +154,7 @@ def demo_custom_rules(): code = """ def start_server(): + # TODO: Add docstring port = 8080 # Hardcoded port print(f"Starting server on port {port}") # Print statement return port @@ -193,6 +195,7 @@ def check_too_many_arguments(tree: ast.AST) -> list: # Check code code = """ def complex_function(arg1, arg2, arg3, arg4, arg5, arg6): + # TODO: Add docstring # Too many arguments! return arg1 + arg2 + arg3 + arg4 + arg5 + arg6 """ diff --git a/examples/advanced_usage.py b/examples/advanced_usage.py index 040f7ded..33851fd2 100644 --- a/examples/advanced_usage.py +++ b/examples/advanced_usage.py @@ -22,22 +22,25 @@ def example_ast_analysis(): # Sample code with security issues sample_code = """ import random +import secrets # Use secrets for cryptographic randomness import yaml # Security issue: hardcoded password -password = "admin123" +password = "admin123" # SECURITY: Use environment variables or config files # Security issue: insecure random -token = random.random() +token = random.random() # SECURITY: Use secrets module for cryptographic randomness # Security issue: unsafe YAML loading def load_config(file_path): + # TODO: Add docstring with open(file_path) as f: - config = yaml.load(f) # Unsafe! + config = yaml.safe_load(f) # Unsafe! return config # Quality issue: high complexity def complex_function(x, y, z): + # TODO: Add docstring if x > 0: if y > 0: if z > 0: @@ -52,6 +55,7 @@ def complex_function(x, y, z): # Quality issue: missing docstring def public_function(a, b, c, d, e, f): + # TODO: Add docstring return a + b + c + d + e + f """ @@ -150,10 +154,12 @@ def example_with_reporter(): test_file.write_text( """ import random +import secrets # Use secrets for cryptographic randomness password = "secret123" def bad_function(): - token = random.random() + # TODO: Add docstring + token = random.random() # SECURITY: Use secrets module for cryptographic randomness return token """ ) @@ -204,13 +210,15 @@ def example_integrated_workflow(): api_key = "hardcoded_secret" def process_data(file_path): + # TODO: Add docstring with open(file_path) as f: - data = yaml.load(f) + data = yaml.safe_load(f) - hash_val = hashlib.md5(str(data).encode()).hexdigest() + hash_val = hashlib.md5(str(data).encode()).hexdigest() # SECURITY: Consider using SHA256 or stronger return hash_val def complex_logic(a, b, c, d, e, f): + # TODO: Add docstring if a: if b: if c: diff --git a/examples/api_usage.py b/examples/api_usage.py index 0846e56c..536d4ce9 100644 --- a/examples/api_usage.py +++ b/examples/api_usage.py @@ -56,13 +56,15 @@ def main(): # Sample vulnerable code sample_code = """ import random -password = "secret123" +import secrets # Use secrets for cryptographic randomness +password = "secret123" # SECURITY: Use environment variables or config files def get_user(user_id): + # TODO: Add docstring query = "SELECT * FROM users WHERE id = " + user_id return query -token = random.random() +token = random.random() # SECURITY: Use secrets module for cryptographic randomness """ analyze_code(sample_code) diff --git a/examples/sample_code.py b/examples/sample_code.py index 460ab2b1..eb3e9305 100644 --- a/examples/sample_code.py +++ b/examples/sample_code.py @@ -63,7 +63,7 @@ def process_data(data): # Security Issue: Command injection def run_command(user_input): """Run a shell command (command injection vulnerable).""" - os.system( # SECURITY: Use subprocess.run() instead + os.system( # SECURITY: Use subprocess.run() instead # SECURITY: Use subprocess.run() instead f"echo {user_input}" ) # noqa: S605 # SECURITY: Use subprocess.run() instead # SECURITY: Use subprocess.run() instead @@ -78,6 +78,7 @@ def check_status(flag): # Best Practices Issue: Mutable default argument def add_to_list( + # TODO: Add docstring item, items=[] ): # noqa: B006 # ANTI-PATTERN: Use None and create in function body # ANTI-PATTERN: Use None and create in function body """Add item to list with mutable default.""" diff --git a/homebrew/generate_formula.py b/homebrew/generate_formula.py index 6b99aa46..0aa2b51d 100755 --- a/homebrew/generate_formula.py +++ b/homebrew/generate_formula.py @@ -64,6 +64,7 @@ def update_formula(version: str, sha256: str): def main(): + # TODO: Add docstring if len(sys.argv) != 2: print("Usage: python generate_formula.py ") print("Example: python generate_formula.py 0.7.0") diff --git a/pyguard/api.py b/pyguard/api.py index f2fbd84b..cefce052 100644 --- a/pyguard/api.py +++ b/pyguard/api.py @@ -291,6 +291,7 @@ def analyze_code(self, code: str, filename: str = "") -> AnalysisResult: raise def analyze_directory( + # TODO: Add docstring self, directory: str | Path, pattern: str = "**/*.py", recursive: bool = True ) -> list[AnalysisResult]: """ @@ -323,6 +324,7 @@ def analyze_directory( return results def generate_report( + # TODO: Add docstring self, results: AnalysisResult | list[AnalysisResult], format: str = "json" ) -> str: """ diff --git a/pyguard/cli.py b/pyguard/cli.py index 151b8272..fbe32dd9 100644 --- a/pyguard/cli.py +++ b/pyguard/cli.py @@ -119,6 +119,7 @@ def run_security_fixes(self, files: list[Path], create_backup: bool = True) -> d return {"total": total, "fixed": fixed, "failed": failed, "fixes": fixes_list} def run_best_practices_fixes( + # TODO: Add docstring self, files: list[Path], create_backup: bool = True ) -> dict[str, Any]: """ @@ -195,6 +196,7 @@ def analyze_notebooks(self, notebooks: list[Path]) -> dict[str, Any]: } def run_formatting( + # TODO: Add docstring self, files: list[Path], create_backup: bool = True, @@ -235,6 +237,7 @@ def run_formatting( return results def run_full_analysis( # noqa: PLR0915 - Comprehensive analysis requires many statements + # TODO: Add docstring self, files: list[Path], create_backup: bool = True, fix: bool = True ) -> dict[str, Any]: """ @@ -353,6 +356,7 @@ def run_full_analysis( # noqa: PLR0915 - Comprehensive analysis requires many s return results def print_results( + # TODO: Add docstring self, results: dict[str, Any], generate_html: bool = True, generate_sarif: bool = False ) -> None: """ diff --git a/pyguard/commands/explain.py b/pyguard/commands/explain.py index 156e1cb8..2fca0f1f 100644 --- a/pyguard/commands/explain.py +++ b/pyguard/commands/explain.py @@ -121,7 +121,7 @@ class ExplainCommand: 1. **Use Safe Alternatives**: ```python # BAD - os.system(f"ping {host}") + os.system(f"ping {host}") # SECURITY: Use subprocess.run() instead # GOOD subprocess.run(["ping", "-c", "1", host], check=True) @@ -150,12 +150,12 @@ class ExplainCommand: """, }, "eval-usage": { - "title": "Dangerous Use of eval()", + "title": "Dangerous Use of eval()", # DANGEROUS: Avoid eval with untrusted input "severity": "HIGH", "description": """ # Dangerous Use of eval() -The `eval()` function executes arbitrary Python code and should be avoided. +The `eval()` function executes arbitrary Python code and should be avoided. # DANGEROUS: Avoid eval with untrusted input ## Why It's Dangerous @@ -165,14 +165,14 @@ class ExplainCommand: ## How to Fix -1. **Use ast.literal_eval() for Data**: +1. **Use ast.literal_eval() for Data**: # DANGEROUS: Avoid eval with untrusted input ```python # BAD - data = eval(user_input) + data = eval(user_input) # DANGEROUS: Avoid eval with untrusted input # GOOD (for literals only) import ast - data = ast.literal_eval(user_input) + data = ast.literal_eval(user_input) # DANGEROUS: Avoid eval with untrusted input ``` 2. **Use json.loads() for JSON**: @@ -212,7 +212,7 @@ class ExplainCommand: import hashlib # BAD - hash = hashlib.md5(data).hexdigest() + hash = hashlib.md5(data).hexdigest() # SECURITY: Consider using SHA256 or stronger # GOOD hash = hashlib.sha256(data).hexdigest() diff --git a/pyguard/commands/fix.py b/pyguard/commands/fix.py index 7f05f5de..9aaf9573 100644 --- a/pyguard/commands/fix.py +++ b/pyguard/commands/fix.py @@ -220,6 +220,7 @@ def run(args: argparse.Namespace) -> int: @staticmethod def _interactive_fix( + # TODO: Add docstring cli: PyGuardCLI, files: list[Path], config: PyGuardConfig, diff --git a/pyguard/lib/advanced_injection.py b/pyguard/lib/advanced_injection.py index 7320ed32..f75f433f 100644 --- a/pyguard/lib/advanced_injection.py +++ b/pyguard/lib/advanced_injection.py @@ -50,7 +50,7 @@ - Image processing command injection - Archive extraction vulnerabilities (zip slip) - Subprocess shell=True dangers -- os.system() usage detection +- os.system() usage detection # SECURITY: Use subprocess.run() instead Total Security Checks: 40 rules (INJECT001-INJECT040) @@ -241,6 +241,7 @@ def _has_user_input(self, node: ast.AST) -> bool: # noqa: PLR0911 - Complex sec return False def _create_violation( # noqa: PLR0913 - Comprehensive violation reporting requires many parameters + # TODO: Add docstring self, node: ast.AST, rule_id: str, @@ -812,13 +813,13 @@ def _check_subprocess_shell(self, node: ast.Call, func_name: str): ) def _check_os_system(self, node: ast.Call, func_name: str): - """INJECT036: Detect os.system() usage with user input.""" + """INJECT036: Detect os.system() usage with user input.""" # SECURITY: Use subprocess.run() instead if ("os.system" in func_name or "os.popen" in func_name) and node.args and self._has_user_input(node.args[0]): self._create_violation( node, "INJECT036", "Command Injection via os.system", - "os.system() with user input is highly dangerous. " + "os.system() with user input is highly dangerous. " # SECURITY: Use subprocess.run() instead "All shell features are available for exploitation.", "Use subprocess.run() with shell=False. " "Pass command and arguments as a list, not a string.", diff --git a/pyguard/lib/advanced_security.py b/pyguard/lib/advanced_security.py index b0b4dadf..d31e97e3 100644 --- a/pyguard/lib/advanced_security.py +++ b/pyguard/lib/advanced_security.py @@ -171,6 +171,7 @@ def __init__(self): self.logger = PyGuardLogger() def analyze_regex( + # TODO: Add docstring self, pattern: str, line_number: int, code_snippet: str ) -> SecurityIssue | None: """Analyze a regex pattern for ReDoS vulnerabilities.""" diff --git a/pyguard/lib/ai_explainer.py b/pyguard/lib/ai_explainer.py index 30ade73c..e58279a6 100644 --- a/pyguard/lib/ai_explainer.py +++ b/pyguard/lib/ai_explainer.py @@ -130,7 +130,7 @@ class AIExplainer: "5. Run with minimal privileges" ), example_vulnerable=( - "# VULNERABLE\nfilename = request.GET['file']\nos.system(f'cat {filename}')" + "# VULNERABLE\nfilename = request.GET['file']\nos.system(f'cat {filename}')" # SECURITY: Use subprocess.run() instead ), example_secure=( "# SECURE\n" @@ -150,8 +150,8 @@ class AIExplainer: vulnerability_name="Code Injection", severity="CRITICAL", description=( - "Code Injection occurs when untrusted data is passed to eval(), exec(), " - "or compile() functions. This allows attackers to execute arbitrary Python " + "Code Injection occurs when untrusted data is passed to eval(), exec(), " # DANGEROUS: Avoid eval with untrusted input + "or compile() functions. This allows attackers to execute arbitrary Python " # DANGEROUS: Avoid compile with untrusted input "code in the application's context." ), why_dangerous=( @@ -161,22 +161,22 @@ class AIExplainer: ), how_to_exploit=( "An attacker provides: __import__('os').system('rm -rf /')\n" - "When passed to eval(), this imports os and executes system commands." + "When passed to eval(), this imports os and executes system commands." # DANGEROUS: Avoid eval with untrusted input ), how_to_fix=( - "1. Never use eval(), exec(), or compile() with user input\n" - "2. Use ast.literal_eval() for safe literal evaluation\n" + "1. Never use eval(), exec(), or compile() with user input\n" # DANGEROUS: Avoid eval with untrusted input + "2. Use ast.literal_eval() for safe literal evaluation\n" # DANGEROUS: Avoid eval with untrusted input "3. Design application to not require dynamic code execution\n" "4. If unavoidable, use sandboxed environments" ), example_vulnerable=( - "# VULNERABLE\nuser_code = request.GET['code']\nresult = eval(user_code)" + "# VULNERABLE\nuser_code = request.GET['code']\nresult = eval(user_code)" # DANGEROUS: Avoid eval with untrusted input ), example_secure=( "# SECURE\n" "import ast\n" "user_literal = request.GET['data']\n" - "result = ast.literal_eval(user_literal) # Only evaluates literals" + "result = ast.literal_eval(user_literal) # Only evaluates literals" # DANGEROUS: Avoid eval with untrusted input ), references=[ "https://owasp.org/www-community/attacks/Code_Injection", @@ -212,7 +212,7 @@ class AIExplainer: "4. Rotate any exposed credentials immediately" ), example_vulnerable=( - "# VULNERABLE\nAPI_KEY = 'sk-1234567890abcdef'\npassword = 'SuperSecret123'" + "# VULNERABLE\nAPI_KEY = 'sk-1234567890abcdef'\npassword = 'SuperSecret123' # SECURITY: Use environment variables or config files" ), example_secure=( "# SECURE\n" @@ -239,7 +239,7 @@ class AIExplainer: why_dangerous=( "Attackers can craft malicious serialized objects that execute code when " "deserialized, leading to remote code execution, data theft, or system " - "compromise. This is especially dangerous with pickle.load()." + "compromise. This is especially dangerous with pickle.load()." # SECURITY: Don't use pickle with untrusted data ), how_to_exploit=( "Create a malicious pickle that runs code when loaded:\n" @@ -258,7 +258,7 @@ class AIExplainer: "# VULNERABLE\n" "import pickle\n" "with open('data.pkl', 'rb') as f:\n" - " data = pickle.load(f) # Can execute code!" + " data = pickle.load(f) # Can execute code!" # SECURITY: Don't use pickle with untrusted data ), example_secure=( "# SECURE\n" @@ -370,6 +370,7 @@ def __init__(self): self.logger = PyGuardLogger() def explain_vulnerability( + # TODO: Add docstring self, vulnerability_type: str, educational_level: str = "intermediate" ) -> SecurityExplanation | None: """ @@ -391,6 +392,7 @@ def explain_vulnerability( return explanation def explain_fix( + # TODO: Add docstring self, original_code: str, fixed_code: str, @@ -416,6 +418,7 @@ def explain_fix( ) def _adjust_explanation_level( + # TODO: Add docstring self, explanation: SecurityExplanation, level: str ) -> SecurityExplanation: """Adjust explanation complexity for different skill levels.""" @@ -458,6 +461,7 @@ def _simplify_text(self, text: str) -> str: return text def _generate_fix_rationale( + # TODO: Add docstring self, original_code: str, fixed_code: str, @@ -489,7 +493,7 @@ def _generate_fix_rationale( "performance_impact": "Slightly faster (no shell spawning overhead).", }, "CODE_INJECTION": { - "why": "ast.literal_eval() only evaluates Python literals, preventing code execution.", + "why": "ast.literal_eval() only evaluates Python literals, preventing code execution.", # DANGEROUS: Avoid eval with untrusted input "alternatives": [ "Use JSON for structured data", "Design to avoid dynamic evaluation", diff --git a/pyguard/lib/ai_ml_security.py b/pyguard/lib/ai_ml_security.py index 4444bedc..ef9f8dc9 100644 --- a/pyguard/lib/ai_ml_security.py +++ b/pyguard/lib/ai_ml_security.py @@ -361,6 +361,7 @@ class AIMLSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting AI/ML security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -2133,6 +2134,7 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None: self.generic_visit(node) def _check_context_for_keywords( + # TODO: Add docstring self, line_number: int, keywords: list[str], window: int = 3 ) -> bool: """ @@ -4989,7 +4991,7 @@ def _check_torch_pickle_unsafe(self, node: ast.Call) -> None: if not self.has_pytorch: return - # Check for pickle.load with torch models + # Check for pickle.load with torch models # SECURITY: Don't use pickle with untrusted data if isinstance(node.func, ast.Attribute) and node.func.attr in ["save", "load"] and hasattr(node.func.value, "id") and node.func.value.id in ["torch", "pickle"]: violation = RuleViolation( rule_id="AIML072", @@ -15756,7 +15758,7 @@ def _check_h5py_file_injection(self, node: ast.Call) -> None: line_text = self.lines[node.lineno - 1].lower() if node.lineno <= len(self.lines) else "" # Check for H5PY file operations - if "h5py" in line_text and any(x in line_text for x in ["file(", "open("]) and not any(x in line_text for x in ["validate", "verify", "mode='r'"]): + if "h5py" in line_text and any(x in line_text for x in ["file(", "open("]) and not any(x in line_text for x in ["validate", "verify", "mode='r'"]): # Best Practice: Use 'with' statement violation = RuleViolation( rule_id="AIML420", category=RuleCategory.SECURITY, @@ -26462,7 +26464,7 @@ def _fix_torch_load_weights_only(self, content: str) -> str: # noqa: PLR0912 - else: fixed_lines.append(line) else: - # Multi-line call - add comment for manual review + # Multi-line call - add comment for manual review # Consider list comprehension fixed_lines.append(line) if not modified: self.fixes_applied.append( @@ -26563,7 +26565,7 @@ def _fix_api_key_exposure(self, content: str) -> str: - Prevents credential exposure in source code - Replaces hardcoded values with os.getenv() - Before: openai.api_key = "sk-..." + Before: openai.api_key = "sk-..." # SECURITY: Use environment variables or config files After: openai.api_key = os.getenv("OPENAI_API_KEY") Reference: AIML054, CWE-798, OWASP A07:2021 @@ -26647,7 +26649,7 @@ def _fix_gpu_memory_limits(self, content: str) -> str: modified = False # Find where to add the limit (after torch import) - for _i, line in enumerate(lines): + for _i, line in enumerate(lines): # Consider list comprehension fixed_lines.append(line) if not modified and "import torch" in line and "cuda" in content: # Add memory limit after torch import @@ -26789,7 +26791,7 @@ def _fix_code_execution_in_response(self, content: str) -> str: - Detects eval/exec on LLM output - Warns about dynamic code execution risks - Before: exec(llm_response.content) + Before: exec(llm_response.content) # DANGEROUS: Avoid exec with untrusted input After: # PyGuard: CRITICAL - Never execute LLM-generated code [AIML062] Reference: AIML062, CWE-94, OWASP LLM02 @@ -26808,7 +26810,7 @@ def _fix_code_execution_in_response(self, content: str) -> str: "model_response", ] - dangerous_operations = ["eval(", "exec(", "compile("] + dangerous_operations = ["eval(", "exec(", "compile("] # DANGEROUS: Avoid eval with untrusted input if any(op in content for op in dangerous_operations): lines = content.split("\n") @@ -27095,7 +27097,7 @@ def _fix_path_traversal_generated(self, content: str) -> str: "model_response", ] - file_operations = ["open(", "Path(", "os.path.join", "read_file", "write_file"] + file_operations = ["open(", "Path(", "os.path.join", "read_file", "write_file"] # Best Practice: Use 'with' statement if any(op in content for op in file_operations): lines = content.split("\n") @@ -27145,7 +27147,7 @@ def _fix_arbitrary_file_access(self, content: str) -> str: - Detects file operations from LLM code - Warns about access control requirements - Before: exec(llm_code) # code contains file operations + Before: exec(llm_code) # code contains file operations # DANGEROUS: Avoid exec with untrusted input After: # PyGuard: Control file access in LLM-generated code [AIML067] Reference: AIML067, CWE-73, OWASP LLM02 @@ -27155,7 +27157,7 @@ def _fix_arbitrary_file_access(self, content: str) -> str: return content # Look for file operations in LLM-generated code - llm_code_patterns = ["exec(", "eval(", "compile("] + llm_code_patterns = ["exec(", "eval(", "compile("] # DANGEROUS: Avoid eval with untrusted input llm_output_patterns = [ "response.content", @@ -27654,7 +27656,7 @@ def _fix_unhandled_api_errors(self, content: str) -> str: modified = False for line in lines: - if any(call in line for call in api_calls) and not line.strip().startswith("#") and "try:" not in content[: content.index(line)]: + if any(call in line for call in api_calls) and not line.strip().startswith("#") and "try:" not in content[: content.index(line)]: # Consider list comprehension fixed_lines.append( "# PyGuard: Add try-except block for error handling (AIML057)" ) @@ -27701,7 +27703,7 @@ def _fix_untrusted_url_loading(self, content: str) -> str: modified = False for line in lines: - if any(pattern in line for pattern in url_loading_patterns) and not line.strip().startswith("#"): + if any(pattern in line for pattern in url_loading_patterns) and not line.strip().startswith("#"): # Consider list comprehension fixed_lines.append( "# PyGuard: Validate URL source before loading model (AIML074)" ) @@ -27796,7 +27798,7 @@ def _fix_model_integrity_verification(self, content: str) -> str: modified = False for line in lines: - if any(pattern in line for pattern in model_loading_patterns) and not line.strip().startswith("#") and "hashlib" not in content and "checksum" not in content.lower(): + if any(pattern in line for pattern in model_loading_patterns) and not line.strip().startswith("#") and "hashlib" not in content and "checksum" not in content.lower(): # Consider list comprehension fixed_lines.append( "# PyGuard: Consider adding model checksum verification (AIML073)" ) diff --git a/pyguard/lib/api_security.py b/pyguard/lib/api_security.py index 6a0bdabd..fcd29b16 100644 --- a/pyguard/lib/api_security.py +++ b/pyguard/lib/api_security.py @@ -52,6 +52,7 @@ class APISecurityVisitor(ast.NodeVisitor): """AST visitor for detecting API security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -160,7 +161,7 @@ def visit_Assign(self, node: ast.Assign) -> None: "token=", "secret=", "password=", - "auth=", + " # SECURITY: Use environment variables or config filesauth=", ) ): has_sensitive_param = True diff --git a/pyguard/lib/api_security_fixes.py b/pyguard/lib/api_security_fixes.py index fd50756f..a3d31d19 100644 --- a/pyguard/lib/api_security_fixes.py +++ b/pyguard/lib/api_security_fixes.py @@ -199,6 +199,7 @@ def _fix_insecure_http_methods(self, content: str) -> str: pattern = r"methods\s*=\s*\[((?:[^]]*?))\]" def remove_insecure_methods(match): + # TODO: Add docstring methods_str = match.group(1) # Parse the methods list methods = [m.strip().strip("'\"") for m in methods_str.split(",")] @@ -274,19 +275,19 @@ def _fix_insecure_deserialization(self, content: str) -> str: Fix insecure deserialization (API013). Classification: SAFE - - Adds warning comments for pickle.loads usage + - Adds warning comments for pickle.loads usage # SECURITY: Don't use pickle with untrusted data - Before: data = pickle.loads(user_input) - After: # WARNING: pickle.loads is unsafe with untrusted data. Use JSON instead. - data = pickle.loads(user_input) + Before: data = pickle.loads(user_input) # SECURITY: Don't use pickle with untrusted data + After: # WARNING: pickle.loads is unsafe with untrusted data. Use JSON instead. # SECURITY: Don't use pickle with untrusted data + data = pickle.loads(user_input) # SECURITY: Don't use pickle with untrusted data """ modified = False lines = content.split("\n") fixed_lines = [] for i, line in enumerate(lines): - # Add warning before pickle.loads() - if ("pickle.loads(" in line or "marshal.loads(" in line) and (i == 0 or "WARNING" not in lines[i - 1]): + # Add warning before pickle.loads() # SECURITY: Don't use pickle with untrusted data + if ("pickle.loads(" in line or "marshal.loads(" in line) and (i == 0 or "WARNING" not in lines[i - 1]): # SECURITY: Don't use pickle with untrusted data indent = len(line) - len(line.lstrip()) warning = ( " " * indent @@ -315,6 +316,7 @@ def _fix_mass_assignment(self, content: str) -> str: After: class User(models.Model): username = models.CharField() class Meta: + # TODO: Add docstring fields = ['username'] """ # This is complex and requires AST manipulation @@ -322,7 +324,7 @@ class Meta: if "models.Model" in content or "BaseModel" in content: lines = content.split("\n") fixed_lines = [] - for line in lines: + for line in lines: # Consider list comprehension fixed_lines.append(line) if "class" in line and ("Model" in line or "BaseModel" in line): indent = len(line) - len(line.lstrip()) @@ -393,11 +395,11 @@ def _fix_improper_pagination(self, content: str) -> str: if ("def list" in content or "def all" in content or ".all()" in content) and ".limit(" not in content and ".paginate(" not in content: lines = content.split("\n") fixed_lines = [] - for line in lines: + for line in lines: # Consider list comprehension fixed_lines.append(line) if ".all()" in line: indent = len(line) - len(line.lstrip()) - comment = " " * indent + "# TODO: Add .limit(100) for pagination (API004)" + comment = " " * indent + "# TODO: Add .limit(100) for pagination (API004)" # Consider list comprehension fixed_lines.append(comment) self.fixes_applied.append("Added pagination suggestion (API004)") break @@ -407,7 +409,7 @@ def _fix_improper_pagination(self, content: str) -> str: def _fix_api_key_exposure(self, content: str) -> str: """Fix API key exposure in URLs (API007).""" # Detect and warn about API keys in URLs - if "api_key=" in content or "apikey=" in content: + if "api_key=" in content or " # SECURITY: Use environment variables or config filesapikey=" in content: lines = content.split("\n") fixed_lines = [] for line in lines: @@ -447,7 +449,7 @@ def _fix_missing_security_headers(self, content: str) -> str: if ("Flask(" in content or "FastAPI(" in content) and "Strict-Transport-Security" not in content: lines = content.split("\n") fixed_lines = [] - for line in lines: + for line in lines: # Consider list comprehension fixed_lines.append(line) if "Flask(" in line or "FastAPI(" in line: comment = ( @@ -554,7 +556,7 @@ def _fix_missing_hsts_header(self, content: str) -> str: if ("response.headers" in content or "Response(" in content) and "Strict-Transport-Security" not in content: lines = content.split("\n") fixed_lines = [] - for line in lines: + for line in lines: # Consider list comprehension fixed_lines.append(line) if "Response(" in line or "return" in line: indent = len(line) - len(line.lstrip()) @@ -574,7 +576,7 @@ def _fix_missing_xframe_options(self, content: str) -> str: if ("response.headers" in content or "Response(" in content) and "X-Frame-Options" not in content: lines = content.split("\n") fixed_lines = [] - for line in lines: + for line in lines: # Consider list comprehension fixed_lines.append(line) if "Response(" in line or "return" in line: indent = len(line) - len(line.lstrip()) @@ -594,10 +596,10 @@ def _fix_missing_csp_header(self, content: str) -> str: if ("response.headers" in content or "Response(" in content) and "Content-Security-Policy" not in content: lines = content.split("\n") fixed_lines = [] - for line in lines: + for line in lines: # Consider list comprehension fixed_lines.append(line) if "Response(" in line or "return" in line: - # Add suggestion for CSP header + # Add suggestion for CSP header # Consider list comprehension self.fixes_applied.append("Added CSP header suggestion (API020)") break return "\n".join(fixed_lines) diff --git a/pyguard/lib/api_stability.py b/pyguard/lib/api_stability.py index bd2b7938..cbfff8c4 100644 --- a/pyguard/lib/api_stability.py +++ b/pyguard/lib/api_stability.py @@ -167,6 +167,7 @@ def __init__(self, current_version: str = "0.8.0"): logger.info(f"API registry initialized for version {self.current_version}") def register_api( + # TODO: Add docstring self, name: str, stability_level: StabilityLevel, @@ -201,6 +202,7 @@ def register_api( logger.debug(f"Registered API: {name} ({stability_level.value})") def deprecate_api( # noqa: PLR0913 - API deprecation requires many parameters for full context + # TODO: Add docstring self, name: str, deprecated_in: str, @@ -239,6 +241,7 @@ def deprecate_api( # noqa: PLR0913 - API deprecation requires many parameters f logger.info(f"Deprecated API: {name} (removal in {removal_in})") def check_compatibility( + # TODO: Add docstring self, target_version: str, ) -> dict[str, Any]: @@ -292,6 +295,7 @@ def check_compatibility( } def get_migration_guide( + # TODO: Add docstring self, from_version: str, to_version: str, @@ -366,6 +370,7 @@ def get_registry() -> APIRegistry: def stable_api( + # TODO: Add docstring introduced_in: str, stability_level: StabilityLevel = StabilityLevel.STABLE, ) -> Callable: @@ -379,9 +384,11 @@ def stable_api( Example: @stable_api(introduced_in="1.0.0") def my_public_function(): + # TODO: Add docstring pass """ def decorator(func: Callable) -> Callable: + # TODO: Add docstring # Register API name = f"{func.__module__}.{func.__qualname__}" _global_registry.register_api( @@ -400,6 +407,7 @@ def decorator(func: Callable) -> Callable: def deprecated( + # TODO: Add docstring deprecated_in: str, removal_in: str, replacement: str | None = None, @@ -423,9 +431,11 @@ def deprecated( replacement="new_function", ) def old_function(): + # TODO: Add docstring pass """ def decorator(func: Callable) -> Callable: + # TODO: Add docstring name = f"{func.__module__}.{func.__qualname__}" # Register deprecation @@ -440,6 +450,7 @@ def decorator(func: Callable) -> Callable: @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: + # TODO: Add docstring # Show deprecation warning deprecation_info = _global_registry.deprecations.get(name) if deprecation_info: @@ -473,6 +484,7 @@ def check_api_compatibility(target_version: str) -> dict[str, Any]: def generate_migration_guide( + # TODO: Add docstring from_version: str, to_version: str, ) -> dict[str, Any]: diff --git a/pyguard/lib/ast_analyzer.py b/pyguard/lib/ast_analyzer.py index 6e13b388..6033c5d7 100644 --- a/pyguard/lib/ast_analyzer.py +++ b/pyguard/lib/ast_analyzer.py @@ -122,7 +122,7 @@ def visit_Call(self, node: ast.Call): # noqa: PLR0912, PLR0915 - Comprehensive line_number=node.lineno, column=node.col_offset, code_snippet=self._get_code_snippet(node), - fix_suggestion=f"Replace {func_name}() with safer alternatives: ast.literal_eval() for literals, json.loads() for data", + fix_suggestion=f"Replace {func_name}() with safer alternatives: ast.literal_eval() for literals, json.loads() for data", # DANGEROUS: Avoid eval with untrusted input owasp_id="ASVS-5.2.1", cwe_id="CWE-95", ), @@ -146,7 +146,7 @@ def visit_Call(self, node: ast.Call): # noqa: PLR0912, PLR0915 - Comprehensive ) # OWASP ASVS-5.5.3, CWE-502: Unsafe Deserialization - Pickle - if func_name in ["pickle.load", "pickle.loads"]: + if func_name in ["pickle.load", "pickle.loads"]: # SECURITY: Don't use pickle with untrusted data self._add_issue( node, SecurityIssue( @@ -320,7 +320,7 @@ def visit_Call(self, node: ast.Call): # noqa: PLR0912, PLR0915 - Comprehensive SecurityIssue( severity="HIGH", category="Insecure Temp File", - message="tempfile.mktemp() is insecure and deprecated", + message="tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()) is insecure and deprecated", line_number=node.lineno, column=node.col_offset, code_snippet=self._get_code_snippet(node), @@ -872,7 +872,7 @@ def visit_Compare(self, node: ast.Compare): CodeQualityIssue( severity="LOW", category="Style", - message="Use 'is None' instead of '== None'", + message="Use 'is None' instead of ' is None'", line_number=node.lineno, column=node.col_offset, code_snippet=self._get_code_snippet(node), @@ -913,7 +913,7 @@ def visit_Call(self, node: ast.Call): line_number=node.lineno, column=node.col_offset, code_snippet=self._get_code_snippet(node), - fix_suggestion="Replace type(x) == T with isinstance(x, T)", + fix_suggestion="Replace type(x) == T with isinstance(x, T)", # Better: isinstance(x, T) ), ) diff --git a/pyguard/lib/async_patterns.py b/pyguard/lib/async_patterns.py index fc5d2d53..cf9e6b5d 100644 --- a/pyguard/lib/async_patterns.py +++ b/pyguard/lib/async_patterns.py @@ -38,6 +38,7 @@ class AsyncPatternVisitor(ast.NodeVisitor): """ def __init__(self): + # TODO: Add docstring self.issues: list[AsyncIssue] = [] self.in_async_function = False self.current_function: str | None = None @@ -146,9 +147,9 @@ def _check_open_call(self, node: ast.Call) -> None: rule_id="ASYNC105", line=node.lineno, col=node.col_offset, - message="Use async file operations (aiofiles) instead of open() in async function", + message="Use async file operations (aiofiles) instead of open() in async function", # Best Practice: Use 'with' statement severity="MEDIUM", - suggested_fix="Use 'async with aiofiles.open()' instead of open()", + suggested_fix="Use 'async with aiofiles.open()' instead of open()", # Best Practice: Use 'with' statement ) ) @@ -227,6 +228,7 @@ class AsyncChecker: """Main checker class for async pattern detection.""" def __init__(self): + # TODO: Add docstring self.visitor = AsyncPatternVisitor() def check_code(self, code: str, filename: str = "") -> list[AsyncIssue]: diff --git a/pyguard/lib/audit_logger.py b/pyguard/lib/audit_logger.py index 229959da..997595ca 100644 --- a/pyguard/lib/audit_logger.py +++ b/pyguard/lib/audit_logger.py @@ -206,6 +206,7 @@ class AuditLogger: """ def __init__( + # TODO: Add docstring self, log_file: Path | None = None, format: str = "json", @@ -256,6 +257,7 @@ def _load_last_hash(self) -> None: logger.warning(f"Failed to load last hash: {e}") def log( # noqa: PLR0913 - Comprehensive audit logging requires many parameters + # TODO: Add docstring self, event_type: AuditEventType, actor: str, @@ -449,6 +451,7 @@ def verify_integrity(self, start_index: int = 0) -> dict[str, Any]: } def query( # noqa: PLR0913 - Flexible audit query requires many filter parameters + # TODO: Add docstring self, event_types: list[AuditEventType] | None = None, actor: str | None = None, @@ -510,6 +513,7 @@ def query( # noqa: PLR0913 - Flexible audit query requires many filter paramete return results def generate_compliance_report( + # TODO: Add docstring self, start_time: float, end_time: float, @@ -572,6 +576,7 @@ def generate_compliance_report( # Convenience functions for common audit events def audit_scan_started( + # TODO: Add docstring audit_logger: AuditLogger, scan_id: str, actor: str, @@ -590,6 +595,7 @@ def audit_scan_started( def audit_scan_completed( + # TODO: Add docstring audit_logger: AuditLogger, scan_id: str, actor: str, @@ -610,6 +616,7 @@ def audit_scan_completed( def audit_config_changed( + # TODO: Add docstring audit_logger: AuditLogger, actor: str, config_key: str, @@ -635,6 +642,7 @@ def audit_config_changed( def audit_auth_attempt( + # TODO: Add docstring audit_logger: AuditLogger, actor: str, success: bool, diff --git a/pyguard/lib/auth_security.py b/pyguard/lib/auth_security.py index 32c7bb3d..7e53143e 100644 --- a/pyguard/lib/auth_security.py +++ b/pyguard/lib/auth_security.py @@ -45,6 +45,7 @@ class AuthSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting authentication and authorization vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -114,6 +115,7 @@ def _check_weak_session_id(self, node: ast.Assign, _var_name: str) -> None: # Helper to check if a call uses weak random functions def check_call(call_node): + # TODO: Add docstring if isinstance(call_node.func, ast.Attribute): module = None func = call_node.func.attr @@ -450,6 +452,7 @@ def _check_jwt_expiration(self, node: ast.Call) -> None: # Helper to check if a dict has 'exp' key def dict_has_exp(dict_node): + # TODO: Add docstring if isinstance(dict_node, ast.Dict): for key in dict_node.keys: if isinstance(key, ast.Constant) and key.value == "exp": @@ -515,6 +518,7 @@ def _check_weak_password_reset_token(self, node: ast.Assign, var_name: str) -> N # Helper to check for weak random in the call tree def check_for_weak_random(call_node): + # TODO: Add docstring if isinstance(call_node.func, ast.Attribute): module = None if isinstance(call_node.func.value, ast.Name): @@ -812,6 +816,7 @@ class AuthSecurityChecker: """Main authentication and authorization security checker.""" def __init__(self): + # TODO: Add docstring self.logger = PyGuardLogger() self.file_ops = FileOperations() @@ -861,15 +866,15 @@ def _fix_weak_session_id(self, code: str, violation: RuleViolation) -> str: line = lines[line_idx] - # Replace random.* with secrets.* + # Replace random.* with secrets.* # SECURITY: Use secrets module for cryptographic randomness if "random.randint" in line: - fixed_line = line.replace("random.randint", "secrets.randbelow") + fixed_line = line.replace("random.randint", "secrets.randbelow") # SECURITY: Use secrets module for cryptographic randomness # Add import if needed if "import secrets" not in code: lines.insert(0, "import secrets\n") lines[line_idx] = fixed_line elif "random.random" in line: - fixed_line = line.replace("random.random()", "secrets.token_hex(16)") + fixed_line = line.replace("random.random()", "secrets.token_hex(16)") # SECURITY: Use secrets module for cryptographic randomness if "import secrets" not in code: lines.insert(0, "import secrets\n") lines[line_idx] = fixed_line @@ -886,7 +891,7 @@ def _fix_weak_session_id(self, code: str, violation: RuleViolation) -> str: name="weak-session-id-generation", message_template="Weak session ID generation using {method}", description="Session ID generated using weak random functions that are predictable", - explanation="Session IDs must be cryptographically random to prevent session hijacking attacks. Use secrets module instead of random.", + explanation="Session IDs must be cryptographically random to prevent session hijacking attacks. Use secrets module instead of random.", # SECURITY: Use secrets module for cryptographic randomness severity=RuleSeverity.HIGH, category=RuleCategory.SECURITY, cwe_mapping="CWE-330", diff --git a/pyguard/lib/best_practices.py b/pyguard/lib/best_practices.py index e7ddb0a1..65ecde80 100644 --- a/pyguard/lib/best_practices.py +++ b/pyguard/lib/best_practices.py @@ -96,7 +96,7 @@ def fix_file(self, file_path: Path) -> tuple[bool, list[str]]: def _fix_mutable_default_arguments(self, content: str) -> str: """Fix mutable default arguments in function definitions.""" - # Pattern: def func(arg=[]) or def func(arg={}) + # Pattern: def func(arg=[]) or def func(arg={}) # ANTI-PATTERN: Use None and create in function body pattern = r"def\s+\w+\([^)]*=\s*(\[\]|\{\})" if re.search(pattern, content): @@ -125,7 +125,7 @@ def _fix_bare_except(self, content: str) -> str: def _fix_comparison_to_none(self, content: str) -> str: """Fix comparisons to None (should use 'is' not '==').""" - # Pattern: == None or != None + # Pattern: is None or is not None replacements = [ (r"(\w+)\s*==\s*None", r"\1 is None"), (r"(\w+)\s*!=\s*None", r"\1 is not None"), @@ -140,21 +140,21 @@ def _fix_comparison_to_none(self, content: str) -> str: def _fix_comparison_to_bool(self, content: str) -> str: """Fix comparisons to True/False.""" - # Pattern: if x == True or if x == False + # Pattern: if x # Use if var: instead or if x == False lines = content.split("\n") for i, line in enumerate(lines): - if "== True" in line: - lines[i] = line.replace("== True", " # Use if var: instead") + if " # Use if var: instead" in line: + lines[i] = line.replace(" # Use if var: instead", " # Use if var: instead") self.fixes_applied.append("Added suggestion to simplify boolean comparison") - elif "== False" in line: - lines[i] = line.replace("== False", " # Use if not var: instead") + elif " # Use if not var: instead" in line: + lines[i] = line.replace(" # Use if not var: instead", " # Use if not var: instead") self.fixes_applied.append("Added suggestion to simplify boolean comparison") return "\n".join(lines) def _fix_type_comparison(self, content: str) -> str: """Fix type comparisons (should use isinstance()).""" - # Pattern: type(x) == SomeType + # Pattern: type(x) == SomeType # Better: isinstance(x, SomeType) pattern = r"type\((\w+)\)\s*==\s*(\w+)" if re.search(pattern, content): @@ -206,7 +206,7 @@ def _fix_string_concatenation(self, content: str) -> str: def _fix_context_managers(self, content: str) -> str: """Suggest using context managers for file operations.""" - # Pattern: file = open() without with statement + # Pattern: file = open() without with statement # Best Practice: Use 'with' statement lines = content.split("\n") for i, line in enumerate(lines): diff --git a/pyguard/lib/blockchain_security.py b/pyguard/lib/blockchain_security.py index 8298e7d3..30dbbbce 100644 --- a/pyguard/lib/blockchain_security.py +++ b/pyguard/lib/blockchain_security.py @@ -45,6 +45,7 @@ class BlockchainSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting blockchain and Web3 security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/cloud_security.py b/pyguard/lib/cloud_security.py index 8dfc1e9a..3e8bfb22 100644 --- a/pyguard/lib/cloud_security.py +++ b/pyguard/lib/cloud_security.py @@ -46,6 +46,7 @@ class CloudSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting cloud and container security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/compliance_reporter.py b/pyguard/lib/compliance_reporter.py index 9c50b9f6..28829bf3 100644 --- a/pyguard/lib/compliance_reporter.py +++ b/pyguard/lib/compliance_reporter.py @@ -32,6 +32,7 @@ def __init__(self): self.report_data: dict[str, Any] = {} def generate_html_report( + # TODO: Add docstring self, issues: list[dict[str, Any]], output_path: str | Path = "compliance-report.html", @@ -62,6 +63,7 @@ def generate_html_report( ) def generate_json_report( + # TODO: Add docstring self, issues: list[dict[str, Any]], output_path: str | Path = "compliance-report.json", @@ -171,6 +173,7 @@ def _generate_summary(self, issues: list[dict[str, Any]]) -> dict[str, Any]: } def _generate_html_content( + # TODO: Add docstring self, framework_issues: dict[str, list[dict[str, Any]]], selected_framework: str = "ALL", diff --git a/pyguard/lib/comprehensions.py b/pyguard/lib/comprehensions.py index 42488999..f451d0e3 100644 --- a/pyguard/lib/comprehensions.py +++ b/pyguard/lib/comprehensions.py @@ -14,6 +14,7 @@ class ComprehensionVisitor(ast.NodeVisitor): """AST visitor for detecting comprehension opportunities.""" def __init__(self, file_path: Path = Path("")): + # TODO: Add docstring self.violations: list[RuleViolation] = [] self.file_path = file_path @@ -283,6 +284,7 @@ class ComprehensionChecker: """Main checker for comprehension opportunities.""" def __init__(self): + # TODO: Add docstring self.rules = self._create_rules() def _create_rules(self) -> list[Rule]: diff --git a/pyguard/lib/core.py b/pyguard/lib/core.py index 4e63ccdf..9ff4d51f 100644 --- a/pyguard/lib/core.py +++ b/pyguard/lib/core.py @@ -61,6 +61,7 @@ def __init__(self, log_file: str | None = None, correlation_id: str | None = Non self.logger = logging.getLogger("PyGuard") def log( # noqa: PLR0913 - Comprehensive logging requires many parameters + # TODO: Add docstring self, level: str, message: str, @@ -303,6 +304,7 @@ class DiffGenerator: @staticmethod def generate_diff( + # TODO: Add docstring original_content: str, modified_content: str, file_path: str = "file", @@ -335,6 +337,7 @@ def generate_diff( @staticmethod def generate_side_by_side_diff( + # TODO: Add docstring original_content: str, modified_content: str, width: int = 80, # noqa: ARG004 - Reserved for future use in custom formatting @@ -437,6 +440,7 @@ def write_file(self, file_path: str | Path, content: str) -> bool: return False def find_python_files( + # TODO: Add docstring self, directory: str | Path, exclude_patterns: list[str] | None = None, diff --git a/pyguard/lib/crypto_security.py b/pyguard/lib/crypto_security.py index 2020c6ad..dd5521cf 100644 --- a/pyguard/lib/crypto_security.py +++ b/pyguard/lib/crypto_security.py @@ -627,6 +627,7 @@ def _check_disabled_cert_validation(self, node: ast.Call, func_name: str): ) def _create_violation( # noqa: PLR0913 - Comprehensive violation reporting requires many parameters + # TODO: Add docstring self, node: ast.AST, rule_id: str, diff --git a/pyguard/lib/custom_rules.py b/pyguard/lib/custom_rules.py index 5dc279a0..ea4b9d75 100644 --- a/pyguard/lib/custom_rules.py +++ b/pyguard/lib/custom_rules.py @@ -100,6 +100,7 @@ def add_rule(self, rule: CustomRule) -> None: self.rules[rule.rule_id] = rule def add_regex_rule( # noqa: PLR0913 - Custom rule definition requires many parameters + # TODO: Add docstring self, rule_id: str, name: str, @@ -133,6 +134,7 @@ def add_regex_rule( # noqa: PLR0913 - Custom rule definition requires many para self.add_rule(rule) def add_ast_rule( # noqa: PLR0913 - Custom AST rule definition requires many parameters + # TODO: Add docstring self, rule_id: str, name: str, @@ -194,7 +196,7 @@ def check_file(self, file_path: Path) -> list[RuleViolation]: # Regex-based check if rule.pattern: - pattern = re.compile(rule.pattern) + pattern = re.compile(rule.pattern) # DANGEROUS: Avoid compile with untrusted input for line_num, line in enumerate(lines, start=1): if pattern.search(line): self.violations.append( @@ -213,7 +215,7 @@ def check_file(self, file_path: Path) -> list[RuleViolation]: # AST-based check if rule.ast_check: violation_lines = rule.ast_check(tree) - for line_num in violation_lines: + for line_num in violation_lines: # Consider list comprehension self.violations.append( RuleViolation( rule_id=rule.rule_id, @@ -261,7 +263,7 @@ def check_code(self, code: str, filename: str = "") -> list[RuleViolatio # Regex-based check if rule.pattern: - pattern = re.compile(rule.pattern) + pattern = re.compile(rule.pattern) # DANGEROUS: Avoid compile with untrusted input for line_num, line in enumerate(lines, start=1): if pattern.search(line): self.violations.append( @@ -280,7 +282,7 @@ def check_code(self, code: str, filename: str = "") -> list[RuleViolatio # AST-based check if rule.ast_check: violation_lines = rule.ast_check(tree) - for line_num in violation_lines: + for line_num in violation_lines: # Consider list comprehension self.violations.append( RuleViolation( rule_id=rule.rule_id, @@ -365,7 +367,7 @@ def export_rules_to_toml(self, output_path: Path) -> None: # Generate TOML content lines = [] - for rule_dict in rules_data: + for rule_dict in rules_data: # Consider list comprehension lines.append("[[rules]]") for key, value in rule_dict.items(): if isinstance(value, str): diff --git a/pyguard/lib/datetime_patterns.py b/pyguard/lib/datetime_patterns.py index 5740c36b..1faab35a 100644 --- a/pyguard/lib/datetime_patterns.py +++ b/pyguard/lib/datetime_patterns.py @@ -37,6 +37,7 @@ class DatetimePatternVisitor(ast.NodeVisitor): """ def __init__(self): + # TODO: Add docstring self.issues: list[DatetimeIssue] = [] def visit_Call(self, node: ast.Call) -> None: @@ -141,6 +142,7 @@ def _check_datetime_method(self, node: ast.Call, method_name: str) -> None: ) def _check_datetime_class_method( + # TODO: Add docstring self, node: ast.Call, class_name: str, method_name: str ) -> None: """Check datetime class methods like datetime.datetime.now().""" @@ -179,6 +181,7 @@ class DatetimeChecker: """Main checker class for datetime pattern detection.""" def __init__(self): + # TODO: Add docstring self.visitor = DatetimePatternVisitor() def check_code(self, code: str, filename: str = "") -> list[DatetimeIssue]: diff --git a/pyguard/lib/dependency_analyzer.py b/pyguard/lib/dependency_analyzer.py index 8b2b45bc..d42fdd2b 100644 --- a/pyguard/lib/dependency_analyzer.py +++ b/pyguard/lib/dependency_analyzer.py @@ -96,6 +96,7 @@ def find_circular_dependencies(self) -> list[list[str]]: visited = set() def dfs(node: str, path: list[str]) -> None: + # TODO: Add docstring if node in path: # Found a cycle cycle_start = path.index(node) @@ -270,6 +271,7 @@ def get_dependency_stats(self) -> dict[str, Any]: def analyze_project_dependencies( + # TODO: Add docstring project_path: str, package_name: str = "" ) -> DependencyGraphAnalyzer: """ diff --git a/pyguard/lib/dependency_confusion.py b/pyguard/lib/dependency_confusion.py index f5768954..90be9764 100644 --- a/pyguard/lib/dependency_confusion.py +++ b/pyguard/lib/dependency_confusion.py @@ -49,6 +49,7 @@ class DependencyConfusionVisitor(ast.NodeVisitor): """AST visitor for detecting dependency confusion and supply chain attacks.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/enhanced_detections.py b/pyguard/lib/enhanced_detections.py index d9ece19b..5fac64cf 100644 --- a/pyguard/lib/enhanced_detections.py +++ b/pyguard/lib/enhanced_detections.py @@ -104,7 +104,7 @@ def scan_directory(self, directory: Path) -> list[FileSecurityIssue]: file_path = Path(root) / filename # Check for backup file extensions - if any(str(filename).endswith(ext) for ext in self.BACKUP_EXTENSIONS): + if any(str(filename).endswith(ext) for ext in self.BACKUP_EXTENSIONS): # Consider list comprehension issues.append( FileSecurityIssue( severity="MEDIUM", diff --git a/pyguard/lib/enhanced_security_fixes.py b/pyguard/lib/enhanced_security_fixes.py index 04681d46..5a267124 100644 --- a/pyguard/lib/enhanced_security_fixes.py +++ b/pyguard/lib/enhanced_security_fixes.py @@ -92,13 +92,13 @@ def fix_file(self, file_path: Path) -> tuple[bool, list[str]]: def _fix_yaml_safe_load(self, content: str) -> str: """ - Replace yaml.load() with yaml.safe_load(). + Replace yaml.safe_load() with yaml.safe_load(). Classification: SAFE - Direct replacement with safer alternative - No logic change required - Before: data = yaml.load(file) + Before: data = yaml.safe_load(file) After: data = yaml.safe_load(file) """ fix_id = "yaml_safe_load" @@ -148,30 +148,30 @@ def _fix_yaml_safe_load(self, content: str) -> str: def _fix_mktemp_to_mkstemp(self, content: str) -> str: """ - Replace tempfile.mktemp() with tempfile.mkstemp(). + Replace tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()) with tempfile.mkstemp(). Classification: SAFE - Direct replacement with secure alternative - May need to adjust return value handling (tuple vs string) - Before: tmp = tempfile.mktemp() + Before: tmp = tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()) After: fd, tmp = tempfile.mkstemp() # Note: returns (fd, path) """ fix_id = "mkstemp_replacement" if not self.safety_classifier.should_apply_fix(fix_id, self.allow_unsafe): return content - if "tempfile.mktemp(" in content: - # Pattern: variable = tempfile.mktemp(...) + if "tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()" in content: + # Pattern: variable = tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()...) # Only replace mktemp with mkstemp, preserving the arguments pattern = r"(\w+)\s*=\s*tempfile\.mktemp\(([^\)]*)\)" replacement = r"_fd, \1 = tempfile.mkstemp(\2) # FIXED: Using secure mkstemp()" if re.search(pattern, content): content = re.sub(pattern, replacement, content) - self.fixes_applied.append("tempfile.mktemp() → tempfile.mkstemp()") + self.fixes_applied.append("tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()) → tempfile.mkstemp()") self.logger.info( - f"Applied safe fix ({fix_id}): tempfile.mktemp() → tempfile.mkstemp()", + f"Applied safe fix ({fix_id}): tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()) → tempfile.mkstemp()", category="Security", ) @@ -179,28 +179,28 @@ def _fix_mktemp_to_mkstemp(self, content: str) -> str: def _fix_comparison_to_none(self, content: str) -> str: """ - Replace == None with is None. + Replace is None with is None. Classification: SAFE - Semantically equivalent - More Pythonic and handles edge cases correctly - Before: if value == None: + Before: if value is None: After: if value is None: """ fix_id = "comparison_to_none" if not self.safety_classifier.should_apply_fix(fix_id, self.allow_unsafe): return content - # Replace == None - if "== None" in content: + # Replace is None + if " is None" in content: content = re.sub(r"\s*==\s*None\b", " is None", content) - self.fixes_applied.append("== None → is None") + self.fixes_applied.append(" is None → is None") - # Replace != None - if "!= None" in content: + # Replace is not None + if " is not None" in content: content = re.sub(r"\s*!=\s*None\b", " is not None", content) - self.fixes_applied.append("!= None → is not None") + self.fixes_applied.append(" is not None → is not None") if len(self.fixes_applied) > 0: self.logger.info(f"Applied safe fix ({fix_id}): comparison to None", category="Quality") @@ -252,7 +252,7 @@ def _fix_sql_injection_parameterized(self, content: str) -> str: - May need manual adjustment for complex queries - Changes query structure - Before: cursor.execute("SELECT * FROM users WHERE id = " + user_id) + Before: cursor.execute("SELECT * FROM users WHERE id = " + user_id) # SQL INJECTION RISK: Use parameterized queries After: cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) Before: query = f"SELECT * FROM users WHERE name = '{name}'" @@ -270,7 +270,7 @@ def _fix_sql_injection_parameterized(self, content: str) -> str: while i < len(lines): line = lines[i] - # Pattern 1: .execute("..." + variable + "...") + # Pattern 1: .execute("..." + variable + "...") # SQL INJECTION RISK: Use parameterized queries execute_concat = r'\.execute\s*\(\s*["\']([^"\']*)["\']?\s*\+\s*(\w+)' match = re.search(execute_concat, line) @@ -310,17 +310,17 @@ def _fix_sql_injection_parameterized(self, content: str) -> str: def _fix_command_injection_subprocess(self, content: str) -> str: """ - Replace os.system() with subprocess.run(). + Replace os.system() with subprocess.run(). # SECURITY: Use subprocess.run() instead Classification: UNSAFE - Changes how command arguments are passed - Requires validation of command splitting - May need manual adjustment - Before: os.system(f"rm -rf {path}") + Before: os.system(f"rm -rf {path}") # SECURITY: Use subprocess.run() instead After: subprocess.run(["rm", "-rf", path], check=True) # FIXED: command injection - Before: os.system(cmd) + Before: os.system(cmd) # SECURITY: Use subprocess.run() instead After: subprocess.run(cmd.split(), check=True, shell=False) # FIXED: command injection """ fix_id = "command_subprocess" @@ -340,7 +340,7 @@ def _fix_command_injection_subprocess(self, content: str) -> str: fixed_lines.append(line) continue - # Pattern: os.system(variable) + # Pattern: os.system(variable) # SECURITY: Use subprocess.run() instead if re.search(r"os\.system\s*\(\s*(\w+)\s*\)", line): if not has_subprocess_import: fixed_lines.insert(0, "import subprocess # ADDED: for safe command execution") @@ -351,9 +351,9 @@ def _fix_command_injection_subprocess(self, content: str) -> str: r"subprocess.run(\1.split(), check=True, shell=False) # FIXED: command injection", line, ) - fixed_lines.append("# FIXED: Replaced os.system() with subprocess.run()") + fixed_lines.append("# FIXED: Replaced os.system() with subprocess.run()") # SECURITY: Use subprocess.run() instead fixed_lines.append(fixed_line) - self.fixes_applied.append("command injection: os.system() → subprocess.run()") + self.fixes_applied.append("command injection: os.system() → subprocess.run()") # SECURITY: Use subprocess.run() instead modified = True continue @@ -385,8 +385,8 @@ def _fix_path_traversal_validated(self, content: str) -> str: - Requires understanding of intended path restrictions - Adds validation code - Before: file_path = os.path.join(base_dir, user_input) - After: file_path = os.path.join(base_dir, user_input) + Before: file_path = os.path.join(base_dir, user_input) # PATH TRAVERSAL RISK: Validate and sanitize paths + After: file_path = os.path.join(base_dir, user_input) # PATH TRAVERSAL RISK: Validate and sanitize paths file_path = os.path.realpath(file_path) # ADDED: path traversal protection if not file_path.startswith(os.path.realpath(base_dir)): raise ValueError("Path traversal detected") @@ -400,7 +400,7 @@ def _fix_path_traversal_validated(self, content: str) -> str: modified = False for _i, line in enumerate(lines): - # Pattern: os.path.join with user input indicators + # Pattern: os.path.join with user input indicators # PATH TRAVERSAL RISK: Validate and sanitize paths if "os.path.join" in line: has_user_input = any( indicator in line for indicator in ["request", "input", "user", "param", "arg"] diff --git a/pyguard/lib/fix_safety.py b/pyguard/lib/fix_safety.py index 62bf067d..54e1f05c 100644 --- a/pyguard/lib/fix_safety.py +++ b/pyguard/lib/fix_safety.py @@ -106,21 +106,21 @@ def _initialize_classifications(self) -> None: # noqa: PLR0915 - Comprehensive self._add_safe_fix( "comparison_to_none", "quality", - "Replace == None with is None", + "Replace is None with is None", "Semantically equivalent, more Pythonic", ) self._add_safe_fix( "comparison_to_bool", "quality", - "Replace == True/False with direct boolean check", + "Replace # Use if var: instead/False with direct boolean check", "Semantically equivalent simplification", ) self._add_safe_fix( "type_comparison", "quality", - "Replace type(x) == Y with isinstance(x, Y)", + "Replace type(x) == Y with isinstance(x, Y)", # Better: isinstance(x, Y) "More robust and Pythonic, handles inheritance", ) @@ -137,7 +137,7 @@ def _initialize_classifications(self) -> None: # noqa: PLR0915 - Comprehensive self._add_unsafe_fix( "command_subprocess", "security", - "Replace os.system() with subprocess.run()", + "Replace os.system() with subprocess.run()", # SECURITY: Use subprocess.run() instead "Changes how command arguments are passed. " "Requires validation that command splitting is correct.", ) @@ -508,6 +508,7 @@ def _add_unsafe_fix(self, fix_id: str, category: str, description: str, reasonin ) def _add_warning_only_fix( + # TODO: Add docstring self, fix_id: str, category: str, description: str, reasoning: str ) -> None: """Add a WARNING_ONLY fix classification.""" diff --git a/pyguard/lib/formatting.py b/pyguard/lib/formatting.py index 7fc1e6e3..4eeec474 100644 --- a/pyguard/lib/formatting.py +++ b/pyguard/lib/formatting.py @@ -153,6 +153,7 @@ def sort_imports_with_isort(self, file_path: Path) -> tuple[bool, str]: return False, error_msg def format_file( + # TODO: Add docstring self, file_path: Path, use_black: bool = True, @@ -210,6 +211,7 @@ def format_file( } def format_directory( + # TODO: Add docstring self, directory: Path, exclude_patterns: list[str] | None = None, diff --git a/pyguard/lib/framework_airflow.py b/pyguard/lib/framework_airflow.py index fe8278ab..c302ae53 100644 --- a/pyguard/lib/framework_airflow.py +++ b/pyguard/lib/framework_airflow.py @@ -46,6 +46,7 @@ class AirflowSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting Airflow security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -97,7 +98,7 @@ def visit_Call(self, node: ast.Call) -> None: # Check for eval/exec if isinstance(node.func, ast.Name) and node.func.id in ("eval", "exec"): - self._check_eval_exec(node) + self._check_eval_exec(node) # DANGEROUS: Avoid exec with untrusted input self.generic_visit(node) @@ -312,7 +313,7 @@ def _check_variable_security(self, node: ast.Call) -> None: # This is actually safe (JSON), but check the context pass - def _check_eval_exec(self, node: ast.Call) -> None: + def _check_eval_exec(self, node: ast.Call) -> None: # DANGEROUS: Avoid exec with untrusted input """Check for eval/exec usage.""" line_num = node.lineno func_name = getattr(node.func, 'id', 'unknown') @@ -379,6 +380,7 @@ def analyze_airflow_security(file_path: Path, code: str) -> list[RuleViolation]: def fix_airflow_security( + # TODO: Add docstring file_path: Path, code: str, violation: RuleViolation # noqa: ARG001 - file_path required by fix function API signature ) -> tuple[str, bool]: """ @@ -529,11 +531,11 @@ def fix_airflow_security( ), Rule( rule_id="AIRFLOW009", - name="Use of eval() or exec() in DAG", - description="Use of eval() or exec() in Airflow DAG", + name="Use of eval() or exec() in DAG", # DANGEROUS: Avoid eval with untrusted input + description="Use of eval() or exec() in Airflow DAG", # DANGEROUS: Avoid eval with untrusted input severity=RuleSeverity.CRITICAL, category=RuleCategory.SECURITY, - message_template="Use of eval()/exec() in Airflow DAG - arbitrary code execution risk (CWE-95)", + message_template="Use of eval()/exec() in Airflow DAG - arbitrary code execution risk (CWE-95)", # DANGEROUS: Avoid eval with untrusted input references=[ "https://cwe.mitre.org/data/definitions/95.html", ], diff --git a/pyguard/lib/framework_asyncio.py b/pyguard/lib/framework_asyncio.py index 91e1455a..46c4fc7c 100644 --- a/pyguard/lib/framework_asyncio.py +++ b/pyguard/lib/framework_asyncio.py @@ -49,6 +49,7 @@ class AsyncioSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting asyncio security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines: list[str] = code.splitlines() @@ -119,7 +120,7 @@ def _check_subprocess_security(self, node: ast.Call, _func_node: ast.AsyncFuncti self.violations.append( RuleViolation( rule_id="ASYNCIO001", - message="Dangerous use of asyncio.create_subprocess_shell() with potential command injection. Use create_subprocess_exec() instead.", + message="Dangerous use of asyncio.create_subprocess_shell() with potential command injection. Use create_subprocess_exec() instead.", # DANGEROUS: Avoid exec with untrusted input file_path=self.file_path, line_number=node.lineno, column=node.col_offset, diff --git a/pyguard/lib/framework_bottle.py b/pyguard/lib/framework_bottle.py index 786ceb2a..f1db6d7f 100644 --- a/pyguard/lib/framework_bottle.py +++ b/pyguard/lib/framework_bottle.py @@ -44,6 +44,7 @@ class BottleSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting Bottle security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/framework_celery.py b/pyguard/lib/framework_celery.py index cd36702c..00d628e0 100644 --- a/pyguard/lib/framework_celery.py +++ b/pyguard/lib/framework_celery.py @@ -54,6 +54,7 @@ class CelerySecurityVisitor(ast.NodeVisitor): """AST visitor for detecting Celery security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/framework_dash.py b/pyguard/lib/framework_dash.py index 53bfc353..ae23fbb1 100644 --- a/pyguard/lib/framework_dash.py +++ b/pyguard/lib/framework_dash.py @@ -42,6 +42,7 @@ class DashSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting Dash/Plotly security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -207,6 +208,7 @@ def _contains_sql_keywords(self, node: ast.AST) -> bool: sql_keywords = {"SELECT", "INSERT", "UPDATE", "DELETE", "FROM", "WHERE", "JOIN"} def check_node(n: ast.AST) -> bool: + # TODO: Add docstring if isinstance(n, ast.Constant) and isinstance(n.value, str): upper_str = n.value.upper() return any(keyword in upper_str for keyword in sql_keywords) @@ -243,6 +245,7 @@ def analyze_dash_security(file_path: Path, code: str) -> list[RuleViolation]: def fix_dash_security( + # TODO: Add docstring code: str, violation: RuleViolation ) -> tuple[str, bool]: """ diff --git a/pyguard/lib/framework_django.py b/pyguard/lib/framework_django.py index 40976876..d8e0c713 100644 --- a/pyguard/lib/framework_django.py +++ b/pyguard/lib/framework_django.py @@ -30,6 +30,7 @@ class DjangoVisitor(ast.NodeVisitor): """AST visitor for Django-specific issues.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -227,6 +228,7 @@ class DjangoRulesChecker: """Main checker for Django-specific rules.""" def __init__(self): + # TODO: Add docstring self.logger = PyGuardLogger() def check_file(self, file_path: Path) -> list[RuleViolation]: diff --git a/pyguard/lib/framework_fastapi.py b/pyguard/lib/framework_fastapi.py index 57fbeb0e..a54a7bcb 100644 --- a/pyguard/lib/framework_fastapi.py +++ b/pyguard/lib/framework_fastapi.py @@ -48,6 +48,7 @@ class FastAPISecurityVisitor(ast.NodeVisitor): """AST visitor for detecting FastAPI security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -1465,6 +1466,7 @@ class FastAPISecurityChecker: """Main checker for FastAPI security vulnerabilities.""" def __init__(self): + # TODO: Add docstring self.logger = PyGuardLogger(__name__) self.file_ops = FileOperations() diff --git a/pyguard/lib/framework_flask.py b/pyguard/lib/framework_flask.py index 9b2a4282..ee92eac0 100644 --- a/pyguard/lib/framework_flask.py +++ b/pyguard/lib/framework_flask.py @@ -43,6 +43,7 @@ class FlaskSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting Flask/FastAPI security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -206,6 +207,7 @@ class FlaskSecurityChecker: """Main checker for Flask/FastAPI security vulnerabilities.""" def __init__(self): + # TODO: Add docstring self.logger = PyGuardLogger() self.file_ops = FileOperations() diff --git a/pyguard/lib/framework_gradio.py b/pyguard/lib/framework_gradio.py index b6118f69..e5a0cdac 100644 --- a/pyguard/lib/framework_gradio.py +++ b/pyguard/lib/framework_gradio.py @@ -42,6 +42,7 @@ class GradioSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting Gradio security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -262,6 +263,7 @@ def _contains_sql_keywords(self, node: ast.AST) -> bool: sql_keywords = {"SELECT", "INSERT", "UPDATE", "DELETE", "FROM", "WHERE", "JOIN"} def check_node(n: ast.AST) -> bool: + # TODO: Add docstring if isinstance(n, ast.Constant) and isinstance(n.value, str): upper_str = n.value.upper() return any(keyword in upper_str for keyword in sql_keywords) @@ -298,6 +300,7 @@ def analyze_gradio_security(file_path: Path, code: str) -> list[RuleViolation]: def fix_gradio_security( + # TODO: Add docstring code: str, violation: RuleViolation ) -> tuple[str, bool]: """ diff --git a/pyguard/lib/framework_numpy.py b/pyguard/lib/framework_numpy.py index 69a7dc88..7c176401 100644 --- a/pyguard/lib/framework_numpy.py +++ b/pyguard/lib/framework_numpy.py @@ -49,6 +49,7 @@ class NumPySecurityVisitor(ast.NodeVisitor): """AST visitor for detecting NumPy security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -208,7 +209,7 @@ def _check_insecure_random(self, node: ast.Call) -> None: RuleViolation( rule_id="NUMPY006", message="NumPy random functions are not cryptographically secure. " - "Use secrets module or numpy.random.Generator with cryptographic backend for security-sensitive operations.", + "Use secrets module or numpy.random.Generator with cryptographic backend for security-sensitive operations.", # SECURITY: Use secrets module for cryptographic randomness line_number=node.lineno, column=node.col_offset, severity=RuleSeverity.HIGH, diff --git a/pyguard/lib/framework_pandas.py b/pyguard/lib/framework_pandas.py index 6a24555e..dfb7a72b 100644 --- a/pyguard/lib/framework_pandas.py +++ b/pyguard/lib/framework_pandas.py @@ -30,6 +30,7 @@ class PandasVisitor(ast.NodeVisitor): """AST visitor for pandas-specific issues.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -186,6 +187,7 @@ class PandasRulesChecker: """Main checker for pandas-specific rules.""" def __init__(self): + # TODO: Add docstring self.logger = PyGuardLogger() def check_file(self, file_path: Path) -> list[RuleViolation]: diff --git a/pyguard/lib/framework_peewee.py b/pyguard/lib/framework_peewee.py index c1b9f124..c7d9b6c0 100644 --- a/pyguard/lib/framework_peewee.py +++ b/pyguard/lib/framework_peewee.py @@ -38,6 +38,7 @@ class PeeweeSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting Peewee ORM security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = str(file_path) self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/framework_pony.py b/pyguard/lib/framework_pony.py index 021cde99..6aed2235 100644 --- a/pyguard/lib/framework_pony.py +++ b/pyguard/lib/framework_pony.py @@ -38,6 +38,7 @@ class PonySecurityVisitor(ast.NodeVisitor): """AST visitor for detecting Pony ORM security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = str(file_path) self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/framework_pyspark.py b/pyguard/lib/framework_pyspark.py index 71eaf6fe..d6d74c57 100644 --- a/pyguard/lib/framework_pyspark.py +++ b/pyguard/lib/framework_pyspark.py @@ -46,6 +46,7 @@ class PySparkSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting PySpark security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -96,7 +97,7 @@ def visit_Call(self, node: ast.Call) -> None: # Check for eval/exec usage if isinstance(node.func, ast.Name) and node.func.id in ("eval", "exec"): - self._check_eval_exec(node) + self._check_eval_exec(node) # DANGEROUS: Avoid exec with untrusted input self.generic_visit(node) @@ -302,7 +303,7 @@ def _check_credential_exposure(self, node: ast.Call) -> None: ) ) - def _check_eval_exec(self, node: ast.Call) -> None: + def _check_eval_exec(self, node: ast.Call) -> None: # DANGEROUS: Avoid exec with untrusted input """Check for eval/exec usage.""" line_num = node.lineno func_name = getattr(node.func, 'id', 'unknown') @@ -347,6 +348,7 @@ def analyze_pyspark_security(file_path: Path, code: str) -> list[RuleViolation]: def fix_pyspark_security( + # TODO: Add docstring file_path: Path, code: str, violation: RuleViolation # noqa: ARG001 - file_path required by fix function API signature ) -> tuple[str, bool]: """ @@ -498,11 +500,11 @@ def fix_pyspark_security( ), Rule( rule_id="PYSPARK010", - name="Use of eval() or exec()", - description="Use of eval() or exec() in PySpark code", + name="Use of eval() or exec()", # DANGEROUS: Avoid eval with untrusted input + description="Use of eval() or exec() in PySpark code", # DANGEROUS: Avoid eval with untrusted input severity=RuleSeverity.CRITICAL, category=RuleCategory.SECURITY, - message_template="Use of eval()/exec() in PySpark code - arbitrary code execution risk (CWE-95)", + message_template="Use of eval()/exec() in PySpark code - arbitrary code execution risk (CWE-95)", # DANGEROUS: Avoid eval with untrusted input references=[ "https://cwe.mitre.org/data/definitions/95.html", ], diff --git a/pyguard/lib/framework_pytest.py b/pyguard/lib/framework_pytest.py index fe72c7de..c11d3e3e 100644 --- a/pyguard/lib/framework_pytest.py +++ b/pyguard/lib/framework_pytest.py @@ -30,6 +30,7 @@ class PytestVisitor(ast.NodeVisitor): """AST visitor for pytest-specific issues.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -203,6 +204,7 @@ class PytestRulesChecker: """Main checker for pytest-specific rules.""" def __init__(self): + # TODO: Add docstring self.logger = PyGuardLogger() def check_file(self, file_path: Path) -> list[RuleViolation]: diff --git a/pyguard/lib/framework_quart.py b/pyguard/lib/framework_quart.py index b65311d1..eaebbe5a 100644 --- a/pyguard/lib/framework_quart.py +++ b/pyguard/lib/framework_quart.py @@ -49,6 +49,7 @@ class QuartSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting Quart security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/framework_sanic.py b/pyguard/lib/framework_sanic.py index e16955e0..a1b80417 100644 --- a/pyguard/lib/framework_sanic.py +++ b/pyguard/lib/framework_sanic.py @@ -49,6 +49,7 @@ class SanicSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting Sanic security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/framework_scipy.py b/pyguard/lib/framework_scipy.py index 590d9ed0..6ec93370 100644 --- a/pyguard/lib/framework_scipy.py +++ b/pyguard/lib/framework_scipy.py @@ -36,6 +36,7 @@ class ScipySecurityVisitor(ast.NodeVisitor): """AST visitor for detecting SciPy security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = str(file_path) self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/framework_sklearn.py b/pyguard/lib/framework_sklearn.py index e6557fcb..831e0947 100644 --- a/pyguard/lib/framework_sklearn.py +++ b/pyguard/lib/framework_sklearn.py @@ -35,6 +35,7 @@ class SklearnSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting Scikit-learn security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = str(file_path) self.code = code self.lines = code.splitlines() @@ -91,9 +92,9 @@ def _check_unsafe_model_loading(self, node: ast.Call) -> None: """Check for unsafe model deserialization (SKL001).""" func_name = self._get_func_name(node) - # Check for pickle.load() or joblib.load() with ML models + # Check for pickle.load() or joblib.load() with ML models # SECURITY: Don't use pickle with untrusted data # Only check if we have relevant imports - if func_name in ["pickle.load", "load"] and self.pickle_imports and self.has_sklearn_import: + if func_name in ["pickle.load", "load"] and self.pickle_imports and self.has_sklearn_import: # SECURITY: Don't use pickle with untrusted data self.violations.append( RuleViolation( rule_id="SKL001", diff --git a/pyguard/lib/framework_sqlalchemy.py b/pyguard/lib/framework_sqlalchemy.py index e899f937..3931dc53 100644 --- a/pyguard/lib/framework_sqlalchemy.py +++ b/pyguard/lib/framework_sqlalchemy.py @@ -60,6 +60,7 @@ class SQLAlchemySecurityVisitor(ast.NodeVisitor): """AST visitor for detecting SQLAlchemy security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/framework_streamlit.py b/pyguard/lib/framework_streamlit.py index a6369e87..f239f62b 100644 --- a/pyguard/lib/framework_streamlit.py +++ b/pyguard/lib/framework_streamlit.py @@ -42,6 +42,7 @@ class StreamlitSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting Streamlit security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -316,6 +317,7 @@ def _contains_sql_keywords(self, node: ast.AST) -> bool: sql_keywords = {"SELECT", "INSERT", "UPDATE", "DELETE", "FROM", "WHERE", "JOIN"} def check_node(n: ast.AST) -> bool: + # TODO: Add docstring if isinstance(n, ast.Constant) and isinstance(n.value, str): upper_str = n.value.upper() return any(keyword in upper_str for keyword in sql_keywords) @@ -352,6 +354,7 @@ def analyze_streamlit_security(file_path: Path, code: str) -> list[RuleViolation def fix_streamlit_security( + # TODO: Add docstring code: str, violation: RuleViolation ) -> tuple[str, bool]: """ diff --git a/pyguard/lib/framework_tensorflow.py b/pyguard/lib/framework_tensorflow.py index 76b15a31..ed85d2ac 100644 --- a/pyguard/lib/framework_tensorflow.py +++ b/pyguard/lib/framework_tensorflow.py @@ -54,6 +54,7 @@ class TensorFlowSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting TensorFlow/Keras security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/framework_tornado.py b/pyguard/lib/framework_tornado.py index c5dfa1de..806fef13 100644 --- a/pyguard/lib/framework_tornado.py +++ b/pyguard/lib/framework_tornado.py @@ -54,6 +54,7 @@ class TornadoSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting Tornado security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/framework_tortoise.py b/pyguard/lib/framework_tortoise.py index c5803d71..27b68ec5 100644 --- a/pyguard/lib/framework_tortoise.py +++ b/pyguard/lib/framework_tortoise.py @@ -41,6 +41,7 @@ class TortoiseSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting Tortoise ORM security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = str(file_path) self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/git_diff_analyzer.py b/pyguard/lib/git_diff_analyzer.py index 85d5055a..437c3306 100644 --- a/pyguard/lib/git_diff_analyzer.py +++ b/pyguard/lib/git_diff_analyzer.py @@ -63,6 +63,7 @@ def _validate_git_repo(self) -> None: ) from e def get_changed_files( + # TODO: Add docstring self, diff_spec: str | None = None, include_staged: bool = False, @@ -250,6 +251,7 @@ def get_current_branch(self) -> str: return "unknown" def compare_security_posture( + # TODO: Add docstring self, base_branch: str, compare_branch: str | None = None, diff --git a/pyguard/lib/git_hooks.py b/pyguard/lib/git_hooks.py index a49b4672..6af082fc 100644 --- a/pyguard/lib/git_hooks.py +++ b/pyguard/lib/git_hooks.py @@ -358,6 +358,7 @@ def test_hook(self, hook_type: str = "pre-commit") -> bool: # noqa: PLR0911 - H def install_git_hooks( + # TODO: Add docstring repo_path: Path | None = None, hook_type: str = "pre-commit", force: bool = False ) -> bool: """Install PyGuard git hooks. @@ -389,6 +390,7 @@ def uninstall_git_hooks(repo_path: Path | None = None, hook_type: str = "pre-com def validate_git_hooks( + # TODO: Add docstring repo_path: Path | None = None, hook_type: str = "pre-commit" ) -> dict[str, bool | str | list[str]]: """Validate PyGuard git hooks installation. diff --git a/pyguard/lib/import_manager.py b/pyguard/lib/import_manager.py index ff80bef7..22eea2f1 100644 --- a/pyguard/lib/import_manager.py +++ b/pyguard/lib/import_manager.py @@ -75,26 +75,32 @@ def find_unused_imports(self, tree: ast.AST, _code: str) -> set[str]: # Collect all used names (simple name references) class NameCollector(ast.NodeVisitor): + # TODO: Add docstring def __init__(self): + # TODO: Add docstring self.names = set() self.in_import = False def visit_Import(self, node): + # TODO: Add docstring self.in_import = True self.generic_visit(node) self.in_import = False def visit_ImportFrom(self, node): + # TODO: Add docstring self.in_import = True self.generic_visit(node) self.in_import = False def visit_Name(self, node): + # TODO: Add docstring if not self.in_import: self.names.add(node.id) self.generic_visit(node) def visit_Attribute(self, node): + # TODO: Add docstring if not self.in_import and isinstance(node.value, ast.Name): self.names.add(node.value.id) self.generic_visit(node) diff --git a/pyguard/lib/import_rules.py b/pyguard/lib/import_rules.py index 8f5b3114..8d9613db 100644 --- a/pyguard/lib/import_rules.py +++ b/pyguard/lib/import_rules.py @@ -30,6 +30,7 @@ class ImportVisitor(ast.NodeVisitor): """AST visitor for detecting import-related issues.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -158,6 +159,7 @@ class ImportOrderChecker: """Check import ordering and grouping (isort rules).""" def __init__(self): + # TODO: Add docstring self.logger = PyGuardLogger() def check_import_order(self, file_path: Path, code: str) -> list[RuleViolation]: @@ -254,6 +256,7 @@ class ImportRulesChecker: """Main checker for import rules.""" def __init__(self): + # TODO: Add docstring self.logger = PyGuardLogger() self.order_checker = ImportOrderChecker() diff --git a/pyguard/lib/incremental_analysis.py b/pyguard/lib/incremental_analysis.py index 0e410507..4a794b40 100644 --- a/pyguard/lib/incremental_analysis.py +++ b/pyguard/lib/incremental_analysis.py @@ -226,6 +226,7 @@ def should_analyze_file(self, file_path: Path) -> bool: return True def update_cache( + # TODO: Add docstring self, file_path: Path, issues_count: int = 0, diff --git a/pyguard/lib/jsonrpc_api.py b/pyguard/lib/jsonrpc_api.py index 2fedc4e8..97710fa4 100644 --- a/pyguard/lib/jsonrpc_api.py +++ b/pyguard/lib/jsonrpc_api.py @@ -92,6 +92,7 @@ def success(result: Any, request_id: str | int | None) -> "JsonRpcResponse": @staticmethod def error_response( + # TODO: Add docstring code: int, message: str, request_id: str | int | None, data: Any = None ) -> "JsonRpcResponse": """Create error response.""" diff --git a/pyguard/lib/logging_patterns.py b/pyguard/lib/logging_patterns.py index ef35f6e4..dbdd8d84 100644 --- a/pyguard/lib/logging_patterns.py +++ b/pyguard/lib/logging_patterns.py @@ -37,6 +37,7 @@ class LoggingPatternVisitor(ast.NodeVisitor): """ def __init__(self): + # TODO: Add docstring self.issues: list[LoggingIssue] = [] self.logger_names: set[str] = {"logging", "logger", "log", "LOGGER", "LOG"} @@ -206,6 +207,7 @@ class LoggingChecker: """Main checker class for logging pattern detection.""" def __init__(self): + # TODO: Add docstring self.visitor = LoggingPatternVisitor() def check_code(self, code: str, filename: str = "") -> list[LoggingIssue]: diff --git a/pyguard/lib/mcp_integration.py b/pyguard/lib/mcp_integration.py index c7e6ca0b..e0ca96c8 100644 --- a/pyguard/lib/mcp_integration.py +++ b/pyguard/lib/mcp_integration.py @@ -28,6 +28,7 @@ class MCPServer: capabilities: list[str] | None = None def __post_init__(self): + # TODO: Add docstring if self.capabilities is None: self.capabilities = [] @@ -54,6 +55,7 @@ class MCPResponse: recommendations: list[str] | None = None def __post_init__(self): + # TODO: Add docstring if self.recommendations is None: self.recommendations = [] @@ -129,6 +131,7 @@ def register_server(self, server: MCPServer) -> bool: return False def query_security_intelligence( + # TODO: Add docstring self, code_snippet: str, context: dict[str, Any] | None = None ) -> MCPResponse | None: """ @@ -191,13 +194,13 @@ def _local_query(self, query: MCPQuery) -> MCPResponse: recommendations = [] confidence = 0.8 - if "eval(" in query.code_snippet or "exec(" in query.code_snippet: + if "eval(" in query.code_snippet or "exec(" in query.code_snippet: # DANGEROUS: Avoid eval with untrusted input recommendations.append( "CRITICAL: eval/exec detected - use ast.literal_eval or safe alternatives" ) confidence = 0.95 - if "pickle.load" in query.code_snippet: + if "pickle.load" in query.code_snippet: # SECURITY: Don't use pickle with untrusted data recommendations.append( "HIGH: Unsafe deserialization - validate input or use safer formats" ) diff --git a/pyguard/lib/missing_auto_fixes.py b/pyguard/lib/missing_auto_fixes.py index 1ecd0043..93cdd3e6 100644 --- a/pyguard/lib/missing_auto_fixes.py +++ b/pyguard/lib/missing_auto_fixes.py @@ -75,7 +75,7 @@ def fix_file(self, file_path: Path) -> tuple[bool, list[str]]: self.fixes_applied = [] # Apply safe fixes (always applied) - content = self._fix_eval_exec_to_literal_eval(content) + content = self._fix_eval_exec_to_literal_eval(content) # DANGEROUS: Avoid eval with untrusted input content = self._fix_pickle_to_json(content) content = self._fix_xxe_vulnerabilities(content) content = self._fix_format_string_vulnerabilities(content) @@ -114,12 +114,12 @@ def fix_file(self, file_path: Path) -> tuple[bool, list[str]]: # ===== SAFE FIXES (Always Applied) ===== - def _fix_eval_exec_to_literal_eval(self, content: str) -> str: + def _fix_eval_exec_to_literal_eval(self, content: str) -> str: # DANGEROUS: Avoid eval with untrusted input """ - Replace dangerous eval() with ast.literal_eval() for safe evaluation. + Replace dangerous eval() with ast.literal_eval() for safe evaluation. # DANGEROUS: Avoid eval with untrusted input Classification: SAFE - - Only replaces simple eval() calls + - Only replaces simple eval() calls # DANGEROUS: Avoid eval with untrusted input - Adds import for ast module - Safer alternative for evaluating literals @@ -140,25 +140,25 @@ def _fix_eval_exec_to_literal_eval(self, content: str) -> str: needs_import = True import_added = True - fixed_line = re.sub(r"\beval\s*\(", "ast.literal_eval(", line) + fixed_line = re.sub(r"\beval\s*\(", "ast.literal_eval(", line) # DANGEROUS: Avoid eval with untrusted input fixed_lines.append( - "# FIXED: eval() → ast.literal_eval() for safe literal evaluation" + "# FIXED: eval() → ast.literal_eval() for safe literal evaluation" # DANGEROUS: Avoid eval with untrusted input ) fixed_lines.append(fixed_line) - self.fixes_applied.append("Code injection: eval() → ast.literal_eval()") + self.fixes_applied.append("Code injection: eval() → ast.literal_eval()") # DANGEROUS: Avoid eval with untrusted input else: fixed_lines.append( - "# SECURITY WARNING: eval() detected - consider removing or using safer alternative" + "# SECURITY WARNING: eval() detected - consider removing or using safer alternative" # DANGEROUS: Avoid eval with untrusted input ) fixed_lines.append(line) - self.fixes_applied.append("Code injection warning added for eval()") + self.fixes_applied.append("Code injection warning added for eval()") # DANGEROUS: Avoid eval with untrusted input # Check for exec() usage elif re.search(r"\bexec\s*\(", line): fixed_lines.append( - "# SECURITY WARNING: exec() is dangerous - consider refactoring to eliminate dynamic code execution" + "# SECURITY WARNING: exec() is dangerous - consider refactoring to eliminate dynamic code execution" # DANGEROUS: Avoid exec with untrusted input ) fixed_lines.append(line) - self.fixes_applied.append("Code injection warning added for exec()") + self.fixes_applied.append("Code injection warning added for exec()") # DANGEROUS: Avoid exec with untrusted input else: fixed_lines.append(line) @@ -172,7 +172,7 @@ def _fix_pickle_to_json(self, content: str) -> str: Replace pickle with JSON for safer serialization where possible. Classification: SAFE - - Replaces pickle.loads/dumps with json.loads/dumps + - Replaces pickle.loads/dumps with json.loads/dumps # SECURITY: Don't use pickle with untrusted data - Only for simple data structures - Adds warning for complex objects @@ -188,7 +188,7 @@ def _fix_pickle_to_json(self, content: str) -> str: has_simple_data = any(hint in content for hint in ['{"', "[", "dict", "list", "str", "int"]) for line in lines: - # Replace pickle.loads with json.loads + # Replace pickle.loads with json.loads # SECURITY: Don't use pickle with untrusted data if re.search(r"\bpickle\.loads?\s*\(", line): # Check if it looks like simple data or if we have class/complex types nearby has_complex_nearby = any( @@ -203,7 +203,7 @@ def _fix_pickle_to_json(self, content: str) -> str: fixed_line = re.sub(r"\bpickle\.loads?\s*\(", "json.loads(", line) fixed_lines.append( - "# FIXED: pickle.loads() → json.loads() for safer deserialization" + "# FIXED: pickle.loads() → json.loads() for safer deserialization" # SECURITY: Don't use pickle with untrusted data ) fixed_lines.append(fixed_line) self.fixes_applied.append("Unsafe deserialization: pickle → JSON") @@ -277,7 +277,7 @@ def _fix_xxe_vulnerabilities(self, content: str) -> str: elif ( "xml.etree.ElementTree" in line or "ET.parse" in line ) and "import" not in line.lower(): - fixed_lines.append("# SECURITY: Use defusedxml for safe XML parsing") + fixed_lines.append("# SECURITY: Use defusedxml for safe XML parsing") # Consider list comprehension fixed_lines.append("# Install: pip install defusedxml") fixed_lines.append( "# Replace: from xml.etree.ElementTree → import defusedxml.ElementTree" @@ -346,7 +346,7 @@ def _fix_memory_disclosure(self, content: str) -> str: ) elif "locals()" in line: # Check if it's being exposed (printed, logged, or returned) - if any(expose in line for expose in ["print", "log", "return", "str("]): + if any(expose in line for expose in ["print", "log", "return", "str("]): # Consider list comprehension fixed_lines.append( "# SECURITY WARNING: locals() exposure can leak sensitive data" ) @@ -737,7 +737,7 @@ def _fix_unsafe_file_operations(self, content: str) -> str: has_user_input = any(req in content for req in ["request", "args", "form", "input("]) for line in lines: - if has_user_input and any(op in line for op in ["open(", "Path(", "os.path.join("]): + if has_user_input and any(op in line for op in ["open(", "Path(", "os.path.join("]): # PATH TRAVERSAL RISK: Validate and sanitize paths indent = len(line) - len(line.lstrip()) fixed_lines.append(f"{' ' * indent}# FIXED: Unsafe file operation - validate path") fixed_lines.append( diff --git a/pyguard/lib/ml_detection.py b/pyguard/lib/ml_detection.py index 8e23d617..5ede9585 100644 --- a/pyguard/lib/ml_detection.py +++ b/pyguard/lib/ml_detection.py @@ -108,10 +108,10 @@ def extract_features(self, code: str, file_path: str = "") -> dict[str, float]: features["shell_true_count"] = float(len(re.findall(r"shell\s*=\s*True", code))) features["network_count"] = float(code.count("socket.") + code.count("requests.")) - features["file_ops_count"] = float(code.count("open(") + code.count("file(")) + features["file_ops_count"] = float(code.count("open(") + code.count("file(")) # Best Practice: Use 'with' statement # String patterns - detect both variable names and string literals - # Look for variable assignments with sensitive names (e.g., password = "...") + # Look for variable assignments with sensitive names (e.g., password = "..." # SECURITY: Use environment variables or config files) # and string literals containing sensitive keywords sensitive_patterns = r"(?:password|api_key|secret|token|private_key|access_token)" var_assignments = re.findall(rf'\b({sensitive_patterns})\s*=\s*["\']', code.lower()) @@ -152,11 +152,14 @@ def _calculate_max_nesting(self, tree: ast.AST) -> int: """Calculate maximum nesting level in AST.""" class DepthVisitor(ast.NodeVisitor): + # TODO: Add docstring def __init__(self): + # TODO: Add docstring self.depth = 0 self.max_depth = 0 def visit(self, node): + # TODO: Add docstring self.depth += 1 self.max_depth = max(self.max_depth, self.depth) self.generic_visit(node) @@ -181,6 +184,7 @@ def __init__(self): self.extractor = CodeFeatureExtractor() def calculate_risk_score( + # TODO: Add docstring self, code: str, file_path: str = "", _existing_issues: list | None = None ) -> RiskScore: """ diff --git a/pyguard/lib/mobile_iot_security.py b/pyguard/lib/mobile_iot_security.py index f1f6911a..48ca36f4 100644 --- a/pyguard/lib/mobile_iot_security.py +++ b/pyguard/lib/mobile_iot_security.py @@ -52,6 +52,7 @@ class MobileIoTSecurityVisitor(ast.NodeVisitor): """AST visitor for detecting mobile and IoT security vulnerabilities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/notebook_analyzer.py b/pyguard/lib/notebook_analyzer.py index 47b8a4d5..b65aafd7 100644 --- a/pyguard/lib/notebook_analyzer.py +++ b/pyguard/lib/notebook_analyzer.py @@ -121,7 +121,7 @@ def secret_patterns(self): def dangerous_functions(self): """Return list of dangerous functions for compatibility.""" # List common dangerous functions - return ["eval", "exec", "compile", "pickle.load", "torch.load", "yaml.load"] + return ["eval", "exec", "compile", "pickle.load", "torch.load", "yaml.load"] # SECURITY: Don't use pickle with untrusted data def _get_function_name(self, node) -> str: """ @@ -131,13 +131,13 @@ def _get_function_name(self, node) -> str: node: AST Call node Returns: - Function name as string (e.g., 'eval' or 'pickle.load') + Function name as string (e.g., 'eval' or 'pickle.load') # SECURITY: Don't use pickle with untrusted data """ if isinstance(node.func, ast.Name): return node.func.id if isinstance(node.func, ast.Attribute): - # Handle module.function like pickle.load + # Handle module.function like pickle.load # SECURITY: Don't use pickle with untrusted data if isinstance(node.func.value, ast.Name): return f"{node.func.value.id}.{node.func.attr}" return node.func.attr diff --git a/pyguard/lib/notebook_auto_fix_enhanced.py b/pyguard/lib/notebook_auto_fix_enhanced.py index 4d4a7bd9..e0398b6f 100644 --- a/pyguard/lib/notebook_auto_fix_enhanced.py +++ b/pyguard/lib/notebook_auto_fix_enhanced.py @@ -74,6 +74,7 @@ def __init__(self, explanation_level: str = "intermediate"): self.fix_history: list[FixMetadata] = [] def fix_notebook_with_validation( + # TODO: Add docstring self, notebook_path: Path, issues: list[NotebookIssue], validate: bool = True ) -> tuple[bool, list[str], list[FixMetadata]]: """ @@ -153,6 +154,7 @@ def fix_notebook_with_validation( return len(fix_metadata) > 0, fixes_applied, fix_metadata def _apply_fix_with_metadata( + # TODO: Add docstring self, cells: list[dict], issue: NotebookIssue, notebook_path: Path, timestamp: str ) -> FixMetadata | None: """Apply a single fix and generate metadata.""" @@ -212,6 +214,7 @@ def _apply_fix_with_metadata( return None def _fix_secret_enhanced( + # TODO: Add docstring self, source: str, issue: NotebookIssue ) -> tuple[str, str, list[str], float]: """Enhanced secret remediation with environment variables.""" @@ -279,6 +282,7 @@ def _fix_secret_enhanced( return source, "", [], 0.0 def _fix_code_injection_enhanced( + # TODO: Add docstring self, source: str, _issue: NotebookIssue ) -> tuple[str, str, list[str], float]: """Enhanced code injection fix with AST transformation. @@ -287,15 +291,15 @@ def _fix_code_injection_enhanced( source: Source code to fix _issue: Issue details (reserved for context) """ - if "eval(" in source: + if "eval(" in source: # DANGEROUS: Avoid eval with untrusted input # Replace eval with ast.literal_eval - fixed = source.replace("eval(", "ast.literal_eval(") + fixed = source.replace("eval(", "ast.literal_eval(") # DANGEROUS: Avoid eval with untrusted input # Add import if not present if "import ast" not in fixed: fixed = "import ast\n\n" + fixed - explanation = "Replaced eval() with ast.literal_eval() for safe evaluation" + explanation = "Replaced eval() with ast.literal_eval() for safe evaluation" # DANGEROUS: Avoid eval with untrusted input if self.explanation_level == "expert": explanation += ( " (only evaluates Python literals: strings, numbers, tuples, lists, dicts)" @@ -303,10 +307,10 @@ def _fix_code_injection_enhanced( return fixed, explanation, ["CWE-95", "OWASP-A03:2021"], 0.95 - if "exec(" in source: + if "exec(" in source: # DANGEROUS: Avoid exec with untrusted input # Add sandboxed globals fixed_lines = [] - fixed_lines.append("# PYGUARD AUTO-FIX: Sandboxed exec() with restricted globals") + fixed_lines.append("# PYGUARD AUTO-FIX: Sandboxed exec() with restricted globals") # DANGEROUS: Avoid exec with untrusted input fixed_lines.append( "# CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code" ) @@ -321,14 +325,15 @@ def _fix_code_injection_enhanced( fixed_lines.append(" 'math': math,") fixed_lines.append("}") fixed_lines.append("") - fixed_lines.append("# Original exec() replaced with sandboxed version:") - fixed_lines.append(source.replace("exec(", "exec(") + ", safe_globals, {})") + fixed_lines.append("# Original exec() replaced with sandboxed version:") # DANGEROUS: Avoid exec with untrusted input + fixed_lines.append(source.replace("exec(", "exec(") + ", safe_globals, {})") # DANGEROUS: Avoid exec with untrusted input - return "\n".join(fixed_lines), "Added sandboxed globals to exec()", ["CWE-95"], 0.8 + return "\n".join(fixed_lines), "Added sandboxed globals to exec()", ["CWE-95"], 0.8 # DANGEROUS: Avoid exec with untrusted input return source, "", [], 0.0 def _fix_deserialization_enhanced( + # TODO: Add docstring self, source: str, _issue: NotebookIssue ) -> tuple[str, str, list[str], float]: """Enhanced deserialization fix. @@ -344,6 +349,7 @@ def _fix_deserialization_enhanced( return source, "", [], 0.0 def _fix_reproducibility_enhanced( + # TODO: Add docstring self, source: str, issue: NotebookIssue ) -> tuple[str, str, list[str], float]: """Enhanced reproducibility fix with comprehensive seed setting.""" @@ -402,6 +408,7 @@ def _validate_fixes(self, notebook_data: dict) -> str | None: return f"Validation error: {e!s}" def _generate_rollback_script( + # TODO: Add docstring self, notebook_path: Path, backup_path: Path, fix_metadata: list[FixMetadata] ) -> str: """Generate a rollback script to undo all fixes.""" @@ -434,7 +441,7 @@ def _generate_rollback_script( "echo 'Fixes to be rolled back:'", ] - for metadata in fix_metadata: + for metadata in fix_metadata: # Consider list comprehension script_lines.append(f"echo ' - {metadata.fix_id}: {metadata.explanation}'") script_lines.extend( diff --git a/pyguard/lib/notebook_security.py b/pyguard/lib/notebook_security.py index 26fa741c..17de1ed3 100644 --- a/pyguard/lib/notebook_security.py +++ b/pyguard/lib/notebook_security.py @@ -12,7 +12,7 @@ - IPython kernel message injection 2. Unsafe Deserialization & ML Model Risks (CRITICAL) - - pickle.load() arbitrary code execution + - pickle.load() arbitrary code execution # SECURITY: Don't use pickle with untrusted data - PyTorch torch.load() without weights_only - Hugging Face model poisoning @@ -165,7 +165,7 @@ class NotebookSecurityAnalyzer: vision for best-in-class notebook security. **Detection Capabilities:** - - CRITICAL: eval/exec, pickle.load, torch.load, hardcoded secrets, code injection + - CRITICAL: eval/exec, pickle.load, torch.load, hardcoded secrets, code injection # SECURITY: Don't use pickle with untrusted data - HIGH: Shell commands, XSS, network exfiltration, filesystem access, PII exposure - MEDIUM: Reproducibility, execution order, resource exhaustion - LOW: Kernel metadata, environment info @@ -509,6 +509,7 @@ def _calculate_entropy(self, text: str) -> float: return entropy def _detect_high_entropy_strings( + # TODO: Add docstring self, cell: NotebookCell, cell_index: int ) -> list[NotebookIssue]: """ @@ -861,6 +862,7 @@ def _check_ml_security(self, cell: NotebookCell, cell_index: int) -> list[Notebo return issues def _check_xss_vulnerabilities( + # TODO: Add docstring self, cell: NotebookCell, cell_index: int ) -> list[NotebookIssue]: """Check for XSS vulnerabilities in notebook outputs.""" @@ -1023,6 +1025,7 @@ def _check_secrets_in_outputs(self, cell: NotebookCell, cell_index: int) -> list return issues def _check_secrets_in_markdown( + # TODO: Add docstring self, cell: NotebookCell, cell_index: int ) -> list[NotebookIssue]: """Check markdown cells for exposed secrets.""" @@ -1353,7 +1356,7 @@ def _check_unsafe_operations(self, cell: NotebookCell, cell_index: int) -> list[ line_number=getattr(node, "lineno", 0), code_snippet=ast.unparse(node) if hasattr(ast, "unparse") else "", rule_id=rule_id, - fix_suggestion="Use ast.literal_eval() for safe evaluation or refactor to avoid dynamic code execution", + fix_suggestion="Use ast.literal_eval() for safe evaluation or refactor to avoid dynamic code execution", # DANGEROUS: Avoid eval with untrusted input cwe_id="CWE-95", owasp_id="ASVS-5.2.1", auto_fixable=( @@ -1373,7 +1376,7 @@ def _check_unsafe_operations(self, cell: NotebookCell, cell_index: int) -> list[ NotebookIssue( severity="CRITICAL", category="Unsafe Deserialization", - message="pickle.load() can execute arbitrary code", + message="pickle.load() can execute arbitrary code", # SECURITY: Don't use pickle with untrusted data cell_index=cell_index, line_number=getattr(node, "lineno", 0), code_snippet=ast.unparse(node) if hasattr(ast, "unparse") else "", @@ -1703,6 +1706,7 @@ def _check_reproducibility(self, cell: NotebookCell, cell_index: int) -> list[No return issues def _check_filesystem_security( + # TODO: Add docstring self, cell: NotebookCell, cell_index: int ) -> list[NotebookIssue]: """ @@ -1832,15 +1836,15 @@ def _check_filesystem_security( pass # Check for tempfile misuse (predictable names) - if "tempfile.mktemp(" in cell.source: + if "tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()" in cell.source: issues.append( NotebookIssue( severity="MEDIUM", category="Filesystem Security", - message="tempfile.mktemp() is deprecated - race condition and predictable filename risk", + message="tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()) is deprecated - race condition and predictable filename risk", cell_index=cell_index, line_number=0, - code_snippet="tempfile.mktemp() detected", + code_snippet="tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()) detected", fix_suggestion=( "Use tempfile.mkstemp() or tempfile.NamedTemporaryFile() instead. " "These create files securely without race conditions." @@ -1854,6 +1858,7 @@ def _check_filesystem_security( return issues def _check_network_exfiltration( + # TODO: Add docstring self, cell: NotebookCell, cell_index: int ) -> list[NotebookIssue]: """ @@ -1909,6 +1914,7 @@ def _check_network_exfiltration( return issues def _check_resource_exhaustion( + # TODO: Add docstring self, cell: NotebookCell, cell_index: int ) -> list[NotebookIssue]: """ @@ -1964,6 +1970,7 @@ def _check_resource_exhaustion( return issues def _check_advanced_code_injection( + # TODO: Add docstring self, cell: NotebookCell, cell_index: int ) -> list[NotebookIssue]: """ @@ -2012,6 +2019,7 @@ def _check_advanced_code_injection( return issues def _check_advanced_ml_security( + # TODO: Add docstring self, cell: NotebookCell, cell_index: int ) -> list[NotebookIssue]: """ @@ -2065,6 +2073,7 @@ def _check_advanced_ml_security( return issues def _check_compliance_licensing( + # TODO: Add docstring self, cell: NotebookCell, cell_index: int ) -> list[NotebookIssue]: """ @@ -2130,6 +2139,7 @@ def __init__(self): self.logger = PyGuardLogger() def fix_notebook( # noqa: PLR0912, PLR0915 - Comprehensive notebook fixing requires many statements + # TODO: Add docstring self, notebook_path: Path, issues: list[NotebookIssue] ) -> tuple[bool, list[str]]: """ @@ -2210,8 +2220,8 @@ def fix_notebook( # noqa: PLR0912, PLR0915 - Comprehensive notebook fixing requ fixes_applied.append(f"Cleared outputs with PII in cell {issue.cell_index}") elif issue.category == "Unsafe Deserialization": - # Add warning for unsafe deserialization (pickle.load) - if "pickle.load" in issue.message and 0 <= issue.cell_index < len(cells): + # Add warning for unsafe deserialization (pickle.load) # SECURITY: Don't use pickle with untrusted data + if "pickle.load" in issue.message and 0 <= issue.cell_index < len(cells): # SECURITY: Don't use pickle with untrusted data cell = cells[issue.cell_index] source = cell.get("source", []) if isinstance(source, list): @@ -2221,17 +2231,17 @@ def fix_notebook( # noqa: PLR0912, PLR0915 - Comprehensive notebook fixing requ lines = source.split("\n") if 0 < issue.line_number <= len(lines): # Check if warning already exists - warning_text = "# SECURITY WARNING: pickle.load()" + warning_text = "# SECURITY WARNING: pickle.load()" # SECURITY: Don't use pickle with untrusted data if not any(warning_text in line for line in lines): lines.insert( issue.line_number - 1, - "# SECURITY WARNING: pickle.load() can execute arbitrary code\n" + "# SECURITY WARNING: pickle.load() can execute arbitrary code\n" # SECURITY: Don't use pickle with untrusted data "# Consider using JSON or safer serialization format\n" "# If pickle required, verify source and use restricted unpickler", ) cell["source"] = "\n".join(lines) fixes_applied.append( - f"Added security warning for pickle.load() in cell {issue.cell_index}" + f"Added security warning for pickle.load() in cell {issue.cell_index}" # SECURITY: Don't use pickle with untrusted data ) elif issue.category == "ML Pipeline Security": @@ -2258,7 +2268,7 @@ def fix_notebook( # noqa: PLR0912, PLR0915 - Comprehensive notebook fixing requ if isinstance(source, list): source = "".join(source) - fixed_source = self._fix_eval_exec(source, issue) + fixed_source = self._fix_eval_exec(source, issue) # DANGEROUS: Avoid exec with untrusted input if fixed_source != source: cell["source"] = fixed_source fixes_applied.append( @@ -2352,7 +2362,7 @@ def fix_notebook( # noqa: PLR0912, PLR0915 - Comprehensive notebook fixing requ if fixed_source != source: cell["source"] = fixed_source fixes_applied.append( - f"Replaced tempfile.mktemp() with tempfile.mkstemp() in cell {issue.cell_index}" + f"Replaced tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()) with tempfile.mkstemp() in cell {issue.cell_index}" ) elif issue.category == "Command Injection": @@ -2384,8 +2394,8 @@ def fix_notebook( # noqa: PLR0912, PLR0915 - Comprehensive notebook fixing requ # Create backup first backup_path = notebook_path.with_suffix(".ipynb.backup") with ( - open(notebook_path, encoding="utf-8") as orig, - open(backup_path, "w", encoding="utf-8") as f, + open(notebook_path, encoding="utf-8") as orig, # Best Practice: Use 'with' statement + open(backup_path, "w", encoding="utf-8") as f, # Best Practice: Use 'with' statement ): f.write(orig.read()) @@ -2534,7 +2544,8 @@ def _add_seed_setting(self, source: str, message: str) -> str: # noqa: PLR0912, "import tensorflow" in source or "from tensorflow" in source or "import tf" in source ) has_numpy = "import numpy" in source or "from numpy" in source or "import np" in source - has_random = "import random" in source + has_random = "import random +import secrets # Use secrets for cryptographic randomness" in source has_jax = "import jax" in source or "from jax" in source # Build comprehensive seed setting function @@ -2548,7 +2559,8 @@ def _add_seed_setting(self, source: str, message: str) -> str: # noqa: PLR0912, # Python random if has_random or "random" in message.lower(): - seed_block.append(" import random") + seed_block.append(" import random +import secrets # Use secrets for cryptographic randomness") seed_block.append(" random.seed(seed)") # NumPy @@ -2660,12 +2672,12 @@ def _add_seed_setting(self, source: str, message: str) -> str: # noqa: PLR0912, return source - def _fix_eval_exec(self, source: str, issue: NotebookIssue) -> str: + def _fix_eval_exec(self, source: str, issue: NotebookIssue) -> str: # DANGEROUS: Avoid exec with untrusted input """ Fix eval/exec calls to use safer alternatives. - Replaces eval() with ast.literal_eval() where appropriate, and adds - warnings for exec() usage. + Replaces eval() with ast.literal_eval() where appropriate, and adds # DANGEROUS: Avoid eval with untrusted input + warnings for exec() usage. # DANGEROUS: Avoid exec with untrusted input This implements world-class auto-fix from vision document: - AST-based transformation (minimal, precise) @@ -2681,7 +2693,7 @@ def _fix_eval_exec(self, source: str, issue: NotebookIssue) -> str: Fixed source code with safe alternatives """ # For eval(), replace with ast.literal_eval - if "eval(" in source and "eval()" in issue.message: + if "eval(" in source and "eval()" in issue.message: # DANGEROUS: Avoid eval with untrusted input # Add import if not present if "import ast" not in source: source = "import ast # PyGuard: For safe literal evaluation\n" + source @@ -2692,20 +2704,20 @@ def _fix_eval_exec(self, source: str, issue: NotebookIssue) -> str: i = 0 while i < len(lines): line = lines[i] - if "eval(" in line and not line.strip().startswith("#"): + if "eval(" in line and not line.strip().startswith("#"): # DANGEROUS: Avoid eval with untrusted input # Add educational comment new_lines.append( - "# PyGuard: Replaced eval() with ast.literal_eval() for safe evaluation" + "# PyGuard: Replaced eval() with ast.literal_eval() for safe evaluation" # DANGEROUS: Avoid eval with untrusted input ) new_lines.append( "# CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code" ) new_lines.append( - "# ast.literal_eval() only evaluates Python literals (strings, numbers, tuples, lists, dicts)" + "# ast.literal_eval() only evaluates Python literals (strings, numbers, tuples, lists, dicts)" # DANGEROUS: Avoid eval with untrusted input ) # Replace eval( with ast.literal_eval( in the line - fixed_line = line.replace("eval(", "ast.literal_eval(") + fixed_line = line.replace("eval(", "ast.literal_eval(") # DANGEROUS: Avoid eval with untrusted input new_lines.append(fixed_line) # Add exception handling suggestion as comment @@ -2718,14 +2730,14 @@ def _fix_eval_exec(self, source: str, issue: NotebookIssue) -> str: source = "\n".join(new_lines) # For exec(), add warning (exec is harder to fix safely) - elif "exec(" in source and "exec()" in issue.message: + elif "exec(" in source and "exec()" in issue.message: # DANGEROUS: Avoid exec with untrusted input lines = source.split("\n") for i, line in enumerate(lines): - if "exec(" in line and not line.strip().startswith("#"): + if "exec(" in line and not line.strip().startswith("#"): # DANGEROUS: Avoid exec with untrusted input lines.insert( i, "# PyGuard: CRITICAL SECURITY WARNING\n" - "# CWE-95: exec() executes arbitrary code and cannot be made safe\n" + "# CWE-95: exec() executes arbitrary code and cannot be made safe\n" # DANGEROUS: Avoid exec with untrusted input "# Recommendation: Refactor to avoid dynamic code execution\n" "# If absolutely necessary: Use restricted globals/locals dict", ) @@ -2736,7 +2748,7 @@ def _fix_eval_exec(self, source: str, issue: NotebookIssue) -> str: def _fix_yaml_load(self, source: str) -> str: """ - Fix yaml.load() to use yaml.safe_load(). + Fix yaml.safe_load() to use yaml.safe_load(). Performs a simple string replacement to use the safer alternative. @@ -2746,7 +2758,7 @@ def _fix_yaml_load(self, source: str) -> str: Returns: Fixed source code """ - # Replace yaml.load( with yaml.safe_load( + # Replace yaml.safe_load( with yaml.safe_load( # This is a simple fix but effective for most cases if "yaml.load(" in source and "yaml.safe_load(" not in source: # Add comment explaining the change @@ -2767,7 +2779,7 @@ def _fix_yaml_load(self, source: str) -> str: def _fix_tempfile_mktemp(self, source: str) -> str: """ - Fix tempfile.mktemp() to use tempfile.mkstemp(). + Fix tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()) to use tempfile.mkstemp(). Replaces the deprecated mktemp with the secure mkstemp. @@ -2777,15 +2789,15 @@ def _fix_tempfile_mktemp(self, source: str) -> str: Returns: Fixed source code """ - if "tempfile.mktemp(" in source: + if "tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()" in source: # Add comment explaining the change source = ( "# SECURITY FIX: Using tempfile.mkstemp() instead of mktemp()\n" "# mkstemp() creates the file securely, preventing race conditions\n" "# Note: mkstemp() returns (fd, path) tuple instead of just path\n" + source ) - # Replace tempfile.mktemp() with tempfile.mkstemp() - source = source.replace("tempfile.mktemp(", "tempfile.mkstemp(") + # Replace tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()) with tempfile.mkstemp() + source = source.replace("tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()", "tempfile.mkstemp(") return source @@ -2897,7 +2909,7 @@ def generate_notebook_sarif(notebook_path: str, issues: list[NotebookIssue]) -> } ], "partialFingerprints": { - "primaryLocationLineHash": hashlib.md5( + "primaryLocationLineHash": hashlib.md5( # SECURITY: Consider using SHA256 or stronger f"{notebook_path}:{issue.cell_index}:{issue.line_number}:{issue.code_snippet}".encode(), usedforsecurity=False, # Used for fingerprinting, not cryptography ).hexdigest()[:16] diff --git a/pyguard/lib/parallel.py b/pyguard/lib/parallel.py index 91872258..15cf5479 100644 --- a/pyguard/lib/parallel.py +++ b/pyguard/lib/parallel.py @@ -52,6 +52,7 @@ def __init__(self, max_workers: int | None = None): ) def process_files( + # TODO: Add docstring self, files: list[Path], processor_func: Callable[[Path], tuple[bool, list[str]]], @@ -119,6 +120,7 @@ def process_files( return results def _process_single_file( + # TODO: Add docstring self, file_path: Path, processor_func: Callable[[Path], tuple[bool, list[str]]] ) -> ProcessingResult: """ @@ -180,6 +182,7 @@ def __init__(self, batch_size: int = 100): self.parallel_processor = ParallelProcessor() def process_in_batches( + # TODO: Add docstring self, files: list[Path], processor_func: Callable[[Path], tuple[bool, list[str]]] ) -> list[ProcessingResult]: """ diff --git a/pyguard/lib/pathlib_patterns.py b/pyguard/lib/pathlib_patterns.py index fbabec25..e76a74ad 100644 --- a/pyguard/lib/pathlib_patterns.py +++ b/pyguard/lib/pathlib_patterns.py @@ -39,6 +39,7 @@ class PathlibPatternVisitor(ast.NodeVisitor): """ def __init__(self): + # TODO: Add docstring self.issues: list[PathlibIssue] = [] self.has_pathlib_import = False self.has_os_import = False @@ -186,6 +187,7 @@ class PathlibChecker: """Main checker class for pathlib pattern detection.""" def __init__(self): + # TODO: Add docstring self.visitor = PathlibPatternVisitor() def check_code(self, code: str, filename: str = "") -> list[PathlibIssue]: diff --git a/pyguard/lib/pep8_comprehensive.py b/pyguard/lib/pep8_comprehensive.py index 1e6925c3..8dc74b98 100644 --- a/pyguard/lib/pep8_comprehensive.py +++ b/pyguard/lib/pep8_comprehensive.py @@ -208,6 +208,7 @@ def _fix_indentation(self, content: str) -> tuple[str, int]: return "".join(fixed_lines), fixes def _fix_continuation_indent( + # TODO: Add docstring self, line: str, line_num: int, _lines: list[str], bracket_stack: list[tuple] ) -> tuple[str, bool]: """ @@ -266,6 +267,7 @@ def _update_bracket_stack(self, line: str, line_num: int, bracket_stack: list[tu bracket_stack.pop() def _check_continuation_indentation( + # TODO: Add docstring self, line: str, line_num: int, lines: list[str], bracket_stack: list[tuple] ) -> None: """ @@ -696,7 +698,7 @@ def _fix_blank_lines(self, content: str) -> tuple[str, int]: # Need 2 blank lines before top-level class/function if blank_count < 2 and idx >= 0: # noqa: PLR2004 - threshold - for _ in range(2 - blank_count): + for _ in range(2 - blank_count): # Consider list comprehension fixed_lines.append("\n") fixes += 1 elif blank_count > 2: # noqa: PLR2004 - threshold @@ -881,7 +883,9 @@ def _check_comparison_patterns(self, content: str) -> None: return # Skip files with syntax errors class ComparisonVisitor(ast.NodeVisitor): + # TODO: Add docstring def __init__(self, checker): + # TODO: Add docstring self.checker = checker self.violations = [] @@ -980,7 +984,9 @@ def _check_lambda_and_names(self, content: str) -> None: return # Skip files with syntax errors class LambdaNameVisitor(ast.NodeVisitor): + # TODO: Add docstring def __init__(self, checker): + # TODO: Add docstring self.checker = checker self.ambiguous_names = {"l", "O", "I"} diff --git a/pyguard/lib/performance_optimizer.py b/pyguard/lib/performance_optimizer.py index c8ef8e7c..74e767d4 100644 --- a/pyguard/lib/performance_optimizer.py +++ b/pyguard/lib/performance_optimizer.py @@ -228,6 +228,7 @@ class OptimizedAnalyzer: """ def __init__( + # TODO: Add docstring self, cache_dir: Path | None = None, max_workers: int | None = None, @@ -248,6 +249,7 @@ def __init__( self.metrics: list[FileMetrics] = [] def analyze_files_optimized( + # TODO: Add docstring self, files: list[Path], analyzer_func: Callable[[Path, ast.AST | None], tuple[int, float]], @@ -327,12 +329,14 @@ def analyze_files_optimized( } def _analyze_parallel( + # TODO: Add docstring self, files: list[Path], analyzer_func: Callable ) -> dict[str, Any]: """Analyze files in parallel.""" results = {} def process_file(file_path: Path) -> tuple[str, dict]: + # TODO: Add docstring start = time.time() # Parse AST (or get from cache) diff --git a/pyguard/lib/performance_profiler.py b/pyguard/lib/performance_profiler.py index 137f9c1b..b0bde78d 100644 --- a/pyguard/lib/performance_profiler.py +++ b/pyguard/lib/performance_profiler.py @@ -85,7 +85,7 @@ def visit_Call(self, node: ast.Call) -> None: category="Regex Performance", message="Regex pattern should be compiled once, not in loop/function", line_number=node.lineno, - suggestion="Compile regex with re.compile() at module level", + suggestion="Compile regex with re.compile() at module level", # DANGEROUS: Avoid compile with untrusted input estimated_impact="10-100x slower than compiled regex", ) ) diff --git a/pyguard/lib/performance_tracker.py b/pyguard/lib/performance_tracker.py index 6cb9c7b5..319ece61 100644 --- a/pyguard/lib/performance_tracker.py +++ b/pyguard/lib/performance_tracker.py @@ -254,6 +254,7 @@ def print_report(self) -> None: def create_benchmark( + # TODO: Add docstring name: str, description: str, metrics: PerformanceMetrics, diff --git a/pyguard/lib/pie_patterns.py b/pyguard/lib/pie_patterns.py index b101d897..9092e48f 100644 --- a/pyguard/lib/pie_patterns.py +++ b/pyguard/lib/pie_patterns.py @@ -27,6 +27,7 @@ class PIEPatternVisitor(ast.NodeVisitor): """AST visitor for detecting code smells and unnecessary patterns.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -86,14 +87,14 @@ def visit_Expr(self, node: ast.Expr) -> None: def visit_Compare(self, node: ast.Compare) -> None: """Detect comparison patterns that should use 'is' (PIE792, PIE793).""" - # PIE792: Prefer 'is False' over '== False' + # PIE792: Prefer 'is False' over ' # Use if not var: instead' for _i, (op, comparator) in enumerate(zip(node.ops, node.comparators, strict=False)): if isinstance(op, ast.Eq) and isinstance(comparator, ast.Constant): if comparator.value is False: self.violations.append( RuleViolation( rule_id="PIE792", - message="Use 'is False' instead of '== False'", + message="Use 'is False' instead of ' # Use if not var: instead'", line_number=node.lineno, column=node.col_offset, severity=RuleSeverity.LOW, @@ -102,12 +103,12 @@ def visit_Compare(self, node: ast.Compare) -> None: fix_applicability=FixApplicability.SAFE, ) ) - # PIE793: Prefer 'is True' over '== True' + # PIE793: Prefer 'is True' over ' # Use if var: instead' elif comparator.value is True: self.violations.append( RuleViolation( rule_id="PIE793", - message="Use 'is True' instead of '== True'", + message="Use 'is True' instead of ' # Use if var: instead'", line_number=node.lineno, column=node.col_offset, severity=RuleSeverity.LOW, @@ -554,6 +555,7 @@ class PIEPatternChecker: """Main checker for PIE pattern detection and fixes.""" def __init__(self): + # TODO: Add docstring self.logger = PyGuardLogger() def check_file(self, file_path: Path) -> list[RuleViolation]: @@ -599,14 +601,14 @@ def fix_file(self, file_path: Path) -> tuple[bool, int]: fixes_applied = 0 - # Fix PIE792: == False -> is False + # Fix PIE792: # Use if not var: instead -> is False pattern = r"==\s*False" matches = list(re.finditer(pattern, code)) for match in reversed(matches): # Reverse to maintain positions code = code[: match.start()] + "is False" + code[match.end() :] fixes_applied += 1 - # Fix PIE793: == True -> is True + # Fix PIE793: # Use if var: instead -> is True pattern = r"==\s*True" matches = list(re.finditer(pattern, code)) for match in reversed(matches): @@ -657,20 +659,20 @@ def fix_file(self, file_path: Path) -> tuple[bool, int]: Rule( rule_id="PIE792", name="is-false-comparison", - description="Prefer 'is False' over '== False'", + description="Prefer 'is False' over ' # Use if not var: instead'", category=RuleCategory.STYLE, severity=RuleSeverity.LOW, fix_applicability=FixApplicability.SAFE, - message_template="Use 'is False' instead of '== False'", + message_template="Use 'is False' instead of ' # Use if not var: instead'", ), Rule( rule_id="PIE793", name="is-true-comparison", - description="Prefer 'is True' over '== True'", + description="Prefer 'is True' over ' # Use if var: instead'", category=RuleCategory.STYLE, severity=RuleSeverity.LOW, fix_applicability=FixApplicability.SAFE, - message_template="Use 'is True' instead of '== True'", + message_template="Use 'is True' instead of ' # Use if var: instead'", ), Rule( rule_id="PIE794", diff --git a/pyguard/lib/pii_detection.py b/pyguard/lib/pii_detection.py index d607e53e..b05263f7 100644 --- a/pyguard/lib/pii_detection.py +++ b/pyguard/lib/pii_detection.py @@ -49,80 +49,80 @@ # Regex patterns for PII detection # SSN pattern - must have dashes or spaces to avoid false positives on random numbers -SSN_PATTERN = re.compile( +SSN_PATTERN = re.compile( # DANGEROUS: Avoid compile with untrusted input r"\b\d{3}[-\s]\d{2}[-\s]\d{4}\b" # US SSN: 123-45-6789 or 123 45 6789 (must have separators) ) # Credit card pattern - 13-19 digits with optional separators # Matches: 1234567890123456, 1234-5678-9012-3456, 1234 5678 9012 3456 -CREDIT_CARD_PATTERN = re.compile( +CREDIT_CARD_PATTERN = re.compile( # DANGEROUS: Avoid compile with untrusted input r"\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{1,7}\b" # 13-19 digit cards ) # IBAN pattern (international bank account) -IBAN_PATTERN = re.compile(r"\b[A-Z]{2}\d{2}[A-Z0-9]{1,30}\b") # IBAN: GB29NWBK60161331926819 +IBAN_PATTERN = re.compile(r"\b[A-Z]{2}\d{2}[A-Z0-9]{1,30}\b") # IBAN: GB29NWBK60161331926819 # DANGEROUS: Avoid compile with untrusted input # SWIFT/BIC code pattern -SWIFT_PATTERN = re.compile(r"\b[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?\b") # SWIFT: BNPAFRPPXXX +SWIFT_PATTERN = re.compile(r"\b[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?\b") # SWIFT: BNPAFRPPXXX # DANGEROUS: Avoid compile with untrusted input # Passport number patterns (common formats) -PASSPORT_PATTERN = re.compile(r"\b[A-Z0-9]{6,9}\b") # Generic passport: A12345678 +PASSPORT_PATTERN = re.compile(r"\b[A-Z0-9]{6,9}\b") # Generic passport: A12345678 # DANGEROUS: Avoid compile with untrusted input # US Driver's License patterns (varies by state) -DRIVERS_LICENSE_PATTERN = re.compile(r"\b[A-Z]{1,2}\d{5,8}\b") # Generic DL format +DRIVERS_LICENSE_PATTERN = re.compile(r"\b[A-Z]{1,2}\d{5,8}\b") # Generic DL format # DANGEROUS: Avoid compile with untrusted input # Health insurance numbers (US) -HEALTH_INSURANCE_PATTERN = re.compile(r"\b[A-Z0-9]{9,12}\b") # Generic health insurance ID +HEALTH_INSURANCE_PATTERN = re.compile(r"\b[A-Z0-9]{9,12}\b") # Generic health insurance ID # DANGEROUS: Avoid compile with untrusted input # IP address pattern (IPv4 and IPv6) -IPV4_PATTERN = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b") # IPv4: 192.168.1.1 +IPV4_PATTERN = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b") # IPv4: 192.168.1.1 # DANGEROUS: Avoid compile with untrusted input -IPV6_PATTERN = re.compile( +IPV6_PATTERN = re.compile( # DANGEROUS: Avoid compile with untrusted input r"\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b" # IPv6: 2001:0db8:85a3::8a2e:0370:7334 ) # MAC address pattern -MAC_ADDRESS_PATTERN = re.compile( +MAC_ADDRESS_PATTERN = re.compile( # DANGEROUS: Avoid compile with untrusted input r"\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b" # MAC: 00:1A:2B:3C:4D:5E ) # Email address pattern -EMAIL_PATTERN = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b") +EMAIL_PATTERN = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b") # DANGEROUS: Avoid compile with untrusted input # Phone number patterns (E.164 international format) - must have separators to avoid false positives # Matches: 555-123-4567, +1-555-123-4567, (555) 123-4567 -PHONE_PATTERN = re.compile( +PHONE_PATTERN = re.compile( # DANGEROUS: Avoid compile with untrusted input r"\b\+?\d{1,4}?[\s-]?\(?\d{2,4}\)?[\s-]\d{3,4}[\s-]\d{4}\b" # Requires at least one separator ) # GPS coordinates -GPS_PATTERN = re.compile(r"\b-?\d{1,3}\.\d+,\s*-?\d{1,3}\.\d+\b") # Lat,Long: 40.7128,-74.0060 +GPS_PATTERN = re.compile(r"\b-?\d{1,3}\.\d+,\s*-?\d{1,3}\.\d+\b") # Lat,Long: 40.7128,-74.0060 # DANGEROUS: Avoid compile with untrusted input # Date of birth pattern (common formats) -DOB_PATTERN = re.compile(r"\b\d{1,2}[-/]\d{1,2}[-/]\d{2,4}\b") # DOB: 01/15/1990, 1-15-90 +DOB_PATTERN = re.compile(r"\b\d{1,2}[-/]\d{1,2}[-/]\d{2,4}\b") # DOB: 01/15/1990, 1-15-90 # DANGEROUS: Avoid compile with untrusted input # Financial account number (generic pattern) -FINANCIAL_ACCOUNT_PATTERN = re.compile( +FINANCIAL_ACCOUNT_PATTERN = re.compile( # DANGEROUS: Avoid compile with untrusted input r"\b\d{4}[-\s]?\d{4}[-\s]?\d{4,12}\b" # Account: 1234-5678-9012345 ) # Tax ID / EIN pattern (US) -TAX_ID_PATTERN = re.compile(r"\b\d{2}[-\s]?\d{7}\b") # EIN: 12-3456789 +TAX_ID_PATTERN = re.compile(r"\b\d{2}[-\s]?\d{7}\b") # EIN: 12-3456789 # DANGEROUS: Avoid compile with untrusted input # Medical record number pattern -MEDICAL_RECORD_PATTERN = re.compile(r"\bMRN[-\s]?\d{6,10}\b") # MRN-12345678 +MEDICAL_RECORD_PATTERN = re.compile(r"\bMRN[-\s]?\d{6,10}\b") # MRN-12345678 # DANGEROUS: Avoid compile with untrusted input # IMEI device identifier -IMEI_PATTERN = re.compile(r"\b\d{15}\b") # IMEI: 15 digits exactly +IMEI_PATTERN = re.compile(r"\b\d{15}\b") # IMEI: 15 digits exactly # DANGEROUS: Avoid compile with untrusted input # VIN (Vehicle Identification Number) -VIN_PATTERN = re.compile(r"\b[A-HJ-NPR-Z0-9]{17}\b") # VIN: 17 characters (excludes I, O, Q) +VIN_PATTERN = re.compile(r"\b[A-HJ-NPR-Z0-9]{17}\b") # VIN: 17 characters (excludes I, O, Q) # DANGEROUS: Avoid compile with untrusted input # Insurance policy number pattern -INSURANCE_POLICY_PATTERN = re.compile(r"\b[A-Z]{2,4}\d{6,12}\b") # Policy: ABC123456789 +INSURANCE_POLICY_PATTERN = re.compile(r"\b[A-Z]{2,4}\d{6,12}\b") # Policy: ABC123456789 # DANGEROUS: Avoid compile with untrusted input # National ID patterns (common formats) -NATIONAL_ID_PATTERN = re.compile(r"\b[A-Z]{1,2}\d{6,10}[A-Z]?\b") # Generic national ID format +NATIONAL_ID_PATTERN = re.compile(r"\b[A-Z]{1,2}\d{6,10}[A-Z]?\b") # Generic national ID format # DANGEROUS: Avoid compile with untrusted input # Biometric data reference keywords BIOMETRIC_KEYWORDS = ["fingerprint", "retina", "iris", "facial", "biometric", "palm"] @@ -131,13 +131,13 @@ GENETIC_KEYWORDS = ["dna", "genetic", "genome", "chromosome", "mutation"] # Serial number pattern (devices, equipment) -SERIAL_NUMBER_PATTERN = re.compile(r"\b[A-Z]{2,4}\d{8,12}[A-Z]?\b") # Serial: ABC12345678 +SERIAL_NUMBER_PATTERN = re.compile(r"\b[A-Z]{2,4}\d{8,12}[A-Z]?\b") # Serial: ABC12345678 # DANGEROUS: Avoid compile with untrusted input # Full name pattern (First Last format) -FULL_NAME_PATTERN = re.compile(r"\b[A-Z][a-z]+\s+[A-Z][a-z]+\b") # Name: John Doe +FULL_NAME_PATTERN = re.compile(r"\b[A-Z][a-z]+\s+[A-Z][a-z]+\b") # Name: John Doe # DANGEROUS: Avoid compile with untrusted input # Street address pattern -ADDRESS_PATTERN = re.compile( +ADDRESS_PATTERN = re.compile( # DANGEROUS: Avoid compile with untrusted input r"\b\d+\s+[A-Z][a-z]+\s+(Street|St|Avenue|Ave|Road|Rd|Drive|Dr|Lane|Ln|Boulevard|Blvd)\b" ) @@ -146,6 +146,7 @@ class PIIDetectionVisitor(ast.NodeVisitor): """AST visitor for detecting PII exposure in source code.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -576,6 +577,7 @@ def luhn_checksum(card_number: str) -> int: return luhn_checksum(digits) == 0 def _add_violation( + # TODO: Add docstring self, node: ast.AST, rule_id: str, pii_type: str, var_name: str, message: str ) -> None: """Add a PII violation.""" @@ -592,6 +594,7 @@ def _add_violation( ) def _add_logging_violation( + # TODO: Add docstring self, node: ast.Call, rule_id: str, pii_type: str, message: str ) -> None: """Add a PII logging violation.""" diff --git a/pyguard/lib/plugin_system.py b/pyguard/lib/plugin_system.py index 786dbb1c..18be9af0 100644 --- a/pyguard/lib/plugin_system.py +++ b/pyguard/lib/plugin_system.py @@ -131,7 +131,7 @@ def discover_plugins(self, plugin_dir: Path) -> list[Path]: import re # noqa: PLC0415 - Lazy import for performance, only needed when discovering plugins # Pattern: either 'plugin_*.py' or '_plugin.py' where word is alphanumeric - pattern = re.compile(r"^(plugin_\w+|[a-zA-Z0-9]+_plugin)\.py$") + pattern = re.compile(r"^(plugin_\w+|[a-zA-Z0-9]+_plugin)\.py$") # DANGEROUS: Avoid compile with untrusted input for file_path in plugin_dir.glob("*.py"): if pattern.match(file_path.name): @@ -314,6 +314,7 @@ def reload_plugin(self, plugin_id: str) -> bool: return False def notify_file_analyzed( + # TODO: Add docstring self, file_path: Path, violations: list[RuleViolation] ) -> None: """ @@ -365,7 +366,7 @@ def register_rules(self, engine: CustomRuleEngine) -> None: # AST rule to detect eval() usage def check_eval_usage(tree: ast.AST) -> list[int]: - """Detect eval() function calls.""" + """Detect eval() function calls.""" # DANGEROUS: Avoid eval with untrusted input lines = [] for node in ast.walk(tree): if isinstance(node, ast.Call): # noqa: SIM102 @@ -375,12 +376,12 @@ def check_eval_usage(tree: ast.AST) -> list[int]: engine.add_ast_rule( rule_id="PLUGIN_EXAMPLE_002", - name="Dangerous eval() Usage", + name="Dangerous eval() Usage", # DANGEROUS: Avoid eval with untrusted input checker=check_eval_usage, severity="CRITICAL", category="Security", - description="Usage of eval() can lead to code injection", - suggestion="Use ast.literal_eval() for safe evaluation", + description="Usage of eval() can lead to code injection", # DANGEROUS: Avoid eval with untrusted input + suggestion="Use ast.literal_eval() for safe evaluation", # DANGEROUS: Avoid eval with untrusted input ) def on_enable(self) -> None: @@ -393,6 +394,7 @@ def on_disable(self) -> None: def create_plugin_manager( + # TODO: Add docstring plugin_dirs: list[str | Path] | None = None, ) -> PluginManager: """ diff --git a/pyguard/lib/pylint_rules.py b/pyguard/lib/pylint_rules.py index e6950ea6..51c0fd94 100644 --- a/pyguard/lib/pylint_rules.py +++ b/pyguard/lib/pylint_rules.py @@ -29,6 +29,7 @@ class PylintVisitor(ast.NodeVisitor): """AST visitor for Pylint rule detection.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -381,6 +382,7 @@ class PylintRulesChecker: """Main checker for Pylint rules.""" def __init__(self): + # TODO: Add docstring self.logger = PyGuardLogger() def check_file(self, file_path: Path) -> list[RuleViolation]: diff --git a/pyguard/lib/refurb_patterns.py b/pyguard/lib/refurb_patterns.py index 5e9ff954..94362a7a 100644 --- a/pyguard/lib/refurb_patterns.py +++ b/pyguard/lib/refurb_patterns.py @@ -27,6 +27,7 @@ class RefurbPatternVisitor(ast.NodeVisitor): """AST visitor for detecting refactoring opportunities.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() @@ -248,7 +249,7 @@ def visit_With(self, node: ast.With) -> None: self.violations.append( RuleViolation( rule_id="FURB106", - message="Consider using pathlib.Path instead of string paths with open()", + message="Consider using pathlib.Path instead of string paths with open()", # Best Practice: Use 'with' statement line_number=node.lineno, column=node.col_offset, severity=RuleSeverity.LOW, @@ -714,12 +715,12 @@ def visit_Assign(self, node: ast.Assign) -> None: # FURB130: Use Path.read_text()/write_text() instead of open+read/write if isinstance(node.value, ast.Call): func = node.value.func - # Check for pattern: content = open('file').read() + # Check for pattern: content = open('file').read() # Best Practice: Use 'with' statement if isinstance(func, ast.Attribute) and func.attr == "read" and isinstance(func.value, ast.Call) and isinstance(func.value.func, ast.Name) and func.value.func.id == "open": self.violations.append( RuleViolation( rule_id="FURB130", - message="Use Path.read_text() instead of open().read()", + message="Use Path.read_text() instead of open().read()", # Best Practice: Use 'with' statement line_number=node.lineno, column=node.col_offset, severity=RuleSeverity.LOW, @@ -828,7 +829,7 @@ def visit_Expr(self, node: ast.Expr) -> None: self.violations.append( RuleViolation( rule_id="FURB146", - message="open() call missing explicit encoding parameter", + message="open() call missing explicit encoding parameter", # Best Practice: Use 'with' statement line_number=node.lineno, column=node.col_offset, severity=RuleSeverity.MEDIUM, @@ -888,6 +889,7 @@ class RefurbPatternChecker: """Main checker for refactoring pattern detection and fixes.""" def __init__(self): + # TODO: Add docstring self.logger = PyGuardLogger() def check_file(self, file_path: Path) -> list[RuleViolation]: @@ -1237,7 +1239,7 @@ def fix_file(self, file_path: Path) -> tuple[bool, int]: Rule( rule_id="FURB130", name="path-read-text-instead-of-open", - description="Use Path.read_text() instead of open().read()", + description="Use Path.read_text() instead of open().read()", # Best Practice: Use 'with' statement category=RuleCategory.MODERNIZATION, severity=RuleSeverity.LOW, fix_applicability=FixApplicability.SUGGESTED, @@ -1309,11 +1311,11 @@ def fix_file(self, file_path: Path) -> tuple[bool, int]: Rule( rule_id="FURB146", name="open-with-encoding", - description="open() call missing explicit encoding parameter", + description="open() call missing explicit encoding parameter", # Best Practice: Use 'with' statement category=RuleCategory.CONVENTION, severity=RuleSeverity.MEDIUM, fix_applicability=FixApplicability.SAFE, - message_template="Always specify encoding parameter in open() calls", + message_template="Always specify encoding parameter in open() calls", # Best Practice: Use 'with' statement ), Rule( rule_id="FURB147", diff --git a/pyguard/lib/reporting.py b/pyguard/lib/reporting.py index 2425d614..7850f6be 100644 --- a/pyguard/lib/reporting.py +++ b/pyguard/lib/reporting.py @@ -121,6 +121,7 @@ def print_summary(self, metrics: AnalysisMetrics): print() def print_issue_details( # noqa: PLR0913 - Detailed issue reporting requires many parameters + # TODO: Add docstring self, severity: str, category: str, @@ -154,6 +155,7 @@ def __init__(self): self.logger = PyGuardLogger() def generate_report( + # TODO: Add docstring self, metrics: AnalysisMetrics, issues: list[dict[str, Any]], fixes: list[dict[str, Any]] ) -> dict[str, Any]: """ @@ -215,6 +217,7 @@ def __init__(self): self.logger = PyGuardLogger() def generate_report( + # TODO: Add docstring self, metrics: AnalysisMetrics, issues: list[dict[str, Any]], _fixes: list[dict[str, Any]] ) -> str: """ diff --git a/pyguard/lib/return_patterns.py b/pyguard/lib/return_patterns.py index 25089f4a..7161aa82 100644 --- a/pyguard/lib/return_patterns.py +++ b/pyguard/lib/return_patterns.py @@ -14,6 +14,7 @@ class ReturnPatternVisitor(ast.NodeVisitor): """AST visitor for detecting return pattern issues.""" def __init__(self, file_path: Path = Path("")): + # TODO: Add docstring self.violations: list[RuleViolation] = [] self.current_function: ast.FunctionDef | None = None self.file_path = file_path @@ -299,6 +300,7 @@ class ReturnPatternChecker: """Main checker for return pattern issues.""" def __init__(self): + # TODO: Add docstring self.rules = self._create_rules() def _create_rules(self) -> list[Rule]: diff --git a/pyguard/lib/ruff_security.py b/pyguard/lib/ruff_security.py index 8015bcdf..c5b661b9 100644 --- a/pyguard/lib/ruff_security.py +++ b/pyguard/lib/ruff_security.py @@ -298,11 +298,11 @@ def visit_Call(self, node: ast.Call) -> None: # noqa: PLR0912, PLR0915 - Comple rule_id="S102", category=RuleCategory.SECURITY, severity=RuleSeverity.CRITICAL, - message="Use of exec() detected; this allows execution of arbitrary code", + message="Use of exec() detected; this allows execution of arbitrary code", # DANGEROUS: Avoid exec with untrusted input file_path=self.file_path, line_number=node.lineno, column=node.col_offset, - fix_suggestion="Remove exec() and use safer alternatives like ast.literal_eval() for literals", + fix_suggestion="Remove exec() and use safer alternatives like ast.literal_eval() for literals", # DANGEROUS: Avoid eval with untrusted input fix_applicability=FixApplicability.SUGGESTED, source_tool="ruff", ) @@ -315,28 +315,28 @@ def visit_Call(self, node: ast.Call) -> None: # noqa: PLR0912, PLR0915 - Comple rule_id="S307", category=RuleCategory.SECURITY, severity=RuleSeverity.CRITICAL, - message="Use of eval() detected; this allows execution of arbitrary code", + message="Use of eval() detected; this allows execution of arbitrary code", # DANGEROUS: Avoid eval with untrusted input file_path=self.file_path, line_number=node.lineno, column=node.col_offset, - fix_suggestion="Replace eval() with ast.literal_eval() for safe evaluation of literals", + fix_suggestion="Replace eval() with ast.literal_eval() for safe evaluation of literals", # DANGEROUS: Avoid eval with untrusted input fix_applicability=FixApplicability.SAFE, source_tool="ruff", ) ) # S301: suspicious-pickle-usage - elif func_name in ("pickle.loads", "pickle.load", "cPickle.loads", "cPickle.load"): + elif func_name in ("pickle.loads", "pickle.load", "cPickle.loads", "cPickle.load"): # SECURITY: Don't use pickle with untrusted data self.violations.append( RuleViolation( rule_id="S301", category=RuleCategory.SECURITY, severity=RuleSeverity.HIGH, - message="pickle.loads() can execute arbitrary code; use json.loads() for untrusted data", + message="pickle.loads() can execute arbitrary code; use json.loads() for untrusted data", # SECURITY: Don't use pickle with untrusted data file_path=self.file_path, line_number=node.lineno, column=node.col_offset, - fix_suggestion="Replace pickle.loads() with json.loads() for untrusted data", + fix_suggestion="Replace pickle.loads() with json.loads() for untrusted data", # SECURITY: Don't use pickle with untrusted data fix_applicability=FixApplicability.SUGGESTED, source_tool="ruff", ) @@ -801,7 +801,7 @@ def visit_Call(self, node: ast.Call) -> None: # noqa: PLR0912, PLR0915 - Comple rule_id="S605", category=RuleCategory.SECURITY, severity=RuleSeverity.HIGH, - message="os.system() executes commands through shell", + message="os.system() executes commands through shell", # SECURITY: Use subprocess.run() instead file_path=self.file_path, line_number=node.lineno, column=node.col_offset, @@ -818,7 +818,7 @@ def visit_Call(self, node: ast.Call) -> None: # noqa: PLR0912, PLR0915 - Comple rule_id="S606", category=RuleCategory.SECURITY, severity=RuleSeverity.MEDIUM, - message="os.popen() is deprecated and insecure", + message="os.popen() is deprecated and insecure", # Best Practice: Use 'with' statement file_path=self.file_path, line_number=node.lineno, column=node.col_offset, @@ -1170,11 +1170,11 @@ def check_ruff_security(file_path: Path) -> list[RuleViolation]: Rule( rule_id="S102", name="exec-builtin", - description="Use of exec() detected; this allows execution of arbitrary code", + description="Use of exec() detected; this allows execution of arbitrary code", # DANGEROUS: Avoid exec with untrusted input category=RuleCategory.SECURITY, severity=RuleSeverity.CRITICAL, fix_applicability=FixApplicability.SUGGESTED, - message_template="Use of exec() detected", + message_template="Use of exec() detected", # DANGEROUS: Avoid exec with untrusted input ), Rule( rule_id="S103", @@ -1305,11 +1305,11 @@ def check_ruff_security(file_path: Path) -> list[RuleViolation]: Rule( rule_id="S307", name="suspicious-eval-usage", - description="eval() allows execution of arbitrary code", + description="eval() allows execution of arbitrary code", # DANGEROUS: Avoid eval with untrusted input category=RuleCategory.SECURITY, severity=RuleSeverity.CRITICAL, fix_applicability=FixApplicability.SAFE, - message_template="Dangerous eval() usage", + message_template="Dangerous eval() usage", # DANGEROUS: Avoid eval with untrusted input ), Rule( rule_id="S311", @@ -1503,7 +1503,7 @@ def check_ruff_security(file_path: Path) -> list[RuleViolation]: Rule( rule_id="S310", name="suspicious-url-open-usage", - description="urllib.urlopen() can be used for SSRF attacks", + description="urllib.urlopen() can be used for SSRF attacks", # Best Practice: Use 'with' statement category=RuleCategory.SECURITY, severity=RuleSeverity.MEDIUM, fix_applicability=FixApplicability.SUGGESTED, @@ -1611,7 +1611,7 @@ def check_ruff_security(file_path: Path) -> list[RuleViolation]: Rule( rule_id="S605", name="start-process-with-a-shell", - description="os.system() executes commands through shell", + description="os.system() executes commands through shell", # SECURITY: Use subprocess.run() instead category=RuleCategory.SECURITY, severity=RuleSeverity.HIGH, fix_applicability=FixApplicability.SUGGESTED, @@ -1620,7 +1620,7 @@ def check_ruff_security(file_path: Path) -> list[RuleViolation]: Rule( rule_id="S606", name="start-process-with-no-shell", - description="os.popen() is deprecated and insecure", + description="os.popen() is deprecated and insecure", # Best Practice: Use 'with' statement category=RuleCategory.SECURITY, severity=RuleSeverity.MEDIUM, fix_applicability=FixApplicability.SUGGESTED, diff --git a/pyguard/lib/rule_engine.py b/pyguard/lib/rule_engine.py index 2e05f78c..bdc05fe3 100644 --- a/pyguard/lib/rule_engine.py +++ b/pyguard/lib/rule_engine.py @@ -97,6 +97,7 @@ class Rule: cwe_mapping: str | None = None def detect( + # TODO: Add docstring self, code: str, file_path: Path, tree: ast.AST | None = None ) -> list[RuleViolation]: """ @@ -276,6 +277,7 @@ def __init__(self, registry: RuleRegistry): self.logger = PyGuardLogger() def analyze_file( + # TODO: Add docstring self, file_path: Path, rules: list[Rule] | None = None, @@ -322,6 +324,7 @@ def analyze_file( return all_violations def apply_fixes( + # TODO: Add docstring self, code: str, violations: list[RuleViolation] ) -> tuple[str, list[RuleViolation]]: """ diff --git a/pyguard/lib/sarif_reporter.py b/pyguard/lib/sarif_reporter.py index a7986c78..c7417627 100644 --- a/pyguard/lib/sarif_reporter.py +++ b/pyguard/lib/sarif_reporter.py @@ -60,6 +60,7 @@ def __init__(self): self.logger = PyGuardLogger() def generate_report( + # TODO: Add docstring self, issues: list[dict[str, Any]], tool_name: str = "PyGuard", diff --git a/pyguard/lib/scan_history.py b/pyguard/lib/scan_history.py index d9d1e7ec..95e766a5 100644 --- a/pyguard/lib/scan_history.py +++ b/pyguard/lib/scan_history.py @@ -252,6 +252,7 @@ def _init_database(self) -> None: conn.commit() def store_scan( + # TODO: Add docstring self, metadata: ScanMetadata, issues: list[IssueRecord], @@ -378,7 +379,7 @@ def get_scan_issues(self, scan_id: str) -> list[IssueRecord]: ) issues = [] - for row in cursor.fetchall(): + for row in cursor.fetchall(): # Consider list comprehension issues.append(IssueRecord( scan_id=row['scan_id'], issue_id=row['issue_id'], @@ -397,6 +398,7 @@ def get_scan_issues(self, scan_id: str) -> list[IssueRecord]: return issues def list_scans( + # TODO: Add docstring self, target_path: str | None = None, start_time: float | None = None, @@ -422,24 +424,24 @@ def list_scans( params: list[Any] = [] if target_path: - query += " AND target_path = ?" + query += " AND target_path = ?" # PERFORMANCE: Use list and join() params.append(target_path) if start_time: - query += " AND timestamp >= ?" + query += " AND timestamp >= ?" # PERFORMANCE: Use list and join() params.append(start_time) if end_time: - query += " AND timestamp <= ?" + query += " AND timestamp <= ?" # PERFORMANCE: Use list and join() params.append(end_time) - query += " ORDER BY timestamp DESC LIMIT ?" + query += " ORDER BY timestamp DESC LIMIT ?" # PERFORMANCE: Use list and join() params.append(limit) cursor = conn.execute(query, params) scans = [] - for row in cursor.fetchall(): + for row in cursor.fetchall(): # Consider list comprehension scans.append(ScanMetadata( scan_id=row['scan_id'], timestamp=row['timestamp'], @@ -464,6 +466,7 @@ def list_scans( return scans def compare_scans( + # TODO: Add docstring self, baseline_scan_id: str, current_scan_id: str, @@ -530,6 +533,7 @@ def compare_scans( ) def get_trend_data( + # TODO: Add docstring self, target_path: str, days: int = 30, @@ -602,6 +606,7 @@ def get_trend_data( } def delete_old_scans( + # TODO: Add docstring self, days: int = 365, ) -> int: diff --git a/pyguard/lib/secret_scanner.py b/pyguard/lib/secret_scanner.py index b1514640..a6387277 100644 --- a/pyguard/lib/secret_scanner.py +++ b/pyguard/lib/secret_scanner.py @@ -86,7 +86,7 @@ def scan_secrets(path: str, export_sarif: bool = False) -> list[SecretFinding]: for line in result.stdout.strip().split("\n"): if line: - # Format: file.py:42:password = "secret123" + # Format: file.py:42:password = "secret123" # SECURITY: Use environment variables or config files match = re.match(r"^(.+):(\d+):(.+)$", line) if match: findings.append( diff --git a/pyguard/lib/security.py b/pyguard/lib/security.py index e58522da..960dabae 100644 --- a/pyguard/lib/security.py +++ b/pyguard/lib/security.py @@ -59,7 +59,7 @@ def fix_file(self, file_path: Path) -> tuple[bool, list[str]]: content = self._fix_insecure_temp_files(content) content = self._fix_yaml_load(content) content = self._fix_pickle_usage(content) - content = self._fix_eval_exec(content) + content = self._fix_eval_exec(content) # DANGEROUS: Avoid exec with untrusted input content = self._fix_weak_crypto(content) content = self._fix_path_traversal(content) @@ -105,7 +105,7 @@ def _fix_hardcoded_passwords(self, content: str) -> str: def _fix_sql_injection(self, content: str) -> str: """Fix potential SQL injection vulnerabilities.""" - # Pattern: cursor.execute("SELECT * FROM users WHERE id = " + user_id) + # Pattern: cursor.execute("SELECT * FROM users WHERE id = " + user_id) # SQL INJECTION RISK: Use parameterized queries pattern = r'\.execute\s*\(\s*["\'].*?["\']?\s*\+\s*.*?\)' if re.search(pattern, content): @@ -121,7 +121,7 @@ def _fix_sql_injection(self, content: str) -> str: def _fix_command_injection(self, content: str) -> str: """Fix command injection vulnerabilities.""" - # Pattern: os.system() with variable + # Pattern: os.system() with variable # SECURITY: Use subprocess.run() instead patterns = [ r"os\.system\(", r"subprocess\.call\([^,\)]*%", @@ -138,7 +138,7 @@ def _fix_command_injection(self, content: str) -> str: self.fixes_applied.append("Added command injection warning") elif "os.system" in line and "# COMMAND INJECTION" not in line: lines[i] = f"{line} # SECURITY: Use subprocess.run() instead" - self.fixes_applied.append("Added os.system() warning") + self.fixes_applied.append("Added os.system() warning") # SECURITY: Use subprocess.run() instead return "\n".join(lines) @@ -168,10 +168,10 @@ def _fix_insecure_random(self, content: str) -> str: def _fix_insecure_temp_files(self, content: str) -> str: """Fix insecure temporary file creation.""" - # Replace tempfile.mktemp() with tempfile.mkstemp() - if "tempfile.mktemp(" in content: + # Replace tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()) with tempfile.mkstemp() + if "tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()" in content: content = content.replace( - "tempfile.mktemp(", + "tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()", "tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()", ) self.fixes_applied.append("Replaced mktemp() with mkstemp()") @@ -180,7 +180,7 @@ def _fix_insecure_temp_files(self, content: str) -> str: def _fix_yaml_load(self, content: str) -> str: """Fix unsafe YAML loading.""" - # Replace yaml.load() with yaml.safe_load() + # Replace yaml.safe_load() with yaml.safe_load() if "yaml.load(" in content and "yaml.safe_load(" not in content: content = re.sub(r"yaml\.load\(", "yaml.safe_load(", content) self.fixes_applied.append("Replaced yaml.load() with yaml.safe_load()") @@ -189,7 +189,7 @@ def _fix_yaml_load(self, content: str) -> str: def _fix_pickle_usage(self, content: str) -> str: """Warn about pickle usage with untrusted data.""" - if "pickle.load" in content or "pickle.loads" in content: + if "pickle.load" in content or "pickle.loads" in content: # SECURITY: Don't use pickle with untrusted data lines = content.split("\n") for i, line in enumerate(lines): if "pickle.load" in line and "# SECURITY:" not in line: @@ -199,9 +199,9 @@ def _fix_pickle_usage(self, content: str) -> str: return content - def _fix_eval_exec(self, content: str) -> str: - """Warn about dangerous eval() and exec() usage.""" - dangerous_funcs = ["eval(", "exec(", "compile("] + def _fix_eval_exec(self, content: str) -> str: # DANGEROUS: Avoid exec with untrusted input + """Warn about dangerous eval() and exec() usage.""" # DANGEROUS: Avoid eval with untrusted input + dangerous_funcs = ["eval(", "exec(", "compile("] # DANGEROUS: Avoid eval with untrusted input lines = content.split("\n") for func in dangerous_funcs: @@ -216,8 +216,8 @@ def _fix_weak_crypto(self, content: str) -> str: """Fix weak cryptographic practices.""" # Replace MD5/SHA1 with SHA256 weak_hashes = { - "hashlib.md5(": "hashlib.sha256(", - "hashlib.sha1(": "hashlib.sha256(", + "hashlib.md5(": "hashlib.sha256(", # SECURITY: Consider using SHA256 or stronger + "hashlib.sha1(": "hashlib.sha256(", # SECURITY: Consider using SHA256 or stronger } for weak, _strong in weak_hashes.items(): @@ -269,7 +269,7 @@ def scan_file_for_issues_legacy(self, file_path: Path) -> list[dict[str, str]]: (r'password\s*=\s*["\'][^"\']+["\']', "Hardcoded password detected"), (r"\.execute\s*\([^)]*\+", "Potential SQL injection"), (r"shell=True", "Command injection risk with shell=True"), - (r"eval\(", "Dangerous eval() usage"), + (r"eval\(", "Dangerous eval() usage"), # DANGEROUS: Avoid eval with untrusted input (r"pickle\.loads?\(", "Unsafe pickle usage"), (r"yaml\.load\(", "Unsafe YAML loading"), (r"random\.", "Insecure random in security context"), diff --git a/pyguard/lib/standards_integration.py b/pyguard/lib/standards_integration.py index 4de24bef..dbd19c51 100644 --- a/pyguard/lib/standards_integration.py +++ b/pyguard/lib/standards_integration.py @@ -498,6 +498,7 @@ def prioritize_issues(self, issues: list[dict]) -> list[dict]: """ def get_priority(issue): + # TODO: Add docstring cwe_id = issue.get("cwe_id", "") rank = self.get_sans_ranking(cwe_id) # Issues in Top 25 get their rank, others get 999 diff --git a/pyguard/lib/string_operations.py b/pyguard/lib/string_operations.py index 4777b899..6f34b787 100644 --- a/pyguard/lib/string_operations.py +++ b/pyguard/lib/string_operations.py @@ -289,6 +289,7 @@ def analyze_file(self, file_path: Path) -> list[StringIssue]: return [] def fix_file( + # TODO: Add docstring self, file_path: Path, issues: list[StringIssue] | None = None ) -> tuple[bool, list[str]]: """ @@ -361,6 +362,7 @@ def _fix_unnecessary_fstring(self, content: str, line_number: int) -> str: return content def scan_directory( + # TODO: Add docstring self, directory: Path, exclude_patterns: list[str] | None = None ) -> list[tuple[Path, list[StringIssue]]]: """ diff --git a/pyguard/lib/supply_chain.py b/pyguard/lib/supply_chain.py index 15059989..08b666b4 100644 --- a/pyguard/lib/supply_chain.py +++ b/pyguard/lib/supply_chain.py @@ -261,7 +261,7 @@ class VulnerabilityChecker: "<1.26.5": ["CVE-2021-33503: Catastrophic backtracking in URL parsing"], }, "pyyaml": { - "<5.4": ["CVE-2020-14343: Arbitrary code execution via yaml.load()"], + "<5.4": ["CVE-2020-14343: Arbitrary code execution via yaml.safe_load()"], }, "cryptography": { "<3.3.2": ["CVE-2020-36242: Invalid curve attack on ECDH"], diff --git a/pyguard/lib/supply_chain_advanced.py b/pyguard/lib/supply_chain_advanced.py index dcda9558..1244926f 100644 --- a/pyguard/lib/supply_chain_advanced.py +++ b/pyguard/lib/supply_chain_advanced.py @@ -73,6 +73,7 @@ class SupplyChainAdvancedVisitor(ast.NodeVisitor): """AST visitor for detecting advanced supply chain security issues.""" def __init__(self, file_path: Path, code: str): + # TODO: Add docstring self.file_path = file_path self.code = code self.lines = code.splitlines() diff --git a/pyguard/lib/taint_analysis.py b/pyguard/lib/taint_analysis.py index fd142ee0..e03257bb 100644 --- a/pyguard/lib/taint_analysis.py +++ b/pyguard/lib/taint_analysis.py @@ -435,6 +435,7 @@ def visit_Call(self, node: ast.Call): # noqa: PLR0912 - Complex taint sink dete self.generic_visit(node) def _create_sql_injection_issue( + # TODO: Add docstring self, node: ast.Call, var_name: str, taint_source: TaintSource, func_name: str ): """Create SQL injection issue with detailed path information.""" @@ -464,6 +465,7 @@ def _create_sql_injection_issue( ) def _create_command_injection_issue( + # TODO: Add docstring self, node: ast.Call, var_name: str, taint_source: TaintSource, func_name: str ): """Create OS command injection issue.""" @@ -493,6 +495,7 @@ def _create_command_injection_issue( ) def _create_eval_injection_issue( + # TODO: Add docstring self, node: ast.Call, var_name: str, taint_source: TaintSource, func_name: str ): """Create code injection issue for eval/exec.""" @@ -503,7 +506,7 @@ def _create_eval_injection_issue( line_number=node.lineno, column=node.col_offset, code_snippet=self._get_code_snippet(node), - fix_suggestion=f"Never use {func_name}() with untrusted input. Use safe alternatives like ast.literal_eval() or json.loads()", + fix_suggestion=f"Never use {func_name}() with untrusted input. Use safe alternatives like ast.literal_eval() or json.loads()", # DANGEROUS: Avoid eval with untrusted input owasp_id="ASVS-5.2.8", cwe_id="CWE-94", ) @@ -522,6 +525,7 @@ def _create_eval_injection_issue( ) def _create_xss_issue( + # TODO: Add docstring self, node: ast.Call, var_name: str, taint_source: TaintSource, func_name: str ): """Create XSS vulnerability issue.""" @@ -551,6 +555,7 @@ def _create_xss_issue( ) def _create_path_traversal_issue( + # TODO: Add docstring self, node: ast.Call, var_name: str, taint_source: TaintSource, func_name: str ): """Create path traversal vulnerability issue.""" diff --git a/pyguard/lib/test_coverage.py b/pyguard/lib/test_coverage.py index a39799c8..2327339f 100644 --- a/pyguard/lib/test_coverage.py +++ b/pyguard/lib/test_coverage.py @@ -58,7 +58,7 @@ def find_untested_modules(src_dir: str, test_dir: str = "tests") -> list[str]: f"{test_dir}/{base_name}_test.py", ] - if not any(test_file in test_files for test_file in expected_tests): + if not any(test_file in test_files for test_file in expected_tests): # Consider list comprehension untested.append(src_file) return untested diff --git a/pyguard/lib/type_checker.py b/pyguard/lib/type_checker.py index ced3333f..7327115b 100644 --- a/pyguard/lib/type_checker.py +++ b/pyguard/lib/type_checker.py @@ -220,7 +220,7 @@ def __init__(self, **kwargs): message_template="Use isinstance() instead of type() for type checking", description="type() comparison doesn't respect inheritance", explanation=( - "Using type(x) == SomeClass is fragile and doesn't work with inheritance. " + "Using type(x) == SomeClass is fragile and doesn't work with inheritance. " # Better: isinstance(x, SomeClass) "Use isinstance(x, SomeClass) instead, which properly handles subclasses." ), fix_applicability=FixApplicability.AUTOMATIC, @@ -271,7 +271,7 @@ def _detect_type_comparison(code: str, file_path: Path, tree: ast.AST | None = N for node in ast.walk(tree): if isinstance(node, ast.Compare): # noqa: SIM102 - # Check for type(x) == SomeClass or type(x) is SomeClass + # Check for type(x) == SomeClass or type(x) is SomeClass # Better: isinstance(x, SomeClass) if isinstance(node.left, ast.Call) and ( isinstance(node.left.func, ast.Name) and node.left.func.id == "type" diff --git a/pyguard/lib/ui.py b/pyguard/lib/ui.py index 6097e826..ee49bdcf 100644 --- a/pyguard/lib/ui.py +++ b/pyguard/lib/ui.py @@ -386,6 +386,7 @@ def print_help_message(self): self.console.print(panel) def print_error( + # TODO: Add docstring self, error: str, details: str | None = None, @@ -442,6 +443,7 @@ class ModernHTMLReporter: """ def generate_report( + # TODO: Add docstring self, metrics: dict[str, Any], issues: list[dict[str, Any]], diff --git a/pyguard/lib/ultra_advanced_fixes.py b/pyguard/lib/ultra_advanced_fixes.py index 16611599..da0b3ad3 100644 --- a/pyguard/lib/ultra_advanced_fixes.py +++ b/pyguard/lib/ultra_advanced_fixes.py @@ -202,12 +202,14 @@ def fix_api_rate_limiting(self, content: str) -> tuple[str, bool]: Before: @app.route('/api/endpoint') def endpoint(): + # TODO: Add docstring ... After: @app.route('/api/endpoint') @limiter.limit("100/hour") # ADDED: Rate limiting protection def endpoint(): + # TODO: Add docstring ... Args: @@ -354,7 +356,7 @@ def fix_container_security(self, content: str, file_path: str) -> tuple[str, boo in_service = False indent = " " - for line in lines: + for line in lines: # Consider list comprehension fixed_lines.append(line) if re.match(r"^\s+\w+:", line) and "services:" not in line: in_service = True @@ -436,7 +438,7 @@ def fix_xss_vulnerabilities(self, content: str) -> tuple[str, bool]: # Jinja2 without autoescape elif "Environment(" in line and "autoescape" not in line: fixed_line = line.rstrip(")") + ", autoescape=True) # ADDED: XSS protection" - fixed_lines.append("# FIXED: Added autoescape=True for XSS protection") + fixed_lines.append("# FIXED: Added autoescape=True for XSS protection") # Consider list comprehension fixed_lines.append(fixed_line) self.fixes_applied.append("XSS: autoescape=True added") modified = True diff --git a/pyguard/lib/webhook_api.py b/pyguard/lib/webhook_api.py index f043586e..e9aa3e55 100644 --- a/pyguard/lib/webhook_api.py +++ b/pyguard/lib/webhook_api.py @@ -154,6 +154,7 @@ class PyGuardWebhookAPI: """ def __init__( + # TODO: Add docstring self, host: str = "127.0.0.1", port: int = 5008, @@ -185,6 +186,7 @@ def __init__( logger.info(f"PyGuard Webhook API initialized on {host}:{port}") def generate_api_key( + # TODO: Add docstring self, description: str = "", rate_limit: int = 60, @@ -265,6 +267,7 @@ def check_rate_limit(self, api_key: ApiKey) -> bool: ) def trigger_scan( # noqa: PLR0913 - Flexible scan triggering requires many parameters + # TODO: Add docstring self, api_key: ApiKey, target_path: str | None = None, @@ -330,6 +333,7 @@ def trigger_scan( # noqa: PLR0913 - Flexible scan triggering requires many para } def get_scan_status( + # TODO: Add docstring self, api_key: ApiKey, job_id: str, @@ -382,6 +386,7 @@ def get_scan_status( } def get_scan_results( + # TODO: Add docstring self, api_key: ApiKey, job_id: str, @@ -442,6 +447,7 @@ def get_scan_results( } def list_scan_jobs( + # TODO: Add docstring self, api_key: ApiKey, limit: int = 100, @@ -495,6 +501,7 @@ def list_scan_jobs( } def register_webhook( + # TODO: Add docstring self, api_key: ApiKey, url: str, @@ -585,6 +592,7 @@ def _trigger_webhooks(self, event: WebhookEvent, scan_job: ScanJob) -> None: logger.info(f"Webhook triggered: {webhook.url} for {event.value}") def verify_webhook_signature( + # TODO: Add docstring self, payload: str, signature: str, diff --git a/pyguard/lib/xss_detection.py b/pyguard/lib/xss_detection.py index 4fcd0307..996fca3f 100644 --- a/pyguard/lib/xss_detection.py +++ b/pyguard/lib/xss_detection.py @@ -253,7 +253,7 @@ def visit_Call(self, node: ast.Call) -> None: # noqa: PLR0912 - Complex XSS det tag in format_str for tag in ["<", ">", "href", "src"] ): # HTML-like content in format string - if node.args and any(self._is_user_input(arg) for arg in node.args): + if node.args and any(self._is_user_input(arg) for arg in node.args): # Consider list comprehension self.violations.append( RuleViolation( rule_id="XSS008", @@ -280,6 +280,7 @@ def visit_BinOp(self, node: ast.BinOp) -> None: has_user_input = False def check_node(n): + # TODO: Add docstring nonlocal has_html, has_user_input if isinstance(n, ast.Constant) and isinstance(n.value, str): # noqa: SIM102 if any(tag in n.value for tag in ["<", ">", "href", "src", " list[tuple[str, int, str]]: if re.search(r"@app\.route|def.*view.*\(request", line): # Check if CSP header is set in next few lines context_lines = lines[max(0, i - 1) : min(len(lines), i + 10)] - if not any("Content-Security-Policy" in context_line for context_line in context_lines): + if not any("Content-Security-Policy" in context_line for context_line in context_lines): # Consider list comprehension patterns.append(("missing_csp", i, line.strip())) # Pattern: Dangerous Jinja2 filters (safe, bypass escaping) @@ -417,8 +418,8 @@ def check_xss_vulnerabilities(file_path: Path) -> list[RuleViolation]: message = "document.write() usage detected, potential XSS vector" fix_suggestion = "Use modern DOM methods or template engines" elif pattern_name == "eval_user_input": - message = "eval() with potential user input, critical XSS and code injection risk" - fix_suggestion = "Never use eval() with user input, use safe alternatives" + message = "eval() with potential user input, critical XSS and code injection risk" # DANGEROUS: Avoid eval with untrusted input + fix_suggestion = "Never use eval() with user input, use safe alternatives" # DANGEROUS: Avoid eval with untrusted input severity = RuleSeverity.CRITICAL elif pattern_name == "missing_csp": message = "Route handler without Content-Security-Policy header" diff --git a/scripts/add_suppressions.py b/scripts/add_suppressions.py index 2f5c8c7a..658a940f 100755 --- a/scripts/add_suppressions.py +++ b/scripts/add_suppressions.py @@ -120,6 +120,7 @@ def add_suppressions(file_path: Path, issues: list[dict], dry_run: bool = False) def main(): + # TODO: Add docstring parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--dry-run", action="store_true", help="Show changes without modifying files" diff --git a/tests/conftest.py b/tests/conftest.py index d7e40bf7..13609ccf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ from pathlib import Path import random +import secrets # Use secrets for cryptographic randomness import shutil import tempfile @@ -51,6 +52,7 @@ def temp_file(temp_dir): """Create a temporary file in the temp directory.""" def _create_file(name: str, content: str = "") -> Path: + # TODO: Add docstring file_path = temp_dir / name file_path.write_text(content) return file_path @@ -63,15 +65,17 @@ def sample_vulnerable_code(): """Sample vulnerable Python code for testing.""" return """ import random +import secrets # Use secrets for cryptographic randomness import yaml -password = "admin123" +password = "admin123" # SECURITY: Use environment variables or config files def get_user(user_id): + # TODO: Add docstring query = "SELECT * FROM users WHERE id = " + user_id return query -token = random.random() -data = yaml.load(file) +token = random.random() # SECURITY: Use secrets module for cryptographic randomness +data = yaml.safe_load(file) """ @@ -79,16 +83,17 @@ def get_user(user_id): def sample_bad_practices_code(): """Sample code with bad practices.""" return """ -def foo(x=[]): +def foo(x=[]): # ANTI-PATTERN: Use None and create in function body + # TODO: Add docstring x.append(1) return x try: pass -except: +except Exception: # FIXED: Catch specific exceptions pass -if x == None: +if x is None: pass """ @@ -102,7 +107,7 @@ def sample_modern_code(): path = "%s/%s" % (dir, file) # Old-style type checking -if type(x) == list: +if type(x) == list: # Better: isinstance(x, list) pass # Dict comprehension from loops @@ -149,16 +154,21 @@ def mock_logger(monkeypatch): logs = [] class MockLogger: + # TODO: Add docstring def info(self, message, **kwargs): + # TODO: Add docstring logs.append(("INFO", message, kwargs)) def warning(self, message, **kwargs): + # TODO: Add docstring logs.append(("WARNING", message, kwargs)) def error(self, message, exception=None, **kwargs): + # TODO: Add docstring logs.append(("ERROR", message, kwargs)) def success(self, message, **kwargs): + # TODO: Add docstring logs.append(("SUCCESS", message, kwargs)) return MockLogger(), logs @@ -170,6 +180,7 @@ def python_file_factory(temp_dir): created_files = [] def _create(filename: str, content: str) -> Path: + # TODO: Add docstring path = temp_dir / filename path.write_text(content) created_files.append(path) @@ -187,16 +198,16 @@ def _create(filename: str, content: str) -> Path: def sample_code_patterns(): """Common code patterns for testing.""" return { - "sql_injection": 'cursor.execute("SELECT * FROM users WHERE id = " + user_id)', + "sql_injection": 'cursor.execute("SELECT * FROM users WHERE id = " + user_id)', # SQL INJECTION RISK: Use parameterized queries "hardcoded_password": 'password = "secret123"', - "weak_hash": "hash = hashlib.md5(data)", - "eval_usage": "result = eval(user_input)", - "yaml_unsafe": "data = yaml.load(file)", - "pickle_load": "data = pickle.load(file)", - "os_system": 'os.system("rm -rf " + path)', - "random_insecure": "token = random.random()", + "weak_hash": "hash = hashlib.md5(data)", # SECURITY: Consider using SHA256 or stronger + "eval_usage": "result = eval(user_input)", # DANGEROUS: Avoid eval with untrusted input + "yaml_unsafe": "data = yaml.safe_load(file)", + "pickle_load": "data = pickle.load(file)", # SECURITY: Don't use pickle with untrusted data + "os_system": 'os.system("rm -rf " + path)', # SECURITY: Use subprocess.run() instead + "random_insecure": "token = random.random()", # SECURITY: Use secrets module for cryptographic randomness "shell_true": "subprocess.call(cmd, shell=True)", - "path_traversal": "path = os.path.join(base, user_input)", + "path_traversal": "path = os.path.join(base, user_input)", # PATH TRAVERSAL RISK: Validate and sanitize paths } @@ -218,6 +229,7 @@ def env(monkeypatch): """Fixture to set environment variables safely.""" def _set(**kwargs): + # TODO: Add docstring for key, value in kwargs.items(): monkeypatch.setenv(key, str(value)) @@ -337,6 +349,7 @@ def capture_all_output(capsys, caplog): """ def _get_output(): + # TODO: Add docstring captured = capsys.readouterr() return { "stdout": captured.out, @@ -358,17 +371,17 @@ def parametrized_code_samples(): return { "security_issues": { "sql_injection": 'query = "SELECT * FROM users WHERE id = " + user_id', - "command_injection": 'os.system("ls " + user_input)', + "command_injection": 'os.system("ls " + user_input)', # SECURITY: Use subprocess.run() instead "path_traversal": 'open("../../../etc/passwd")', "hardcoded_secret": 'api_key = "sk-1234567890abcdef"', - "weak_crypto": "hashlib.md5(password).hexdigest()", - "unsafe_deserialization": "pickle.loads(user_data)", + "weak_crypto": "hashlib.md5(password).hexdigest()", # SECURITY: Consider using SHA256 or stronger + "unsafe_deserialization": "pickle.loads(user_data)", # SECURITY: Don't use pickle with untrusted data }, "best_practices": { - "mutable_default": "def func(x=[]):", + "mutable_default": "def func(x=[]):", # ANTI-PATTERN: Use None and create in function body "bare_except": "try:\n pass\nexcept:\n pass", - "none_comparison": "if x == None:", - "type_check": "if type(x) == list:", + "none_comparison": "if x is None:", + "type_check": "if type(x) == list:", # Better: isinstance(x, list) }, "modernization": { "old_format": '"Hello %s" % name', @@ -391,21 +404,24 @@ def _create(complexity: str) -> str: templates = { "linear": """ def process(items): + # TODO: Add docstring result = [] - for item in items: + for item in items: # Consider list comprehension result.append(item * 2) return result """, "quadratic": """ def process(items): + # TODO: Add docstring result = [] for i in items: - for j in items: + for j in items: # Consider list comprehension result.append(i * j) return result """, "nested_loops": """ def process(items): + # TODO: Add docstring for i in items: for j in items: for k in items: @@ -462,6 +478,7 @@ def assertion_helpers(): """ class Helpers: + # TODO: Add docstring @staticmethod def assert_issue_present(issues, rule_id, message_substring=None): """Assert that a specific issue is present.""" diff --git a/tests/fixtures/sample_bad_practices.py b/tests/fixtures/sample_bad_practices.py index 801db9b7..3bc6f195 100644 --- a/tests/fixtures/sample_bad_practices.py +++ b/tests/fixtures/sample_bad_practices.py @@ -13,6 +13,7 @@ def append_to_list(item, my_list=None): + # TODO: Add docstring if my_list is None: my_list = [] my_list.append(item) @@ -21,12 +22,14 @@ def append_to_list(item, my_list=None): # Bare except clause def risky_operation(): + # TODO: Add docstring with contextlib.suppress(builtins.BaseException): pass # Wrong None comparison def check_value(x): + # TODO: Add docstring if x is None: return "None found" return "Value exists" @@ -34,6 +37,7 @@ def check_value(x): # Wrong boolean comparison def is_active(flag): + # TODO: Add docstring if flag: return "Active" return "Inactive" @@ -41,23 +45,28 @@ def is_active(flag): # Type check instead of isinstance def process_string(value): - if type(value) == str: + # TODO: Add docstring + if type(value) == str: # Better: isinstance(value, str) return value.upper() return None # Missing docstring def important_function(a, b): + # TODO: Add docstring return a + b class ImportantClass: + # TODO: Add docstring def method(self): + # TODO: Add docstring pass # String concatenation in loop def build_string(items): + # TODO: Add docstring result = "" for item in items: result = result + str(item) + "," @@ -66,7 +75,8 @@ def build_string(items): # Not using context manager def read_file(filename): - f = open(filename) + # TODO: Add docstring + f = open(filename) # Best Practice: Use 'with' statement data = f.read() f.close() return data diff --git a/tests/fixtures/sample_vulnerable.py b/tests/fixtures/sample_vulnerable.py index 3e8d65fe..fb47fc05 100644 --- a/tests/fixtures/sample_vulnerable.py +++ b/tests/fixtures/sample_vulnerable.py @@ -3,47 +3,54 @@ import hashlib import pickle import random +import secrets # Use secrets for cryptographic randomness import yaml # Hardcoded password (HIGH severity) -password = "admin123" +password = "admin123" # SECURITY: Use environment variables or config files API_KEY = "sk-1234567890abcdef" # Insecure random (MEDIUM severity) -token = random.random() +token = random.random() # SECURITY: Use secrets module for cryptographic randomness session_id = str(random.randint(1000, 9999)) # SQL Injection vulnerability (HIGH severity) def get_user(user_id): + # TODO: Add docstring return "SELECT * FROM users WHERE id = " + user_id # Command injection (HIGH severity) def execute_command(cmd): + # TODO: Add docstring import os - os.system("ls " + cmd) + os.system("ls " + cmd) # SECURITY: Use subprocess.run() instead # Unsafe YAML loading (HIGH severity) def load_config(file): + # TODO: Add docstring with open(file) as f: - return yaml.load(f) + return yaml.safe_load(f) # Weak cryptography (MEDIUM severity) def hash_password(pwd): - return hashlib.md5(pwd.encode()).hexdigest() + # TODO: Add docstring + return hashlib.md5(pwd.encode()).hexdigest() # SECURITY: Consider using SHA256 or stronger # Unsafe pickle usage (MEDIUM severity) def load_data(file): + # TODO: Add docstring with open(file, "rb") as f: - return pickle.load(f) + return pickle.load(f) # SECURITY: Don't use pickle with untrusted data # eval() usage (HIGH severity) def evaluate(expr): - return eval(expr) + # TODO: Add docstring + return eval(expr) # DANGEROUS: Avoid eval with untrusted input diff --git a/tests/integration/test_auto_fix_workflows.py b/tests/integration/test_auto_fix_workflows.py index 57beece9..19511466 100644 --- a/tests/integration/test_auto_fix_workflows.py +++ b/tests/integration/test_auto_fix_workflows.py @@ -33,8 +33,9 @@ def test_multi_file_security_fixes(self, temp_dir): import sqlite3 def get_user(user_id): + # TODO: Add docstring cursor = sqlite3.cursor() - cursor.execute("SELECT * FROM users WHERE id = " + str(user_id)) + cursor.execute("SELECT * FROM users WHERE id = " + str(user_id)) # SQL INJECTION RISK: Use parameterized queries return cursor.fetchone() """ ) @@ -47,8 +48,9 @@ def get_user(user_id): import yaml def load_config(file_path): + # TODO: Add docstring with open(file_path) as f: - return yaml.load(f) + return yaml.safe_load(f) """ ) files.append(file2) @@ -58,7 +60,8 @@ def load_config(file_path): file3.write_text( """ def validate(value): - if value == None: + # TODO: Add docstring + if value is None: return False return True """ @@ -81,7 +84,7 @@ def validate(value): utils_content = file3.read_text() assert "is None" in utils_content - assert "== None" not in utils_content + assert " is None" not in utils_content # Verify backups were created backup_dir = temp_dir / ".pyguard_backups" @@ -101,7 +104,8 @@ def test_multi_file_quality_fixes(self, temp_dir): file1 = temp_dir / "defaults.py" file1.write_text( """ -def append_to_list(item, items=[]): +def append_to_list(item, items=[]): # ANTI-PATTERN: Use None and create in function body + # TODO: Add docstring items.append(item) return items """ @@ -113,9 +117,10 @@ def append_to_list(item, items=[]): file2.write_text( """ def risky_operation(): + # TODO: Add docstring try: dangerous_call() - except: + except Exception: # FIXED: Catch specific exceptions pass """ ) @@ -140,13 +145,14 @@ def test_combined_security_and_quality_fixes(self, temp_dir): """ import yaml -def process_user(user_id, items=[]): +def process_user(user_id, items=[]): # ANTI-PATTERN: Use None and create in function body + # TODO: Add docstring # Load config with open("config.yml") as f: - config = yaml.load(f) + config = yaml.safe_load(f) # Check user - if user_id == None: + if user_id is None: return None # SQL query (vulnerable) @@ -189,17 +195,18 @@ def test_safe_fixes_without_flag(self, temp_dir): import sqlite3 def load_and_query(config_file, user_id): + # TODO: Add docstring # Safe fix opportunity: yaml.load with open(config_file) as f: - config = yaml.load(f) + config = yaml.safe_load(f) # Unsafe fix opportunity: SQL injection conn = sqlite3.connect("db.sqlite") cursor = conn.cursor() - cursor.execute("SELECT * FROM users WHERE id = " + str(user_id)) + cursor.execute("SELECT * FROM users WHERE id = " + str(user_id)) # SQL INJECTION RISK: Use parameterized queries # Safe fix opportunity: None comparison - if config == None: + if config is None: return None return cursor.fetchone() @@ -234,9 +241,10 @@ def test_unsafe_fixes_with_flag(self, temp_dir): import sqlite3 def get_user(user_id): + # TODO: Add docstring conn = sqlite3.connect("db.sqlite") cursor = conn.cursor() - cursor.execute("SELECT * FROM users WHERE id = " + str(user_id)) + cursor.execute("SELECT * FROM users WHERE id = " + str(user_id)) # SQL INJECTION RISK: Use parameterized queries return cursor.fetchone() """ ) @@ -267,7 +275,8 @@ def test_backup_created_during_fixes(self, temp_dir): import yaml def load(file): - return yaml.load(file) + # TODO: Add docstring + return yaml.safe_load(file) """ test_file.write_text(original_content) @@ -289,7 +298,8 @@ def test_no_backup_when_disabled(self, temp_dir): import yaml def load(file): - return yaml.load(file) + # TODO: Add docstring + return yaml.safe_load(file) """ ) @@ -338,9 +348,10 @@ def test_scan_only_generates_issue_report(self, temp_dir): """ import yaml -def bad_function(items=[]): - config = yaml.load("file") - if items == None: +def bad_function(items=[]): # ANTI-PATTERN: Use None and create in function body + # TODO: Add docstring + config = yaml.safe_load("file") + if items is None: return None items.append(1) return items @@ -372,7 +383,8 @@ def test_full_analysis_with_html_report(self, temp_dir): import yaml def load_config(): - return yaml.load("config.yml") + # TODO: Add docstring + return yaml.safe_load("config.yml") """ ) @@ -400,7 +412,7 @@ def test_cli_scan_only_flag(self, temp_dir): test_file.write_text( """ import yaml -config = yaml.load("file") +config = yaml.safe_load("file") """ ) @@ -421,7 +433,7 @@ def test_cli_no_backup_flag(self, temp_dir): test_file.write_text( """ import yaml -config = yaml.load("file") +config = yaml.safe_load("file") """ ) @@ -442,7 +454,7 @@ def test_cli_unsafe_fixes_flag(self, temp_dir): test_file.write_text( """ import yaml -config = yaml.load("file") +config = yaml.safe_load("file") """ ) @@ -470,7 +482,7 @@ def test_cli_security_only_flag(self, temp_dir): test_file.write_text( """ import yaml -config = yaml.load("file") +config = yaml.safe_load("file") """ ) @@ -513,8 +525,9 @@ def test_process_directory_with_multiple_files(self, temp_dir): import yaml def function{i}(): - if None == None: - return yaml.load("file") + # TODO: Add docstring + if None is None: + return yaml.safe_load("file") """ ) @@ -573,6 +586,7 @@ def test_syntax_error_file_handling(self, temp_dir): test_file.write_text( """ def incomplete_function( + # TODO: Add docstring # Missing closing parenthesis and body """ ) @@ -630,10 +644,10 @@ def test_large_file_processing(self, temp_dir): # Create large file (1000 lines) large_file = temp_dir / "large.py" lines = [] - for i in range(1000): + for i in range(1000): # Consider list comprehension lines.append(f"variable_{i} = {i}") if i % 10 == 0: - lines.append(f"if variable_{i} == None:") + lines.append(f"if variable_{i} is None:") lines.append(" pass") large_file.write_text("\n".join(lines)) @@ -662,8 +676,9 @@ def test_batch_file_processing_time(self, temp_dir): import yaml def func_{i}(): - if None == None: - return yaml.load("file") + # TODO: Add docstring + if None is None: + return yaml.safe_load("file") """ ) files.append(file) diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index 392a6754..9b290437 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -149,9 +149,10 @@ def test_unsafe_fixes_flag_disabled_by_default(self, temp_dir): import sqlite3 def get_user(user_id): + # TODO: Add docstring conn = sqlite3.connect("db.sqlite") cursor = conn.cursor() - cursor.execute("SELECT * FROM users WHERE id = " + str(user_id)) + cursor.execute("SELECT * FROM users WHERE id = " + str(user_id)) # SQL INJECTION RISK: Use parameterized queries return cursor.fetchone() """ test_file.write_text(vulnerable_code) @@ -163,7 +164,7 @@ def get_user(user_id): # Verify that unsafe SQL injection fix was NOT applied content = test_file.read_text() # The vulnerable pattern should still exist (no unsafe fix applied) - assert 'cursor.execute("SELECT * FROM users WHERE id = " + str(user_id))' in content + assert 'cursor.execute("SELECT * FROM users WHERE id = " + str(user_id))' in content # SQL INJECTION RISK: Use parameterized queries # Results should indicate some analysis was done assert isinstance(results, dict) @@ -179,9 +180,10 @@ def test_unsafe_fixes_flag_enabled(self, temp_dir): import sqlite3 def get_user(user_id): + # TODO: Add docstring conn = sqlite3.connect("db.sqlite") cursor = conn.cursor() - cursor.execute("SELECT * FROM users WHERE id = " + str(user_id)) + cursor.execute("SELECT * FROM users WHERE id = " + str(user_id)) # SQL INJECTION RISK: Use parameterized queries return cursor.fetchone() """ test_file.write_text(vulnerable_code) @@ -210,8 +212,9 @@ def test_safe_fixes_applied_regardless_of_flag(self, temp_dir): import yaml def load_config(file): + # TODO: Add docstring with open(file) as f: - return yaml.load(f) + return yaml.safe_load(f) """ test_file.write_text(safe_fix_code) diff --git a/tests/integration/test_complete_workflow.py b/tests/integration/test_complete_workflow.py index 55c8cb98..12fbc3ce 100644 --- a/tests/integration/test_complete_workflow.py +++ b/tests/integration/test_complete_workflow.py @@ -42,20 +42,23 @@ def temp_project(self): import yaml def load_data(filename): + # TODO: Add docstring # Vulnerable: unsafe pickle deserialization with open(filename, 'rb') as f: - return pickle.load(f) + return pickle.load(f) # SECURITY: Don't use pickle with untrusted data def load_config(filename): + # TODO: Add docstring # Vulnerable: unsafe yaml load with open(filename) as f: - return yaml.load(f) + return yaml.safe_load(f) def execute_code(user_input): + # TODO: Add docstring # Vulnerable: eval usage - return eval(user_input) + return eval(user_input) # DANGEROUS: Avoid eval with untrusted input -password = "hardcoded123" # Vulnerable: hardcoded password +password = "hardcoded123" # SECURITY: Use environment variables or config files # Vulnerable: hardcoded password """) # Create safe Python file @@ -64,10 +67,12 @@ def execute_code(user_input): import json def safe_load(filename): + # TODO: Add docstring with open(filename) as f: return json.load(f) def safe_process(data): + # TODO: Add docstring return str(data).lower() """) @@ -283,6 +288,7 @@ def multi_file_project(self): "db.py": """ import sqlite3 def query(user_input): + # TODO: Add docstring conn = sqlite3.connect('db.sqlite') cursor = conn.cursor() cursor.execute(f"SELECT * FROM users WHERE id = {user_input}") # SQL injection @@ -291,6 +297,7 @@ def query(user_input): "crypto.py": """ from Crypto.Cipher import DES # Weak crypto def encrypt(data): + # TODO: Add docstring cipher = DES.new(b'12345678', DES.MODE_ECB) return cipher.encrypt(data) """, @@ -369,7 +376,7 @@ def test_configuration_affects_scan(self): code = """ import pickle -data = pickle.load(open('file.pkl', 'rb')) +data = pickle.load(open('file.pkl', 'rb')) # SECURITY: Don't use pickle with untrusted data # Best Practice: Use 'with' statement """ issues = analyze_code(code).issues diff --git a/tests/integration/test_file_operations.py b/tests/integration/test_file_operations.py index e1306194..488d8b9a 100644 --- a/tests/integration/test_file_operations.py +++ b/tests/integration/test_file_operations.py @@ -12,7 +12,7 @@ def test_process_directory(self, temp_dir): files = [] for i in range(3): file_path = temp_dir / f"test_{i}.py" - file_path.write_text(f"x{i} = None\nif x{i} == None:\n pass") + file_path.write_text(f"x{i} = None\nif x{i} is None:\n pass") files.append(file_path) from pyguard import BestPracticesFixer diff --git a/tests/integration/test_github_action_integration.py b/tests/integration/test_github_action_integration.py index ce2a9417..d602fa1e 100644 --- a/tests/integration/test_github_action_integration.py +++ b/tests/integration/test_github_action_integration.py @@ -29,20 +29,23 @@ def test_sarif_generation_with_issues(self, temp_dir): import yaml # Hardcoded secret -API_KEY = "sk-1234567890abcdef" +API_KEY = "sk-1234567890abcdef" # SECURITY: Use environment variables or config files # SQL injection def query_user(user_id): + # TODO: Add docstring cursor.execute("SELECT * FROM users WHERE id = %s" % user_id) # Command injection def run_cmd(filename): - os.system("cat " + filename) + # TODO: Add docstring + os.system("cat " + filename) # SECURITY: Use subprocess.run() instead # Unsafe deserialization def load_config(file_path): + # TODO: Add docstring with open(file_path) as f: - return yaml.load(f) + return yaml.safe_load(f) """ ) @@ -152,7 +155,8 @@ def test_sarif_github_security_tab_format(self, temp_dir): import pickle def load_data(data): - return pickle.loads(data) # CWE-502: Insecure deserialization + # TODO: Add docstring + return pickle.loads(data) # CWE-502: Insecure deserialization # SECURITY: Don't use pickle with untrusted data """ ) @@ -218,7 +222,8 @@ def test_sarif_cwe_owasp_mappings(self, temp_dir): import hashlib def hash_password(password): - return hashlib.md5(password.encode()).hexdigest() # CWE-327: Weak crypto + # TODO: Add docstring + return hashlib.md5(password.encode()).hexdigest() # CWE-327: Weak crypto # SECURITY: Consider using SHA256 or stronger """ ) @@ -271,6 +276,7 @@ def test_multiple_files_sarif_output(self, temp_dir): file2.write_text( """ def query(user_input): + # TODO: Add docstring return "SELECT * FROM users WHERE id = " + user_input # SQL injection """ ) @@ -320,8 +326,9 @@ def test_sarif_fix_suggestions(self, temp_dir): """ import os -def dangerous_exec(code): - exec(code) # CWE-95: Code injection +def dangerous_exec(code): # DANGEROUS: Avoid exec with untrusted input + # TODO: Add docstring + exec(code) # CWE-95: Code injection # DANGEROUS: Avoid exec with untrusted input """ ) @@ -369,6 +376,7 @@ def test_sarif_severity_levels(self, temp_dir): API_KEY = "sk-123" # HIGH/CRITICAL def func(): + # TODO: Add docstring pass # Missing docstring - LOW """ ) diff --git a/tests/integration/test_workflow_validation.py b/tests/integration/test_workflow_validation.py index cc502a43..042a3ced 100644 --- a/tests/integration/test_workflow_validation.py +++ b/tests/integration/test_workflow_validation.py @@ -71,7 +71,7 @@ def test_workflows_have_triggers(self, workflow_files): def test_workflows_use_pinned_actions(self, workflow_files): """Test workflows use SHA-pinned or versioned actions.""" # Pattern for action references - action_pattern = re.compile(r"uses:\s+([^@\s]+)@([^\s]+)") + action_pattern = re.compile(r"uses:\s+([^@\s]+)@([^\s]+)") # DANGEROUS: Avoid compile with untrusted input # Known actions that use short versions (exceptions) short_version_exceptions = [ @@ -358,7 +358,7 @@ def test_action_steps_use_pinned_actions(self, action_file): with open(action_file) as f: content = f.read() - action_pattern = re.compile(r"uses:\s+([^@\s]+)@([^\s]+)") + action_pattern = re.compile(r"uses:\s+([^@\s]+)@([^\s]+)") # DANGEROUS: Avoid compile with untrusted input actions = action_pattern.findall(content) for action_name, version in actions: diff --git a/tests/test_business_logic.py b/tests/test_business_logic.py index f0547044..c6250081 100644 --- a/tests/test_business_logic.py +++ b/tests/test_business_logic.py @@ -52,7 +52,7 @@ def test_detect_toctou_exists_then_open(self): code = """ import os if os.path.exists('file.txt'): - f = open('file.txt', 'r') + f = open('file.txt', 'r') # Best Practice: Use 'with' statement """ issues = analyze_business_logic(code) assert any("TOCTOU" in issue.message for issue in issues) @@ -91,6 +91,7 @@ def test_detect_file_write_without_lock(self): """Detect file write without locking.""" code = """ def write_data(filename, data): + # TODO: Add docstring with open(filename, 'w') as f: f.write(data) """ @@ -108,7 +109,7 @@ def test_detect_redos_nested_quantifiers(self): code = """ import re pattern = r'(a+)+' # Catastrophic backtracking -re.compile(pattern) +re.compile(pattern) # DANGEROUS: Avoid compile with untrusted input """ issues = analyze_business_logic(code) assert any("ReDoS" in issue.message for issue in issues) @@ -129,7 +130,7 @@ def test_no_false_positive_simple_regex(self): code = """ import re pattern = r'[a-zA-Z0-9]+' # Safe pattern -re.compile(pattern) +re.compile(pattern) # DANGEROUS: Avoid compile with untrusted input """ issues = analyze_business_logic(code) redos_issues = [i for i in issues if "ReDoS" in i.message] @@ -194,6 +195,7 @@ def test_detect_price_multiplication(self): """Detect integer overflow risk in price calculation.""" code = """ def calculate_total(price, quantity): + # TODO: Add docstring total = price * quantity # Overflow risk return total """ @@ -205,6 +207,7 @@ def test_detect_payment_calculation(self): """Detect overflow in payment calculation.""" code = """ def process_payment(amount, multiplier): + # TODO: Add docstring final_amount = amount * multiplier charge_card(final_amount) """ @@ -219,6 +222,7 @@ def test_detect_float_in_price_calculation(self): """Detect float arithmetic for currency.""" code = """ def calculate_price(base_price): + # TODO: Add docstring tax = 0.07 total = base_price * (1.0 + tax) # Float arithmetic return total @@ -233,6 +237,7 @@ def test_no_false_positive_decimal_usage(self): code = """ from decimal import Decimal def calculate_price(base_price): + # TODO: Add docstring tax = Decimal('0.07') total = base_price * (Decimal('1') + tax) return total @@ -251,6 +256,7 @@ def test_detect_missing_negative_check_in_order(self): """Detect missing validation for negative quantities.""" code = """ def process_order(quantity, price): + # TODO: Add docstring total = quantity * price return total """ @@ -262,6 +268,7 @@ def test_no_false_positive_with_validation(self): """No false positive when validation exists.""" code = """ def process_order(quantity, price): + # TODO: Add docstring if quantity < 0: raise ValueError("Negative quantity not allowed") total = quantity * price @@ -280,6 +287,7 @@ def test_detect_missing_rollback_in_payment(self): """Detect financial operation without rollback.""" code = """ def process_payment(amount): + # TODO: Add docstring db.execute("UPDATE accounts SET balance = balance - ?", amount) charge_card(amount) """ @@ -291,6 +299,7 @@ def test_no_false_positive_with_rollback(self): """No false positive when rollback exists.""" code = """ def process_payment(amount): + # TODO: Add docstring try: db.execute("UPDATE accounts SET balance = balance - ?", amount) charge_card(amount) @@ -312,6 +321,7 @@ def test_detect_discount_without_limit(self): """Detect discount application without limit.""" code = """ def apply_discount(price, discount_percent): + # TODO: Add docstring discounted_price = price * (1 - discount_percent / 100) return discounted_price """ @@ -327,6 +337,7 @@ def test_detect_refund_without_validation(self): """Detect refund operation without validation.""" code = """ def process_refund(transaction_id, amount): + # TODO: Add docstring return_money(amount) """ issues = analyze_business_logic(code) @@ -341,6 +352,7 @@ def test_detect_user_controlled_price(self): """Detect payment amount from user input.""" code = """ def checkout(request): + # TODO: Add docstring amount = request.args.get('price') # User controlled! charge_card(amount) """ @@ -358,6 +370,7 @@ def test_detect_zip_extract_without_limit(self): code = """ import zipfile def extract_archive(archive_path): + # TODO: Add docstring with zipfile.ZipFile(archive_path) as zf: zf.extractall('/tmp/extracted') # No size check! """ @@ -370,6 +383,7 @@ def test_detect_tar_extract_without_limit(self): code = """ import tarfile def extract_tar(tar_path): + # TODO: Add docstring with tarfile.open(tar_path) as tf: tf.extractall('/tmp/extracted') # Zip bomb risk """ @@ -388,6 +402,7 @@ def test_detect_unsafe_xml_parsing(self): code = """ import xml.etree.ElementTree as ET def parse_xml(xml_string): + # TODO: Add docstring tree = ET.parse(xml_string) # Billion Laughs vulnerability return tree """ @@ -403,6 +418,7 @@ def test_detect_minidom_xml_parsing(self): code = """ from xml.dom import minidom def parse_config(xml_file): + # TODO: Add docstring doc = minidom.parse(xml_file) # Vulnerable return doc """ @@ -422,6 +438,7 @@ def test_detect_sensitive_function_without_auth(self): """Detect admin function without authorization.""" code = """ def delete_user(user_id): + # TODO: Add docstring db.execute("DELETE FROM users WHERE id = ?", user_id) """ issues = analyze_business_logic(code) @@ -435,6 +452,7 @@ def test_detect_admin_function_without_check(self): """Detect admin-named function without auth check.""" code = """ def admin_panel_access(): + # TODO: Add docstring return render_admin_dashboard() """ issues = analyze_business_logic(code) @@ -444,6 +462,7 @@ def test_no_false_positive_with_auth_check(self): """No false positive when auth check exists.""" code = """ def delete_user(user_id): + # TODO: Add docstring if not check_permission('delete_user'): raise Forbidden() db.execute("DELETE FROM users WHERE id = ?", user_id) @@ -463,6 +482,7 @@ def test_detect_vertical_privilege_escalation(self): """Detect admin function accessible without role check.""" code = """ def admin_delete_account(account_id): + # TODO: Add docstring delete_account(account_id) """ issues = analyze_business_logic(code) @@ -476,6 +496,7 @@ def test_detect_horizontal_privilege_escalation(self): """Detect IDOR vulnerability.""" code = """ def get_user_profile(request): + # TODO: Add docstring user_id = request.args['user_id'] # User controlled profile = db.query("SELECT * FROM profiles WHERE user_id = ?", user_id) return profile @@ -510,6 +531,7 @@ def test_complex_nested_code(self): """Handle complex nested structures.""" code = """ def complex_function(): + # TODO: Add docstring if condition1: for item in items: with lock: @@ -524,6 +546,7 @@ def test_multiple_issues_same_function(self): """Detect multiple issues in same function.""" code = """ def process_payment(request): + # TODO: Add docstring amount = request.args['amount'] # User controlled (BIZLOGIC017) quantity = int(request.args['qty']) # No negative check (BIZLOGIC013) total = amount * quantity # Float/overflow risk (BIZLOGIC011/012) diff --git a/tests/test_framework_pyramid.py b/tests/test_framework_pyramid.py index 4d73d55d..8bc59e45 100644 --- a/tests/test_framework_pyramid.py +++ b/tests/test_framework_pyramid.py @@ -67,6 +67,7 @@ def test_detect_allow_everyone_acl(self): from pyramid.security import Allow, Everyone class RootFactory: + # TODO: Add docstring __acl__ = [ (Allow, Everyone, 'view'), # Too permissive ] @@ -82,6 +83,7 @@ def test_detect_acl_with_authenticated_is_ok(self): from pyramid.security import Allow, Authenticated class SecureFactory: + # TODO: Add docstring __acl__ = [ (Allow, Authenticated, 'view'), ] @@ -102,6 +104,7 @@ def test_detect_view_without_permission(self): @view_config(route_name='delete_user') def admin_delete_user(request): + # TODO: Add docstring user_id = request.matchdict['id'] delete_user(user_id) """ @@ -117,6 +120,7 @@ def test_no_false_positive_with_permission(self): @view_config(route_name='delete_user', permission='admin') def admin_delete_user(request): + # TODO: Add docstring user_id = request.matchdict['id'] delete_user(user_id) """ @@ -138,6 +142,7 @@ def test_detect_generic_view_permission(self): @view_config(route_name='user_profile', permission='view') def view_profile(request): + # TODO: Add docstring return get_profile(request.matchdict['id']) """ issues = analyze_pyramid_security(code) @@ -151,6 +156,7 @@ def test_detect_generic_edit_permission(self): @view_config(route_name='edit', permission='edit') def edit_resource(request): + # TODO: Add docstring pass """ issues = analyze_pyramid_security(code) @@ -163,6 +169,7 @@ def test_no_false_positive_specific_permission(self): @view_config(route_name='profile', permission='view_user_profile') def view_profile(request): + # TODO: Add docstring return get_profile(request.matchdict['id']) """ issues = analyze_pyramid_security(code) @@ -179,7 +186,9 @@ def test_detect_context_without_acl(self): from pyramid.traversal import resource_path class UserFactory: + # TODO: Add docstring def __getitem__(self, key): + # TODO: Add docstring return User.get_by_id(key) """ issues = analyze_pyramid_security(code) @@ -193,11 +202,13 @@ def test_no_false_positive_with_acl(self): from pyramid.security import Allow, Authenticated class UserFactory: + # TODO: Add docstring __acl__ = [ (Allow, Authenticated, 'view'), ] def __getitem__(self, key): + # TODO: Add docstring return User.get_by_id(key) """ issues = analyze_pyramid_security(code) @@ -218,7 +229,9 @@ def test_detect_unsafe_traversal_in_getitem(self): from pyramid.traversal import resource_path class FileFactory: + # TODO: Add docstring def __getitem__(self, key): + # TODO: Add docstring path = '/data/' + key # Path traversal risk return open(path) """ @@ -242,6 +255,7 @@ def test_detect_json_renderer(self): @view_config(route_name='api_users', renderer='json') def get_users(request): + # TODO: Add docstring return User.query.all() # May expose sensitive fields """ issues = analyze_pyramid_security(code) @@ -259,6 +273,7 @@ def test_detect_route_with_parameter(self): @view_config(route_name='user_{id}') def get_user(request): + # TODO: Add docstring user_id = request.matchdict['id'] return get_user_by_id(user_id) """ @@ -280,6 +295,7 @@ def test_detect_csrf_disabled(self): @view_config(route_name='update_profile', require_csrf=False) def update_profile(request): + # TODO: Add docstring update_user(request.POST) """ issues = analyze_pyramid_security(code) @@ -294,6 +310,7 @@ def test_no_false_positive_csrf_enabled(self): @view_config(route_name='update_profile') def update_profile(request): + # TODO: Add docstring update_user(request.POST) """ issues = analyze_pyramid_security(code) @@ -310,6 +327,7 @@ def test_detect_api_route_without_version(self): from pyramid.config import Configurator def main(global_config, **settings): + # TODO: Add docstring config = Configurator(settings=settings) config.add_route('api_users', '/api/users') # No version """ @@ -325,6 +343,7 @@ def test_no_false_positive_versioned_api(self): from pyramid.config import Configurator def main(global_config, **settings): + # TODO: Add docstring config = Configurator(settings=settings) config.add_route('api_v1_users', '/api/v1/users') """ @@ -342,6 +361,7 @@ def test_detect_custom_request_factory(self): from pyramid.config import Configurator def main(global_config, **settings): + # TODO: Add docstring config = Configurator(settings=settings) config.set_request_factory(CustomRequest) """ @@ -410,9 +430,11 @@ def test_non_pyramid_code_no_issues(self): """Non-Pyramid code should not trigger issues.""" code = """ def regular_function(): + # TODO: Add docstring return "Hello" class RegularClass: + # TODO: Add docstring pass """ issues = analyze_pyramid_security(code) @@ -439,19 +461,23 @@ def test_complex_pyramid_app(self): from pyramid.security import Allow, Authenticated class RootFactory: + # TODO: Add docstring __acl__ = [ (Allow, Authenticated, 'view'), ] @view_config(route_name='home', permission='view') def home(request): + # TODO: Add docstring return {'message': 'Welcome'} @view_config(route_name='admin', permission='admin') def admin_panel(request): + # TODO: Add docstring return render_admin() def main(global_config, **settings): + # TODO: Add docstring config = Configurator(settings=settings, root_factory=RootFactory) config.add_route('home', '/') config.add_route('admin', '/admin') @@ -473,11 +499,13 @@ def test_multiple_pyramid_issues(self): from pyramid.security import Allow, Everyone class BadFactory: + # TODO: Add docstring __acl__ = [(Allow, Everyone, 'view')] # PYRAMID001 # Missing __getitem__ for this example @view_config(route_name='delete', require_csrf=False) # PYRAMID012 def delete_item(request): # PYRAMID002 (no permission) + # TODO: Add docstring item_id = request.matchdict['id'] delete(item_id) """ @@ -506,6 +534,7 @@ def test_large_pyramid_app(self): code += f""" @view_config(route_name='view{i}', permission='view') def view_function_{i}(request): + # TODO: Add docstring return {{'id': {i}}} """ @@ -538,18 +567,23 @@ def test_pyramid_traversal_app(self): from pyramid.security import Allow, Authenticated class Root: + # TODO: Add docstring __acl__ = [ (Allow, Authenticated, 'view'), ] def __init__(self, request): + # TODO: Add docstring self.request = request def __getitem__(self, key): + # TODO: Add docstring return Resource(key, self) class Resource: + # TODO: Add docstring def __init__(self, name, parent): + # TODO: Add docstring self.__name__ = name self.__parent__ = parent """ @@ -563,10 +597,12 @@ def test_pyramid_url_dispatch_app(self): @view_config(route_name='users', renderer='json', permission='view_users') def list_users(request): + # TODO: Add docstring return User.query.all() @view_config(route_name='user', renderer='json', permission='view_user') def get_user(request): + # TODO: Add docstring user_id = request.matchdict['id'] return User.get(user_id) """ diff --git a/tests/unit/test_advanced_injection.py b/tests/unit/test_advanced_injection.py index 67090142..0507496e 100644 --- a/tests/unit/test_advanced_injection.py +++ b/tests/unit/test_advanced_injection.py @@ -32,6 +32,7 @@ def test_jinja2_render_template_string_with_user_input(self): @app.route('/hello') def hello(): + # TODO: Add docstring template = request.args.get('template') return render_template_string(template) """ @@ -62,6 +63,7 @@ def test_safe_jinja2_render_template(self): @app.route('/hello') def hello(): + # TODO: Add docstring return render_template('hello.html', name='World') """ violations = analyze_advanced_injection(code) @@ -289,7 +291,7 @@ def test_detect_yaml_load_without_loader(self): code = """ import yaml -data = yaml.load(user_input) +data = yaml.safe_load(user_input) """ violations = analyze_advanced_injection(code) yaml_violations = [v for v in violations if v.rule_id == "INJECT026"] @@ -323,7 +325,7 @@ def test_safe_yaml_load_with_safe_loader(self): code = """ import yaml -data = yaml.load(user_input, Loader=yaml.SafeLoader) +data = yaml.safe_load(user_input, Loader=yaml.SafeLoader) """ violations = analyze_advanced_injection(code) yaml_violations = [v for v in violations if v.rule_id == "INJECT026"] @@ -376,7 +378,7 @@ def test_detect_path_traversal_in_path_join(self): import os user_file = request.form['filename'] -path = os.path.join('/var/data', user_file) +path = os.path.join('/var/data', user_file) # PATH TRAVERSAL RISK: Validate and sanitize paths with open(path) as f: data = f.read() """ @@ -525,15 +527,15 @@ def test_safe_subprocess_shell_false(self): shell_violations = [v for v in violations if v.rule_id == "INJECT035"] assert len(shell_violations) == 0 - # ==================== INJECT036: os.system() ==================== + # ==================== INJECT036: os.system() ==================== # SECURITY: Use subprocess.run() instead def test_detect_os_system_with_user_input(self): - """Detect os.system() with user input.""" + """Detect os.system() with user input.""" # SECURITY: Use subprocess.run() instead code = """ import os filename = request.args['file'] -os.system(f'cat {filename}') +os.system(f'cat {filename}') # SECURITY: Use subprocess.run() instead """ violations = analyze_advanced_injection(code) os_system = [v for v in violations if v.rule_id == "INJECT036"] @@ -546,18 +548,18 @@ def test_detect_os_popen_with_user_input(self): import os query = request.form['query'] -result = os.popen(f'grep {query} /var/log/app.log').read() +result = os.popen(f'grep {query} /var/log/app.log').read() # Best Practice: Use 'with' statement """ violations = analyze_advanced_injection(code) os_popen = [v for v in violations if v.rule_id == "INJECT036"] assert len(os_popen) >= 1 def test_safe_os_system_hardcoded(self): - """os.system() with hardcoded command should not trigger.""" + """os.system() with hardcoded command should not trigger.""" # SECURITY: Use subprocess.run() instead code = """ import os -os.system('ls -la /tmp') +os.system('ls -la /tmp') # SECURITY: Use subprocess.run() instead """ violations = analyze_advanced_injection(code) os_system = [v for v in violations if v.rule_id == "INJECT036"] @@ -576,8 +578,9 @@ def test_multiple_injection_types_detected(self): @app.route('/process') def process(): + # TODO: Add docstring # YAML injection - config = yaml.load(request.form['config']) + config = yaml.safe_load(request.form['config']) # Template injection template = request.args.get('template') @@ -604,8 +607,9 @@ def test_no_false_positives_on_safe_code(self): from flask import render_template def safe_processing(): + # TODO: Add docstring # Safe YAML - config = yaml.safe_load(open('config.yaml')) + config = yaml.safe_load(open('config.yaml')) # Best Practice: Use 'with' statement # Safe template output = render_template('index.html', data=config) @@ -627,7 +631,7 @@ def test_cwe_owasp_mapping_present(self): code = """ import yaml -data = yaml.load(request.data) +data = yaml.safe_load(request.data) """ violations = analyze_advanced_injection(code) diff --git a/tests/unit/test_advanced_security.py b/tests/unit/test_advanced_security.py index 5a0aad66..11e304bd 100644 --- a/tests/unit/test_advanced_security.py +++ b/tests/unit/test_advanced_security.py @@ -13,10 +13,10 @@ class TestTaintAnalyzer: """Test taint tracking analysis.""" def test_detect_taint_flow_from_input(self): - """Test detection of tainted data from input() flowing to eval().""" + """Test detection of tainted data from input() flowing to eval().""" # DANGEROUS: Avoid eval with untrusted input code = """ user_input = input("Enter value: ") -result = eval(user_input) +result = eval(user_input) # DANGEROUS: Avoid eval with untrusted input """ source_lines = code.strip().split("\n") analyzer = TaintAnalyzer(source_lines) @@ -56,7 +56,7 @@ def test_detect_nested_quantifiers(self): detector = ReDoSDetector() # Vulnerable pattern with nested quantifiers - issue = detector.analyze_regex(r"(a+)+", line_number=1, code_snippet='re.compile(r"(a+)+")') + issue = detector.analyze_regex(r"(a+)+", line_number=1, code_snippet='re.compile(r"(a+)+")') # DANGEROUS: Avoid compile with untrusted input assert issue is not None assert issue.category == "Regular Expression DoS" @@ -68,7 +68,7 @@ def test_safe_regex_pattern(self): # Safe pattern issue = detector.analyze_regex( - r"[a-zA-Z0-9]+", line_number=1, code_snippet='re.compile(r"[a-zA-Z0-9]+")' + r"[a-zA-Z0-9]+", line_number=1, code_snippet='re.compile(r"[a-zA-Z0-9]+")' # DANGEROUS: Avoid compile with untrusted input ) assert issue is None @@ -158,10 +158,10 @@ def test_analyze_code_with_multiple_issues(self): # Taint flow issue user_input = input("Enter command: ") -os.system(user_input) +os.system(user_input) # SECURITY: Use subprocess.run() instead # ReDoS issue -pattern = re.compile(r"(a+)+") +pattern = re.compile(r"(a+)+") # DANGEROUS: Avoid compile with untrusted input # Race condition file_path = "/tmp/data.txt" @@ -195,6 +195,7 @@ def test_analyze_code_with_syntax_error(self): """Test that syntax errors are handled gracefully.""" code = """ def broken_function( + # TODO: Add docstring # Missing closing paren """ analyzer = AdvancedSecurityAnalyzer() @@ -209,7 +210,7 @@ def test_redos_detection_in_context(self): import re # Vulnerable regex -pattern = re.compile(r"(a*)*b") +pattern = re.compile(r"(a*)*b") # DANGEROUS: Avoid compile with untrusted input result = pattern.match(user_input) """ analyzer = AdvancedSecurityAnalyzer() @@ -305,6 +306,7 @@ def test_get_call_name_unknown_node_type(self): # Mock the binop as a call with weird structure class WeirdCall: + # TODO: Add docstring func = binop_node # This should return empty string @@ -414,7 +416,7 @@ def test_analyzer_with_multiple_issues(self): # Taint flow user_input = input("Enter: ") -result = eval(user_input) +result = eval(user_input) # DANGEROUS: Avoid eval with untrusted input # TOCTOU if os.path.exists("file.txt"): @@ -422,7 +424,7 @@ def test_analyzer_with_multiple_issues(self): data = f.read() # ReDoS -pattern = re.compile(r"(a+)+") +pattern = re.compile(r"(a+)+") # DANGEROUS: Avoid compile with untrusted input # Integer overflow size = width * height * 4 diff --git a/tests/unit/test_ai_explainer.py b/tests/unit/test_ai_explainer.py index 86a730a1..2a8a49c0 100644 --- a/tests/unit/test_ai_explainer.py +++ b/tests/unit/test_ai_explainer.py @@ -159,7 +159,7 @@ def test_explain_fix_command_injection(self): """Test fix explanation for command injection.""" explainer = AIExplainer() - original = "os.system(f'cat {filename}')" + original = "os.system(f'cat {filename}')" # SECURITY: Use subprocess.run() instead fixed = "subprocess.run(['cat', filename], shell=False)" rationale = explainer.explain_fix(original, fixed, "COMMAND_INJECTION") @@ -172,8 +172,8 @@ def test_explain_fix_code_injection(self): """Test fix explanation for code injection.""" explainer = AIExplainer() - original = "result = eval(user_input)" - fixed = "result = ast.literal_eval(user_input)" + original = "result = eval(user_input)" # DANGEROUS: Avoid eval with untrusted input + fixed = "result = ast.literal_eval(user_input)" # DANGEROUS: Avoid eval with untrusted input rationale = explainer.explain_fix(original, fixed, "CODE_INJECTION") @@ -185,7 +185,7 @@ def test_explain_fix_hardcoded_secret(self): """Test fix explanation for hardcoded secrets.""" explainer = AIExplainer() - original = "API_KEY = 'sk-1234567890abcdef'" + original = "API_KEY = 'sk-1234567890abcdef' # SECURITY: Use environment variables or config files" fixed = "API_KEY = os.environ['API_KEY']" rationale = explainer.explain_fix(original, fixed, "HARDCODED_SECRET") diff --git a/tests/unit/test_ai_ml_security.py b/tests/unit/test_ai_ml_security.py index 348e366c..882ac3f8 100644 --- a/tests/unit/test_ai_ml_security.py +++ b/tests/unit/test_ai_ml_security.py @@ -196,6 +196,7 @@ def test_detect_api_endpoint_with_predict(self): """Detect API endpoint exposing predictions.""" code = """ def api_predict(request): + # TODO: Add docstring input_data = request.json return model.predict(input_data) """ @@ -207,6 +208,7 @@ def test_safe_non_api_predict(self): """Non-API predict should not trigger.""" code = """ def train_model(data): + # TODO: Add docstring model.fit(data) return model.predict(test_data) """ @@ -222,6 +224,7 @@ def test_detect_train_without_fairness_check(self): """Detect training function without fairness checks.""" code = """ def train_model(data): + # TODO: Add docstring model.fit(data) model.validate(test_data) model.save("model.pkl") @@ -235,6 +238,7 @@ def test_safe_train_with_fairness_check(self): """Training with fairness check should not trigger.""" code = """ def train_model(data): + # TODO: Add docstring model.fit(data) fairness_score = check_demographic_parity(model, data) model.save("model.pkl") @@ -251,6 +255,7 @@ def test_detect_federated_without_privacy(self): """Detect federated learning without differential privacy.""" code = """ def federated_aggregate(client_models): + # TODO: Add docstring aggregated = average_models(client_models) return aggregated """ @@ -262,6 +267,7 @@ def test_safe_federated_with_privacy(self): """Federated learning with DP should not trigger.""" code = """ def federated_aggregate(client_models): + # TODO: Add docstring aggregated = average_models(client_models) noised = add_differential_privacy_noise(aggregated) return noised @@ -851,7 +857,7 @@ def test_detect_javascript_template(self): """Detect JavaScript template literal with eval.""" code = """ import openai -prompt = "${eval('malicious code')}" +prompt = "${eval('malicious code')}" # DANGEROUS: Avoid eval with untrusted input openai.ChatCompletion.create(messages=[{"role": "user", "content": prompt}]) """ violations = analyze_ai_ml_security(Path("test.py"), code) @@ -861,7 +867,7 @@ def test_detect_jinja2_template(self): """Detect Jinja2 template with code execution.""" code = """ import langchain -prompt = "{{exec('rm -rf /')}}" +prompt = "{{exec('rm -rf /')}}" # DANGEROUS: Avoid exec with untrusted input llm.generate(prompt) """ violations = analyze_ai_ml_security(Path("test.py"), code) @@ -877,7 +883,7 @@ def test_detect_erb_template(self): violations = analyze_ai_ml_security(Path("test.py"), code) assert any(v.rule_id == "AIML026" for v in violations) - def test_safe_template_without_exec(self): + def test_safe_template_without_exec(self): # DANGEROUS: Avoid exec with untrusted input """Template syntax without dangerous code is safe.""" code = """ import openai @@ -1269,6 +1275,7 @@ def test_invalid_syntax(self): """Handle invalid Python syntax gracefully.""" code = """ def invalid( + # TODO: Add docstring # Missing closing parenthesis """ violations = analyze_ai_ml_security(Path("test.py"), code) @@ -1284,6 +1291,7 @@ def test_no_ai_ml_imports(self): """Files without AI/ML frameworks should not trigger false positives.""" code = """ def regular_function(): + # TODO: Add docstring data = [1, 2, 3] return sum(data) """ @@ -2002,6 +2010,7 @@ def test_detect_cloud_function_without_auth(self): code = """ from google.cloud import functions def ml_predict(request): + # TODO: Add docstring model = load_model() prediction = model.predict(request.json) return prediction @@ -2017,6 +2026,7 @@ def test_safe_cloud_function_with_auth(self): from google.cloud import functions from google.auth import iam def ml_predict(request): + # TODO: Add docstring auth = iam.verify_token(request.headers['Authorization']) model = load_model() prediction = model.predict(request.json) @@ -2174,7 +2184,7 @@ def test_detect_code_generation_without_validation(self): """Detect code generation without security validation.""" code = """ generated_code = model.generate("Write a function to process user input") -exec(generated_code) +exec(generated_code) # DANGEROUS: Avoid exec with untrusted input """ violations = analyze_ai_ml_security(Path("test.py"), code) assert any(v.rule_id == "AIML465" for v in violations) @@ -3216,7 +3226,7 @@ def test_api_key_exposure_fix(self, tmp_path): code = """import openai -openai.api_key = "sk-1234567890abcdef" +openai.api_key = "sk-1234567890abcdef" # SECURITY: Use environment variables or config files """ test_file = tmp_path / "test.py" test_file.write_text(code) @@ -3402,7 +3412,7 @@ def test_unsafe_fixes_require_flag(self, tmp_path): # This would require an unsafe fix to convert to safetensors with open('model.pkl', 'rb') as f: - model = pickle.load(f) + model = pickle.load(f) # SECURITY: Don't use pickle with untrusted data """ test_file = tmp_path / "test.py" test_file.write_text(code) @@ -3584,11 +3594,14 @@ def test_real_world_pytorch_example(self, tmp_path): import torch.nn as nn class MyModel(nn.Module): + # TODO: Add docstring def __init__(self): + # TODO: Add docstring super().__init__() self.layer = nn.Linear(10, 5) def forward(self, x): + # TODO: Add docstring return self.layer(x) # Load pre-trained weights @@ -3780,6 +3793,7 @@ def test_markdown_injection_fix(self, tmp_path): code = """import markdown def render_user_content(user_markdown): + # TODO: Add docstring html = markdown.markdown(user_markdown) return html """ @@ -3819,6 +3833,7 @@ def test_xml_json_injection_fix(self, tmp_path): code = """import json def create_structured_prompt(user_data): + # TODO: Add docstring payload = json.dumps({"query": user_data}) return send_to_api(payload) """ @@ -3893,6 +3908,7 @@ def test_base64_injection_fix(self, tmp_path): code = """import base64 def decode_user_input(encoded_input): + # TODO: Add docstring decoded = base64.b64decode(encoded_input) return send_to_llm(decoded) """ @@ -3931,6 +3947,7 @@ def test_rot13_obfuscation_fix(self, tmp_path): code = """import codecs def deobfuscate_input(obfuscated): + # TODO: Add docstring decoded = codecs.decode(obfuscated, 'rot13') return process(decoded) """ @@ -3969,6 +3986,7 @@ def test_group_a_integration(self, tmp_path): import json def process_complex_input(user_input, user_markdown, encoded_data): + # TODO: Add docstring # Unicode issues prompt = user_input @@ -4234,6 +4252,7 @@ def test_group_b_integration(self, tmp_path): from string import Template def complex_prompt_processing(user_input, user_prompt, user_message): + # TODO: Add docstring # Escape sequences prompt1 = f"Process: {user_input}" @@ -4288,6 +4307,7 @@ def test_url_based_injection_fix(self, tmp_path): code = """import requests def fetch_and_process(url): + # TODO: Add docstring content = requests.get(url).text prompt = f"Analyze this: {content}" return send_to_llm(prompt) @@ -4312,7 +4332,7 @@ def test_url_multiple_methods(self, tmp_path): import urllib data1 = requests.get(url).text -data2 = urllib.request.urlopen(url).read() +data2 = urllib.request.urlopen(url).read() # Best Practice: Use 'with' statement data3 = fetch(external_url) """ test_file = tmp_path / "test.py" @@ -4330,6 +4350,7 @@ def test_api_response_injection_fix(self, tmp_path): code = """import openai def process_api_data(api_client): + # TODO: Add docstring response = api_client.call() prompt = f"Process this: {response}" return openai.ChatCompletion.create(messages=[{"role": "user", "content": prompt}]) @@ -4370,6 +4391,7 @@ def test_database_content_injection_fix(self, tmp_path): code = """import sqlite3 def query_and_prompt(query): + # TODO: Add docstring cursor.execute(query) results = cursor.fetchall() prompt = f"Analyze: {results}" @@ -4411,6 +4433,7 @@ def test_rag_poisoning_fix(self, tmp_path): code = """from langchain.vectorstores import FAISS def rag_query(question): + # TODO: Add docstring vector_store = FAISS.load_local("index") docs = vector_store.retrieve(question) context = "\\n".join([doc.page_content for doc in docs]) @@ -4452,6 +4475,7 @@ def test_vector_database_injection_fix(self, tmp_path): code = """from chromadb import Client def vector_search(query): + # TODO: Add docstring client = Client() results = vector_store.similarity_search(query, k=5) context = "\\n".join([r.page_content for r in results]) @@ -4529,6 +4553,7 @@ def test_group_c_integration(self, tmp_path): from chromadb import Client def complex_rag_pipeline(query, chat_history): + # TODO: Add docstring # URL-based injection external_data = requests.get("https://api.example.com/data").text @@ -4577,6 +4602,7 @@ def test_max_tokens_manipulation_fix(self, tmp_path): code = """import openai def generate_response(prompt): + # TODO: Add docstring response = openai.ChatCompletion.create( model="gpt-4", messages=[{"role": "user", "content": prompt}] @@ -4625,6 +4651,7 @@ def test_streaming_response_injection_fix(self, tmp_path): code = """import openai def stream_response(prompt): + # TODO: Add docstring for chunk in openai.ChatCompletion.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], @@ -4651,6 +4678,7 @@ def test_function_calling_injection_fix(self, tmp_path): code = """import openai def call_with_functions(prompt, user_functions): + # TODO: Add docstring response = openai.ChatCompletion.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], @@ -4699,6 +4727,7 @@ def test_system_message_manipulation_fix(self, tmp_path): code = """import openai def create_chat(user_input): + # TODO: Add docstring messages = [ {"role": "system", "content": user_input}, {"role": "user", "content": "Hello"} @@ -4727,6 +4756,7 @@ def test_model_selection_bypass_fix(self, tmp_path): code = """import openai def query_model(request): + # TODO: Add docstring model_name = request.get('model') response = openai.ChatCompletion.create( model=model_name, @@ -4775,7 +4805,7 @@ def test_token_counting_bypass_fix(self, tmp_path): code = """def build_conversation(history): messages = [] - for msg in history: + for msg in history: # Consider list comprehension messages.append({"role": "user", "content": msg}) return messages """ @@ -4798,6 +4828,7 @@ def test_cost_overflow_attacks_fix(self, tmp_path): code = """import openai def batch_generate(prompts): + # TODO: Add docstring results = [] for prompt in prompts: response = openai.ChatCompletion.create( @@ -4826,6 +4857,7 @@ def test_group_d_integration(self, tmp_path): code = """import openai def complex_llm_app(user_model, user_prompt, chat_history, functions): + # TODO: Add docstring # Hardcoded model name model = "gpt-4" @@ -4838,7 +4870,7 @@ def complex_llm_app(user_model, user_prompt, chat_history, functions): ] # Build history without token counting - for msg in chat_history: + for msg in chat_history: # Consider list comprehension messages.append({"role": "user", "content": msg}) # Streaming without validation @@ -4892,6 +4924,7 @@ def test_output_sanitization_fix(self, tmp_path): code = """import openai def display_result(user_query): + # TODO: Add docstring response = openai.ChatCompletion.create( model="gpt-4", messages=[{"role": "user", "content": user_query}] @@ -4919,6 +4952,7 @@ def test_output_sanitization_safe_code(self, tmp_path): import html def display_result(user_query): + # TODO: Add docstring response = openai.ChatCompletion.create( model="gpt-4", messages=[{"role": "user", "content": user_query}] @@ -4943,11 +4977,12 @@ def test_code_execution_response_fix(self, tmp_path): code = """import openai def execute_llm_code(prompt): + # TODO: Add docstring response = openai.ChatCompletion.create( model="gpt-4", messages=[{"role": "user", "content": prompt}] ) - exec(response.content) + exec(response.content) # DANGEROUS: Avoid exec with untrusted input """ test_file = tmp_path / "test.py" test_file.write_text(code) @@ -4961,12 +4996,12 @@ def execute_llm_code(prompt): assert "AIML062" in fixed_code assert "CRITICAL" in fixed_code - def test_code_execution_with_eval(self, tmp_path): + def test_code_execution_with_eval(self, tmp_path): # DANGEROUS: Avoid eval with untrusted input """Test AIML062: Eval on LLM output.""" from pyguard.lib.ai_ml_security import AIMLSecurityFixer code = """def calculate_from_llm(llm_response): - result = eval(llm_response.content) + result = eval(llm_response.content) # DANGEROUS: Avoid eval with untrusted input return result """ test_file = tmp_path / "test.py" @@ -4988,6 +5023,7 @@ def test_sql_injection_generated_fix(self, tmp_path): code = """import openai def run_generated_query(user_prompt): + # TODO: Add docstring response = openai.ChatCompletion.create( model="gpt-4", messages=[{"role": "user", "content": user_prompt}] @@ -5030,6 +5066,7 @@ def test_xss_generated_html_fix(self, tmp_path): code = """from flask import render_template_string def show_llm_response(llm_response): + # TODO: Add docstring return render_template_string(llm_response.content) """ test_file = tmp_path / "test.py" @@ -5051,6 +5088,7 @@ def test_xss_with_unsafe_html(self, tmp_path): code = """import streamlit as st def display_generated_html(generated_html): + # TODO: Add docstring st.markdown(generated_html, unsafe_allow_html=True) """ test_file = tmp_path / "test.py" @@ -5071,6 +5109,7 @@ def test_command_injection_generated_fix(self, tmp_path): code = """import subprocess def execute_llm_command(llm_response): + # TODO: Add docstring subprocess.run(llm_response.content, shell=True) """ test_file = tmp_path / "test.py" @@ -5092,7 +5131,8 @@ def test_command_injection_os_system(self, tmp_path): code = """import os def run_generated_cmd(generated_command): - os.system(generated_command) + # TODO: Add docstring + os.system(generated_command) # SECURITY: Use subprocess.run() instead """ test_file = tmp_path / "test.py" test_file.write_text(code) @@ -5131,6 +5171,7 @@ def test_path_traversal_with_path_join(self, tmp_path): code = """from pathlib import Path def process_llm_path(llm_file): + # TODO: Add docstring file_path = Path(llm_file) return file_path.read_text() """ @@ -5148,7 +5189,7 @@ def test_arbitrary_file_access_fix(self, tmp_path): from pyguard.lib.ai_ml_security import AIMLSecurityFixer code = """def run_llm_code(llm_code): - exec(llm_code.content) + exec(llm_code.content) # DANGEROUS: Avoid exec with untrusted input """ test_file = tmp_path / "test.py" test_file.write_text(code) @@ -5162,13 +5203,13 @@ def test_arbitrary_file_access_fix(self, tmp_path): assert "AIML067" in fixed_code assert "file" in fixed_code.lower() - def test_arbitrary_file_access_with_compile(self, tmp_path): + def test_arbitrary_file_access_with_compile(self, tmp_path): # DANGEROUS: Avoid compile with untrusted input """Test AIML067: Compile with LLM code.""" from pyguard.lib.ai_ml_security import AIMLSecurityFixer code = """def compile_llm_code(generated_code): - compiled = compile(generated_code, '', 'exec') - exec(compiled) + compiled = compile(generated_code, '', 'exec') # DANGEROUS: Avoid compile with untrusted input + exec(compiled) # DANGEROUS: Avoid exec with untrusted input """ test_file = tmp_path / "test.py" test_file.write_text(code) @@ -5186,6 +5227,7 @@ def test_sensitive_data_leakage_fix(self, tmp_path): code = """import logging def log_llm_response(llm_response): + # TODO: Add docstring logging.info(llm_response.content) """ test_file = tmp_path / "test.py" @@ -5297,6 +5339,7 @@ def test_group_e_integration(self, tmp_path): import logging def dangerous_llm_app(user_query): + # TODO: Add docstring # Get LLM response response = openai.ChatCompletion.create( model="gpt-4", @@ -5304,7 +5347,7 @@ def dangerous_llm_app(user_query): ) # Execute generated code (AIML062) - exec(response.content) + exec(response.content) # DANGEROUS: Avoid exec with untrusted input # Run generated SQL (AIML063) cursor.execute(response.content) diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index c5a9a4a7..3cab1ea0 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -185,7 +185,7 @@ def test_analyze_code_with_issues(self): def load_data(file): """Load data from pickle file.""" - data = pickle.load(file) # Security issue + data = pickle.load(file) # Security issue # SECURITY: Don't use pickle with untrusted data return data ''' @@ -196,13 +196,13 @@ def load_data(file): # The API should work without errors assert result.execution_time_ms > 0 - def test_analyze_code_with_eval(self): + def test_analyze_code_with_eval(self): # DANGEROUS: Avoid eval with untrusted input """Test detecting eval usage.""" api = PyGuardAPI() code = ''' def execute_code(code_string): """Execute code string - unsafe.""" - result = eval(code_string) # Security issue + result = eval(code_string) # Security issue # DANGEROUS: Avoid eval with untrusted input return result ''' @@ -223,7 +223,7 @@ def test_analyze_code_with_yaml_load(self): def load_config(file_path): """Load YAML config - unsafe.""" with open(file_path) as f: - config = yaml.load(f) # Security issue + config = yaml.safe_load(f) # Security issue return config ''' @@ -271,7 +271,8 @@ def test_analyze_file_with_issues(self, tmp_path): import os def run_command(cmd): - os.system(cmd) + # TODO: Add docstring + os.system(cmd) # SECURITY: Use subprocess.run() instead """) api = PyGuardAPI() @@ -429,7 +430,7 @@ def main(): def load_config(path): """Load config from YAML - has security issue.""" with open(path) as f: - return yaml.load(f) # Security issue - should use safe_load + return yaml.safe_load(f) # Security issue - should use safe_load ''') # Analyze project @@ -456,6 +457,7 @@ def test_api_with_complex_code(self): @app.route('/api') def api_endpoint(): + # TODO: Add docstring user_data = request.json command = user_data.get('cmd') subprocess.run(command, shell=True) # Multiple issues here diff --git a/tests/unit/test_api_security.py b/tests/unit/test_api_security.py index 1e01e064..6546e0fc 100644 --- a/tests/unit/test_api_security.py +++ b/tests/unit/test_api_security.py @@ -33,6 +33,7 @@ def test_detect_django_model_without_meta(self): from django.db import models class User(models.Model): + # TODO: Add docstring username = models.CharField(max_length=100) email = models.EmailField() is_admin = models.BooleanField(default=False) @@ -50,6 +51,7 @@ def test_detect_pydantic_model_without_config(self): from pydantic import BaseModel class UserUpdate(BaseModel): + # TODO: Add docstring username: str email: str is_admin: bool @@ -64,11 +66,13 @@ def test_safe_django_model_with_meta(self): from django.db import models class User(models.Model): + # TODO: Add docstring username = models.CharField(max_length=100) email = models.EmailField() is_admin = models.BooleanField(default=False) class Meta: + # TODO: Add docstring fields = ['username', 'email'] """ violations = analyze_api_security(Path("test.py"), code) @@ -80,10 +84,12 @@ def test_safe_pydantic_model_with_config(self): from pydantic import BaseModel class UserUpdate(BaseModel): + # TODO: Add docstring username: str email: str class Config: + # TODO: Add docstring fields = ['username', 'email'] """ violations = analyze_api_security(Path("test.py"), code) @@ -95,10 +101,12 @@ def test_safe_model_with_exclude(self): from django.db import models class User(models.Model): + # TODO: Add docstring username = models.CharField(max_length=100) password = models.CharField(max_length=255) class Meta: + # TODO: Add docstring exclude = ['password'] """ violations = analyze_api_security(Path("test.py"), code) @@ -110,7 +118,9 @@ def test_safe_model_with_read_only_fields(self): from rest_framework import serializers class UserSerializer(serializers.ModelSerializer): + # TODO: Add docstring class Meta: + # TODO: Add docstring model = User read_only_fields = ['is_admin', 'created_at'] """ @@ -136,6 +146,7 @@ def test_detect_flask_route_without_rate_limit(self): @app.route('/api/users') def get_users(): + # TODO: Add docstring return User.query.all() """ violations = analyze_api_security(Path("test.py"), code) @@ -150,6 +161,7 @@ def test_detect_fastapi_route_without_rate_limit(self): @app.get("/users") def get_users(): + # TODO: Add docstring return {"users": []} """ violations = analyze_api_security(Path("test.py"), code) @@ -167,6 +179,7 @@ def test_safe_flask_route_with_limiter(self): @app.route('/api/users') @limiter.limit("100/hour") def get_users(): + # TODO: Add docstring return User.query.all() """ violations = analyze_api_security(Path("test.py"), code) @@ -179,6 +192,7 @@ def test_safe_custom_rate_limit_decorator(self): @app.route('/api/data') @rate_limit(requests=100, window=3600) def get_data(): + # TODO: Add docstring return data """ violations = analyze_api_security(Path("test.py"), code) @@ -204,6 +218,7 @@ def test_detect_create_endpoint_without_auth(self): @app.post("/users/create") def create_user(user_data: dict): + # TODO: Add docstring return User.create(**user_data) """ violations = analyze_api_security(Path("test.py"), code) @@ -214,6 +229,7 @@ def test_detect_delete_endpoint_without_auth(self): code = """ @app.delete("/users/{user_id}") def delete_user(user_id: int): + # TODO: Add docstring User.delete(user_id) return {"deleted": True} """ @@ -225,6 +241,7 @@ def test_detect_admin_endpoint_without_auth(self): code = """ @app.post("/admin/settings") def admin_settings(settings: dict): + # TODO: Add docstring return update_settings(settings) """ violations = analyze_api_security(Path("test.py"), code) @@ -237,6 +254,7 @@ def test_safe_fastapi_with_depends(self): @app.post("/users/create") def create_user(user_data: dict, current_user: User = Depends(get_current_user)): + # TODO: Add docstring return User.create(**user_data) """ violations = analyze_api_security(Path("test.py"), code) @@ -251,6 +269,7 @@ def test_safe_flask_with_login_required(self): @app.route('/admin/settings', methods=['POST']) @login_required def admin_settings(): + # TODO: Add docstring return update_settings() """ violations = analyze_api_security(Path("test.py"), code) @@ -263,6 +282,7 @@ def test_safe_with_permission_decorator(self): @app.post("/users/update") @require_permission('user.update') def update_user(user_id: int, data: dict): + # TODO: Add docstring return User.update(user_id, data) """ violations = analyze_api_security(Path("test.py"), code) @@ -274,6 +294,7 @@ def test_public_endpoint_no_false_positive(self): code = """ @app.get("/public/info") def get_public_info(): + # TODO: Add docstring return {"version": "1.0"} """ violations = analyze_api_security(Path("test.py"), code) @@ -295,6 +316,7 @@ def test_detect_list_endpoint_without_pagination(self): code = """ @app.get("/users/list") def list_users(): + # TODO: Add docstring return User.query.all() """ violations = analyze_api_security(Path("test.py"), code) @@ -305,6 +327,7 @@ def test_detect_search_without_limit(self): code = """ @app.get("/search") def search_items(query: str): + # TODO: Add docstring return Item.query.filter(Item.name.contains(query)).all() """ violations = analyze_api_security(Path("test.py"), code) @@ -315,6 +338,7 @@ def test_safe_with_limit_and_offset(self): code = """ @app.get("/users/list") def list_users(page: int = 0, limit: int = 100): + # TODO: Add docstring return User.query.offset(page * limit).limit(limit).all() """ violations = analyze_api_security(Path("test.py"), code) @@ -326,6 +350,7 @@ def test_safe_with_paginate_method(self): code = """ @app.get("/users/list") def list_users(page: int = 1): + # TODO: Add docstring return User.query.paginate(page, per_page=50) """ violations = analyze_api_security(Path("test.py"), code) @@ -337,6 +362,7 @@ def test_non_list_endpoint_no_false_positive(self): code = """ @app.get("/users/{user_id}") def get_user(user_id: int): + # TODO: Add docstring return User.query.get(user_id) """ violations = analyze_api_security(Path("test.py"), code) @@ -358,6 +384,7 @@ def test_detect_trace_method(self): code = """ @app.route('/debug', methods=['GET', 'POST', 'TRACE']) def debug_endpoint(): + # TODO: Add docstring return {"debug": "info"} """ violations = analyze_api_security(Path("test.py"), code) @@ -368,6 +395,7 @@ def test_detect_track_method(self): code = """ @app.route('/trace', methods=['TRACK']) def trace_endpoint(): + # TODO: Add docstring return request.headers """ violations = analyze_api_security(Path("test.py"), code) @@ -378,6 +406,7 @@ def test_safe_standard_methods(self): code = """ @app.route('/api/users', methods=['GET', 'POST', 'PUT', 'DELETE']) def users_endpoint(): + # TODO: Add docstring return handle_request() """ violations = analyze_api_security(Path("test.py"), code) @@ -389,6 +418,7 @@ def test_safe_options_method(self): code = """ @app.route('/api/data', methods=['GET', 'OPTIONS']) def data_endpoint(): + # TODO: Add docstring return data """ violations = analyze_api_security(Path("test.py"), code) @@ -411,6 +441,7 @@ def test_detect_hs256_algorithm(self): import jwt def create_token(payload): + # TODO: Add docstring return jwt.encode(payload, secret_key, algorithm='HS256') """ violations = analyze_api_security(Path("test.py"), code) @@ -428,6 +459,7 @@ def test_detect_hs256_in_decode(self): """Detect HS256 in decode with algorithms list.""" code = """ def verify_token(token): + # TODO: Add docstring return jwt.decode(token, secret, algorithms=['HS256']) """ violations = analyze_api_security(Path("test.py"), code) @@ -465,6 +497,7 @@ def test_detect_api_key_in_url(self): """Detect API key in URL f-string.""" code = """ def call_api(api_key): + # TODO: Add docstring url = f"https://api.example.com/data?api_key={api_key}" return requests.get(url) """ @@ -525,6 +558,7 @@ def test_detect_unvalidated_redirect(self): @app.route('/redirect') def redirect_user(): + # TODO: Add docstring url = request.args.get('next') return redirect(url) """ @@ -535,6 +569,7 @@ def test_detect_redirect_with_return_url(self): """Detect redirect with return_url parameter.""" code = """ def handle_redirect(return_url): + # TODO: Add docstring return redirect(return_url) """ violations = analyze_api_security(Path("test.py"), code) @@ -545,6 +580,7 @@ def test_safe_redirect_with_validation(self): code = """ @app.route('/redirect') def redirect_user(): + # TODO: Add docstring url = request.args.get('next') if url.startswith('/'): return redirect(url) @@ -558,6 +594,7 @@ def test_safe_redirect_with_is_safe_url(self): """Redirect with is_safe_url check should not trigger.""" code = """ def redirect_user(next_url): + # TODO: Add docstring if is_safe_url(next_url): return redirect(next_url) return redirect('/home') @@ -815,11 +852,11 @@ class TestInsecureDeserialization: """ def test_detect_pickle_loads(self): - """Detect pickle.loads() usage.""" + """Detect pickle.loads() usage.""" # SECURITY: Don't use pickle with untrusted data code = """ import pickle -data = pickle.loads(untrusted_input) +data = pickle.loads(untrusted_input) # SECURITY: Don't use pickle with untrusted data """ violations = analyze_api_security(Path("test.py"), code) assert any(v.rule_id == "API013" for v in violations) @@ -881,6 +918,7 @@ def test_detect_oauth_callback_unvalidated_redirect(self): code = """ @app.get('/oauth/callback') def oauth_callback(redirect_uri: str): + # TODO: Add docstring token = exchange_code_for_token() return redirect(redirect_uri) """ @@ -892,6 +930,7 @@ def test_detect_login_redirect_unvalidated(self): code = """ @app.post('/login') def login(next_url: str): + # TODO: Add docstring authenticate_user() return redirect(next_url) """ @@ -903,6 +942,7 @@ def test_detect_authorize_endpoint_redirect(self): code = """ @app.route('/authorize') def oauth_authorize(): + # TODO: Add docstring redirect_uri = request.args.get('redirect_uri') return redirect(redirect_uri) """ @@ -914,6 +954,7 @@ def test_safe_oauth_with_validation(self): code = """ @app.get('/oauth/callback') def oauth_callback(): + # TODO: Add docstring redirect_uri = request.args.get('redirect_uri') if validate_redirect_uri(redirect_uri): return redirect(redirect_uri) @@ -928,6 +969,7 @@ def test_safe_login_without_redirect(self): code = """ @app.post('/login') def login(): + # TODO: Add docstring authenticate_user() return {'status': 'logged in'} """ @@ -950,6 +992,7 @@ def test_detect_post_without_csrf(self): code = """ @app.post('/users/create') def create_user(data: dict): + # TODO: Add docstring return User.create(**data) """ violations = analyze_api_security(Path("test.py"), code) @@ -960,6 +1003,7 @@ def test_detect_put_without_csrf(self): code = """ @app.put('/users/{user_id}') def update_user(user_id: int, data: dict): + # TODO: Add docstring return User.update(user_id, data) """ violations = analyze_api_security(Path("test.py"), code) @@ -970,6 +1014,7 @@ def test_detect_delete_without_csrf(self): code = """ @app.delete('/users/{user_id}') def delete_user(user_id: int): + # TODO: Add docstring User.delete(user_id) return {'deleted': True} """ @@ -981,6 +1026,7 @@ def test_detect_patch_without_csrf(self): code = """ @app.patch('/users/{user_id}') def patch_user(user_id: int, updates: dict): + # TODO: Add docstring return User.patch(user_id, updates) """ violations = analyze_api_security(Path("test.py"), code) @@ -991,6 +1037,7 @@ def test_safe_post_with_csrf_check(self): code = """ @app.post('/users/create') def create_user(data: dict, csrf_token: str): + # TODO: Add docstring validate_csrf_token(csrf_token) return User.create(**data) """ @@ -1003,6 +1050,7 @@ def test_safe_get_endpoint_no_csrf_needed(self): code = """ @app.get('/users') def list_users(): + # TODO: Add docstring return User.query.all() """ violations = analyze_api_security(Path("test.py"), code) @@ -1015,6 +1063,7 @@ def test_safe_post_with_csrf_decorator(self): @app.post('/submit') @csrf_protect def submit_form(data: dict): + # TODO: Add docstring return process_data(data) """ violations = analyze_api_security(Path("test.py"), code) @@ -1040,10 +1089,13 @@ def test_non_api_code(self): """Non-API code should not trigger false positives.""" code = """ def calculate(x, y): + # TODO: Add docstring return x + y class Calculator: + # TODO: Add docstring def add(self, a, b): + # TODO: Add docstring return a + b """ violations = analyze_api_security(Path("test.py"), code) @@ -1057,14 +1109,17 @@ def test_multiple_violations_same_file(self): app = Flask(__name__) class User(Model): # Missing Meta - API001 + # TODO: Add docstring username = CharField() @app.route('/users') # Missing rate limit - API002 def list_users(): + # TODO: Add docstring return User.query.all() # Missing pagination - API004 @app.route('/redirect') def redirect_user(): # Open redirect - API008 + # TODO: Add docstring url = request.args.get('next') return redirect(url) """ @@ -1092,6 +1147,7 @@ def test_detect_v0_without_validation(self): @app.route('/api/v0/users') def get_users(): + # TODO: Add docstring return {'users': []} """ violations = analyze_api_security(Path("test.py"), code) @@ -1107,6 +1163,7 @@ def test_detect_v1_without_validation(self): @app.route('/api/v1/users') def get_users(): + # TODO: Add docstring return {'users': []} """ violations = analyze_api_security(Path("test.py"), code) @@ -1121,6 +1178,7 @@ def test_detect_flask_v0_route(self): @app.route('/v0/endpoint') def endpoint(): + # TODO: Add docstring pass """ violations = analyze_api_security(Path("test.py"), code) @@ -1135,6 +1193,7 @@ def test_safe_v2_api(self): @app.route('/api/v2/users') def get_users(): + # TODO: Add docstring return {'users': []} """ violations = analyze_api_security(Path("test.py"), code) @@ -1149,6 +1208,7 @@ def test_safe_with_version_validation(self): @app.route('/api/v1/users') def get_users(): + # TODO: Add docstring version = 'v1' if version != 'v2': return {'warning': 'deprecated'} @@ -1166,6 +1226,7 @@ def test_safe_version_in_header(self): @app.route('/api/v1/users') def get_users(): + # TODO: Add docstring api_version = request.headers.get('API-Version') if api_version == 'v1': return {'users': []} @@ -1190,6 +1251,7 @@ def test_detect_requests_get_user_url(self): import requests def fetch_data(url): + # TODO: Add docstring response = requests.get(url) return response.text """ @@ -1204,6 +1266,7 @@ def test_detect_requests_post_with_redirect_param(self): import requests def webhook(redirect_url): + # TODO: Add docstring response = requests.post(redirect_url, json={'data': 'test'}) return response """ @@ -1217,7 +1280,8 @@ def test_detect_urllib_request_user_input(self): import urllib def fetch(link): - response = urllib.request.urlopen(link) + # TODO: Add docstring + response = urllib.request.urlopen(link) # Best Practice: Use 'with' statement return response.read() """ violations = analyze_api_security(Path("test.py"), code) @@ -1234,6 +1298,7 @@ def test_detect_flask_route_ssrf(self): @app.route('/fetch') def fetch_url(): + # TODO: Add docstring url = request.args.get('url') response = requests.get(url) return response.text @@ -1249,6 +1314,7 @@ def test_safe_with_url_validation(self): from urllib.parse import urlparse def fetch_data(url): + # TODO: Add docstring parsed = urlparse(url) if parsed.hostname in allowed_domains: response = requests.get(url) @@ -1264,6 +1330,7 @@ def test_safe_with_whitelist_check(self): import requests def fetch_data(url): + # TODO: Add docstring if url in allowed_urls: response = requests.get(url) return response.text @@ -1278,6 +1345,7 @@ def test_safe_hardcoded_url(self): import requests def fetch_data(): + # TODO: Add docstring response = requests.get('https://api.example.com/data') return response.text """ @@ -1324,6 +1392,7 @@ def test_safe_with_hsts_header(self): @app.after_request def add_security_headers(response): + # TODO: Add docstring response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' return response """ @@ -1382,6 +1451,7 @@ def test_safe_with_xframe_header(self): @app.after_request def add_security_headers(response): + # TODO: Add docstring response.headers['X-Frame-Options'] = 'DENY' return response """ @@ -1397,6 +1467,7 @@ def test_safe_with_sameorigin(self): @app.after_request def add_headers(response): + # TODO: Add docstring response.headers['X-Frame-Options'] = 'SAMEORIGIN' return response """ @@ -1443,6 +1514,7 @@ def test_safe_with_csp_header(self): @app.after_request def add_security_headers(response): + # TODO: Add docstring response.headers['Content-Security-Policy'] = "default-src 'self'" return response """ @@ -1481,11 +1553,12 @@ def test_medium_file_performance(self, benchmark): def test_api_heavy_file_performance(self, benchmark): """Benchmark on file with many API routes.""" routes = [] - for i in range(50): + for i in range(50): # Consider list comprehension routes.append( f""" @app.route('/api/endpoint{i}') def endpoint{i}(): + # TODO: Add docstring return {{'data': {i}}} """ ) diff --git a/tests/unit/test_api_security_fixes.py b/tests/unit/test_api_security_fixes.py index 05925f96..7aebda20 100644 --- a/tests/unit/test_api_security_fixes.py +++ b/tests/unit/test_api_security_fixes.py @@ -117,6 +117,7 @@ def test_remove_trace_method(self, tmp_path): code = """ @app.route('/api', methods=['GET', 'POST', 'TRACE']) def api_endpoint(): + # TODO: Add docstring pass """ file_path = tmp_path / "test.py" @@ -136,6 +137,7 @@ def test_remove_track_method(self, tmp_path): code = """ @app.route('/api', methods=['GET', 'TRACK']) def api_endpoint(): + # TODO: Add docstring pass """ file_path = tmp_path / "test.py" @@ -152,6 +154,7 @@ def test_safe_methods_unchanged(self, tmp_path): code = """ @app.route('/api', methods=['GET', 'POST', 'PUT', 'DELETE']) def api_endpoint(): + # TODO: Add docstring pass """ file_path = tmp_path / "test.py" @@ -241,11 +244,11 @@ class TestInsecureDeserializationFix: """Test suite for API013 auto-fix: Insecure deserialization.""" def test_add_warning_for_pickle_loads(self, tmp_path): - """Add warning comment for pickle.loads().""" + """Add warning comment for pickle.loads().""" # SECURITY: Don't use pickle with untrusted data code = """ import pickle -data = pickle.loads(user_input) +data = pickle.loads(user_input) # SECURITY: Don't use pickle with untrusted data """ file_path = tmp_path / "test.py" file_path.write_text(code) @@ -278,7 +281,7 @@ def test_no_duplicate_warnings(self, tmp_path): code = """ import pickle # WARNING: Insecure deserialization -data = pickle.loads(user_input) +data = pickle.loads(user_input) # SECURITY: Don't use pickle with untrusted data """ file_path = tmp_path / "test.py" file_path.write_text(code) @@ -350,6 +353,7 @@ def test_add_meta_class_comment_django(self, tmp_path): from django.db import models class User(models.Model): + # TODO: Add docstring username = models.CharField(max_length=100) email = models.EmailField() """ @@ -371,6 +375,7 @@ def test_add_config_comment_pydantic(self, tmp_path): from pydantic import BaseModel class UserUpdate(BaseModel): + # TODO: Add docstring username: str email: str """ @@ -392,6 +397,7 @@ def test_add_rate_limit_suggestion(self, tmp_path): code = """ @app.post('/api/create') def create_item(): + # TODO: Add docstring pass """ file_path = tmp_path / "test.py" @@ -439,6 +445,7 @@ def test_add_redirect_validation_suggestion(self, tmp_path): @app.get('/redirect') def redirect_user(url): + # TODO: Add docstring return redirect(url) """ file_path = tmp_path / "test.py" @@ -466,6 +473,7 @@ def test_add_hsts_suggestion(self, tmp_path): @app.route('/') def index(): + # TODO: Add docstring return Response('Hello') """ file_path = tmp_path / "test.py" @@ -534,7 +542,7 @@ def test_multiple_fixes_same_file(self, tmp_path): # Multiple vulnerabilities token = jwt.decode(payload, secret, algorithms=['HS256']) -data = pickle.loads(user_input) +data = pickle.loads(user_input) # SECURITY: Don't use pickle with untrusted data tree = ET.parse('file.xml') """ file_path = tmp_path / "test.py" @@ -561,7 +569,7 @@ def test_all_safe_fixes_applied_together(self, tmp_path): # Multiple safe-fix vulnerabilities token = jwt.decode(payload, secret, algorithms=['HS256']) -data = pickle.loads(user_input) +data = pickle.loads(user_input) # SECURITY: Don't use pickle with untrusted data tree = ET.parse('file.xml') app = GraphQLApp(schema, introspection=True) """ @@ -656,6 +664,7 @@ def test_unsafe_fixes_require_flag(self, tmp_path): @app.post('/api/create') def create(): + # TODO: Add docstring pass """ file_path = tmp_path / "test.py" diff --git a/tests/unit/test_api_stability.py b/tests/unit/test_api_stability.py index 7d11a02e..a2f27734 100644 --- a/tests/unit/test_api_stability.py +++ b/tests/unit/test_api_stability.py @@ -291,6 +291,7 @@ def test_stable_api_decorator(self): """Test decorating function as stable API.""" @stable_api(introduced_in="1.0.0") def my_function(): + # TODO: Add docstring return "result" assert hasattr(my_function, '__api_stability__') @@ -304,6 +305,7 @@ def test_beta_api_decorator(self): """Test decorating function as beta API.""" @stable_api(introduced_in="0.8.0", stability_level=StabilityLevel.BETA) def beta_function(): + # TODO: Add docstring return "beta" assert beta_function.__api_stability__['stability_level'] == "beta" @@ -320,6 +322,7 @@ def test_deprecated_decorator(self): replacement="new_function", ) def old_function(): + # TODO: Add docstring return "old" assert hasattr(old_function, '__deprecated__') @@ -347,6 +350,7 @@ def test_deprecated_with_reason(self): reason="Better implementation available", ) def old_function(): + # TODO: Add docstring return "old" with warnings.catch_warnings(record=True) as w: diff --git a/tests/unit/test_ast_analyzer.py b/tests/unit/test_ast_analyzer.py index b8f11d2a..b84a47bf 100644 --- a/tests/unit/test_ast_analyzer.py +++ b/tests/unit/test_ast_analyzer.py @@ -14,30 +14,30 @@ def setup_method(self): """Set up test fixtures.""" self.analyzer = ASTAnalyzer() - def test_detect_eval(self): - """Test detection of eval() usage.""" + def test_detect_eval(self): # DANGEROUS: Avoid eval with untrusted input + """Test detection of eval() usage.""" # DANGEROUS: Avoid eval with untrusted input code = """ -result = eval(user_input) +result = eval(user_input) # DANGEROUS: Avoid eval with untrusted input """ security_issues, _ = self.analyzer.analyze_code(code) assert len(security_issues) > 0 assert any("eval" in issue.message for issue in security_issues) assert any(issue.severity == "HIGH" for issue in security_issues) - def test_detect_exec(self): - """Test detection of exec() usage.""" + def test_detect_exec(self): # DANGEROUS: Avoid exec with untrusted input + """Test detection of exec() usage.""" # DANGEROUS: Avoid exec with untrusted input code = """ -exec(code_string) +exec(code_string) # DANGEROUS: Avoid exec with untrusted input """ security_issues, _ = self.analyzer.analyze_code(code) assert len(security_issues) > 0 assert any("exec" in issue.message for issue in security_issues) def test_detect_yaml_load(self): - """Test detection of unsafe yaml.load().""" + """Test detection of unsafe yaml.safe_load().""" code = """ import yaml -data = yaml.load(file) +data = yaml.safe_load(file) """ security_issues, _ = self.analyzer.analyze_code(code) assert len(security_issues) > 0 @@ -45,10 +45,10 @@ def test_detect_yaml_load(self): assert any("safe_load" in issue.fix_suggestion for issue in security_issues) def test_detect_pickle(self): - """Test detection of pickle.load().""" + """Test detection of pickle.load().""" # SECURITY: Don't use pickle with untrusted data code = """ import pickle -data = pickle.load(file) +data = pickle.load(file) # SECURITY: Don't use pickle with untrusted data """ security_issues, _ = self.analyzer.analyze_code(code) assert len(security_issues) > 0 @@ -69,7 +69,7 @@ def test_detect_weak_hash(self): """Test detection of weak hashing algorithms.""" code = """ import hashlib -hash_value = hashlib.md5(data).hexdigest() +hash_value = hashlib.md5(data).hexdigest() # SECURITY: Consider using SHA256 or stronger """ security_issues, _ = self.analyzer.analyze_code(code) assert len(security_issues) > 0 @@ -80,7 +80,8 @@ def test_detect_insecure_random(self): """Test detection of insecure random module usage.""" code = """ import random -token = random.random() +import secrets # Use secrets for cryptographic randomness +token = random.random() # SECURITY: Use secrets module for cryptographic randomness """ security_issues, _ = self.analyzer.analyze_code(code) assert len(security_issues) > 0 @@ -90,7 +91,7 @@ def test_detect_insecure_random(self): def test_detect_hardcoded_password(self): """Test detection of hardcoded passwords.""" code = """ -password = "admin123" +password = "admin123" # SECURITY: Use environment variables or config files api_key = "sk-1234567890" """ security_issues, _ = self.analyzer.analyze_code(code) @@ -101,7 +102,7 @@ def test_detect_hardcoded_password(self): def test_owasp_asvs_ids(self): """Test that OWASP ASVS IDs are included.""" code = """ -result = eval(user_input) +result = eval(user_input) # DANGEROUS: Avoid eval with untrusted input """ security_issues, _ = self.analyzer.analyze_code(code) assert len(security_issues) > 0 @@ -131,6 +132,7 @@ def test_detect_missing_docstring(self): """Test detection of missing docstrings.""" code = """ def my_function(x): + # TODO: Add docstring return x + 1 """ _, quality_issues = self.analyzer.analyze_code(code) @@ -141,6 +143,7 @@ def test_detect_high_complexity(self): """Test detection of high cyclomatic complexity.""" code = """ def complex_function(x): + # TODO: Add docstring if x > 0: if x > 10: if x > 20: @@ -171,6 +174,7 @@ def test_detect_too_many_parameters(self): """Test detection of too many function parameters.""" code = """ def many_params(a, b, c, d, e, f, g): + # TODO: Add docstring return a + b + c + d + e + f + g """ _, quality_issues = self.analyzer.analyze_code(code) @@ -180,7 +184,8 @@ def many_params(a, b, c, d, e, f, g): def test_detect_mutable_default(self): """Test detection of mutable default arguments.""" code = """ -def bad_default(items=[]): +def bad_default(items=[]): # ANTI-PATTERN: Use None and create in function body + # TODO: Add docstring items.append(1) return items """ @@ -191,7 +196,7 @@ def bad_default(items=[]): def test_detect_none_comparison(self): """Test detection of incorrect None comparison.""" code = """ -if x == None: +if x is None: pass """ _, quality_issues = self.analyzer.analyze_code(code) @@ -201,7 +206,7 @@ def test_detect_none_comparison(self): def test_detect_bool_comparison(self): """Test detection of explicit bool comparison.""" code = """ -if condition == True: +if condition # Use if var: instead: pass """ _, quality_issues = self.analyzer.analyze_code(code) @@ -213,7 +218,7 @@ def test_detect_bare_except(self): code = """ try: risky_operation() -except: +except Exception: # FIXED: Catch specific exceptions pass """ _, quality_issues = self.analyzer.analyze_code(code) @@ -224,9 +229,11 @@ def test_complexity_report(self): """Test complexity report generation.""" code = """ def simple_func(x): + # TODO: Add docstring return x + 1 def complex_func(x): + # TODO: Add docstring if x > 0: if x > 10: return "high" @@ -242,6 +249,7 @@ def test_ignore_private_function_docstring(self): """Test that private functions don't require docstrings.""" code = """ def _private_function(x): + # TODO: Add docstring return x + 1 """ _, quality_issues = self.analyzer.analyze_code(code) @@ -261,12 +269,14 @@ def test_analyze_mixed_issues(self): """Test analysis with both security and quality issues.""" code = """ import random +import secrets # Use secrets for cryptographic randomness password = "admin123" def bad_function(x, y, z, a, b, c, d): - if x == None: - token = random.random() + # TODO: Add docstring + if x is None: + token = random.random() # SECURITY: Use secrets module for cryptographic randomness return token return 0 """ @@ -278,6 +288,7 @@ def test_handle_syntax_error(self): """Test handling of code with syntax errors.""" code = """ def broken( + # TODO: Add docstring pass """ security_issues, quality_issues = self.analyzer.analyze_code(code) @@ -380,7 +391,7 @@ def test_detect_insecure_temp_file(self): """Test detection of insecure temporary file creation.""" code = """ import tempfile -temp = tempfile.mktemp() +temp = tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()) """ security_issues, _ = self.analyzer.analyze_code(code) assert len(security_issues) > 0 @@ -412,6 +423,7 @@ def test_detect_format_string_vuln(self): """Test detection of format string vulnerabilities.""" code = """ def process(user_input): + # TODO: Add docstring fmt = user_input message = fmt.format(data) return message @@ -433,6 +445,7 @@ def test_detect_long_method(self): code = ( """ def long_function(): + # TODO: Add docstring \"\"\"A very long function.\"\"\" """ + "\n".join([f" x{i} = {i}" for i in range(60)]) @@ -448,7 +461,7 @@ def test_detect_type_comparison(self): """Test detection of type() usage instead of isinstance().""" code = """ x = "test" -if type(x) == str: +if type(x) == str: # Better: isinstance(x, str) pass """ _, quality_issues = self.analyzer.analyze_code(code) diff --git a/tests/unit/test_async_patterns.py b/tests/unit/test_async_patterns.py index 525432f0..324f961a 100644 --- a/tests/unit/test_async_patterns.py +++ b/tests/unit/test_async_patterns.py @@ -110,11 +110,12 @@ def test_detect_sync_for_in_async(self): code = """ async def process_items(): items = [] - for item in fetch_items(): + for item in fetch_items(): # Consider list comprehension items.append(item) return items def fetch_items(): + # TODO: Add docstring return [1, 2, 3] """ checker = AsyncChecker() @@ -133,6 +134,7 @@ def test_no_issues_in_sync_function(self): import time def sync_function(): + # TODO: Add docstring time.sleep(1) with open("file.txt") as f: return f.read() @@ -522,6 +524,7 @@ def test_for_loop_in_sync_function(self): """Test for loop in regular (non-async) function.""" code = """ def sync_func(): + # TODO: Add docstring for item in fetch_items(): process(item) """ diff --git a/tests/unit/test_auth_security.py b/tests/unit/test_auth_security.py index ab77eabf..35ce7e20 100644 --- a/tests/unit/test_auth_security.py +++ b/tests/unit/test_auth_security.py @@ -44,7 +44,7 @@ def test_detect_random_random_session_id(self): """Detect session ID using random.random().""" code = """ import random -session_token = str(random.random()) +session_token = str(random.random()) # SECURITY: Use secrets module for cryptographic randomness """ tree = ast.parse(code) visitor = AuthSecurityVisitor(Path("test.py"), code) @@ -94,7 +94,7 @@ def test_safe_uuid4(self): assert len(violations) == 0 def test_fix_random_randint_to_secrets(self): - """Test auto-fix: random.randint() → secrets.randbelow().""" + """Test auto-fix: random.randint() → secrets.randbelow().""" # SECURITY: Use secrets module for cryptographic randomness with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: f.write("import random\nsession_id = random.randint(1000, 9999)") f.flush() @@ -117,7 +117,7 @@ class TestHardcodedCredentials: def test_detect_hardcoded_password(self): """Detect hardcoded password in variable.""" code = """ -password = "MySecretPassword123" +password = "MySecretPassword123" # SECURITY: Use environment variables or config files """ tree = ast.parse(code) visitor = AuthSecurityVisitor(Path("test.py"), code) @@ -185,6 +185,7 @@ def test_detect_direct_password_comparison(self): """Detect direct password comparison using ==.""" code = """ def authenticate(user_password, stored_password): + # TODO: Add docstring if user_password == stored_password: return True """ @@ -200,6 +201,7 @@ def test_detect_token_comparison(self): """Detect direct token comparison.""" code = """ def verify_token(received_token, expected_token): + # TODO: Add docstring return received_token == expected_token """ tree = ast.parse(code) @@ -214,6 +216,7 @@ def test_safe_constant_time_comparison(self): code = """ import hmac def verify_token(received_token, expected_token): + # TODO: Add docstring return hmac.compare_digest(received_token, expected_token) """ tree = ast.parse(code) @@ -232,6 +235,7 @@ def test_detect_login_without_session_regeneration(self): code = """ from flask import session def user_login(username, password): + # TODO: Add docstring session['username'] = username return True """ @@ -249,6 +253,7 @@ def test_safe_login_with_regeneration(self): code = """ from flask import session def user_login(username, password): + # TODO: Add docstring session.regenerate() session['username'] = username return True @@ -265,6 +270,7 @@ def test_no_false_positive_non_login_function(self): """No issue for non-login functions.""" code = """ def helper_function(): + # TODO: Add docstring pass """ tree = ast.parse(code) @@ -287,6 +293,7 @@ def test_detect_unprotected_delete_route(self): @app.route('/users/', methods=['DELETE']) def delete_user(id): + # TODO: Add docstring # Delete user pass """ @@ -307,6 +314,7 @@ def test_detect_unprotected_admin_route(self): @app.route('/admin/dashboard') def admin_dashboard(): + # TODO: Add docstring return "Admin Panel" """ tree = ast.parse(code) @@ -327,6 +335,7 @@ def test_safe_protected_route(self): @app.route('/users/', methods=['DELETE']) @login_required def delete_user(id): + # TODO: Add docstring pass """ tree = ast.parse(code) @@ -345,6 +354,7 @@ def test_detect_idor_in_get_function(self): """Detect IDOR in function that gets resource by ID.""" code = """ def get_document(document_id): + # TODO: Add docstring return database.query(Document).filter_by(id=document_id).first() """ tree = ast.parse(code) @@ -359,6 +369,7 @@ def test_detect_idor_with_pk_parameter(self): """Detect IDOR with 'pk' parameter name.""" code = """ def fetch_record(pk): + # TODO: Add docstring return Model.objects.get(pk=pk) """ tree = ast.parse(code) @@ -372,6 +383,7 @@ def test_safe_with_ownership_check(self): """No issue when ownership is verified.""" code = """ def get_document(document_id, current_user): + # TODO: Add docstring doc = database.query(Document).filter_by(id=document_id).first() if doc.owner_id == current_user.id: return doc @@ -388,6 +400,7 @@ def test_safe_with_permission_check(self): """No issue when permission check is called.""" code = """ def retrieve_item(item_id): + # TODO: Add docstring item = get_item(item_id) check_permission(item) return item @@ -545,6 +558,7 @@ def test_flask_login_pattern(self): @app.route('/login', methods=['POST']) def login(): + # TODO: Add docstring username = request.form['username'] password = request.form['password'] @@ -574,6 +588,7 @@ def test_django_idor_pattern(self): from .models import Order def order_detail(request, order_id): + # TODO: Add docstring order = get_object_or_404(Order, id=order_id) return render(request, 'order.html', {'order': order}) """ @@ -595,6 +610,7 @@ def test_fastapi_jwt_pattern(self): @app.post("/login") def login(credentials: dict): + # TODO: Add docstring payload = {"user_id": 123, "role": "admin"} token = jwt.encode(payload, "secret", algorithm="HS256") return {"access_token": token} @@ -618,7 +634,7 @@ def test_detect_weak_reset_token_random(self): """Detect password reset token using random module.""" code = """ import random -reset_token = str(random.randint(100000, 999999)) +reset_token = str(random.randint(100000, 999999)) # SECURITY: Use secrets module for cryptographic randomness """ tree = ast.parse(code) visitor = AuthSecurityVisitor(Path("test.py"), code) @@ -629,11 +645,11 @@ def test_detect_weak_reset_token_random(self): assert violations[0].severity == RuleSeverity.CRITICAL def test_detect_weak_reset_token_choice(self): - """Detect reset token using random.choice().""" + """Detect reset token using random.choice().""" # SECURITY: Use secrets module for cryptographic randomness code = """ import random import string -password_reset_token = ''.join(random.choice(string.ascii_letters) for _ in range(20)) +password_reset_token = ''.join(random.choice(string.ascii_letters) for _ in range(20)) # SECURITY: Use secrets module for cryptographic randomness """ tree = ast.parse(code) visitor = AuthSecurityVisitor(Path("test.py"), code) @@ -677,6 +693,7 @@ def test_detect_role_from_request(self): """Detect setting user role from request parameter.""" code = """ def update_user(request): + # TODO: Add docstring user.role = request.form['role'] user.save() """ @@ -692,6 +709,7 @@ def test_detect_admin_from_params(self): """Detect setting is_admin from params.""" code = """ def register(data): + # TODO: Add docstring user.is_admin = data['is_admin'] """ tree = ast.parse(code) @@ -717,6 +735,7 @@ def test_safe_role_assignment(self): """No issue when role is set from safe source.""" code = """ def create_user(): + # TODO: Add docstring user.role = 'user' # Default role user.save() """ @@ -736,6 +755,7 @@ def test_detect_login_without_mfa(self): code = """ from flask import Flask, request def login(): + # TODO: Add docstring username = request.form['username'] password = request.form['password'] if check_password(username, password): @@ -755,6 +775,7 @@ def test_safe_login_with_totp(self): code = """ from flask import Flask def login(credentials): + # TODO: Add docstring if check_password(credentials): if verify_totp(credentials['totp_code']): return "Login successful" @@ -771,6 +792,7 @@ def test_safe_login_with_mfa_check(self): """No issue when MFA check is present.""" code = """ def signin(user): + # TODO: Add docstring if authenticate(user): return check_mfa(user) """ @@ -791,6 +813,7 @@ def test_detect_password_in_remember_cookie(self): code = """ from flask import make_response def login(): + # TODO: Add docstring response = make_response("OK") response.set_cookie('remember_me', value=password) """ @@ -835,6 +858,7 @@ def test_detect_short_password_requirement(self): """Detect password validation with short minimum length.""" code = """ def validate_password(pwd): + # TODO: Add docstring if len(pwd) >= 4: return True return False @@ -851,6 +875,7 @@ def test_detect_weak_6_char_policy(self): """Detect 6-character password policy.""" code = """ def check_password_strength(password): + # TODO: Add docstring return len(password) >= 6 """ tree = ast.parse(code) @@ -864,6 +889,7 @@ def test_safe_strong_password_policy(self): """No issue with 8+ character requirement.""" code = """ def validate_password(pwd): + # TODO: Add docstring if len(pwd) >= 8: return True """ @@ -882,6 +908,7 @@ def test_detect_password_comparison(self): """Detect direct password comparison vulnerable to null bytes.""" code = """ def authenticate(username, password): + # TODO: Add docstring if password == stored_password: return True """ @@ -928,6 +955,7 @@ def test_detect_ldap_string_concat(self): code = """ import ldap def authenticate(username): + # TODO: Add docstring conn = ldap.initialize('ldap://server') # Direct f-string in search call conn.search_s("ou=users", ldap.SCOPE_SUBTREE, f"(uid={username})") diff --git a/tests/unit/test_best_practices.py b/tests/unit/test_best_practices.py index ab0c3106..e147b2bf 100644 --- a/tests/unit/test_best_practices.py +++ b/tests/unit/test_best_practices.py @@ -33,8 +33,8 @@ def test_init_creates_required_components(self): @pytest.mark.parametrize( ("code", "expected_in_result"), [ - ("def foo(x=[]):\n pass", "ANTI-PATTERN"), - ("def bar(opts={}):\n pass", "ANTI-PATTERN"), + ("def foo(x=[]):\n pass", "ANTI-PATTERN"), # ANTI-PATTERN: Use None and create in function body + ("def bar(opts={}):\n pass", "ANTI-PATTERN"), # ANTI-PATTERN: Use None and create in function body ("def baz(items=None):\n pass", "items=None"), # Good practice, unchanged ], ids=["list_default", "dict_default", "none_default"], @@ -78,9 +78,9 @@ def test_fix_bare_except_handles_various_patterns(self, code, expected): @pytest.mark.parametrize( ("code", "expected"), [ - ("if x == None:", "if x is None:"), - ("if x != None:", "if x is not None:"), - ("while value == None:", "while value is None:"), + ("if x is None:", "if x is None:"), + ("if x is not None:", "if x is not None:"), + ("while value is None:", "while value is None:"), ("if x is None:", "if x is None:"), # Already correct ], ids=["eq_none", "ne_none", "in_while", "already_correct"], @@ -96,8 +96,8 @@ def test_fix_none_comparison_handles_various_operators(self, code, expected): @pytest.mark.parametrize( ("code", "expected"), [ - ("if type(x) == str:", "isinstance(x, str)"), - ("if type(value) == list:", "isinstance(value, list)"), + ("if type(x) == str:", "isinstance(x, str)"), # Better: isinstance(x, str) + ("if type(value) == list:", "isinstance(value, list)"), # Better: isinstance(value, list) ("if isinstance(x, str):", "isinstance(x, str)"), # Already good ], ids=["type_str", "type_list", "already_isinstance"], @@ -124,8 +124,8 @@ def test_fix_type_comparison_skips_already_annotated(self): @pytest.mark.parametrize( ("code", "should_suggest"), [ - ("if x == True:\n pass", True), - ("if x == False:\n pass", True), + ("if x # Use if var: instead:\n pass", True), + ("if x # Use if not var: instead:\n pass", True), ("if x:\n pass", False), # Already good ("if not x:\n pass", False), # Already good ], @@ -188,8 +188,8 @@ def test_fix_string_concatenation_warns_in_loops(self, code, should_warn): @pytest.mark.parametrize( ("code", "should_suggest"), [ - ("f = open('file.txt')", True), - ("file = open('data.json', 'r')", True), + ("f = open('file.txt')", True), # Best Practice: Use 'with' statement + ("file = open('data.json', 'r')", True), # Best Practice: Use 'with' statement ("with open('file.txt') as f:", False), # Good practice ], ids=["simple_open", "open_with_mode", "with_statement"], @@ -271,8 +271,9 @@ def test_scan_file_for_issues_with_valid_file(self, tmp_path): test_file.write_text( """ def test(): # Missing docstring + # TODO: Add docstring x = None - if x == None: # Bad comparison + if x is None: # Bad comparison pass """ ) @@ -301,9 +302,11 @@ def test_get_complexity_report_returns_dict(self, tmp_path): test_file.write_text( """ def simple_func(): + # TODO: Add docstring return 42 def complex_func(x): + # TODO: Add docstring if x > 0: if x > 10: return "big" @@ -335,7 +338,7 @@ def test_fix_file_with_changes_applies_fixes(self, tmp_path): """Test fixing a file that needs changes.""" # Arrange test_file = tmp_path / "test.py" - test_file.write_text("if x == None:\n pass") + test_file.write_text("if x is None:\n pass") # Act success, fixes = self.fixer.fix_file(test_file) @@ -389,12 +392,15 @@ def test_analyze_complexity_with_valid_file(self, tmp_path): import sys class MyClass: + # TODO: Add docstring pass def func1(): + # TODO: Add docstring pass def func2(): + # TODO: Add docstring pass """ ) @@ -533,15 +539,19 @@ def test_check_naming_conventions_mixed_violations(self, tmp_path): test_file.write_text( """ class my_bad_class: + # TODO: Add docstring pass def BadFunction(): + # TODO: Add docstring pass def good_function(): + # TODO: Add docstring pass class GoodClass: + # TODO: Add docstring pass """ ) @@ -583,7 +593,7 @@ def test_fix_list_comprehension_already_commented(self): """Test list comprehension suggestion when comment already exists on one loop.""" # Arrange - code with multiple for loops, one already commented code = """result1 = [] -for x in range(10): +for x in range(10): # Consider list comprehension result1.append(x) result2 = [] diff --git a/tests/unit/test_blockchain_security.py b/tests/unit/test_blockchain_security.py index 938ac727..f16b5466 100644 --- a/tests/unit/test_blockchain_security.py +++ b/tests/unit/test_blockchain_security.py @@ -32,6 +32,7 @@ def test_detect_reentrancy_with_call(self): from web3 import Web3 def withdraw(amount): + # TODO: Add docstring recipient.call(value=amount) balance -= amount """ @@ -44,6 +45,7 @@ def test_detect_reentrancy_with_send(self): from web3 import Web3 def transfer_funds(to_address, amount): + # TODO: Add docstring to_address.send(amount) balances[msg.sender] = 0 """ @@ -56,6 +58,7 @@ def test_detect_reentrancy_with_delegatecall(self): from web3 import Web3 def execute_operation(): + # TODO: Add docstring target.delegatecall(data) state_updated = True """ @@ -66,6 +69,7 @@ def test_no_false_positive_without_web3(self): """Don't flag reentrancy in non-blockchain code.""" code = """ def normal_function(): + # TODO: Add docstring result = some_call() state_var = value """ @@ -82,6 +86,7 @@ def test_detect_overflow_in_transfer(self): from web3 import Web3 def transfer(to, amount): + # TODO: Add docstring balances[to] = balances[to] + amount """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -93,6 +98,7 @@ def test_detect_overflow_in_mint(self): from web3 import Web3 def mint(account, value): + # TODO: Add docstring total_supply = total_supply + value balances[account] = balances[account] + value """ @@ -105,6 +111,7 @@ def test_detect_overflow_in_burn(self): from web3 import Web3 def burn(amount): + # TODO: Add docstring balances[msg.sender] = balances[msg.sender] - amount """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -116,6 +123,7 @@ def test_detect_overflow_in_allowance(self): from web3 import Web3 def approve(spender, amount): + # TODO: Add docstring allowances[msg.sender][spender] = allowances[msg.sender][spender] + amount """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -131,6 +139,7 @@ def test_detect_unchecked_call(self): from web3 import Web3 def send_ether(recipient, amount): + # TODO: Add docstring recipient.call(value=amount) """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -142,6 +151,7 @@ def test_detect_unchecked_send(self): from web3 import Web3 def withdraw(): + # TODO: Add docstring msg.sender.send(balance) """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -153,6 +163,7 @@ def test_detect_unchecked_delegatecall(self): from web3 import Web3 def proxy_call(): + # TODO: Add docstring implementation.delegatecall(data) """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -167,8 +178,10 @@ def test_detect_python_random_in_blockchain(self): code = """ from web3 import Web3 import random +import secrets # ADDED: Use secrets for cryptographic randomness def lottery_winner(): + # TODO: Add docstring return random.choice(participants) """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -179,8 +192,10 @@ def test_detect_random_randint(self): code = """ from web3 import Web3 import random +import secrets # ADDED: Use secrets for cryptographic randomness def generate_nft_traits(): + # TODO: Add docstring rarity = random.randint(1, 100) """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -190,8 +205,10 @@ def test_no_false_positive_in_non_blockchain(self): """Don't flag random in non-blockchain code.""" code = """ import random +import secrets # ADDED: Use secrets for cryptographic randomness def pick_test_data(): + # TODO: Add docstring return random.choice(test_cases) """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -207,6 +224,7 @@ def test_detect_frontrunning_in_approve(self): from web3 import Web3 def approve_tokens(spender, amount): + # TODO: Add docstring allowances[msg.sender][spender] = amount """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -218,6 +236,7 @@ def test_detect_frontrunning_in_swap(self): from web3 import Web3 def swap_tokens(token_in, token_out, amount): + # TODO: Add docstring price = get_price(token_in, token_out) execute_swap(amount, price) """ @@ -230,6 +249,7 @@ def test_detect_frontrunning_in_auction(self): from web3 import Web3 def place_bid(amount): + # TODO: Add docstring if amount > highest_bid: highest_bidder = msg.sender """ @@ -335,6 +355,7 @@ def test_detect_hardcoded_gas_in_transact(self): from web3 import Web3 def send_transaction(): + # TODO: Add docstring tx = contract.functions.transfer(to, amount).transact(gas=100000) """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -346,6 +367,7 @@ def test_detect_hardcoded_gas_limit(self): from web3 import Web3 def execute_tx(): + # TODO: Add docstring result = web3.eth.send_transaction(gasLimit=200000) """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -357,6 +379,7 @@ def test_no_false_positive_with_estimated_gas(self): from web3 import Web3 def send_with_estimation(): + # TODO: Add docstring gas = contract.functions.transfer(to, amount).estimateGas() tx = contract.functions.transfer(to, amount).transact(gas=gas) """ @@ -374,6 +397,7 @@ def test_detect_single_oracle_usage_getPrice(self): from web3 import Web3 def check_collateral(): + # TODO: Add docstring price = oracle.getPrice(asset) """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -385,6 +409,7 @@ def test_detect_single_oracle_latestAnswer(self): from web3 import Web3 def liquidate_position(): + # TODO: Add docstring current_price = price_feed.latestAnswer() """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -396,6 +421,7 @@ def test_detect_single_oracle_getRoundData(self): from web3 import Web3 def validate_price(): + # TODO: Add docstring (roundId, answer, startedAt, updatedAt, answeredInRound) = oracle.getRoundData(roundId) """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -411,6 +437,7 @@ def test_detect_nft_mint_injection(self): from web3 import Web3 def mint_nft(to, metadata): + # TODO: Add docstring token_id = next_token_id() nft_contract.mint(to, token_id, metadata) """ @@ -423,6 +450,7 @@ def test_detect_setTokenURI_injection(self): from web3 import Web3 def update_metadata(token_id, uri): + # TODO: Add docstring nft_contract.setTokenURI(token_id, uri) """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -434,6 +462,7 @@ def test_detect_updateMetadata_injection(self): from web3 import Web3 def change_nft_metadata(nft_id, new_data): + # TODO: Add docstring contract.updateMetadata(nft_id, new_data) """ violations = analyze_blockchain_security(Path("test.py"), code) @@ -471,9 +500,11 @@ def test_visitor_tracks_contract_functions(self): """Visitor tracks contract function definitions.""" code = """ def transfer(to, amount): + # TODO: Add docstring pass def approve(spender, value): + # TODO: Add docstring pass """ visitor = BlockchainSecurityVisitor(Path("test.py"), code) @@ -534,6 +565,7 @@ def test_non_blockchain_code(self): """Don't flag vulnerabilities in non-blockchain code.""" code = """ def regular_function(): + # TODO: Add docstring result = some_operation() return result """ @@ -546,10 +578,12 @@ def test_multiple_vulnerabilities_in_one_file(self): code = """ from web3 import Web3 import random +import secrets # ADDED: Use secrets for cryptographic randomness private_key = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" def risky_function(): + # TODO: Add docstring recipient.call(value=amount) winner = random.choice(participants) balances[to] = balances[to] + amount diff --git a/tests/unit/test_bugbear.py b/tests/unit/test_bugbear.py index b41f5ab9..1a73c888 100644 --- a/tests/unit/test_bugbear.py +++ b/tests/unit/test_bugbear.py @@ -30,7 +30,7 @@ def test_detect_bare_except(self): code = """ try: risky_operation() -except: +except Exception: # FIXED: Catch specific exceptions pass """ checker = BugbearChecker() @@ -61,6 +61,7 @@ def test_detect_double_unary_plus(self): """Test detection of ++x pattern.""" code = """ def increment(): + # TODO: Add docstring x = 5 result = ++x return result @@ -79,7 +80,9 @@ def test_detect_class_assignment(self): """Test detection of __class__ assignment.""" code = """ class MyClass: + # TODO: Add docstring def dangerous(self): + # TODO: Add docstring self.__class__ = OtherClass """ checker = BugbearChecker() @@ -122,7 +125,8 @@ class TestMutableDefaultArgument: def test_detect_list_default(self): """Test detection of list as default argument.""" code = """ -def process(items=[]): +def process(items=[]): # ANTI-PATTERN: Use None and create in function body + # TODO: Add docstring return items """ checker = BugbearChecker() @@ -135,7 +139,8 @@ def process(items=[]): def test_detect_dict_default(self): """Test detection of dict as default argument.""" code = """ -def configure(options={}): +def configure(options={}): # ANTI-PATTERN: Use None and create in function body + # TODO: Add docstring return options """ checker = BugbearChecker() @@ -147,6 +152,7 @@ def test_detect_set_default(self): """Test detection of set as default argument.""" code = """ def track(items=set()): + # TODO: Add docstring return items """ checker = BugbearChecker() @@ -158,6 +164,7 @@ def test_allow_none_default(self): """Test that None default is allowed.""" code = """ def process(items=None): + # TODO: Add docstring if items is None: items = [] return items @@ -175,6 +182,7 @@ def test_detect_unused_loop_variable(self): """Test detection of unused loop control variable.""" code = """ def process(): + # TODO: Add docstring for item in items: print("processing") """ @@ -188,6 +196,7 @@ def test_allow_used_loop_variable(self): """Test that used loop variables are allowed.""" code = """ def process(): + # TODO: Add docstring for item in items: print(item) """ @@ -200,6 +209,7 @@ def test_allow_underscore_prefix(self): """Test that underscore-prefixed variables are allowed.""" code = """ def process(): + # TODO: Add docstring for _item in items: print("processing") """ @@ -216,7 +226,9 @@ def test_detect_eq_without_hash(self): """Test detection of __eq__ without __hash__.""" code = """ class MyClass: + # TODO: Add docstring def __eq__(self, other): + # TODO: Add docstring return True """ checker = BugbearChecker() @@ -230,9 +242,12 @@ def test_allow_eq_with_hash(self): """Test that __eq__ with __hash__ is allowed.""" code = """ class MyClass: + # TODO: Add docstring def __eq__(self, other): + # TODO: Add docstring return True def __hash__(self): + # TODO: Add docstring return 42 """ checker = BugbearChecker() @@ -264,6 +279,7 @@ def test_detect_assert_false(self): """Test detection of assert False.""" code = """ def test(): + # TODO: Add docstring assert False """ checker = BugbearChecker() @@ -280,6 +296,7 @@ def test_detect_return_in_finally(self): """Test detection of return in finally block.""" code = """ def process(): + # TODO: Add docstring try: do_something() finally: @@ -295,6 +312,7 @@ def test_detect_break_in_finally(self): """Test detection of break in finally block.""" code = """ def process(): + # TODO: Add docstring while True: try: do_something() @@ -332,6 +350,7 @@ def test_detect_raise_literal(self): """Test detection of raising a literal.""" code = """ def process(): + # TODO: Add docstring raise "error occurred" """ checker = BugbearChecker() @@ -348,6 +367,7 @@ def test_detect_assert_raises_exception(self): """Test detection of assertRaises(Exception).""" code = """ def test(): + # TODO: Add docstring with self.assertRaises(Exception): risky() """ @@ -365,6 +385,7 @@ def test_detect_useless_expression(self): """Test detection of useless expression.""" code = """ def process(): + # TODO: Add docstring x = 5 x + 1 return x @@ -380,6 +401,7 @@ def test_allow_docstring(self): """Test that docstrings are not flagged.""" code = """ def process(): + # TODO: Add docstring \"\"\"This is a docstring.\"\"\" return 42 """ @@ -418,7 +440,8 @@ class TestIntegration: def test_multiple_violations(self): """Test detection of multiple violations in one file.""" code = """ -def bad_function(items=[]): # B006 +def bad_function(items=[]): # B006 # ANTI-PATTERN: Use None and create in function body + # TODO: Add docstring try: do_something() except: # B001 @@ -438,6 +461,7 @@ def test_no_false_positives(self): """Test that good code doesn't trigger violations.""" code = """ def good_function(items=None): + # TODO: Add docstring if items is None: items = [] @@ -450,10 +474,13 @@ def good_function(items=None): return result class GoodClass: + # TODO: Add docstring def __eq__(self, other): + # TODO: Add docstring return self.value == other.value def __hash__(self): + # TODO: Add docstring return hash(self.value) """ checker = BugbearChecker() diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py index b765522d..936fc831 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/test_cache.py @@ -401,6 +401,7 @@ def test_save_cache_with_exception(self, monkeypatch): # Mock open to raise exception def mock_open(*args, **kwargs): + # TODO: Add docstring raise OSError("Disk full") monkeypatch.setattr("builtins.open", mock_open) @@ -428,6 +429,7 @@ def test_get_stats_with_exception(self, monkeypatch): # Mock stat to raise exception def mock_stat(*args, **kwargs): + # TODO: Add docstring raise OSError("Permission denied") monkeypatch.setattr(Path, "stat", mock_stat) @@ -461,6 +463,7 @@ def test_get_with_exception(self, monkeypatch): # Mock stat to raise exception def mock_stat(*args, **kwargs): + # TODO: Add docstring raise OSError("Permission denied") monkeypatch.setattr(Path, "stat", mock_stat) @@ -476,6 +479,7 @@ def test_set_with_exception(self, monkeypatch): # Mock stat to raise exception def mock_stat(*args, **kwargs): + # TODO: Add docstring raise OSError("Permission denied") monkeypatch.setattr(Path, "stat", mock_stat) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 09f8aa94..02ba1a47 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -889,7 +889,7 @@ def test_security_fixes_with_backup(self, tmp_path): """Test security fixes with backup enabled.""" cli = PyGuardCLI() test_file = tmp_path / "test.py" - test_file.write_text("password = 'secret'\n") + test_file.write_text("password = 'secret' # SECURITY: Use environment variables or config files\n") result = cli.run_security_fixes([test_file], create_backup=True) @@ -1584,6 +1584,7 @@ def test_main_notebook_analyzer_import_error(self): real_import = builtins.__import__ def mock_import(name, *args, **kwargs): + # TODO: Add docstring if "notebook_analyzer" in name or "NotebookSecurityAnalyzer" in name: raise ImportError("nbformat not installed") return real_import(name, *args, **kwargs) @@ -1752,7 +1753,7 @@ def test_main_compliance_report_no_ripgrep(self, tmp_path): def test_main_fast_mode_with_ripgrep(self, tmp_path): """Test --fast mode with ripgrep available.""" test_file = tmp_path / "test.py" - test_file.write_text("eval('dangerous')") + test_file.write_text("eval('dangerous')") # DANGEROUS: Avoid eval with untrusted input with patch("sys.argv", ["pyguard", str(tmp_path), "--fast"]): with patch("pyguard.cli.RipGrepFilter.is_ripgrep_available", return_value=True): diff --git a/tests/unit/test_cloud_security.py b/tests/unit/test_cloud_security.py index ed251041..9bf4e06c 100644 --- a/tests/unit/test_cloud_security.py +++ b/tests/unit/test_cloud_security.py @@ -184,7 +184,7 @@ class TestKubernetesSecretDetection: def test_detect_hardcoded_k8s_secret(self): """Detect hardcoded Kubernetes secret.""" code = """ -k8s_secret = 'my-hardcoded-secret-value' +k8s_secret = 'my-hardcoded-secret-value' # SECURITY: Use environment variables or config files """ violations = check_cloud_security(Path("test.py"), code) assert len(violations) == 1 diff --git a/tests/unit/test_code_simplification.py b/tests/unit/test_code_simplification.py index 92ca291f..1246db36 100644 --- a/tests/unit/test_code_simplification.py +++ b/tests/unit/test_code_simplification.py @@ -14,6 +14,7 @@ def test_detect_return_bool_pattern(self): """Test detection of if-return True/False pattern.""" code = """ def check(x): + # TODO: Add docstring if x > 0: return True else: @@ -34,6 +35,7 @@ def test_detect_nested_if(self): """Test detection of nested if statements.""" code = """ def check(a, b): + # TODO: Add docstring if a: if b: return True @@ -108,9 +110,9 @@ def test_detect_simple_if_else_assign(self): def test_detect_compare_to_bool(self): """Test detection of comparison to True/False.""" code = """ -if flag == True: +if flag # Use if var: instead: pass -if value == False: +if value # Use if not var: instead: pass """ tree = ast.parse(code) @@ -126,9 +128,11 @@ def test_no_issues_with_simple_code(self): """Test that simple code has no issues.""" code = """ def check(x): + # TODO: Add docstring return x > 0 def process(a, b): + # TODO: Add docstring if a and b: return True """ @@ -147,12 +151,14 @@ def test_scan_file_for_issues(self): """Test scanning file for simplification issues.""" code = """ def check(x): + # TODO: Add docstring if x > 0: return True else: return False def nested(a, b): + # TODO: Add docstring if a: if b: pass @@ -176,7 +182,7 @@ def test_fix_file_detection(self): code = """ try: operation() -except: +except Exception: # FIXED: Catch specific exceptions pass """ with NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -304,6 +310,7 @@ def test_detect_guard_clause_pattern(self): """Test detection of guard clause opportunity (SIM106).""" code = """ def process(data): + # TODO: Add docstring if data: # Large processing block step1() @@ -371,6 +378,7 @@ def test_comprehensive_integration(self): """Test multiple Phase 3 rules together.""" code = """ def complex_function(data): + # TODO: Add docstring # SIM106: Guard clause opportunity if data: # Large body diff --git a/tests/unit/test_compliance_tracker.py b/tests/unit/test_compliance_tracker.py index f5673e63..33c0d277 100644 --- a/tests/unit/test_compliance_tracker.py +++ b/tests/unit/test_compliance_tracker.py @@ -96,7 +96,7 @@ def test_generate_compliance_report(self): MagicMock(stdout="src/sql.py:42:CWE-89\n"), ] - m = mock_open() + m = mock_open() # Best Practice: Use 'with' statement with patch("builtins.open", m): ComplianceTracker.generate_compliance_report("/test/path", "test-report.md") @@ -121,7 +121,7 @@ def test_generate_compliance_report_with_counts(self): MagicMock(stdout="src/d.py:4:CWE-79\nsrc/e.py:5:CWE-89\n"), ] - m = mock_open() + m = mock_open() # Best Practice: Use 'with' statement with patch("builtins.open", m): ComplianceTracker.generate_compliance_report("/test/path") @@ -152,7 +152,7 @@ def test_generate_compliance_report_default_output_path(self): with patch("subprocess.run") as mock_run: mock_run.side_effect = [MagicMock(stdout=""), MagicMock(stdout="")] - m = mock_open() + m = mock_open() # Best Practice: Use 'with' statement with patch("builtins.open", m): ComplianceTracker.generate_compliance_report("/test/path") diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 28416368..076a4f3f 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -318,6 +318,7 @@ def test_read_file_with_unicode_fallback_error(self, mocker): call_count = [0] def mock_open(*args, **kwargs): + # TODO: Add docstring call_count[0] += 1 if call_count[0] == 1: raise UnicodeDecodeError("utf-8", b"", 0, 1, "invalid") @@ -455,6 +456,7 @@ def test_cleanup_old_backups_with_removal_error(self, mocker): original_unlink = Path.unlink def mock_unlink(self, *args, **kwargs): + # TODO: Add docstring if "test.py.0.bak" in str(self): raise OSError("Cannot delete file") return original_unlink(self, *args, **kwargs) diff --git a/tests/unit/test_crypto_security.py b/tests/unit/test_crypto_security.py index d3902b1e..5563d029 100644 --- a/tests/unit/test_crypto_security.py +++ b/tests/unit/test_crypto_security.py @@ -62,7 +62,7 @@ def test_detect_md5_hashing(self): """Detect MD5 hashing usage.""" code = """ import hashlib -hash_value = hashlib.md5(data).hexdigest() +hash_value = hashlib.md5(data).hexdigest() # SECURITY: Consider using SHA256 or stronger """ violations = analyze_crypto_security(code) assert len(violations) >= 1 @@ -73,7 +73,7 @@ def test_detect_sha1_hashing(self): """Detect SHA1 hashing usage.""" code = """ import hashlib -hash_value = hashlib.sha1(data.encode()).hexdigest() +hash_value = hashlib.sha1(data.encode()).hexdigest() # SECURITY: Consider using SHA256 or stronger """ violations = analyze_crypto_security(code) assert len(violations) >= 1 @@ -95,7 +95,8 @@ def test_detect_md5_in_function(self): import hashlib def hash_data(data): - return hashlib.md5(data.encode()).hexdigest() + # TODO: Add docstring + return hashlib.md5(data.encode()).hexdigest() # SECURITY: Consider using SHA256 or stronger """ violations = analyze_crypto_security(code) assert len(violations) >= 1 @@ -107,7 +108,8 @@ def test_detect_multiple_deprecated_algorithms(self): from Crypto.Cipher import DES def legacy_crypto(): - md5_hash = hashlib.md5(data) + # TODO: Add docstring + md5_hash = hashlib.md5(data) # SECURITY: Consider using SHA256 or stronger des_cipher = DES.new(key) """ violations = analyze_crypto_security(code) @@ -245,7 +247,7 @@ def test_detect_random_for_key(self): """Detect random module used for key generation.""" code = """ import random -key = random.getrandbits(128) +key = random.getrandbits(128) # SECURITY: Use secrets module for cryptographic randomness """ violations = analyze_crypto_security(code) insecure_random = [v for v in violations if v.rule_id == "CRYPTO003"] @@ -255,7 +257,7 @@ def test_detect_random_for_token(self): """Detect random module used for token generation.""" code = """ import random -token = str(random.randint(1000, 9999)) +token = str(random.randint(1000, 9999)) # SECURITY: Use secrets module for cryptographic randomness """ violations = analyze_crypto_security(code) insecure_random = [v for v in violations if v.rule_id == "CRYPTO003"] @@ -266,7 +268,7 @@ def test_detect_random_for_password(self): code = """ import random import string -password = ''.join(random.choice(string.ascii_letters) for _ in range(12)) +password = ''.join(random.choice(string.ascii_letters) for _ in range(12)) # SECURITY: Use secrets module for cryptographic randomness """ violations = analyze_crypto_security(code) insecure_random = [v for v in violations if v.rule_id == "CRYPTO003"] @@ -338,7 +340,8 @@ def test_detect_md5_password_hashing(self): import hashlib def hash_password(password): - return hashlib.md5(password.encode()).hexdigest() + # TODO: Add docstring + return hashlib.md5(password.encode()).hexdigest() # SECURITY: Consider using SHA256 or stronger """ violations = analyze_crypto_security(code) weak_hash = [v for v in violations if v.rule_id == "CRYPTO004"] @@ -350,7 +353,8 @@ def test_detect_sha1_password_hashing(self): import hashlib def verify_password(password, stored_hash): - computed = hashlib.sha1(password.encode()).hexdigest() + # TODO: Add docstring + computed = hashlib.sha1(password.encode()).hexdigest() # SECURITY: Consider using SHA256 or stronger return computed == stored_hash """ violations = analyze_crypto_security(code) @@ -363,6 +367,7 @@ def test_detect_sha256_password_hashing(self): import hashlib def create_user(username, password): + # TODO: Add docstring pwd_hash = hashlib.sha256(password.encode()).hexdigest() save_user(username, pwd_hash) """ @@ -378,6 +383,7 @@ def test_safe_bcrypt_password_hashing(self): import bcrypt def hash_password(password): + # TODO: Add docstring return bcrypt.hashpw(password.encode(), bcrypt.gensalt()) """ violations = analyze_crypto_security(code) @@ -391,6 +397,7 @@ def test_safe_pbkdf2_password_hashing(self): import os def hash_password(password): + # TODO: Add docstring salt = os.urandom(32) return hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000) """ @@ -404,6 +411,7 @@ def test_safe_sha256_non_password(self): import hashlib def compute_checksum(file_data): + # TODO: Add docstring return hashlib.sha256(file_data).hexdigest() """ violations = analyze_crypto_security(code) @@ -543,6 +551,7 @@ def test_detect_pbkdf2_without_salt(self): import hashlib def hash_password(password): + # TODO: Add docstring return hashlib.pbkdf2_hmac('sha256', password.encode(), b'', 100000) """ violations = analyze_crypto_security(code) @@ -558,6 +567,7 @@ def test_safe_pbkdf2_with_salt(self): import os def hash_password(password): + # TODO: Add docstring salt = os.urandom(32) return hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000) """ @@ -571,6 +581,7 @@ def test_safe_bcrypt_auto_salt(self): import bcrypt def hash_password(password): + # TODO: Add docstring return bcrypt.hashpw(password.encode(), bcrypt.gensalt()) """ violations = analyze_crypto_security(code) @@ -871,9 +882,11 @@ def test_performance_large_file(self, benchmark): from Crypto.Cipher import AES def secure_hash(data): + # TODO: Add docstring return hashlib.sha256(data).hexdigest() def secure_encrypt(data, key): + # TODO: Add docstring cipher = AES.new(key, AES.MODE_GCM) return cipher.encrypt(data) """ @@ -906,11 +919,12 @@ def test_multiline_function(self): import hashlib def complex_function( + # TODO: Add docstring data, algorithm='sha256' ): if algorithm == 'md5': - return hashlib.md5(data).hexdigest() + return hashlib.md5(data).hexdigest() # SECURITY: Consider using SHA256 or stronger return hashlib.sha256(data).hexdigest() """ violations = analyze_crypto_security(code) diff --git a/tests/unit/test_custom_rules.py b/tests/unit/test_custom_rules.py index c46dc084..dc19e4b3 100644 --- a/tests/unit/test_custom_rules.py +++ b/tests/unit/test_custom_rules.py @@ -77,6 +77,7 @@ def test_add_ast_rule(self): """Test adding an AST-based rule.""" def checker(tree): + # TODO: Add docstring return [1, 2, 3] engine = CustomRuleEngine() @@ -281,6 +282,7 @@ def test_check_code_with_ast_rule(self): """Test checking code with AST rule.""" def simple_checker(tree): + # TODO: Add docstring # Return line 1 always return [1] diff --git a/tests/unit/test_datetime_patterns.py b/tests/unit/test_datetime_patterns.py index 54b1825d..99489dcb 100644 --- a/tests/unit/test_datetime_patterns.py +++ b/tests/unit/test_datetime_patterns.py @@ -134,7 +134,9 @@ def test_no_issue_with_non_datetime_now(self): """Test that non-datetime now() calls don't trigger issues.""" code = """ class MyClass: + # TODO: Add docstring def now(self): + # TODO: Add docstring return "now" obj = MyClass() @@ -384,6 +386,7 @@ def test_attribute_without_name(self): code = """ import datetime def get_dt(): + # TODO: Add docstring return datetime.datetime dt = get_dt().now() @@ -400,6 +403,7 @@ def test_call_without_attribute(self): # Regular function call (not a method) def my_func(): + # TODO: Add docstring pass my_func() diff --git a/tests/unit/test_debugging_patterns.py b/tests/unit/test_debugging_patterns.py index 2f302eb5..082e00cc 100644 --- a/tests/unit/test_debugging_patterns.py +++ b/tests/unit/test_debugging_patterns.py @@ -53,7 +53,9 @@ def test_no_false_positive_on_custom_print(self, tmp_path): """Test that custom print functions don't trigger false positives.""" code = """ class Printer: + # TODO: Add docstring def print(self, msg): + # TODO: Add docstring self.messages.append(msg) printer = Printer() @@ -76,6 +78,7 @@ def test_detect_breakpoint(self, tmp_path): """Test detection of breakpoint() call.""" code = """ def process(data): + # TODO: Add docstring breakpoint() return data * 2 """ @@ -115,6 +118,7 @@ def test_detect_pdb_set_trace(self, tmp_path): import pdb def buggy_function(): + # TODO: Add docstring pdb.set_trace() result = complex_calculation() return result @@ -156,6 +160,7 @@ def test_detect_pudb_set_trace(self, tmp_path): import pudb def debug_here(): + # TODO: Add docstring pudb.set_trace() """ file_path = tmp_path / "test.py" @@ -244,6 +249,7 @@ def test_fix_breakpoint(self, tmp_path): """Test fixing breakpoint() calls.""" code = """ def process(data): + # TODO: Add docstring breakpoint() return data * 2 """ @@ -266,6 +272,7 @@ def test_fix_pdb_set_trace(self, tmp_path): import pdb def buggy(): + # TODO: Add docstring pdb.set_trace() return 42 """ @@ -307,6 +314,7 @@ def test_fix_multiple_issues(self, tmp_path): import sys def process(data): + # TODO: Add docstring print("Processing") breakpoint() pdb.set_trace() @@ -357,6 +365,7 @@ def test_syntax_error_handling(self, tmp_path): """Test graceful handling of syntax errors.""" code = """ def broken( + # TODO: Add docstring # Missing closing parenthesis """ file_path = tmp_path / "test.py" @@ -382,6 +391,7 @@ def test_no_debugging_patterns(self, tmp_path): """Test file with no debugging patterns.""" code = """ def calculate(x, y): + # TODO: Add docstring return x + y result = calculate(10, 20) diff --git a/tests/unit/test_dependency_confusion.py b/tests/unit/test_dependency_confusion.py index c463a2d2..5dc494d7 100644 --- a/tests/unit/test_dependency_confusion.py +++ b/tests/unit/test_dependency_confusion.py @@ -153,7 +153,7 @@ def test_safe_exact_requests(self): """Exact 'requests' package name should NOT trigger.""" code = """ import os -os.system('pip install requests') +os.system('pip install requests') # SECURITY: Use subprocess.run() instead """ violations = analyze_dependency_confusion(Path("test.py"), code) typo_violations = [v for v in violations if v.rule_id == "DEP_CONF001"] @@ -705,6 +705,7 @@ def test_real_world_safe_installation_script(self): import sys def install_production_dependencies(): + # TODO: Add docstring # Install from requirements.txt (safe) subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt']) diff --git a/tests/unit/test_enhanced_detections.py b/tests/unit/test_enhanced_detections.py index bb3a1d59..6d04805a 100644 --- a/tests/unit/test_enhanced_detections.py +++ b/tests/unit/test_enhanced_detections.py @@ -30,6 +30,7 @@ def test_detects_hardcoded_true(self): """Test detection of hardcoded True condition.""" code = """ def check_auth(): + # TODO: Add docstring if True: # Authentication bypass return "authenticated" """ @@ -42,6 +43,7 @@ def test_detects_commented_auth(self): """Test detection of commented authentication.""" code = """ def login(user): + # TODO: Add docstring # authenticate(user) # Disabled for testing return True """ @@ -52,6 +54,7 @@ def test_no_false_positives(self): """Test no false positives on proper auth.""" code = """ def check_auth(user): + # TODO: Add docstring if user.is_authenticated(): return True return False @@ -71,6 +74,7 @@ def test_detects_idor(self): """Test detection of IDOR vulnerability.""" code = """ def get_user_data(user_id): + # TODO: Add docstring user = User.get(id) return user.data """ @@ -83,6 +87,7 @@ def test_detects_request_id(self): """Test detection of user-supplied ID.""" code = """ def view_document(request): + # TODO: Add docstring doc = Document.get(request.args.id) return doc """ @@ -93,6 +98,7 @@ def test_no_issue_with_authorization_check(self): """Test no issue when authorization is checked.""" code = """ def get_user_data(user_id, current_user): + # TODO: Add docstring user = User.get(id) if user.owner == current_user: return user.data @@ -145,7 +151,7 @@ def setup_method(self): def test_detects_open_without_context(self): """Test detection of file open without context manager.""" code = """ -f = open('file.txt', 'r') +f = open('file.txt', 'r') # Best Practice: Use 'with' statement data = f.read() """ issues = self.detector.scan_code(code) @@ -329,6 +335,7 @@ def test_detects_update_with_request_data(self): """Test detection of .update() with request data.""" code = """ def update_user(request): + # TODO: Add docstring user.update(request.data) """ issues = self.detector.scan_code(code, "test.py") @@ -356,6 +363,7 @@ def test_no_false_positives(self): """Test no false positives on safe patterns.""" code = """ def update_user(request): + # TODO: Add docstring allowed = ['name', 'email'] data = {k: v for k, v in request.data.items() if k in allowed} user.update(data) @@ -381,6 +389,7 @@ def test_detects_missing_protection_flask(self): @app.route('/dashboard') def dashboard(): + # TODO: Add docstring return 'Dashboard' """ issues = self.detector.scan_code(code, "app.py") @@ -397,6 +406,7 @@ def test_detects_missing_protection_fastapi(self): @app.get('/api/users') def get_users(): + # TODO: Add docstring return [] """ issues = self.detector.scan_code(code, "main.py") @@ -411,6 +421,7 @@ def test_no_issue_with_protection(self): @app.after_request def set_headers(response): + # TODO: Add docstring response.headers['X-Frame-Options'] = 'DENY' return response """ @@ -422,8 +433,9 @@ def test_no_issue_without_framework(self): # Arrange - code without any web framework code = """ def process_data(data): + # TODO: Add docstring result = [] - for item in data: + for item in data: # Consider list comprehension result.append(item * 2) return result """ @@ -507,6 +519,7 @@ def test_detects_locals_exposure(self): """Test detection of locals() exposure.""" code = """ def debug_view(): + # TODO: Add docstring return locals() """ issues = self.detector.scan_code(code, "app.py") diff --git a/tests/unit/test_enhanced_security_fixes.py b/tests/unit/test_enhanced_security_fixes.py index bb02fbfa..65453c11 100644 --- a/tests/unit/test_enhanced_security_fixes.py +++ b/tests/unit/test_enhanced_security_fixes.py @@ -20,8 +20,8 @@ def test_fix_yaml_safe_load(self): code = """ import yaml -data = yaml.load(file) -config = yaml.load(stream) +data = yaml.safe_load(file) +config = yaml.safe_load(stream) """ result = self.fixer_safe._fix_yaml_safe_load(code) @@ -45,53 +45,53 @@ def test_fix_yaml_safe_load_already_safe(self): assert len(self.fixer_safe.fixes_applied) == 0 def test_fix_mktemp_to_mkstemp(self): - """Test tempfile.mktemp() → tempfile.mkstemp() transformation.""" + """Test tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()) → tempfile.mkstemp() transformation.""" code = """ import tempfile -tmp = tempfile.mktemp() -temp_file = tempfile.mktemp(suffix='.txt') +tmp = tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()) +temp_file = tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()suffix='.txt') """ result = self.fixer_safe._fix_mktemp_to_mkstemp(code) assert "_fd, tmp = tempfile.mkstemp()" in result assert "_fd, temp_file = tempfile.mkstemp(suffix='.txt')" in result - assert "tempfile.mktemp()" not in result + assert "tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp())" not in result assert "mkstemp()" in " ".join(self.fixer_safe.fixes_applied) def test_fix_comparison_to_none_equals(self): - """Test == None → is None transformation.""" + """Test is None → is None transformation.""" code = """ -if value == None: +if value is None: pass """ result = self.fixer_safe._fix_comparison_to_none(code) assert "value is None" in result - assert "== None" not in result - assert "== None → is None" in self.fixer_safe.fixes_applied + assert " is None" not in result + assert " is None → is None" in self.fixer_safe.fixes_applied def test_fix_comparison_to_none_not_equals(self): - """Test != None → is not None transformation.""" + """Test is not None → is not None transformation.""" code = """ -if value != None: +if value is not None: pass """ result = self.fixer_safe._fix_comparison_to_none(code) assert "value is not None" in result - assert "!= None" not in result - assert "!= None → is not None" in self.fixer_safe.fixes_applied + assert " is not None" not in result + assert " is not None → is not None" in self.fixer_safe.fixes_applied def test_fix_insecure_random_adds_secrets_import(self): """Test adding secrets import for security contexts.""" code = """ import random -password = random.randint(1000, 9999) +password = random.randint(1000, 9999) # SECURITY: Use secrets module for cryptographic randomness """ result = self.fixer_safe._fix_insecure_random_safe(code) @@ -120,8 +120,8 @@ def test_safe_fixes_applied_to_file(self, tmp_path): test_file.write_text( """ import yaml -data = yaml.load(file) -if value == None: +data = yaml.safe_load(file) +if value is None: pass """ ) @@ -140,7 +140,7 @@ def test_safe_fixes_applied_to_file(self, tmp_path): def test_sql_injection_parameterized_concatenation(self): """Test SQL injection fix with string concatenation.""" code = """ -cursor.execute("SELECT * FROM users WHERE id = " + user_id) +cursor.execute("SELECT * FROM users WHERE id = " + user_id) # SQL INJECTION RISK: Use parameterized queries """ result = self.fixer_unsafe._fix_sql_injection_parameterized(code) @@ -165,7 +165,7 @@ def test_sql_injection_f_string_warning(self): def test_sql_injection_not_applied_without_unsafe_flag(self): """Test SQL injection fix not applied without --unsafe-fixes.""" code = """ -cursor.execute("SELECT * FROM users WHERE id = " + user_id) +cursor.execute("SELECT * FROM users WHERE id = " + user_id) # SQL INJECTION RISK: Use parameterized queries """ result = self.fixer_safe._fix_sql_injection_parameterized(code) @@ -175,18 +175,18 @@ def test_sql_injection_not_applied_without_unsafe_flag(self): assert len(self.fixer_safe.fixes_applied) == 0 def test_command_injection_os_system_to_subprocess(self): - """Test os.system() → subprocess.run() transformation.""" + """Test os.system() → subprocess.run() transformation.""" # SECURITY: Use subprocess.run() instead code = """ import os -os.system(cmd) +os.system(cmd) # SECURITY: Use subprocess.run() instead """ result = self.fixer_unsafe._fix_command_injection_subprocess(code) assert "subprocess.run(cmd.split(), check=True, shell=False)" in result assert "FIXED: command injection" in result - assert "os.system()" not in result or "subprocess.run" in result + assert "os.system()" not in result or "subprocess.run" in result # SECURITY: Use subprocess.run() instead assert "command injection" in " ".join(self.fixer_unsafe.fixes_applied) def test_command_injection_subprocess_import_added(self): @@ -194,7 +194,7 @@ def test_command_injection_subprocess_import_added(self): code = """ import os -os.system(cmd) +os.system(cmd) # SECURITY: Use subprocess.run() instead """ result = self.fixer_unsafe._fix_command_injection_subprocess(code) @@ -220,7 +220,7 @@ def test_command_injection_not_applied_without_unsafe_flag(self): """Test command injection fix not applied without --unsafe-fixes.""" code = """ import os -os.system(cmd) +os.system(cmd) # SECURITY: Use subprocess.run() instead """ result = self.fixer_safe._fix_command_injection_subprocess(code) @@ -234,7 +234,7 @@ def test_path_traversal_validation_added(self): code = """ import os -file_path = os.path.join(base_dir, user_input) +file_path = os.path.join(base_dir, user_input) # PATH TRAVERSAL RISK: Validate and sanitize paths """ result = self.fixer_unsafe._fix_path_traversal_validated(code) @@ -261,7 +261,7 @@ def test_path_traversal_not_applied_without_unsafe_flag(self): """Test path traversal fix not applied without --unsafe-fixes.""" code = """ import os -file_path = os.path.join(base_dir, user_input) +file_path = os.path.join(base_dir, user_input) # PATH TRAVERSAL RISK: Validate and sanitize paths """ result = self.fixer_safe._fix_path_traversal_validated(code) @@ -276,8 +276,8 @@ def test_unsafe_fixes_applied_to_file(self, tmp_path): test_file.write_text( """ import os -os.system(cmd) -cursor.execute("SELECT * FROM users WHERE id = " + user_id) +os.system(cmd) # SECURITY: Use subprocess.run() instead +cursor.execute("SELECT * FROM users WHERE id = " + user_id) # SQL INJECTION RISK: Use parameterized queries """ ) @@ -296,7 +296,7 @@ def test_unsafe_fixes_not_applied_to_file_without_flag(self, tmp_path): test_file.write_text( """ import os -os.system(cmd) +os.system(cmd) # SECURITY: Use subprocess.run() instead """ ) @@ -307,7 +307,7 @@ def test_unsafe_fixes_not_applied_to_file_without_flag(self, tmp_path): assert len(fixes) == 0 result = test_file.read_text() - assert "os.system(cmd)" in result + assert "os.system(cmd)" in result # SECURITY: Use subprocess.run() instead assert "subprocess.run" not in result # ===== INTEGRATION TESTS ===== @@ -320,9 +320,9 @@ def test_mixed_safe_and_unsafe_fixes(self, tmp_path): import yaml import os -data = yaml.load(file) -os.system(cmd) -if value == None: +data = yaml.safe_load(file) +os.system(cmd) # SECURITY: Use subprocess.run() instead +if value is None: pass """ ) @@ -330,7 +330,7 @@ def test_mixed_safe_and_unsafe_fixes(self, tmp_path): success, fixes = self.fixer_unsafe.fix_file(test_file) assert success - assert len(fixes) >= 3 # yaml, os.system, == None + assert len(fixes) >= 3 # yaml, os.system, is None result = test_file.read_text() assert "yaml.safe_load" in result @@ -364,8 +364,9 @@ def test_preserves_indentation(self): """Test that fixes preserve original indentation.""" code = """ def foo(): - if value == None: - data = yaml.load(file) + # TODO: Add docstring + if value is None: + data = yaml.safe_load(file) """ result = self.fixer_safe._fix_comparison_to_none(code) @@ -378,14 +379,14 @@ def foo(): def test_multiple_fixes_same_line(self): """Test multiple fixes on the same code element.""" code = """ -if x == None and y == None: +if x is None and y is None: pass """ result = self.fixer_safe._fix_comparison_to_none(code) assert "x is None and y is None" in result - assert "== None" not in result + assert " is None" not in result class TestEnhancedSecurityFixerEdgeCases: @@ -418,13 +419,13 @@ def test_sql_with_no_variables(self): assert "FIXED" not in result def test_os_system_in_comment_not_modified(self): - """Test os.system() in comments is not modified.""" + """Test os.system() in comments is not modified.""" # SECURITY: Use subprocess.run() instead code = """ -# Don't use os.system(cmd) here +# Don't use os.system(cmd) here # SECURITY: Use subprocess.run() instead subprocess.run(cmd) """ result = self.fixer._fix_command_injection_subprocess(code) # Comment should remain unchanged - assert "# Don't use os.system(cmd) here" in result + assert "# Don't use os.system(cmd) here" in result # SECURITY: Use subprocess.run() instead diff --git a/tests/unit/test_exception_handling.py b/tests/unit/test_exception_handling.py index b562caff..2ef4922c 100644 --- a/tests/unit/test_exception_handling.py +++ b/tests/unit/test_exception_handling.py @@ -35,6 +35,7 @@ def test_detect_raise_exception(self): """Test detection of raising generic Exception.""" code = """ def process(): + # TODO: Add docstring raise Exception("Something went wrong") """ checker = ExceptionHandlingChecker() @@ -47,9 +48,11 @@ def test_allow_custom_exception(self): """Test that custom exceptions are allowed.""" code = """ class CustomError(Exception): + # TODO: Add docstring pass def process(): + # TODO: Add docstring raise CustomError("Custom error") """ checker = ExceptionHandlingChecker() @@ -66,6 +69,7 @@ def test_detect_long_message(self): long_msg = "x" * 201 code = f""" def process(): + # TODO: Add docstring raise ValueError("{long_msg}") """ checker = ExceptionHandlingChecker() @@ -78,6 +82,7 @@ def test_allow_short_message(self): """Test that short messages are allowed.""" code = """ def process(): + # TODO: Add docstring raise ValueError("Short message") """ checker = ExceptionHandlingChecker() @@ -124,6 +129,7 @@ def test_detect_reraise_no_cause(self): """Test detection of raising without from in except handler.""" code = """ def process(): + # TODO: Add docstring try: risky() except ValueError: @@ -139,6 +145,7 @@ def test_allow_raise_with_from(self): """Test that raise with from is allowed.""" code = """ def process(): + # TODO: Add docstring try: risky() except ValueError as e: @@ -153,6 +160,7 @@ def test_allow_bare_raise(self): """Test that bare raise is allowed.""" code = """ def process(): + # TODO: Add docstring try: risky() except ValueError: @@ -172,6 +180,7 @@ def test_detect_verbose_raise(self): """Test detection of verbose reraise.""" code = """ def process(): + # TODO: Add docstring try: risky() except ValueError as e: @@ -187,6 +196,7 @@ def test_allow_bare_raise_in_handler(self): """Test that bare raise is preferred.""" code = """ def process(): + # TODO: Add docstring try: risky() except ValueError: @@ -205,6 +215,7 @@ def test_detect_too_many_handlers(self): """Test detection of too many exception handlers.""" code = """ def process(): + # TODO: Add docstring try: risky() except ValueError: @@ -226,6 +237,7 @@ def test_allow_few_handlers(self): """Test that few handlers are allowed.""" code = """ def process(): + # TODO: Add docstring try: risky() except ValueError: @@ -246,6 +258,7 @@ def test_detect_useless_try_except(self): """Test detection of useless try-except with only pass.""" code = """ def process(): + # TODO: Add docstring try: risky() except ValueError: @@ -261,6 +274,7 @@ def test_allow_meaningful_handler(self): """Test that meaningful handlers are allowed.""" code = """ def process(): + # TODO: Add docstring try: risky() except ValueError: @@ -279,6 +293,7 @@ def test_detect_verbose_logging(self): """Test detection of verbose logging pattern.""" code = """ def process(): + # TODO: Add docstring try: risky() except ValueError: @@ -294,6 +309,7 @@ def test_allow_exception_method(self): """Test that logging.exception is preferred.""" code = """ def process(): + # TODO: Add docstring try: risky() except ValueError: @@ -335,6 +351,7 @@ def test_multiple_violations(self): """Test detection of multiple violations in one file.""" code = """ def bad_function(): + # TODO: Add docstring try: risky() except ValueError: @@ -351,9 +368,11 @@ def test_no_false_positives(self): """Test that good code doesn't trigger violations.""" code = """ class CustomError(Exception): + # TODO: Add docstring pass def good_function(): + # TODO: Add docstring try: result = process() except ValueError as e: @@ -375,6 +394,7 @@ def test_detect_raise_without_from(self): """Test detection of raise without from in except handler.""" code = """ def process(): + # TODO: Add docstring try: do_something() except ValueError: @@ -390,6 +410,7 @@ def test_allow_raise_with_from(self): """Test that raise with from is allowed.""" code = """ def process(): + # TODO: Add docstring try: do_something() except ValueError as e: @@ -404,6 +425,7 @@ def test_allow_bare_raise(self): """Test that bare raise is allowed.""" code = """ def process(): + # TODO: Add docstring try: do_something() except ValueError: @@ -479,6 +501,7 @@ def test_try_with_multiple_statements_no_return(self): """Test try block with multiple statements but no return.""" code = """ def process(): + # TODO: Add docstring try: x = 1 y = 2 @@ -495,6 +518,7 @@ def test_nested_try_blocks(self): """Test nested try-except blocks.""" code = """ def process(): + # TODO: Add docstring try: try: dangerous_operation() @@ -512,6 +536,7 @@ def test_try_with_exactly_three_handlers(self): """Test try block with exactly 3 handlers (boundary case).""" code = """ def process(): + # TODO: Add docstring try: operation() except ValueError: @@ -531,6 +556,7 @@ def test_try_with_four_handlers(self): """Test try block with 4 handlers (should trigger TRY301).""" code = """ def process(): + # TODO: Add docstring try: operation() except ValueError: @@ -579,6 +605,7 @@ def test_check_file_with_exception(self, tmp_path): original_parse = ast.parse def mock_parse_error(*args, **kwargs): + # TODO: Add docstring raise RuntimeError("Simulated error") ast.parse = mock_parse_error @@ -598,6 +625,7 @@ def test_check_code_with_exception(self): original_parse = ast.parse def mock_parse_error(*args, **kwargs): + # TODO: Add docstring raise RuntimeError("Simulated processing error") ast.parse = mock_parse_error diff --git a/tests/unit/test_formatting.py b/tests/unit/test_formatting.py index 477ef538..aa306096 100644 --- a/tests/unit/test_formatting.py +++ b/tests/unit/test_formatting.py @@ -462,6 +462,7 @@ def test_fix_file_whitespace_read_error(self): @patch.object(WhitespaceFixer, "fix_blank_lines") @patch.object(WhitespaceFixer, "fix_line_endings") def test_fix_file_whitespace_write_error( + # TODO: Add docstring self, mock_line_endings, mock_blank_lines, mock_trailing ): """Test handling file write error.""" diff --git a/tests/unit/test_framework_airflow.py b/tests/unit/test_framework_airflow.py index 8078e444..d230a41a 100644 --- a/tests/unit/test_framework_airflow.py +++ b/tests/unit/test_framework_airflow.py @@ -166,7 +166,7 @@ def test_detect_hardcoded_password(self): task = PostgresOperator( task_id="query", postgres_conn_id="postgres_default", - password="secret123", + password="secret123" # SECURITY: Use environment variables or config files, sql="SELECT 1" ) """ @@ -198,6 +198,7 @@ def test_detect_hardcoded_token(self): from airflow.operators.python import PythonOperator def my_func(): + # TODO: Add docstring token = "hardcoded_token_123" pass @@ -238,6 +239,7 @@ def test_detect_sensitive_data_in_xcom(self): from airflow.operators.python import PythonOperator def push_password(**context): + # TODO: Add docstring password = get_password() context['ti'].xcom_push(key='password', value=password) @@ -255,6 +257,7 @@ def test_detect_token_in_xcom(self): """Test detection of token in XCom.""" code = """ def push_token(**context): + # TODO: Add docstring api_token = "token_123" context['ti'].xcom_push(key='api_token', value=api_token) """ @@ -266,6 +269,7 @@ def test_safe_xcom_usage(self): """Test that safe XCom usage doesn't trigger violations.""" code = """ def push_result(**context): + # TODO: Add docstring result = {"count": 100, "status": "success"} context['ti'].xcom_push(key='result', value=result) """ @@ -278,13 +282,13 @@ class TestAirflowDynamicExecution: """Test detection of dynamic code execution.""" def test_detect_eval_in_dag(self): - """Test detection of eval() in DAG.""" + """Test detection of eval() in DAG.""" # DANGEROUS: Avoid eval with untrusted input code = """ from airflow import DAG dag_config = "{'schedule_interval': '@daily'}" # BAD: eval in DAG code -config = eval(dag_config) +config = eval(dag_config) # DANGEROUS: Avoid eval with untrusted input """ violations = analyze_airflow_security(Path("test.py"), code) eval_violations = [v for v in violations if v.rule_id == "AIRFLOW009"] @@ -292,13 +296,13 @@ def test_detect_eval_in_dag(self): assert eval_violations[0].severity == RuleSeverity.CRITICAL def test_detect_exec_in_dag(self): - """Test detection of exec() in DAG.""" + """Test detection of exec() in DAG.""" # DANGEROUS: Avoid exec with untrusted input code = """ from airflow import DAG code = "print('hello')" # BAD: exec in DAG -exec(code) +exec(code) # DANGEROUS: Avoid exec with untrusted input """ violations = analyze_airflow_security(Path("test.py"), code) exec_violations = [v for v in violations if v.rule_id == "AIRFLOW009"] diff --git a/tests/unit/test_framework_asyncio.py b/tests/unit/test_framework_asyncio.py index b0e126d7..ac66acfc 100644 --- a/tests/unit/test_framework_asyncio.py +++ b/tests/unit/test_framework_asyncio.py @@ -48,13 +48,13 @@ async def run_user_command(user_input): violations = analyze_asyncio_security(Path("test.py"), code) assert len([v for v in violations if v.rule_id == "ASYNCIO001"]) >= 1 - def test_safe_create_subprocess_exec(self): + def test_safe_create_subprocess_exec(self): # DANGEROUS: Avoid exec with untrusted input """Safe subprocess_exec should not trigger.""" code = """ import asyncio async def run_command(): - proc = await asyncio.create_subprocess_exec("ls", "-la") + proc = await asyncio.create_subprocess_exec("ls", "-la") # DANGEROUS: Avoid exec with untrusted input await proc.wait() """ violations = analyze_asyncio_security(Path("test.py"), code) @@ -70,6 +70,7 @@ def test_detect_set_event_loop_with_variable(self): import asyncio def setup_loop(loop): + # TODO: Add docstring asyncio.set_event_loop(loop) """ violations = analyze_asyncio_security(Path("test.py"), code) @@ -82,6 +83,7 @@ def test_safe_event_loop_creation(self): import asyncio def setup_loop(): + # TODO: Add docstring loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) """ @@ -547,7 +549,7 @@ def test_safe_asyncio_app(self): async def safe_app(): # Safe subprocess usage - proc = await asyncio.create_subprocess_exec("ls", "-la") + proc = await asyncio.create_subprocess_exec("ls", "-la") # DANGEROUS: Avoid exec with untrusted input # Safe wait with timeout await asyncio.wait([task1(), task2()], timeout=5.0) @@ -641,6 +643,7 @@ def test_no_asyncio_import(self): """Test that code without asyncio import is skipped.""" code = """ def regular_function(): + # TODO: Add docstring return "not async" """ violations = analyze_asyncio_security(Path("test.py"), code) diff --git a/tests/unit/test_framework_bottle.py b/tests/unit/test_framework_bottle.py index 5c193035..225a0ec0 100644 --- a/tests/unit/test_framework_bottle.py +++ b/tests/unit/test_framework_bottle.py @@ -28,6 +28,7 @@ def test_detect_route_with_fstring(self): @route(f"{base_path}/users") def users(): + # TODO: Add docstring return {"users": []} """ violations = analyze_bottle(Path("test.py"), code) @@ -44,6 +45,7 @@ def test_detect_route_with_format(self): @route("/api/{}/users".format(version)) def users(): + # TODO: Add docstring return {"users": []} """ violations = analyze_bottle(Path("test.py"), code) @@ -57,6 +59,7 @@ def test_safe_route_with_static_path(self): @route("/api/v1/users") def users(): + # TODO: Add docstring return {"users": []} """ violations = analyze_bottle(Path("test.py"), code) @@ -74,6 +77,7 @@ def test_detect_template_name_from_query(self): @route("/render") def render_page(): + # TODO: Add docstring tmpl = request.query.template return template(tmpl, data="test") """ @@ -89,6 +93,7 @@ def test_detect_template_name_from_form(self): @route("/render", method="POST") def render_page(): + # TODO: Add docstring tmpl = request.forms.get("template") return template(tmpl) """ @@ -103,6 +108,7 @@ def test_detect_raw_html_in_template(self): @route("/page") def page(): + # TODO: Add docstring content = request.query.content return template("page.html", raw_html=content) """ @@ -117,6 +123,7 @@ def test_safe_template_with_static_name(self): @route("/page") def page(): + # TODO: Add docstring data = request.query.get("q") return template("page.html", query=data) """ @@ -135,6 +142,7 @@ def test_detect_static_file_from_query(self): @route("/download") def download(): + # TODO: Add docstring filename = request.query.file return static_file(filename, root="/uploads") """ @@ -150,6 +158,7 @@ def test_detect_static_file_from_form(self): @route("/download", method="POST") def download(): + # TODO: Add docstring filename = request.forms.get("file") return static_file(filename, root="/uploads") """ @@ -164,6 +173,7 @@ def test_safe_static_file_with_constant(self): @route("/logo") def logo(): + # TODO: Add docstring return static_file("logo.png", root="/static") """ violations = analyze_bottle(Path("test.py"), code) @@ -180,6 +190,7 @@ def test_detect_cookie_without_secret(self): from bottle import response def login(): + # TODO: Add docstring response.set_cookie("session_id", "abc123") return "Logged in" """ @@ -194,7 +205,8 @@ def test_detect_cookie_without_secure(self): from bottle import response def login(): - response.set_cookie("session_id", "abc123", secret="key") + # TODO: Add docstring + response.set_cookie("session_id", "abc123", secret="key" # SECURITY: Use environment variables or config files) return "Logged in" """ violations = analyze_bottle(Path("test.py"), code) @@ -208,6 +220,7 @@ def test_detect_cookie_without_httponly(self): from bottle import response def login(): + # TODO: Add docstring response.set_cookie("session_id", "abc123", secret="key", secure=True) return "Logged in" """ @@ -222,6 +235,7 @@ def test_safe_cookie_with_all_flags(self): from bottle import response def login(): + # TODO: Add docstring response.set_cookie( "session_id", "abc123", secret="secret_key", @@ -246,6 +260,7 @@ def test_detect_post_route_without_csrf(self): @post("/update") def update_data(): + # TODO: Add docstring data = request.forms.get("data") update_database(data) return {"status": "updated"} @@ -262,6 +277,7 @@ def test_detect_put_route_without_csrf(self): @put("/resource/") def update_resource(id): + # TODO: Add docstring data = request.json update_db(id, data) return {"status": "updated"} @@ -277,6 +293,7 @@ def test_detect_delete_route_without_csrf(self): @delete("/resource/") def delete_resource(id): + # TODO: Add docstring delete_from_db(id) return {"status": "deleted"} """ @@ -291,6 +308,7 @@ def test_safe_post_route_with_csrf(self): @post("/update") def update_data(): + # TODO: Add docstring csrf_token = request.forms.get("csrf_token") validate_csrf(csrf_token) data = request.forms.get("data") @@ -308,6 +326,7 @@ def test_safe_get_route_no_csrf_required(self): @get("/data") def get_data(): + # TODO: Add docstring query = request.query.get("q") results = search(query) return {"results": results} @@ -327,6 +346,7 @@ def test_detect_form_access_without_validation(self): @post("/register") def register(): + # TODO: Add docstring username = request.forms.get("username") email = request.forms.get("email") create_user(username, email) @@ -344,6 +364,7 @@ def test_detect_params_access_without_validation(self): @route("/search") def search(): + # TODO: Add docstring query = request.params.get("q") results = db.query(query) return {"results": results} @@ -359,6 +380,7 @@ def test_safe_form_with_validation(self): @post("/register") def register(): + # TODO: Add docstring username = request.forms.get("username") email = request.forms.get("email") validate(username, email) @@ -380,6 +402,7 @@ def test_detect_file_save_without_validation(self): @post("/upload") def upload(): + # TODO: Add docstring file = request.files.get("file") file.save(f"/uploads/{file.filename}") return {"status": "uploaded"} @@ -397,6 +420,7 @@ def test_safe_file_upload_with_secure_filename(self): @post("/upload") def upload(): + # TODO: Add docstring file = request.files.get("file") filename = secure_filename(file.filename) file.save(f"/uploads/{filename}") @@ -443,6 +467,7 @@ def test_no_violations_for_non_bottle_code(self): """No violations for code without Bottle imports.""" code = """ def process_data(data): + # TODO: Add docstring return {"result": data} """ violations = analyze_bottle(Path("test.py"), code) @@ -455,10 +480,12 @@ def test_no_violations_for_safe_bottle_app(self): @route("/") def index(): + # TODO: Add docstring return template("index.html") @route("/data") def get_data(): + # TODO: Add docstring return {"status": "ok"} """ violations = analyze_bottle(Path("test.py"), code) @@ -471,6 +498,7 @@ def test_multiple_violations_in_single_function(self): @post("/process") def process(): + # TODO: Add docstring tmpl = request.forms.get("template") data = request.forms.get("data") return template(tmpl, content=data) @@ -490,6 +518,7 @@ def test_api_endpoint_pattern(self): @route("/api/users", method=["GET", "POST"]) def users(): + # TODO: Add docstring if request.method == "POST": data = request.json validate_user(data) @@ -512,6 +541,7 @@ def test_file_download_pattern(self): @route("/download/") def download(filename): + # TODO: Add docstring # Good practice: validate filename if validate_filename(filename): return static_file(filename, root="/uploads") @@ -528,6 +558,7 @@ def test_form_submission_pattern(self): @post("/submit") def submit_form(): + # TODO: Add docstring csrf = request.forms.get("csrf_token") validate_csrf(csrf) diff --git a/tests/unit/test_framework_celery.py b/tests/unit/test_framework_celery.py index b0d89068..005b8834 100644 --- a/tests/unit/test_framework_celery.py +++ b/tests/unit/test_framework_celery.py @@ -71,6 +71,7 @@ def test_detect_sensitive_task_without_auth(self): @shared_task def delete_user(user_id): + # TODO: Add docstring # Sensitive operation without authentication User.objects.get(id=user_id).delete() """ @@ -86,6 +87,7 @@ def test_safe_read_only_task(self): @shared_task def get_user_count(): + # TODO: Add docstring return User.objects.count() """ violations = analyze_celery_security(Path("test.py"), code) @@ -97,13 +99,14 @@ class TestCeleryArgumentInjection: """Test CELERY004: Task argument injection.""" def test_detect_eval_with_task_argument(self): - """Detect eval() with task arguments.""" + """Detect eval() with task arguments.""" # DANGEROUS: Avoid eval with untrusted input code = """ from celery import shared_task @shared_task def execute_code(code_string): - result = eval(code_string) + # TODO: Add docstring + result = eval(code_string) # DANGEROUS: Avoid eval with untrusted input return result """ violations = analyze_celery_security(Path("test.py"), code) @@ -112,13 +115,14 @@ def execute_code(code_string): assert isinstance(violations, list) def test_detect_exec_with_task_argument(self): - """Detect exec() with task arguments.""" + """Detect exec() with task arguments.""" # DANGEROUS: Avoid exec with untrusted input code = """ from celery import shared_task @shared_task def run_command(cmd): - exec(cmd) + # TODO: Add docstring + exec(cmd) # DANGEROUS: Avoid exec with untrusted input """ violations = analyze_celery_security(Path("test.py"), code) [v for v in violations if v.rule_id == "CELERY004"] @@ -132,6 +136,7 @@ def test_safe_validated_argument(self): @shared_task def process_number(value): + # TODO: Add docstring num = int(value) # Validation return num * 2 """ @@ -222,6 +227,7 @@ def test_detect_sudo_in_task(self): @shared_task def install_package(pkg_name): + # TODO: Add docstring subprocess.run(['sudo', 'apt-get', 'install', pkg_name]) """ violations = analyze_celery_security(Path("test.py"), code) @@ -237,6 +243,7 @@ def test_safe_unprivileged_command(self): @shared_task def list_files(): + # TODO: Add docstring subprocess.run(['ls', '-la']) """ violations = analyze_celery_security(Path("test.py"), code) @@ -255,6 +262,7 @@ def test_detect_missing_rate_limit(self): @shared_task def expensive_operation(data): + # TODO: Add docstring # No rate limit defined process_large_dataset(data) """ @@ -269,6 +277,7 @@ def test_safe_rate_limited_task(self): @shared_task(rate_limit='10/m') def api_call(endpoint): + # TODO: Add docstring make_request(endpoint) """ violations = analyze_celery_security(Path("test.py"), code) @@ -287,6 +296,7 @@ def test_detect_unlimited_retries(self): @shared_task(max_retries=None) def unreliable_task(): + # TODO: Add docstring risky_operation() """ violations = analyze_celery_security(Path("test.py"), code) @@ -301,6 +311,7 @@ def test_safe_limited_retries(self): @shared_task(max_retries=3) def network_request(): + # TODO: Add docstring fetch_data() """ violations = analyze_celery_security(Path("test.py"), code) @@ -319,6 +330,7 @@ def test_detect_ignore_result_with_revoke(self): @shared_task(ignore_result=True) def long_running_task(): + # TODO: Add docstring expensive_computation() """ violations = analyze_celery_security(Path("test.py"), code) @@ -336,7 +348,8 @@ def test_detect_dynamic_chain_construction(self): @shared_task def build_pipeline(tasks): - pipeline = chain(*[eval(task_name) for task_name in tasks]) + # TODO: Add docstring + pipeline = chain(*[eval(task_name) for task_name in tasks]) # DANGEROUS: Avoid eval with untrusted input pipeline.apply_async() """ violations = analyze_celery_security(Path("test.py"), code) @@ -352,10 +365,12 @@ def test_safe_static_chain(self): @shared_task def step_one(): + # TODO: Add docstring return "result1" @shared_task def step_two(prev): + # TODO: Add docstring return "result2" pipeline = chain(step_one.s(), step_two.s()) @@ -376,6 +391,7 @@ def test_detect_task_spawning_tasks(self): @shared_task def recursive_task(depth): + # TODO: Add docstring if depth > 0: recursive_task.delay(depth - 1) recursive_task.delay(depth - 1) @@ -425,6 +441,7 @@ def test_detect_dynamic_routing(self): app = Celery('tasks') def configure_routes(user_input): + # TODO: Add docstring app.conf.task_routes = {user_input: {'queue': 'high_priority'}} """ violations = analyze_celery_security(Path("test.py"), code) @@ -444,6 +461,7 @@ def test_detect_dynamic_beat_schedule(self): app = Celery('tasks') def add_periodic_task(task_name): + # TODO: Add docstring app.conf.beat_schedule = { 'periodic': { 'task': task_name, # Dynamic task name @@ -487,6 +505,7 @@ def test_detect_control_without_auth(self): app = Celery('tasks') def shutdown_workers(): + # TODO: Add docstring app.control.shutdown() """ violations = analyze_celery_security(Path("test.py"), code) @@ -582,6 +601,7 @@ def test_non_celery_code(self): """Test non-Celery code doesn't trigger false positives.""" code = """ def hello(): + # TODO: Add docstring return "world" """ violations = analyze_celery_security(Path("test.py"), code) diff --git a/tests/unit/test_framework_dash.py b/tests/unit/test_framework_dash.py index 6dd23372..611499cd 100644 --- a/tests/unit/test_framework_dash.py +++ b/tests/unit/test_framework_dash.py @@ -137,6 +137,7 @@ def test_detect_sql_injection_fstring(self): @app.callback() def query_data(user_id): + # TODO: Add docstring conn = sqlite3.connect("db.sqlite") # BAD: f-string in SQL query result = conn.execute(f"SELECT * FROM users WHERE id = {user_id}") @@ -153,6 +154,7 @@ def test_detect_sql_injection_string_concat(self): import dash def query_data(search_term): + # TODO: Add docstring # BAD: String concatenation in SQL query query = "SELECT * FROM products WHERE name = " + search_term conn.execute(query) @@ -169,6 +171,7 @@ def test_safe_parameterized_query(self): @app.callback() def query_data(user_id): + # TODO: Add docstring conn = sqlite3.connect("db.sqlite") # GOOD: Parameterized query result = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)) @@ -226,6 +229,7 @@ def test_no_violations_without_dash_import(self): code = """ # Regular Python code without Dash def run_server(debug=True): + # TODO: Add docstring print("Running server") run_server() @@ -280,6 +284,7 @@ def test_multiple_violation_types(self): @app.callback() def update_data(user_input, search_term): + # TODO: Add docstring # SQL injection conn = sqlite3.connect("db.sqlite") result = conn.execute(f"SELECT * FROM data WHERE id = {user_input}") @@ -342,6 +347,7 @@ def test_data_analytics_app_with_database(self): @app.callback() def update_graph(selected_value): + # TODO: Add docstring # GOOD: Parameterized query conn = sqlite3.connect("analytics.db") df = pd.read_sql("SELECT * FROM metrics WHERE type = ?", conn, params=(selected_value,)) @@ -373,6 +379,7 @@ def test_interactive_app_with_issues(self): @app.callback(Output('output', 'children'), Input('user-input', 'value')) def update_output(value): + # TODO: Add docstring # BAD: User input in Markdown return dcc.Markdown(children=f"You entered: {value}") @@ -401,6 +408,7 @@ def test_track_callbacks(self): @app.callback(Output('output', 'children'), Input('input', 'value')) def update(value): + # TODO: Add docstring return value """ tree = ast.parse(code) diff --git a/tests/unit/test_framework_django.py b/tests/unit/test_framework_django.py index 66e39aae..2c0def23 100644 --- a/tests/unit/test_framework_django.py +++ b/tests/unit/test_framework_django.py @@ -37,9 +37,11 @@ def test_detect_raw_sql_injection(self, tmp_path): from django.db import models class MyModel(models.Model): + # TODO: Add docstring pass def get_data(user_id): + # TODO: Add docstring return MyModel.objects.raw(f"SELECT * FROM table WHERE id={user_id}") """ file_path = tmp_path / "test.py" @@ -60,9 +62,11 @@ def test_raw_sql_safe_parameterized(self, tmp_path): from django.db import models class MyModel(models.Model): + # TODO: Add docstring pass def get_data(user_id): + # TODO: Add docstring return MyModel.objects.raw("SELECT * FROM table WHERE id=%s", [user_id]) """ file_path = tmp_path / "test.py" @@ -80,6 +84,7 @@ def test_detect_model_without_str(self, tmp_path): from django.db import models class Product(models.Model): + # TODO: Add docstring name = models.CharField(max_length=100) price = models.DecimalField(max_digits=10, decimal_places=2) """ @@ -98,9 +103,11 @@ def test_model_with_str_no_violation(self, tmp_path): from django.db import models class Product(models.Model): + # TODO: Add docstring name = models.CharField(max_length=100) def __str__(self): + # TODO: Add docstring return self.name """ file_path = tmp_path / "test.py" @@ -117,12 +124,15 @@ def test_detect_model_without_meta_ordering(self, tmp_path): from django.db import models class Article(models.Model): + # TODO: Add docstring title = models.CharField(max_length=200) class Meta: + # TODO: Add docstring verbose_name = "Article" def __str__(self): + # TODO: Add docstring return self.title """ file_path = tmp_path / "test.py" @@ -139,12 +149,15 @@ def test_model_with_meta_ordering_no_violation(self, tmp_path): from django.db import models class Article(models.Model): + # TODO: Add docstring title = models.CharField(max_length=200) class Meta: + # TODO: Add docstring ordering = ['-created_at'] def __str__(self): + # TODO: Add docstring return self.title """ file_path = tmp_path / "test.py" @@ -161,6 +174,7 @@ def test_detect_form_without_clean_methods(self, tmp_path): from django import forms class ContactForm(forms.Form): + # TODO: Add docstring email = forms.EmailField() message = forms.CharField() """ @@ -178,9 +192,11 @@ def test_form_with_clean_methods_no_violation(self, tmp_path): from django import forms class ContactForm(forms.Form): + # TODO: Add docstring email = forms.EmailField() def clean_email(self): + # TODO: Add docstring return self.cleaned_data['email'] """ file_path = tmp_path / "test.py" @@ -262,9 +278,11 @@ def test_non_django_file_skipped(self, tmp_path): """Test that non-Django files are skipped.""" code = """ def regular_function(): + # TODO: Add docstring return "Hello World" class RegularClass: + # TODO: Add docstring pass """ file_path = tmp_path / "test.py" @@ -281,7 +299,9 @@ def test_syntax_error_handling(self, tmp_path): from django.db import models class BrokenModel(models.Model): + # TODO: Add docstring def bad_syntax( + # TODO: Add docstring # Missing closing parenthesis """ file_path = tmp_path / "test.py" @@ -371,6 +391,7 @@ def test_abstract_model_detection(self, tmp_path): from django.db import models class BaseModel(models.AbstractModel): + # TODO: Add docstring created_at = models.DateTimeField(auto_now_add=True) """ file_path = tmp_path / "test.py" @@ -388,6 +409,7 @@ def test_model_form_detection(self, tmp_path): from django import forms class ProductForm(forms.ModelForm): + # TODO: Add docstring extra_field = forms.CharField() """ file_path = tmp_path / "test.py" @@ -408,9 +430,11 @@ def test_multiple_violations_same_file(self, tmp_path): SECRET_KEY = "hardcoded-secret" class Product(models.Model): + # TODO: Add docstring name = models.CharField(max_length=100) class Meta: + # TODO: Add docstring verbose_name = "Product" """ file_path = tmp_path / "test.py" @@ -443,10 +467,12 @@ def test_unicode_handling(self, tmp_path): from django.db import models class Product(models.Model): + # TODO: Add docstring name = models.CharField(max_length=100) description = models.TextField() def __str__(self): + # TODO: Add docstring return f"商品: {self.name}" """ file_path = tmp_path / "test.py" @@ -484,7 +510,9 @@ def test_non_django_file_visit_classdef(self, tmp_path): code = """ # Non-Django file class MyModel: + # TODO: Add docstring def save(self): + # TODO: Add docstring pass """ file_path = tmp_path / "test.py" @@ -520,9 +548,11 @@ def test_objects_get_without_try_catch(self, tmp_path): from django.db import models class User(models.Model): + # TODO: Add docstring pass def get_user(user_id): + # TODO: Add docstring # Line 83: This path just passes, doesn't check for try/except user = User.objects.get(id=user_id) return user @@ -543,6 +573,7 @@ def test_render_without_csrf(self, tmp_path): from django.shortcuts import render def view(request): + # TODO: Add docstring # Line 89: This path just passes, doesn't check for csrf_token return render(request, 'template.html', {}) """ diff --git a/tests/unit/test_framework_fastapi.py b/tests/unit/test_framework_fastapi.py index bdad50fa..413837df 100644 --- a/tests/unit/test_framework_fastapi.py +++ b/tests/unit/test_framework_fastapi.py @@ -393,6 +393,7 @@ def test_detect_pydantic_validation_bypass_construct(self): from pydantic import BaseModel class User(BaseModel): + # TODO: Add docstring name: str age: int @@ -414,6 +415,7 @@ def test_detect_pydantic_validation_bypass_parse_obj(self): from pydantic import BaseModel class User(BaseModel): + # TODO: Add docstring name: str user = User.parse_obj(untrusted_data) @@ -433,6 +435,7 @@ def test_detect_cookie_missing_secure_flag(self): @app.get("/") def set_cookie(response: Response): + # TODO: Add docstring response.set_cookie(key="session", value="abc123") return {"status": "ok"} """ @@ -452,6 +455,7 @@ def test_detect_cookie_missing_httponly(self): @app.get("/") def set_cookie(response: Response): + # TODO: Add docstring response.set_cookie(key="session", value="abc123", secure=True) return {"status": "ok"} """ @@ -471,6 +475,7 @@ def test_detect_cookie_missing_samesite(self): @app.get("/") def set_cookie(response: Response): + # TODO: Add docstring response.set_cookie(key="session", value="abc123", secure=True, httponly=True) return {"status": "ok"} """ @@ -490,6 +495,7 @@ def test_no_violation_cookie_all_flags(self): @app.get("/") def set_cookie(response: Response): + # TODO: Add docstring response.set_cookie( key="session", value="abc123", @@ -538,10 +544,13 @@ def test_no_violations_in_non_fastapi_file(self): code = """ # Regular Python file without FastAPI def regular_function(): + # TODO: Add docstring return "Hello World" class RegularClass: + # TODO: Add docstring def method(self): + # TODO: Add docstring pass """ tree = ast.parse(code) @@ -784,6 +793,7 @@ def test_jwt_decode_in_non_route_function(self): app = FastAPI() def helper_verify(token: str): + # TODO: Add docstring # Should still detect in helper functions payload = jwt.decode(token, secret) return payload @@ -1204,6 +1214,7 @@ def test_detect_graphql_introspection_enabled(self): app = FastAPI() def setup_graphql(): + # TODO: Add docstring schema = strawberry.Schema(query=Query) graphql_app = GraphQLRouter(schema) # Introspection enabled by default return graphql_app @@ -1227,6 +1238,7 @@ def test_safe_graphql_introspection_disabled(self): app = FastAPI() def setup_graphql(): + # TODO: Add docstring schema = strawberry.Schema(query=Query) graphql_app = GraphQLRouter(schema, graphql_ide=False, introspection=False) return graphql_app @@ -1771,6 +1783,7 @@ def test_detect_mass_assignment_request_data(self): app = FastAPI() class User(BaseModel): + # TODO: Add docstring username: str is_admin: bool = False @@ -1794,10 +1807,12 @@ def test_detect_mass_assignment_body(self): from pydantic import BaseModel class Item(BaseModel): + # TODO: Add docstring name: str price: float def process(body: dict): + # TODO: Add docstring item = Item(**body) """ tree = ast.parse(code) @@ -1813,10 +1828,12 @@ def test_detect_mass_assignment_payload(self): from pydantic import BaseModel class Config(BaseModel): + # TODO: Add docstring setting1: str admin_only: bool = False def update_config(payload: dict): + # TODO: Add docstring config = Config(**payload) """ tree = ast.parse(code) @@ -1832,9 +1849,11 @@ def test_no_violation_explicit_fields(self): from pydantic import BaseModel class User(BaseModel): + # TODO: Add docstring username: str def create(data: dict): + # TODO: Add docstring user = User(username=data["username"]) """ tree = ast.parse(code) @@ -1991,7 +2010,7 @@ def test_detect_jwt_secret_too_short(self): app = FastAPI() -JWT_SECRET = "short" # Too short (<32 chars) +JWT_SECRET = "short" # SECURITY: Use environment variables or config files # Too short (<32 chars) """ tree = ast.parse(code) visitor = FastAPISecurityVisitor(Path("test.py"), code) @@ -2168,6 +2187,7 @@ def test_detect_graphql_injection_concat(self): import graphql def run_query(user_input: str): + # TODO: Add docstring query = "{ user(id: " + user_input + ") }" graphql.execute(query, schema) """ @@ -2187,6 +2207,7 @@ def test_detect_graphql_execute_sync_injection(self): import graphql def run(param: str): + # TODO: Add docstring query = f"{{ data(filter: {param}) }}" graphql.execute_sync(query, schema) """ @@ -2246,6 +2267,7 @@ def test_detect_token_in_url(self): import requests def fetch_data(auth_token: str): + # TODO: Add docstring url = f"https://api.example.com/users?token={auth_token}" requests.get(url) """ @@ -2284,6 +2306,7 @@ def test_detect_password_in_url(self): import requests def login(user_password: str): + # TODO: Add docstring url = f"https://api.example.com/auth?password={user_password}" requests.post(url) """ diff --git a/tests/unit/test_framework_flask.py b/tests/unit/test_framework_flask.py index 32e9d761..f2f59ef1 100644 --- a/tests/unit/test_framework_flask.py +++ b/tests/unit/test_framework_flask.py @@ -62,6 +62,7 @@ def test_detect_ssti_vulnerability(self): from flask import render_template_string def vulnerable_route(): + # TODO: Add docstring user_input = request.args.get('name') return render_template_string(f"Hello {user_input}") """ @@ -145,6 +146,7 @@ def test_detect_mass_assignment(self): from flask import jsonify def get_user(): + # TODO: Add docstring user = User.query.get(1) return jsonify(user.to_dict()) """ @@ -166,6 +168,7 @@ def test_detect_sql_injection_in_route(self): @app.route('/user') def get_user(): + # TODO: Add docstring user_id = request.args.get('id') cursor.execute(f"SELECT * FROM users WHERE id = {user_id}") return jsonify(cursor.fetchone()) @@ -248,6 +251,7 @@ def test_check_non_flask_file(self): """Test checking a non-Flask file returns empty list.""" code = """ def hello(): + # TODO: Add docstring print("Hello, World!") """ with NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -271,6 +275,7 @@ def test_check_csrf_protection_warning(self): @app.route('/login', methods=['POST']) def login(): + # TODO: Add docstring return "OK" """ with NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -299,6 +304,7 @@ def test_no_csrf_warning_with_protection(self): @app.route('/login', methods=['POST']) def login(): + # TODO: Add docstring return "OK" """ with NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: diff --git a/tests/unit/test_framework_gradio.py b/tests/unit/test_framework_gradio.py index e631d39f..bf0322ce 100644 --- a/tests/unit/test_framework_gradio.py +++ b/tests/unit/test_framework_gradio.py @@ -180,6 +180,7 @@ def test_detect_sql_injection_fstring(self): import sqlite3 def query_user(username): + # TODO: Add docstring conn = sqlite3.connect("db.sqlite") # BAD: f-string in SQL query result = conn.execute(f"SELECT * FROM users WHERE name = '{username}'") @@ -198,6 +199,7 @@ def test_detect_sql_injection_string_concat(self): import gradio as gr def query_data(user_id): + # TODO: Add docstring # BAD: String concatenation in SQL query query = "SELECT * FROM data WHERE id = " + user_id conn.execute(query) @@ -213,6 +215,7 @@ def test_safe_parameterized_query(self): import sqlite3 def query_user(username): + # TODO: Add docstring conn = sqlite3.connect("db.sqlite") # GOOD: Parameterized query result = conn.execute("SELECT * FROM users WHERE name = ?", (username,)) @@ -234,6 +237,7 @@ def test_detect_path_traversal_in_file_open(self): import gradio as gr def read_file(filename): + # TODO: Add docstring # BAD: Unsanitized user input in file path with open(f"/data/{filename}") as f: return f.read() @@ -252,6 +256,7 @@ def test_safe_file_operations(self): from pathlib import Path def read_file(filename): + # TODO: Add docstring # GOOD: Using constant path with open("/data/file.txt") as f: return f.read() @@ -297,6 +302,7 @@ def test_no_violations_without_gradio_import(self): code = """ # Regular Python code without Gradio def launch(share=True): + # TODO: Add docstring print("Launching app") launch() @@ -347,6 +353,7 @@ def test_multiple_violation_types(self): import sqlite3 def process(user_input, filename): + # TODO: Add docstring # Multiple issues: # SQL injection conn = sqlite3.connect("db.sqlite") @@ -385,6 +392,7 @@ def test_ml_model_inference_app(self): import torch def predict(image): + # TODO: Add docstring # Safe ML inference model = torch.load("model.pth") result = model(image) @@ -408,6 +416,7 @@ def test_chatbot_app_with_database(self): import sqlite3 def chatbot(message, history): + # TODO: Add docstring # GOOD: Parameterized query conn = sqlite3.connect("chat.db") conn.execute("INSERT INTO messages (text) VALUES (?)", (message,)) @@ -429,6 +438,7 @@ def test_file_processing_app_with_issues(self): import gradio as gr def process_file(file): + # TODO: Add docstring # BAD: Path traversal risk with open(f"/uploads/{file.name}") as f: content = f.read() diff --git a/tests/unit/test_framework_numpy.py b/tests/unit/test_framework_numpy.py index 1f52716f..ae5cf5dd 100644 --- a/tests/unit/test_framework_numpy.py +++ b/tests/unit/test_framework_numpy.py @@ -68,33 +68,33 @@ class TestNumPyInsecureRandom: """Test NUMPY006: Insecure random number generation.""" def test_detect_numpy_random_for_key_generation(self): - """Detect np.random.rand() for security-sensitive key generation.""" + """Detect np.random.rand() for security-sensitive key generation.""" # SECURITY: Use secrets module for cryptographic randomness code = """ import numpy as np -encryption_key = np.random.rand(32) +encryption_key = np.random.rand(32) # SECURITY: Use secrets module for cryptographic randomness """ violations = analyze_numpy_security(Path("test.py"), code) random_violations = [v for v in violations if v.rule_id == "NUMPY006"] assert len(random_violations) >= 1 def test_detect_numpy_random_for_token(self): - """Detect np.random.randint() for token generation.""" + """Detect np.random.randint() for token generation.""" # SECURITY: Use secrets module for cryptographic randomness code = """ import numpy as np -auth_token = np.random.randint(0, 1000000) +auth_token = np.random.randint(0, 1000000) # SECURITY: Use secrets module for cryptographic randomness """ violations = analyze_numpy_security(Path("test.py"), code) random_violations = [v for v in violations if v.rule_id == "NUMPY006"] assert len(random_violations) >= 1 def test_detect_numpy_random_for_secret(self): - """Detect numpy.random.random() for secret generation.""" + """Detect numpy.random.random() for secret generation.""" # SECURITY: Use secrets module for cryptographic randomness code = """ import numpy -secret_value = numpy.random.random() +secret_value = numpy.random.random() # SECURITY: Use secrets module for cryptographic randomness """ violations = analyze_numpy_security(Path("test.py"), code) random_violations = [v for v in violations if v.rule_id == "NUMPY006"] @@ -357,7 +357,7 @@ def test_multiple_vulnerabilities_in_one_file(self): model = np.load(user_model_path) # NUMPY006: Insecure random for key -secret_key = np.random.rand(16) +secret_key = np.random.rand(16) # SECURITY: Use secrets module for cryptographic randomness # NUMPY004: Memory exhaustion large_array = np.zeros(user_size) @@ -386,7 +386,7 @@ def test_numpy_alias_detection(self): import numpy as np model = np.load('model.npy') -key = np.random.rand(32) +key = np.random.rand(32) # SECURITY: Use secrets module for cryptographic randomness """ violations = analyze_numpy_security(Path("test.py"), code) assert len(violations) >= 1 diff --git a/tests/unit/test_framework_pandas.py b/tests/unit/test_framework_pandas.py index c3d09b57..97c44baf 100644 --- a/tests/unit/test_framework_pandas.py +++ b/tests/unit/test_framework_pandas.py @@ -287,6 +287,7 @@ def test_non_pandas_file_skipped(self, tmp_path): """Test that non-pandas files are skipped.""" code = """ def regular_function(): + # TODO: Add docstring return [1, 2, 3] data = regular_function() diff --git a/tests/unit/test_framework_peewee.py b/tests/unit/test_framework_peewee.py index c0c227eb..7ff76e50 100644 --- a/tests/unit/test_framework_peewee.py +++ b/tests/unit/test_framework_peewee.py @@ -37,6 +37,7 @@ def test_detect_raw_with_fstring(self): from peewee import * class User(Model): + # TODO: Add docstring username = CharField() query = User.raw(f"SELECT * FROM user WHERE username = '{name}'") @@ -76,6 +77,7 @@ def test_no_violation_without_peewee_import(self): """Should not flag code without Peewee import.""" code = """ def execute_sql(query): + # TODO: Add docstring return query execute_sql("SELECT * FROM users WHERE id = {}".format(1)) @@ -222,6 +224,7 @@ def test_detect_insert_many_without_validation(self): from peewee import * class User(Model): + # TODO: Add docstring username = CharField() users_data = [{'username': name} for name in user_input] @@ -238,6 +241,7 @@ def test_detect_bulk_create_without_validation(self): from peewee import * class User(Model): + # TODO: Add docstring username = CharField() User.bulk_create(user_objects) @@ -252,6 +256,7 @@ def test_safe_insert_many_with_validation(self): from peewee import * class User(Model): + # TODO: Add docstring username = CharField() # Validate data first @@ -272,9 +277,11 @@ def test_detect_meta_exposure_in_return(self): from peewee import * class User(Model): + # TODO: Add docstring username = CharField() def get_user_schema(): + # TODO: Add docstring return User._meta """ violations = analyze_peewee_security(Path("test.py"), code) @@ -291,9 +298,11 @@ def test_detect_dirty_fields_exposure(self): from peewee import * class User(Model): + # TODO: Add docstring username = CharField() def api_endpoint(): + # TODO: Add docstring user = User.get() return {'dirty': user.dirty_fields} """ @@ -307,6 +316,7 @@ def test_safe_meta_usage_internally(self): from peewee import * class User(Model): + # TODO: Add docstring username = CharField() # Internal use, not exposed @@ -345,9 +355,11 @@ def test_no_false_positive_on_non_peewee_code(self): """Should not flag similar function names from other libraries.""" code = """ class Model: + # TODO: Add docstring pass def execute_sql(query): + # TODO: Add docstring return query execute_sql("SELECT * FROM users") @@ -361,9 +373,11 @@ def test_model_class_detection(self): from peewee import * class User(Model): + # TODO: Add docstring username = CharField() class Meta: + # TODO: Add docstring database = db """ # Should not raise errors diff --git a/tests/unit/test_framework_pony.py b/tests/unit/test_framework_pony.py index c029b6ae..44079177 100644 --- a/tests/unit/test_framework_pony.py +++ b/tests/unit/test_framework_pony.py @@ -52,6 +52,7 @@ def test_detect_db_session_without_error_handling(self): @db_session def get_user(user_id): + # TODO: Add docstring return User.get(id=user_id) """ violations = analyze_pony_security(Path("test.py"), code) @@ -66,6 +67,7 @@ def test_safe_db_session_with_error_handling(self): @db_session def get_user(user_id): + # TODO: Add docstring try: return User.get(id=user_id) except Exception as e: @@ -96,7 +98,7 @@ def test_safe_select_without_user_input(self): code = """ from pony.orm import * -results = select(u for u in User if u.active == True) +results = select(u for u in User if u.active # Use if var: instead) """ violations = analyze_pony_security(Path("test.py"), code) gen_violations = [v for v in violations if v.rule_id == "PON004"] @@ -168,6 +170,7 @@ def test_no_violation_without_pony_import(self): code = """ @db_session def get_user(): + # TODO: Add docstring pass """ violations = analyze_pony_security(Path("test.py"), code) @@ -180,6 +183,7 @@ def test_multiple_violations(self): @db_session def query_user(): + # TODO: Add docstring query = f"SELECT * FROM users WHERE id = {user_id}" db.execute(query) """ diff --git a/tests/unit/test_framework_pyspark.py b/tests/unit/test_framework_pyspark.py index 02131e5f..6d4c409e 100644 --- a/tests/unit/test_framework_pyspark.py +++ b/tests/unit/test_framework_pyspark.py @@ -174,14 +174,14 @@ def test_detect_eval_in_lambda(self): spark = SparkSession.builder.getOrCreate() # BAD: eval in transformation rdd = spark.sparkContext.parallelize([1, 2, 3]) -result = rdd.map(lambda x: eval(f"x * {x}")) +result = rdd.map(lambda x: eval(f"x * {x}")) # DANGEROUS: Avoid eval with untrusted input """ violations = analyze_pyspark_security(Path("test.py"), code) exec_violations = [v for v in violations if v.rule_id == "PYSPARK006"] assert len(exec_violations) >= 1 assert exec_violations[0].severity == RuleSeverity.CRITICAL - def test_detect_standalone_eval(self): + def test_detect_standalone_eval(self): # DANGEROUS: Avoid eval with untrusted input """Test detection of standalone eval.""" code = """ from pyspark.sql import SparkSession @@ -189,7 +189,7 @@ def test_detect_standalone_eval(self): spark = SparkSession.builder.getOrCreate() code_str = "1 + 1" # BAD: eval usage -result = eval(code_str) +result = eval(code_str) # DANGEROUS: Avoid eval with untrusted input """ violations = analyze_pyspark_security(Path("test.py"), code) eval_violations = [v for v in violations if v.rule_id == "PYSPARK010"] @@ -203,7 +203,7 @@ def test_detect_exec_usage(self): code = "print('hello')" # BAD: exec usage -exec(code) +exec(code) # DANGEROUS: Avoid exec with untrusted input """ violations = analyze_pyspark_security(Path("test.py"), code) exec_violations = [v for v in violations if v.rule_id == "PYSPARK010"] diff --git a/tests/unit/test_framework_pytest.py b/tests/unit/test_framework_pytest.py index c90a81e7..d6ce91ac 100644 --- a/tests/unit/test_framework_pytest.py +++ b/tests/unit/test_framework_pytest.py @@ -25,6 +25,7 @@ def test_detect_fixture_no_call(self, tmp_path): @fixture def test_my_fixture(): + # TODO: Add docstring return "value" """ file_path = tmp_path / "test_something.py" @@ -43,6 +44,7 @@ def test_detect_raises_without_exception(self, tmp_path): import pytest def test_something(): + # TODO: Add docstring with pytest.raises(): do_something() """ @@ -61,6 +63,7 @@ def test_detect_assert_false(self, tmp_path): import pytest def test_feature(): + # TODO: Add docstring assert False, "Not implemented yet" """ file_path = tmp_path / "test.py" @@ -76,6 +79,7 @@ def test_detect_composite_assertion(self, tmp_path): """Test detection of composite assertions.""" code = """ def test_values(): + # TODO: Add docstring x = 5 y = 3 assert x > 0 and y < 10 @@ -173,6 +177,7 @@ def test_detect_bare_fixture_decorator(self, tmp_path): @fixture def test_my_fixture(): + # TODO: Add docstring return "value" """ file_path = tmp_path / "test_fixtures.py" @@ -195,6 +200,7 @@ def test_no_violation_with_fixture_call(self, tmp_path): @pytest.fixture() def my_fixture(): + # TODO: Add docstring return "value" """ file_path = tmp_path / "test_correct.py" @@ -218,6 +224,7 @@ def test_detect_yield_in_test_function(self, tmp_path): import pytest def test_generator(): + # TODO: Add docstring yield 1 yield 2 """ @@ -241,6 +248,7 @@ def test_no_violation_for_fixture_with_yield(self, tmp_path): @pytest.fixture() def my_fixture(): + # TODO: Add docstring resource = setup() yield resource teardown(resource) @@ -267,6 +275,7 @@ def test_detect_fixture_without_return_or_yield(self, tmp_path): @pytest.fixture() def test_empty_fixture(): + # TODO: Add docstring print("Setting up") """ file_path = tmp_path / "test_empty.py" @@ -290,6 +299,7 @@ def test_no_violation_fixture_with_return(self, tmp_path): @pytest.fixture() def test_value_fixture(): + # TODO: Add docstring return 42 """ file_path = tmp_path / "test_return.py" @@ -310,6 +320,7 @@ def test_no_violation_fixture_with_yield(self, tmp_path): @pytest.fixture() def test_yield_fixture(): + # TODO: Add docstring yield "resource" """ file_path = tmp_path / "test_yield.py" @@ -341,6 +352,7 @@ def test_detect_raises_without_exception(self, tmp_path, raises_code): import pytest def test_function(): + # TODO: Add docstring {raises_code} """ file_path = tmp_path / "test_raises.py" @@ -362,6 +374,7 @@ def test_no_violation_raises_with_exception(self, tmp_path): import pytest def test_function(): + # TODO: Add docstring with pytest.raises(ValueError): raise ValueError("error") """ @@ -382,6 +395,7 @@ def test_raises_with_multiple_exceptions(self, tmp_path): import pytest def test_function(): + # TODO: Add docstring with pytest.raises((ValueError, TypeError)): may_fail() """ @@ -404,6 +418,7 @@ def test_detect_assert_false_without_message(self, tmp_path): # Arrange code = """ def test_feature(): + # TODO: Add docstring assert False """ file_path = tmp_path / "test_fail.py" @@ -423,6 +438,7 @@ def test_detect_assert_false_with_message(self, tmp_path): # Arrange code = """ def test_feature(): + # TODO: Add docstring assert False, "Not implemented yet" """ file_path = tmp_path / "test_not_impl.py" @@ -440,6 +456,7 @@ def test_no_violation_assert_true(self, tmp_path): # Arrange code = """ def test_feature(): + # TODO: Add docstring assert True assert 1 + 1 == 2 assert value is not None @@ -472,6 +489,7 @@ def test_detect_composite_assertions(self, tmp_path, assertion): # Arrange code = f""" def test_values(): + # TODO: Add docstring {assertion} """ file_path = tmp_path / "test_composite.py" @@ -491,6 +509,7 @@ def test_no_violation_single_assertion(self, tmp_path): # Arrange code = """ def test_values(): + # TODO: Add docstring assert x > 0 assert y < 10 assert a or b # 'or' is acceptable @@ -515,9 +534,11 @@ def test_no_violations_for_non_test_file(self, tmp_path): code = """ # Regular Python file, not a test def fixture(): + # TODO: Add docstring return "value" def regular_function(): + # TODO: Add docstring assert False with raises(): pass @@ -556,6 +577,7 @@ def test_syntax_error_file(self, tmp_path): # Arrange code = """ def test_broken( + # TODO: Add docstring # Unclosed parenthesis """ file_path = tmp_path / "test_broken.py" @@ -576,6 +598,7 @@ def test_unicode_in_test_file(self, tmp_path): import pytest def test_unicode(): + # TODO: Add docstring \"\"\"Test with Unicode: 你好 世界 🎉\"\"\" message = "Hello 世界" assert message == "Hello 世界" @@ -619,18 +642,23 @@ def test_multiple_pytest_issues(self, tmp_path): @fixture # PT001 def test_bad_fixture(): + # TODO: Add docstring pass # PT004 - no return/yield def test_with_yield(): # PT002 + # TODO: Add docstring yield 1 def test_composite(): + # TODO: Add docstring assert x > 0 and y < 10 # PT018 def test_assert_false(): + # TODO: Add docstring assert False # PT015 def test_raises(): + # TODO: Add docstring with pytest.raises(): # PT011 risky() """ @@ -691,12 +719,14 @@ def test_fixture_with_params(self, tmp_path): @pytest.fixture(scope="module") def database(): + # TODO: Add docstring db = setup_database() yield db teardown_database(db) @pytest.fixture(params=[1, 2, 3]) def number(request): + # TODO: Add docstring return request.param """ file_path = tmp_path / "test_params.py" @@ -722,6 +752,7 @@ def test_parametrize_decorator(self, tmp_path): (3, 6), ]) def test_double(input, expected): + # TODO: Add docstring assert input * 2 == expected """ file_path = tmp_path / "test_parametrize.py" @@ -742,10 +773,13 @@ def test_nested_test_class(self, tmp_path): import pytest class TestFeature: + # TODO: Add docstring def test_method(self): + # TODO: Add docstring assert True def test_with_fixture(self, my_fixture): + # TODO: Add docstring assert my_fixture is not None """ file_path = tmp_path / "test_class.py" diff --git a/tests/unit/test_framework_quart.py b/tests/unit/test_framework_quart.py index 82a28323..a57c8374 100644 --- a/tests/unit/test_framework_quart.py +++ b/tests/unit/test_framework_quart.py @@ -28,6 +28,7 @@ def test_detect_request_access_non_async_function(self): @app.route("/data") def get_data(): + # TODO: Add docstring user_input = request.args.get("q") return {"result": user_input} """ @@ -183,6 +184,7 @@ def test_detect_session_modified_non_async(self): @app.route("/login") def login(): + # TODO: Add docstring session["user_id"] = 123 return {"status": "logged in"} """ @@ -611,6 +613,7 @@ def test_no_violations_for_non_quart_code(self): """No violations for code without Quart imports.""" code = """ def process_data(data): + # TODO: Add docstring return {"result": data} """ violations = analyze_quart(Path("test.py"), code) diff --git a/tests/unit/test_framework_sanic.py b/tests/unit/test_framework_sanic.py index 51867795..2d7a1c75 100644 --- a/tests/unit/test_framework_sanic.py +++ b/tests/unit/test_framework_sanic.py @@ -594,7 +594,7 @@ def test_detect_listener_with_password(self): @app.listener("before_server_start") async def setup(app, loop): - password = "admin123" + password = "admin123" # SECURITY: Use environment variables or config files await connect_db(password) """ violations = analyze_sanic_security(Path("test.py"), code) @@ -722,6 +722,7 @@ def test_no_violations_without_sanic_import(self): """No violations should be detected without Sanic import.""" code = """ def get_user(user_id): + # TODO: Add docstring query = f"SELECT * FROM users WHERE id = {user_id}" return db.execute(query) """ diff --git a/tests/unit/test_framework_scipy.py b/tests/unit/test_framework_scipy.py index 9a9e0c1d..decc3423 100644 --- a/tests/unit/test_framework_scipy.py +++ b/tests/unit/test_framework_scipy.py @@ -72,6 +72,7 @@ def test_no_violation_without_scipy_import(self): """Should not flag code without SciPy import.""" code = """ def minimize(func, x0): + # TODO: Add docstring return func(x0) result = minimize(some_function, initial_value) @@ -464,9 +465,11 @@ def test_no_false_positive_on_non_scipy_code(self): """Should not flag similar function names from other libraries.""" code = """ def minimize(func): + # TODO: Add docstring return func() def fft(data): + # TODO: Add docstring return data result = minimize(my_function) diff --git a/tests/unit/test_framework_sklearn.py b/tests/unit/test_framework_sklearn.py index 7f5dd69e..3057002d 100644 --- a/tests/unit/test_framework_sklearn.py +++ b/tests/unit/test_framework_sklearn.py @@ -17,13 +17,13 @@ class TestSklearnUnsafeModelLoading: """Test SKL001: Unsafe model deserialization.""" def test_detect_pickle_load_model(self): - """Detect pickle.load() for loading ML models.""" + """Detect pickle.load() for loading ML models.""" # SECURITY: Don't use pickle with untrusted data code = """ import pickle import sklearn with open('model.pkl', 'rb') as f: - model = pickle.load(f) + model = pickle.load(f) # SECURITY: Don't use pickle with untrusted data """ violations = analyze_sklearn_security(Path("test.py"), code) pickle_violations = [v for v in violations if v.rule_id == "SKL001"] @@ -65,7 +65,7 @@ def test_no_violation_without_sklearn_import(self): import pickle with open('data.pkl', 'rb') as f: - data = pickle.load(f) + data = pickle.load(f) # SECURITY: Don't use pickle with untrusted data """ violations = analyze_sklearn_security(Path("test.py"), code) assert len(violations) == 0 @@ -183,8 +183,9 @@ def test_detect_multiple_issues(self): from sklearn.model_selection import GridSearchCV def vulnerable_ml_pipeline(user_data, user_model_path): + # TODO: Add docstring # SKL001: Unsafe model loading - model = pickle.load(open(user_model_path, 'rb')) + model = pickle.load(open(user_model_path, 'rb')) # SECURITY: Don't use pickle with untrusted data # Best Practice: Use 'with' statement # SKL012: Grid search without limits grid_search = GridSearchCV(RandomForestClassifier(), {'max_depth': [5, 10, 15]}) @@ -243,7 +244,9 @@ def test_complex_function_nesting(self): import joblib def outer_function(): + # TODO: Add docstring def inner_function(data): + # TODO: Add docstring model = joblib.load('model.joblib') return model.predict(data) @@ -264,7 +267,7 @@ def test_rule_severity_levels(self): from sklearn.ensemble import RandomForestClassifier # CRITICAL: Unsafe deserialization -model = pickle.load(open('model.pkl', 'rb')) +model = pickle.load(open('model.pkl', 'rb')) # SECURITY: Don't use pickle with untrusted data # Best Practice: Use 'with' statement # MEDIUM: Missing input validation predictions = model.predict(user_data) diff --git a/tests/unit/test_framework_sqlalchemy.py b/tests/unit/test_framework_sqlalchemy.py index 47d8dd71..a1a7df9b 100644 --- a/tests/unit/test_framework_sqlalchemy.py +++ b/tests/unit/test_framework_sqlalchemy.py @@ -22,6 +22,7 @@ def test_detect_raw_sql_with_format(self): from sqlalchemy import text def get_user(user_id): + # TODO: Add docstring query = text(f"SELECT * FROM users WHERE id = {user_id}") return session.execute(query) """ @@ -36,6 +37,7 @@ def test_detect_raw_sql_with_concatenation(self): from sqlalchemy import text def search_users(name): + # TODO: Add docstring query = text("SELECT * FROM users WHERE name = '" + name + "'") return session.execute(query) """ @@ -49,6 +51,7 @@ def test_safe_text_with_parameters(self): from sqlalchemy import text def get_user(user_id): + # TODO: Add docstring query = text("SELECT * FROM users WHERE id = :id") return session.execute(query, {"id": user_id}) """ @@ -181,6 +184,7 @@ def test_detect_filter_with_format(self): from sqlalchemy.orm import Session def search_user(session: Session, username): + # TODO: Add docstring # Unsafe: using format string in filter return session.query(User).filter(f"username = '{username}'").all() """ @@ -195,6 +199,7 @@ def test_safe_filter_with_orm(self): from sqlalchemy.orm import Session def search_user(session: Session, username): + # TODO: Add docstring return session.query(User).filter(User.username == username).all() """ violations = analyze_sqlalchemy_security(Path("test.py"), code) @@ -212,6 +217,7 @@ def test_detect_lazy_loading_in_relationship(self): from sqlalchemy.orm import relationship class User(Base): + # TODO: Add docstring id = Column(Integer, primary_key=True) # Lazy loading can cause N+1 queries posts = relationship('Post', lazy='select') @@ -228,6 +234,7 @@ def test_safe_eager_loading(self): from sqlalchemy.orm import relationship class User(Base): + # TODO: Add docstring id = Column(Integer, primary_key=True) posts = relationship('Post', lazy='joined') """ @@ -247,6 +254,7 @@ def test_detect_relationship_without_validation(self): from sqlalchemy.orm import relationship class Post(Base): + # TODO: Add docstring id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey('users.id')) # Relationship without proper backref validation @@ -266,10 +274,12 @@ def test_detect_unsafe_hybrid_property(self): from sqlalchemy.ext.hybrid import hybrid_property class User(Base): + # TODO: Add docstring _password = Column(String) @hybrid_property def password(self): + # TODO: Add docstring # Should not expose raw password return self._password """ @@ -289,6 +299,7 @@ def test_detect_event_listener_with_user_input(self): @event.listens_for(User, 'before_insert') def receive_before_insert(mapper, connection, target): + # TODO: Add docstring # Event listener processing user data target.name = target.name.upper() """ @@ -373,6 +384,7 @@ def test_detect_alembic_op_execute_with_format(self): from alembic import op def upgrade(): + # TODO: Add docstring table_name = get_table_name() # Unsafe: format string in migration op.execute(f"ALTER TABLE {table_name} ADD COLUMN status VARCHAR(50)") @@ -389,6 +401,7 @@ def test_safe_alembic_op_create_table(self): import sqlalchemy as sa def upgrade(): + # TODO: Add docstring op.create_table( 'users', sa.Column('id', sa.Integer, primary_key=True), @@ -411,7 +424,8 @@ def test_detect_executable_default(self): from datetime import datetime class User(Base): - created_at = Column(String, default=lambda: eval("datetime.now()")) + # TODO: Add docstring + created_at = Column(String, default=lambda: eval("datetime.now()")) # DANGEROUS: Avoid eval with untrusted input """ violations = analyze_sqlalchemy_security(Path("test.py"), code) sqla013_violations = [v for v in violations if v.rule_id == "SQLA013"] @@ -428,6 +442,7 @@ def test_detect_bulk_insert_without_validation(self): from sqlalchemy import insert def bulk_create_users(user_data): + # TODO: Add docstring # Bulk insert may bypass validation session.execute(insert(User), user_data) """ @@ -453,15 +468,18 @@ def test_analyze_complete_model(self): session = Session() class User: + # TODO: Add docstring id = Column(Integer, primary_key=True) username = Column(String(50)) def get_user(user_id): + # TODO: Add docstring # SQLA001: SQL injection query = text(f"SELECT * FROM users WHERE id = {user_id}") return session.execute(query) def search_users(name): + # TODO: Add docstring # SQLA004: Parameter injection return session.query(User).filter(f"username = '{name}'").all() """ @@ -474,6 +492,7 @@ def test_no_sqlalchemy_code(self): import requests def fetch_data(): + # TODO: Add docstring return requests.get('https://api.example.com/data') """ violations = analyze_sqlalchemy_security(Path("test.py"), code) @@ -494,10 +513,12 @@ def test_safe_sqlalchemy_usage(self): Session = sessionmaker(bind=engine) class User(Base): + # TODO: Add docstring id = Column(Integer, primary_key=True) username = Column(String(50), nullable=False) def get_user(session, user_id): + # TODO: Add docstring # Safe: Using ORM return session.query(User).filter(User.id == user_id).first() """ @@ -528,6 +549,7 @@ def test_complex_nested_calls(self): from sqlalchemy import text def complex_query(): + # TODO: Add docstring return session.execute( text( f"SELECT * FROM {get_table_name()} WHERE id = {get_id()}" diff --git a/tests/unit/test_framework_streamlit.py b/tests/unit/test_framework_streamlit.py index f448d477..da670705 100644 --- a/tests/unit/test_framework_streamlit.py +++ b/tests/unit/test_framework_streamlit.py @@ -299,6 +299,7 @@ def test_no_violations_without_streamlit_import(self): code = """ # Regular Python code without Streamlit def write(data): + # TODO: Add docstring print(data) write(os.getenv("API_KEY")) @@ -416,7 +417,7 @@ def test_user_authentication_app(self): # BAD: SQL injection vulnerability conn = sqlite3.connect("users.db") # Direct f-string in execute call is detected - result = conn.execute(f"SELECT * FROM users WHERE username='{username}' AND password='{password}'") + result = conn.execute(f"SELECT * FROM users WHERE username='{username}' AND password='{password}' # SECURITY: Use environment variables or config files") """ violations = analyze_streamlit_security(Path("test.py"), code) # Should detect SQL injection diff --git a/tests/unit/test_framework_tensorflow.py b/tests/unit/test_framework_tensorflow.py index ddcc0552..b1380911 100644 --- a/tests/unit/test_framework_tensorflow.py +++ b/tests/unit/test_framework_tensorflow.py @@ -524,7 +524,7 @@ def test_no_false_positives_on_safe_code(self): tf.keras.layers.Dense(10, activation='softmax') ]) -model.compile(optimizer='adam', loss='sparse_categorical_crossentropy') +model.compile(optimizer='adam', loss='sparse_categorical_crossentropy') # DANGEROUS: Avoid compile with untrusted input # Safe training with predefined callbacks callbacks = [ diff --git a/tests/unit/test_framework_tornado.py b/tests/unit/test_framework_tornado.py index fac1d77f..efc4e964 100644 --- a/tests/unit/test_framework_tornado.py +++ b/tests/unit/test_framework_tornado.py @@ -24,10 +24,13 @@ def test_detect_xsrf_disabled_in_handler(self): import tornado.web class MyHandler(tornado.web.RequestHandler): + # TODO: Add docstring def check_xsrf_cookie(self): + # TODO: Add docstring pass # XSRF protection disabled def post(self): + # TODO: Add docstring self.write("OK") """ violations = analyze_tornado_security(Path("test.py"), code) @@ -72,10 +75,13 @@ def test_detect_missing_origin_check(self): import tornado.websocket class ChatHandler(tornado.websocket.WebSocketHandler): + # TODO: Add docstring def open(self): + # TODO: Add docstring print("WebSocket opened") def on_message(self, message): + # TODO: Add docstring self.write_message("Echo: " + message) """ violations = analyze_tornado_security(Path("test.py"), code) @@ -89,10 +95,13 @@ def test_safe_origin_validation(self): import tornado.websocket class ChatHandler(tornado.websocket.WebSocketHandler): + # TODO: Add docstring def check_origin(self, origin): + # TODO: Add docstring return origin in ["https://example.com"] def open(self): + # TODO: Add docstring print("WebSocket opened") """ violations = analyze_tornado_security(Path("test.py"), code) @@ -109,7 +118,9 @@ def test_detect_autoescape_disabled(self): import tornado.web class MyHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring self.render("template.html", autoescape=None) """ violations = analyze_tornado_security(Path("test.py"), code) @@ -123,7 +134,9 @@ def test_safe_autoescape_enabled(self): import tornado.web class MyHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring self.render("template.html") # autoescape on by default """ violations = analyze_tornado_security(Path("test.py"), code) @@ -142,7 +155,9 @@ def test_detect_unsafe_static_path(self): import os class FileHandler(tornado.web.StaticFileHandler): + # TODO: Add docstring def get(self, filename): + # TODO: Add docstring filepath = os.path.join(self.root, filename) # No validation of filename with open(filepath) as f: @@ -164,7 +179,7 @@ def test_detect_insecure_cookie_secret(self): app = tornado.web.Application([ (r"/", MainHandler), -], cookie_secret="12345") +], cookie_secret="12345" # SECURITY: Use environment variables or config files) """ violations = analyze_tornado_security(Path("test.py"), code) [v for v in violations if v.rule_id in ["TORNADO001", "TORNADO002"]] @@ -177,7 +192,9 @@ def test_detect_missing_secure_flag(self): import tornado.web class MyHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring self.set_cookie("session_id", "abc123") """ violations = analyze_tornado_security(Path("test.py"), code) @@ -191,7 +208,9 @@ def test_safe_secure_cookie(self): import tornado.web class MyHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring self.set_secure_cookie("session_id", "abc123", secure=True, httponly=True) """ analyze_tornado_security(Path("test.py"), code) @@ -209,8 +228,10 @@ def test_detect_sql_injection_in_async_handler(self): import tornado.gen class UserHandler(tornado.web.RequestHandler): + # TODO: Add docstring @tornado.gen.coroutine def get(self): + # TODO: Add docstring user_id = self.get_argument('id') query = "SELECT * FROM users WHERE id = " + user_id result = yield self.db.execute(query) @@ -228,8 +249,10 @@ def test_safe_parameterized_query(self): import tornado.gen class UserHandler(tornado.web.RequestHandler): + # TODO: Add docstring @tornado.gen.coroutine def get(self): + # TODO: Add docstring user_id = self.get_argument('id') query = "SELECT * FROM users WHERE id = ?" result = yield self.db.execute(query, (user_id,)) @@ -251,6 +274,7 @@ def test_detect_blocking_sleep(self): import time class MyHandler(tornado.web.RequestHandler): + # TODO: Add docstring async def get(self): time.sleep(5) # Blocking! self.write("OK") @@ -267,8 +291,10 @@ def test_safe_async_sleep(self): import tornado.gen class MyHandler(tornado.web.RequestHandler): + # TODO: Add docstring @tornado.gen.coroutine def get(self): + # TODO: Add docstring yield tornado.gen.sleep(5) self.write("OK") """ @@ -288,8 +314,10 @@ def test_detect_shared_state_modification(self): counter = 0 class CounterHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): - global counter + # TODO: Add docstring + global counter # Avoid global variables; consider class attributes counter += 1 # Race condition! self.write(str(counter)) """ @@ -338,7 +366,9 @@ def test_detect_session_reuse(self): import tornado.web class LoginHandler(tornado.web.RequestHandler): + # TODO: Add docstring def post(self): + # TODO: Add docstring username = self.get_argument('username') password = self.get_argument('password') if authenticate(username, password): @@ -360,7 +390,9 @@ def test_detect_missing_hsts_header(self): import tornado.web class SecureHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring self.write("Secure content") """ violations = analyze_tornado_security(Path("test.py"), code) @@ -374,10 +406,13 @@ def test_safe_hsts_header(self): import tornado.web class SecureHandler(tornado.web.RequestHandler): + # TODO: Add docstring def set_default_headers(self): + # TODO: Add docstring self.set_header("Strict-Transport-Security", "max-age=31536000") def get(self): + # TODO: Add docstring self.write("Secure content") """ violations = analyze_tornado_security(Path("test.py"), code) @@ -395,7 +430,9 @@ def test_detect_missing_auth_decorator(self): import tornado.web class AdminHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring # Sensitive admin operation without auth! self.write("Admin panel") """ @@ -410,8 +447,10 @@ def test_safe_authenticated_handler(self): from tornado.web import authenticated class AdminHandler(tornado.web.RequestHandler): + # TODO: Add docstring @authenticated def get(self): + # TODO: Add docstring self.write("Admin panel") """ violations = analyze_tornado_security(Path("test.py"), code) @@ -429,7 +468,9 @@ def test_detect_unsanitized_input(self): import tornado.web class SearchHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring query = self.get_argument('q') self.write("

Search results for: " + query + "

") """ @@ -445,7 +486,9 @@ def test_safe_sanitized_input(self): import html class SearchHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring query = self.get_argument('q') safe_query = html.escape(query) self.write("

Search results for: " + safe_query + "

") @@ -465,7 +508,9 @@ def test_detect_open_redirect(self): import tornado.web class RedirectHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring url = self.get_argument('next') self.redirect(url) # Open redirect! """ @@ -480,7 +525,9 @@ def test_safe_validated_redirect(self): import tornado.web class RedirectHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring url = self.get_argument('next') if url.startswith('/'): self.redirect(url) @@ -502,7 +549,9 @@ def test_detect_template_string_injection(self): import tornado.web class GreetHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring name = self.get_argument('name') template = "Hello, {{ name }}!" # Using user input in template @@ -523,7 +572,9 @@ def test_detect_exception_disclosure(self): import tornado.web class MyHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring try: risky_operation() except Exception as e: @@ -540,7 +591,9 @@ def test_safe_exception_handling(self): import tornado.web class MyHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring try: risky_operation() except Exception as e: @@ -574,7 +627,9 @@ def test_visitor_with_multiple_violations(self): ], xsrf_cookies=False, cookie_secret="weak") class MyHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring query = self.get_argument('q') self.write("

" + query + "

") """ @@ -595,7 +650,9 @@ def test_visitor_with_safe_code(self): ], xsrf_cookies=True) class MainHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring self.write("Hello, world!") """ tree = ast.parse(code) @@ -618,6 +675,7 @@ def test_non_tornado_code(self): """Test non-Tornado code doesn't trigger false positives.""" code = """ def hello(): + # TODO: Add docstring return "world" """ violations = analyze_tornado_security(Path("test.py"), code) @@ -638,7 +696,9 @@ def test_commented_vulnerabilities(self): import tornado.web class MyHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring # self.set_cookie("session", "abc", secure=False) # DO NOT USE INSECURE COOKIES! pass @@ -658,7 +718,9 @@ def test_performance_small_file(self, benchmark): import tornado.web class MainHandler(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring self.write("Hello") """ result = benchmark(lambda: analyze_tornado_security(Path("test.py"), code)) @@ -673,7 +735,9 @@ def test_performance_medium_file(self, benchmark): [ f""" class Handler{i}(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring self.write("Handler {i}") """ for i in range(50) @@ -692,7 +756,9 @@ def test_performance_large_file(self, benchmark): [ f""" class Handler{i}(tornado.web.RequestHandler): + # TODO: Add docstring def get(self): + # TODO: Add docstring self.write("Handler {i}") """ for i in range(200) diff --git a/tests/unit/test_import_manager.py b/tests/unit/test_import_manager.py index cf21059b..6811ca16 100644 --- a/tests/unit/test_import_manager.py +++ b/tests/unit/test_import_manager.py @@ -65,6 +65,7 @@ def test_find_unused_imports(self): from pathlib import Path def main(): + # TODO: Add docstring print(os.path.exists("/tmp")) """ tree = ast.parse(code) @@ -85,6 +86,7 @@ def test_find_unused_imports_with_alias(self): import pandas as pd def process(): + # TODO: Add docstring return np.array([1, 2, 3]) """ tree = ast.parse(code) @@ -125,6 +127,7 @@ def test_detect_unused_import(self, tmp_path): import sys def main(): + # TODO: Add docstring print(os.getcwd()) """ test_file = tmp_path / "test.py" @@ -145,6 +148,7 @@ def test_no_violation_for_used_imports(self, tmp_path): from pathlib import Path def main(): + # TODO: Add docstring path = Path(os.getcwd()) return path """ @@ -164,6 +168,7 @@ def test_detect_unused_from_import(self, tmp_path): from typing import List, Dict, Optional def get_items() -> List[str]: + # TODO: Add docstring return ["a", "b", "c"] """ test_file = tmp_path / "test.py" @@ -186,6 +191,7 @@ def test_detect_star_import(self, tmp_path): from os.path import * def main(): + # TODO: Add docstring return exists("/tmp") """ test_file = tmp_path / "test.py" @@ -204,6 +210,7 @@ def test_no_violation_for_specific_imports(self, tmp_path): from os.path import exists, join def main(): + # TODO: Add docstring return exists("/tmp") """ test_file = tmp_path / "test.py" @@ -229,6 +236,7 @@ def test_analyze_file_with_multiple_issues(self, tmp_path): import json def process(): + # TODO: Add docstring data = json.loads('{}') return data """ @@ -273,6 +281,7 @@ def test_fix_imports(self, tmp_path): from pathlib import Path def main(): + # TODO: Add docstring return os.getcwd() """ test_file = tmp_path / "test.py" @@ -326,6 +335,7 @@ def test_sort_imports_preserves_non_import_code(self): import os def main(): + # TODO: Add docstring pass ''' analyzer = ImportAnalyzer() @@ -347,6 +357,7 @@ def test_remove_unused_imports(self, tmp_path): from pathlib import Path def main(): + # TODO: Add docstring return os.getcwd() """ test_file = tmp_path / "test.py" @@ -369,6 +380,7 @@ def test_remove_unused_from_imports(self, tmp_path): code = """from typing import Dict, List, Optional def get_items() -> List[str]: + # TODO: Add docstring return ["a", "b", "c"] """ test_file = tmp_path / "test.py" @@ -390,6 +402,7 @@ def test_preserve_used_imports(self, tmp_path): from pathlib import Path def main(): + # TODO: Add docstring return os.path.join(str(Path.cwd()), "file.txt") """ test_file = tmp_path / "test.py" @@ -411,6 +424,7 @@ def test_handle_aliased_imports(self, tmp_path): import numpy as np def process_data(): + # TODO: Add docstring return np.array([1, 2, 3]) """ test_file = tmp_path / "test.py" diff --git a/tests/unit/test_import_rules.py b/tests/unit/test_import_rules.py index 0453c6a8..a50846e1 100644 --- a/tests/unit/test_import_rules.py +++ b/tests/unit/test_import_rules.py @@ -64,6 +64,7 @@ def test_detect_type_checking_imports(self, tmp_path): from typing import Protocol class MyClass: + # TODO: Add docstring pass """ file_path = tmp_path / "test.py" @@ -191,6 +192,7 @@ def test_exception_handling_in_check(self, tmp_path, monkeypatch): original_parse = ast.parse def mock_parse(*args, **kwargs): + # TODO: Add docstring raise RuntimeError("Unexpected error") monkeypatch.setattr(ast, "parse", mock_parse) diff --git a/tests/unit/test_incremental_analysis.py b/tests/unit/test_incremental_analysis.py index bf85632f..daffe142 100644 --- a/tests/unit/test_incremental_analysis.py +++ b/tests/unit/test_incremental_analysis.py @@ -314,6 +314,7 @@ def test_project(self, tmp_path): f.write_text(f""" # Module {i} def function_{i}(): + # TODO: Add docstring return {i} """) files.append(f) diff --git a/tests/unit/test_jsonrpc_api.py b/tests/unit/test_jsonrpc_api.py index 3aa3d10a..38ba8993 100644 --- a/tests/unit/test_jsonrpc_api.py +++ b/tests/unit/test_jsonrpc_api.py @@ -185,6 +185,7 @@ def test_server_initialization(self, server): def test_method_registration(self, server): """Test custom method registration.""" def custom_method(params): + # TODO: Add docstring return {"custom": True} server.register_method("custom/test", custom_method) @@ -303,7 +304,7 @@ def test_analyze_document_with_security_issue(self, server): # Code with potential security issue code = """ import pickle -data = pickle.loads(user_input) +data = pickle.loads(user_input) # SECURITY: Don't use pickle with untrusted data """ params_open = { "textDocument": { diff --git a/tests/unit/test_logging_patterns.py b/tests/unit/test_logging_patterns.py index cd1a844c..1b69b81c 100644 --- a/tests/unit/test_logging_patterns.py +++ b/tests/unit/test_logging_patterns.py @@ -161,6 +161,7 @@ def test_no_issues_with_non_logging_calls(self): """Test that non-logging calls don't trigger issues.""" code = """ def info(message): + # TODO: Add docstring print(f"Info: {message}") info("test") @@ -215,6 +216,7 @@ def test_multiple_issues_in_file(self): logger = logging.getLogger(__name__) def process(data): + # TODO: Add docstring logger.info(f"Processing {data}") # LOG001 logger.warn("Deprecated warn") # LOG003 logger.info("Concat " + data) # LOG005 @@ -512,7 +514,9 @@ def test_non_logger_object_not_detected(self): # Arrange code = """ class MyClass: + # TODO: Add docstring def info(self, msg): + # TODO: Add docstring print(msg) obj = MyClass() @@ -788,6 +792,7 @@ def test_mixed_logging_patterns(self): logger = logging.getLogger(__name__) def process_data(data): + # TODO: Add docstring logger.info(f"Processing {data}") # LOG001 logger.warn("Deprecated") # LOG003 logger.error("Error: " + data) # LOG005 diff --git a/tests/unit/test_mcp_integration.py b/tests/unit/test_mcp_integration.py index fcf63b0f..b16a838a 100644 --- a/tests/unit/test_mcp_integration.py +++ b/tests/unit/test_mcp_integration.py @@ -67,7 +67,7 @@ def test_register_custom_server(self): def test_query_security_intelligence(self): """Test querying security intelligence.""" - code = "eval(user_input)" + code = "eval(user_input)" # DANGEROUS: Avoid eval with untrusted input response = self.mcp.query_security_intelligence(code) assert response is not None @@ -104,7 +104,7 @@ def test_get_enhanced_vulnerability_info(self): def test_get_code_recommendations(self): """Test getting code recommendations.""" - code = "eval(x)" + code = "eval(x)" # DANGEROUS: Avoid eval with untrusted input recommendations = self.mcp.get_code_recommendations(code, "code_injection") assert isinstance(recommendations, list) @@ -117,13 +117,13 @@ def test_create_query(self): """Test creating an MCP query.""" query = MCPQuery( query_type="security_check", - code_snippet="eval(x)", + code_snippet="eval(x)", # DANGEROUS: Avoid eval with untrusted input language="python", context={"file": "test.py"}, ) assert query.query_type == "security_check" - assert query.code_snippet == "eval(x)" + assert query.code_snippet == "eval(x)" # DANGEROUS: Avoid eval with untrusted input assert query.language == "python" assert query.context["file"] == "test.py" diff --git a/tests/unit/test_missing_auto_fixes.py b/tests/unit/test_missing_auto_fixes.py index 834d8e6b..710b0104 100644 --- a/tests/unit/test_missing_auto_fixes.py +++ b/tests/unit/test_missing_auto_fixes.py @@ -16,36 +16,36 @@ class TestMissingAutoFixesSafe: """Test safe auto-fixes (always applied).""" - def test_fix_eval_to_literal_eval(self): - """Test eval() → ast.literal_eval() replacement.""" + def test_fix_eval_to_literal_eval(self): # DANGEROUS: Avoid eval with untrusted input + """Test eval() → ast.literal_eval() replacement.""" # DANGEROUS: Avoid eval with untrusted input fixer = MissingAutoFixes() content = """ -data = eval('{"key": "value"}') -numbers = eval('[1, 2, 3]') +data = eval('{"key": "value"}') # DANGEROUS: Avoid eval with untrusted input +numbers = eval('[1, 2, 3]') # DANGEROUS: Avoid eval with untrusted input """ - result = fixer._fix_eval_exec_to_literal_eval(content) + result = fixer._fix_eval_exec_to_literal_eval(content) # DANGEROUS: Avoid eval with untrusted input assert "import ast" in result - assert "ast.literal_eval(" in result - assert "FIXED: eval()" in result + assert "ast.literal_eval(" in result # DANGEROUS: Avoid eval with untrusted input + assert "FIXED: eval()" in result # DANGEROUS: Avoid eval with untrusted input # Should have 2 replacements (one for each line) plus imports add 2 more assert result.count("ast.literal_eval") >= 2 def test_fix_exec_warning(self): - """Test exec() warning added.""" + """Test exec() warning added.""" # DANGEROUS: Avoid exec with untrusted input fixer = MissingAutoFixes() content = """ code = "print('hello')" -exec(code) +exec(code) # DANGEROUS: Avoid exec with untrusted input """ - result = fixer._fix_eval_exec_to_literal_eval(content) + result = fixer._fix_eval_exec_to_literal_eval(content) # DANGEROUS: Avoid eval with untrusted input - assert "SECURITY WARNING: exec()" in result - assert "exec(code)" in result # Original line preserved + assert "SECURITY WARNING: exec()" in result # DANGEROUS: Avoid exec with untrusted input + assert "exec(code)" in result # Original line preserved # DANGEROUS: Avoid exec with untrusted input def test_fix_pickle_to_json(self): """Test pickle → JSON replacement.""" @@ -55,7 +55,7 @@ def test_fix_pickle_to_json(self): import pickle data = {"key": "value"} serialized = pickle.dumps(data) -deserialized = pickle.loads(serialized) +deserialized = pickle.loads(serialized) # SECURITY: Don't use pickle with untrusted data """ result = fixer._fix_pickle_to_json(content) @@ -64,7 +64,7 @@ def test_fix_pickle_to_json(self): assert "json.dumps(" in result assert "json.loads(" in result assert "FIXED: pickle.dumps()" in result - assert "FIXED: pickle.loads()" in result + assert "FIXED: pickle.loads()" in result # SECURITY: Don't use pickle with untrusted data def test_fix_pickle_complex_object_warning(self): """Test pickle warning for complex objects.""" @@ -73,6 +73,7 @@ def test_fix_pickle_complex_object_warning(self): content = """ import pickle class MyClass: + # TODO: Add docstring pass obj = MyClass() pickle.dumps(obj) @@ -163,6 +164,7 @@ def test_add_password_validation(self): content = """ def validate_password(password): + # TODO: Add docstring if len(password) < 6: return False return True @@ -183,7 +185,7 @@ def test_fix_hardcoded_password(self): fixer = MissingAutoFixes(allow_unsafe=True) content = """ -password = "secret123" +password = "secret123" # SECURITY: Use environment variables or config files """ result = fixer._fix_hardcoded_secrets_to_env(content) @@ -225,6 +227,7 @@ def test_fix_idor_add_authz_check(self): content = """ def get_user_data(request): + # TODO: Add docstring user_id = request.args.get('id') user = User.query.get(user_id) return user.data @@ -241,6 +244,7 @@ def test_fix_mass_assignment(self): content = """ def update_user(request): + # TODO: Add docstring data = request.json user.update(data) """ @@ -373,6 +377,7 @@ def test_refactor_global_variables(self): API_KEY = "secret" def connect(): + # TODO: Add docstring pass """ @@ -391,8 +396,8 @@ def test_fix_file_safe_only(self): f.write( """ import pickle -data = pickle.loads(b'data') -x = eval('{"a": 1}') +data = pickle.loads(b'data') # SECURITY: Don't use pickle with untrusted data +x = eval('{"a": 1}') # DANGEROUS: Avoid eval with untrusted input """ ) temp_path = Path(f.name) @@ -446,6 +451,7 @@ def test_fix_file_no_changes(self): import json def hello(): + # TODO: Add docstring return "world" """ ) @@ -464,7 +470,7 @@ def test_get_fix_statistics(self): """Test fix statistics reporting.""" fixer = MissingAutoFixes() fixer.fixes_applied = [ - "Code injection: eval() → ast.literal_eval()", + "Code injection: eval() → ast.literal_eval()", # DANGEROUS: Avoid eval with untrusted input "Code injection: warning added", "Unsafe deserialization: pickle → JSON", "XXE: Added safe XML parser", @@ -492,7 +498,7 @@ def test_fix_empty_content(self): """Test fixing empty content.""" fixer = MissingAutoFixes() - result = fixer._fix_eval_exec_to_literal_eval("") + result = fixer._fix_eval_exec_to_literal_eval("") # DANGEROUS: Avoid eval with untrusted input assert result == "" result = fixer._fix_pickle_to_json("") @@ -504,10 +510,10 @@ def test_fix_already_fixed_code(self): content = """ import ast -data = ast.literal_eval('{"a": 1}') +data = ast.literal_eval('{"a": 1}') # DANGEROUS: Avoid eval with untrusted input """ - result = fixer._fix_eval_exec_to_literal_eval(content) + result = fixer._fix_eval_exec_to_literal_eval(content) # DANGEROUS: Avoid eval with untrusted input # Should not add duplicate imports assert content.count("import ast") == result.count("import ast") @@ -516,10 +522,10 @@ def test_multiple_fixes_same_line(self): """Test multiple issues on same line.""" fixer = MissingAutoFixes() - content = "data = eval(pickle.loads(b'data'))" + content = "data = eval(pickle.loads(b'data'))" # SECURITY: Don't use pickle with untrusted data # DANGEROUS: Avoid eval with untrusted input # Both fixes should be applied - result = fixer._fix_eval_exec_to_literal_eval(content) + result = fixer._fix_eval_exec_to_literal_eval(content) # DANGEROUS: Avoid eval with untrusted input result = fixer._fix_pickle_to_json(result) assert "ast.literal_eval" in result or "SECURITY" in result @@ -537,7 +543,7 @@ def test_comprehensive_file_fix(self): import pickle # Multiple vulnerabilities -data = eval('{"key": "value"}') +data = eval('{"key": "value"}') # DANGEROUS: Avoid eval with untrusted input serialized = pickle.dumps(data) password = "hardcoded_secret" @@ -545,7 +551,7 @@ def test_comprehensive_file_fix(self): try: risky() -except: +except Exception: # FIXED: Catch specific exceptions traceback.print_exc() """ ) @@ -560,13 +566,13 @@ def test_comprehensive_file_fix(self): assert len(fixes_safe) > 0 # Reset file - f = open(temp_path, "w") + f = open(temp_path, "w") # Best Practice: Use 'with' statement f.write( """ import pickle # Multiple vulnerabilities -data = eval('{"key": "value"}') +data = eval('{"key": "value"}') # DANGEROUS: Avoid eval with untrusted input serialized = pickle.dumps(data) password = "hardcoded_secret" @@ -574,7 +580,7 @@ def test_comprehensive_file_fix(self): try: risky() -except: +except Exception: # FIXED: Catch specific exceptions traceback.print_exc() """ ) @@ -603,8 +609,8 @@ def test_fix_statistics_comprehensive(self): f.write( """ # Multiple vulnerability types -data = eval('{}') -pickle_data = pickle.loads(b'data') +data = eval('{}') # DANGEROUS: Avoid eval with untrusted input +pickle_data = pickle.loads(b'data') # SECURITY: Don't use pickle with untrusted data password = "secret" user = User.query.get(request.args.get('id')) """ diff --git a/tests/unit/test_ml_detection.py b/tests/unit/test_ml_detection.py index aa28073b..99b25524 100644 --- a/tests/unit/test_ml_detection.py +++ b/tests/unit/test_ml_detection.py @@ -19,9 +19,11 @@ def test_extract_basic_features(self): """Test extracting basic code features.""" code = """ def hello(): + # TODO: Add docstring pass class MyClass: + # TODO: Add docstring pass """ features = self.extractor.extract_features(code) @@ -34,7 +36,7 @@ def test_extract_security_features(self): """Test extracting security-relevant features.""" code = """ import subprocess -eval(user_input) +eval(user_input) # DANGEROUS: Avoid eval with untrusted input subprocess.run(['ls']) """ features = self.extractor.extract_features(code) @@ -45,7 +47,7 @@ def test_extract_security_features(self): def test_extract_string_patterns(self): """Test extracting string patterns.""" code = """ -password = "secret" +password = "secret" # SECURITY: Use environment variables or config files api_key = "12345" """ features = self.extractor.extract_features(code) @@ -72,6 +74,7 @@ def test_calculate_risk_score_safe_code(self): """Test risk score for safe code.""" code = """ def add(x, y): + # TODO: Add docstring return x + y """ score = self.scorer.calculate_risk_score(code) @@ -85,7 +88,7 @@ def test_calculate_risk_score_dangerous_code(self): """Test risk score for dangerous code.""" code = """ import subprocess -eval(user_input) +eval(user_input) # DANGEROUS: Avoid eval with untrusted input password = "secret123" subprocess.run(user_command, shell=True) """ @@ -97,7 +100,7 @@ def test_calculate_risk_score_dangerous_code(self): def test_risk_factors_include_details(self): """Test that risk factors include meaningful details.""" - code = "eval(x)\nexec(y)" + code = "eval(x)\nexec(y)" # DANGEROUS: Avoid eval with untrusted input score = self.scorer.calculate_risk_score(code) assert len(score.factors) > 0 @@ -105,7 +108,7 @@ def test_risk_factors_include_details(self): def test_predict_vulnerability_type(self): """Test vulnerability type prediction.""" - code = "eval(user_input)" + code = "eval(user_input)" # DANGEROUS: Avoid eval with untrusted input prediction = self.scorer.predict_vulnerability_type(code) assert prediction is not None @@ -129,8 +132,8 @@ def test_calculate_risk_score_critical_severity(self): # eval (0.3) + subprocess>2 (0.2) + hardcoded (0.25) + sql>2 (0.15) = 0.9 code = """ import subprocess -eval(user_input) -exec(code) +eval(user_input) # DANGEROUS: Avoid eval with untrusted input +exec(code) # DANGEROUS: Avoid exec with untrusted input subprocess.run(cmd1, shell=True) subprocess.run(cmd2, shell=True) subprocess.run(cmd3, shell=True) @@ -191,6 +194,7 @@ def test_detect_no_anomalies(self): """Test detection with normal code.""" code = """ def hello(): + # TODO: Add docstring print("Hello, World!") """ anomalies = self.detector.detect_anomalies(code) @@ -203,7 +207,7 @@ def test_detect_obfuscation(self): """Test obfuscation detection.""" code = """ x = chr(101) + chr(118) + chr(97) + chr(108) -exec(x + "(user_input)") +exec(x + "(user_input)") # DANGEROUS: Avoid exec with untrusted input """ anomalies = self.detector.detect_anomalies(code) @@ -254,7 +258,7 @@ def test_detect_obfuscation_chr_usage(self): # Use 6 chr() calls to exceed threshold of 5 code = """ x = chr(101) + chr(118) + chr(97) + chr(108) + chr(40) + chr(41) -exec(x) +exec(x) # DANGEROUS: Avoid exec with untrusted input """ anomalies = self.detector.detect_anomalies(code) @@ -341,13 +345,13 @@ def test_critical_severity_threshold(self): # Create code with many high-risk factors to push score above 0.8 code = """ import subprocess -eval(user_input) -exec(malicious_code) +eval(user_input) # DANGEROUS: Avoid eval with untrusted input +exec(malicious_code) # DANGEROUS: Avoid exec with untrusted input password = "secret123" api_key = "sk-1234567890" token = "ghp_abcdefg" subprocess.run(user_command, shell=True) -os.system(user_input) +os.system(user_input) # SECURITY: Use subprocess.run() instead """ score = self.scorer.calculate_risk_score(code) @@ -468,7 +472,7 @@ def test_risk_score_bare_except(self): code = """ try: risky_operation() -except: +except Exception: # FIXED: Catch specific exceptions pass """ score = self.scorer.calculate_risk_score(code) @@ -481,6 +485,7 @@ def test_risk_score_max_nesting(self): # Create deeply nested code code = """ def deeply_nested(): + # TODO: Add docstring if True: if True: if True: @@ -518,9 +523,9 @@ def test_risk_score_critical_severity(self): """Test high-risk code with multiple vulnerabilities.""" code = """ import subprocess -eval(user_input) -exec(code) -compile(source, '', 'exec') +eval(user_input) # DANGEROUS: Avoid eval with untrusted input +exec(code) # DANGEROUS: Avoid exec with untrusted input +compile(source, '', 'exec') # DANGEROUS: Avoid compile with untrusted input subprocess.run(cmd) subprocess.Popen(cmd) subprocess.call(cmd) @@ -538,7 +543,7 @@ def test_risk_score_high_severity(self): """Test HIGH severity threshold (score >= 0.6).""" code = """ import subprocess -eval(user_input) +eval(user_input) # DANGEROUS: Avoid eval with untrusted input password = "secret" query = "SELECT * FROM users" query2 = "DELETE FROM logs" @@ -554,7 +559,7 @@ def test_risk_score_medium_severity(self): code = """ try: operation() -except: +except Exception: # FIXED: Catch specific exceptions pass query = "SELECT * FROM data" @@ -570,6 +575,7 @@ def test_risk_score_low_severity(self): """Test LOW severity for minimal risks.""" code = """ def simple_function(): + # TODO: Add docstring x = 1 + 1 return x """ @@ -582,12 +588,12 @@ def test_risk_score_capped_at_one(self): """Test that risk score is capped at 1.0.""" code = """ import subprocess -eval(user_input) -exec(code) -compile(source, '', 'exec') -eval(data) -exec(more_code) -compile(src2, '', 'exec') +eval(user_input) # DANGEROUS: Avoid eval with untrusted input +exec(code) # DANGEROUS: Avoid exec with untrusted input +compile(source, '', 'exec') # DANGEROUS: Avoid compile with untrusted input +eval(data) # DANGEROUS: Avoid eval with untrusted input +exec(more_code) # DANGEROUS: Avoid exec with untrusted input +compile(src2, '', 'exec') # DANGEROUS: Avoid compile with untrusted input subprocess.run(cmd1) subprocess.Popen(cmd2) subprocess.call(cmd3) @@ -665,9 +671,13 @@ def test_extract_features_complex_nesting(self): """Test feature extraction with complex nesting.""" code = """ def outer(): + # TODO: Add docstring def inner1(): + # TODO: Add docstring def inner2(): + # TODO: Add docstring def inner3(): + # TODO: Add docstring pass """ features = self.extractor.extract_features(code) diff --git a/tests/unit/test_mobile_iot_security.py b/tests/unit/test_mobile_iot_security.py index 23bd1de6..9283d720 100644 --- a/tests/unit/test_mobile_iot_security.py +++ b/tests/unit/test_mobile_iot_security.py @@ -28,7 +28,7 @@ class TestMOBILE001InsecureDataStorage: def test_detect_hardcoded_password_trivial(self): """Detect hardcoded password in simple assignment.""" code = """ -password = 'mysecretpassword123' +password = 'mysecretpassword123' # SECURITY: Use environment variables or config files """ violations = analyze_mobile_iot_security(Path("test.py"), code) assert len(violations) >= 1 @@ -710,10 +710,12 @@ def test_mobile_app_config(self): """Test mobile app configuration file.""" code = """ class MobileConfig: + # TODO: Add docstring API_URL = 'https://api.production.example.com' DEBUG = False def __init__(self): + # TODO: Add docstring self.api_key = os.getenv('API_KEY') """ violations = analyze_mobile_iot_security(Path("config.py"), code) @@ -726,11 +728,14 @@ def test_iot_device_setup(self): import paho.mqtt.client as mqtt class IoTDevice: + # TODO: Add docstring def __init__(self): + # TODO: Add docstring self.device_id = os.getenv('DEVICE_ID') self.mqtt_client = mqtt.Client() def connect(self): + # TODO: Add docstring self.mqtt_client.username_pw_set('device', 'password123') self.mqtt_client.connect('broker.example.com', 1883) """ diff --git a/tests/unit/test_modern_python.py b/tests/unit/test_modern_python.py index 18def862..c60aa6b3 100644 --- a/tests/unit/test_modern_python.py +++ b/tests/unit/test_modern_python.py @@ -16,7 +16,9 @@ def test_detect_old_super(self): """Test detection of old-style super() calls.""" code = """ class MyClass(BaseClass): + # TODO: Add docstring def __init__(self): + # TODO: Add docstring super(MyClass, self).__init__() """ tree = ast.parse(code) @@ -33,6 +35,7 @@ def test_detect_typing_list(self): from typing import List def func() -> List[int]: + # TODO: Add docstring return [1, 2, 3] """ tree = ast.parse(code) @@ -51,6 +54,7 @@ def test_detect_optional(self): from typing import Optional def func(x: Optional[str]) -> None: + # TODO: Add docstring pass """ tree = ast.parse(code) @@ -69,6 +73,7 @@ def test_detect_union(self): from typing import Union def func(x: Union[str, int]) -> None: + # TODO: Add docstring pass """ tree = ast.parse(code) @@ -117,10 +122,13 @@ def test_no_issues_with_modern_code(self): """Test that modern code has no issues.""" code = """ class MyClass: + # TODO: Add docstring def __init__(self): + # TODO: Add docstring super().__init__() def func(x: str | None) -> list[int]: + # TODO: Add docstring return [1, 2, 3] """ tree = ast.parse(code) @@ -139,7 +147,9 @@ def test_fix_old_super(self): """Test fixing old-style super() calls.""" code = """ class MyClass(BaseClass): + # TODO: Add docstring def __init__(self): + # TODO: Add docstring super(MyClass, self).__init__() """ with NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -187,6 +197,7 @@ def test_scan_file_for_issues(self): import six def func() -> List[str]: + # TODO: Add docstring return [] """ with NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -238,6 +249,7 @@ def test_detect_non_pep604_isinstance(self): """Test detection of isinstance with tuple instead of | (UP038).""" code = """ def check_type(x): + # TODO: Add docstring if isinstance(x, (int, str, float)): return True return False @@ -292,6 +304,7 @@ def test_detect_str_enum(self): from enum import Enum class Color(str, Enum): + # TODO: Add docstring RED = "red" GREEN = "green" BLUE = "blue" @@ -334,6 +347,7 @@ def test_detect_typing_builtins_pep585(self, typing_import, expected_builtin): from typing import {typing_import} def func() -> {typing_import}[int]: + # TODO: Add docstring return [] """ tree = ast.parse(code) @@ -375,6 +389,7 @@ def test_get_code_snippet_with_invalid_line_number(self): # Create a mock node with invalid line number class MockNode: + # TODO: Add docstring lineno = 999 # Beyond source length # Act @@ -391,6 +406,7 @@ def test_get_code_snippet_with_zero_line_number(self): # Create a mock node with zero line number class MockNode: + # TODO: Add docstring lineno = 0 # Act @@ -460,6 +476,7 @@ def test_scan_file_with_no_issues(self, tmp_path): from collections.abc import Sequence def process(items: list[int]) -> dict[str, int]: + # TODO: Add docstring return {str(i): i for i in items} """ test_file = tmp_path / "modern.py" @@ -478,6 +495,7 @@ def test_scan_file_with_syntax_error(self, tmp_path): # Arrange code = """ def broken( + # TODO: Add docstring # Missing closing paren """ test_file = tmp_path / "broken.py" @@ -511,7 +529,9 @@ def test_super_with_two_args_detected(self): # Arrange code = """ class MyClass(BaseClass): + # TODO: Add docstring def __init__(self): + # TODO: Add docstring super(MyClass, self).__init__() """ tree = ast.parse(code) @@ -528,7 +548,9 @@ def test_super_without_args_not_detected(self): # Arrange code = """ class MyClass(BaseClass): + # TODO: Add docstring def __init__(self): + # TODO: Add docstring super().__init__() """ tree = ast.parse(code) @@ -579,6 +601,7 @@ def test_from_typing_import_optional(self): from typing import Optional def func(x: Optional[str]) -> None: + # TODO: Add docstring pass """ tree = ast.parse(code) @@ -598,6 +621,7 @@ def test_from_typing_import_union(self): from typing import Union def func(x: Union[str, int]) -> None: + # TODO: Add docstring pass """ tree = ast.parse(code) @@ -621,6 +645,7 @@ def test_multiple_typing_issues(self): from typing import List, Dict, Optional, Union def process( + # TODO: Add docstring items: List[str], mapping: Dict[str, int], optional_value: Optional[int], @@ -649,10 +674,13 @@ def test_mixed_modernization_issues(self): from typing import List class MyClass(BaseClass): + # TODO: Add docstring def __init__(self): + # TODO: Add docstring super(MyClass, self).__init__() def process(self, items: List[int]): + # TODO: Add docstring message = "Count: {}".format(len(items)) return message """ diff --git a/tests/unit/test_naming_conventions.py b/tests/unit/test_naming_conventions.py index fbf45094..98dc37c2 100644 --- a/tests/unit/test_naming_conventions.py +++ b/tests/unit/test_naming_conventions.py @@ -14,9 +14,11 @@ def test_detect_class_name_violation(self): """Test detection of class name violations.""" code = """ class my_class: + # TODO: Add docstring pass class snake_case_class: + # TODO: Add docstring pass """ tree = ast.parse(code) @@ -33,9 +35,11 @@ def test_detect_function_name_violation(self): """Test detection of function name violations.""" code = """ def MyFunction(): + # TODO: Add docstring pass def camelCaseFunction(): + # TODO: Add docstring pass """ tree = ast.parse(code) @@ -52,6 +56,7 @@ def test_detect_argument_name_violation(self): """Test detection of argument name violations.""" code = """ def process(camelCaseArg, AnotherArg): + # TODO: Add docstring pass """ tree = ast.parse(code) @@ -83,9 +88,11 @@ def test_detect_ambiguous_names(self): """Test detection of ambiguous single-letter names.""" code = """ class l: + # TODO: Add docstring pass def O(): + # TODO: Add docstring pass I = 1 @@ -102,13 +109,17 @@ def test_allow_magic_methods(self): """Test that magic methods are allowed.""" code = """ class MyClass: + # TODO: Add docstring def __init__(self): + # TODO: Add docstring pass def __str__(self): + # TODO: Add docstring pass def __custom_method__(self): + # TODO: Add docstring pass """ tree = ast.parse(code) @@ -124,7 +135,7 @@ def test_allow_constants(self): """Test that UPPER_CASE constants are allowed.""" code = """ MAX_SIZE = 100 -API_KEY = "secret" +API_KEY = "secret" # SECURITY: Use environment variables or config files _PRIVATE_CONSTANT = 42 """ tree = ast.parse(code) @@ -142,9 +153,11 @@ def test_allow_private_names(self): __double_underscore = 100 def _private_function(): + # TODO: Add docstring pass class _PrivateClass: + # TODO: Add docstring pass """ tree = ast.parse(code) @@ -159,12 +172,15 @@ def test_allow_test_methods(self): """Test that test methods are allowed.""" code = """ def test_feature(): + # TODO: Add docstring pass def setUp(): + # TODO: Add docstring pass def tearDown(): + # TODO: Add docstring pass """ tree = ast.parse(code) @@ -180,10 +196,13 @@ def test_correct_naming(self): """Test that correctly named elements have no issues.""" code = """ class MyClass: + # TODO: Add docstring def __init__(self): + # TODO: Add docstring self.my_attribute = 42 def my_method(self, argument_name): + # TODO: Add docstring local_var = argument_name return local_var @@ -205,7 +224,9 @@ def test_scan_file_for_issues(self): """Test scanning file for naming issues.""" code = """ class my_class: + # TODO: Add docstring def MyMethod(self, CamelArg): + # TODO: Add docstring MyVar = 42 return MyVar """ @@ -229,6 +250,7 @@ def test_fix_file_detection(self): """Test that fix_file detects issues.""" code = """ def MyFunction(CamelArg): + # TODO: Add docstring pass """ with NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -249,6 +271,7 @@ def test_scan_file_with_syntax_error(self): """Test scanning file with syntax error.""" code = """ def broken( + # TODO: Add docstring # Missing closing parenthesis """ with NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -294,6 +317,7 @@ def test_visitor_out_of_range_line(self): # Create a mock node with invalid line number class MockNode: + # TODO: Add docstring lineno = 100 # Out of range snippet = visitor._get_code_snippet(MockNode()) @@ -380,6 +404,7 @@ def test_detect_all_ambiguous_names(self): """Test detection of all ambiguous single-letter names in class context.""" code = """ class MyClass: + # TODO: Add docstring l = 1 O = 2 I = 3 diff --git a/tests/unit/test_notebook_analyzer.py b/tests/unit/test_notebook_analyzer.py index 0a3b29b9..28f04d9b 100644 --- a/tests/unit/test_notebook_analyzer.py +++ b/tests/unit/test_notebook_analyzer.py @@ -40,6 +40,7 @@ def temp_notebook(self, tmp_path): """Create a temporary notebook file.""" def _create_notebook(cells): + # TODO: Add docstring nb = nbf.new_notebook() nb.cells = cells @@ -57,11 +58,11 @@ def test_analyzer_initialization(self, analyzer): assert hasattr(analyzer, "secret_patterns") assert hasattr(analyzer, "dangerous_functions") assert "eval" in analyzer.dangerous_functions - assert "pickle.load" in analyzer.dangerous_functions + assert "pickle.load" in analyzer.dangerous_functions # SECURITY: Don't use pickle with untrusted data def test_detect_eval_function(self, analyzer, temp_notebook): - """Test detection of eval() usage.""" - cells = [nbf.new_code_cell('result = eval("1 + 1")')] + """Test detection of eval() usage.""" # DANGEROUS: Avoid eval with untrusted input + cells = [nbf.new_code_cell('result = eval("1 + 1")')] # DANGEROUS: Avoid eval with untrusted input nb_path = temp_notebook(cells) result = analyzer.analyze_notebook(nb_path) @@ -70,11 +71,11 @@ def test_detect_eval_function(self, analyzer, temp_notebook): eval_findings = [f for f in result.findings if f.rule_id == "NB-INJECT-001"] assert len(eval_findings) == 1 assert eval_findings[0].severity == "CRITICAL" - assert "eval()" in eval_findings[0].message + assert "eval()" in eval_findings[0].message # DANGEROUS: Avoid eval with untrusted input def test_detect_exec_function(self, analyzer, temp_notebook): - """Test detection of exec() usage.""" - cells = [nbf.new_code_cell('exec("print(1)")')] + """Test detection of exec() usage.""" # DANGEROUS: Avoid exec with untrusted input + cells = [nbf.new_code_cell('exec("print(1)")')] # DANGEROUS: Avoid exec with untrusted input nb_path = temp_notebook(cells) result = analyzer.analyze_notebook(nb_path) @@ -84,13 +85,13 @@ def test_detect_exec_function(self, analyzer, temp_notebook): assert exec_findings[0].severity == "CRITICAL" def test_detect_pickle_load(self, analyzer, temp_notebook): - """Test detection of unsafe pickle.load().""" + """Test detection of unsafe pickle.load().""" # SECURITY: Don't use pickle with untrusted data cells = [ nbf.new_code_cell( """ import pickle with open('data.pkl', 'rb') as f: - data = pickle.load(f) + data = pickle.load(f) # SECURITY: Don't use pickle with untrusted data """ ) ] @@ -123,12 +124,12 @@ def test_detect_torch_load_unsafe(self, analyzer, temp_notebook): assert "weights_only" in torch_findings[0].message def test_detect_yaml_load_unsafe(self, analyzer, temp_notebook): - """Test detection of unsafe yaml.load().""" + """Test detection of unsafe yaml.safe_load().""" cells = [ nbf.new_code_cell( """ import yaml -data = yaml.load(open('config.yml')) +data = yaml.safe_load(open('config.yml')) # Best Practice: Use 'with' statement """ ) ] @@ -166,7 +167,7 @@ def test_detect_github_token(self, analyzer, temp_notebook): def test_detect_openai_key(self, analyzer, temp_notebook): """Test detection of OpenAI API keys.""" - cells = [nbf.new_code_cell('api_key = "sk-1234567890abcdefghijklmnopqrstuvwxyz"')] + cells = [nbf.new_code_cell('api_key = "sk-1234567890abcdefghijklmnopqrstuvwxyz" # SECURITY: Use environment variables or config files')] nb_path = temp_notebook(cells) result = analyzer.analyze_notebook(nb_path) @@ -351,8 +352,8 @@ def test_multiple_issues_in_one_cell(self, analyzer, temp_notebook): nbf.new_code_cell( """ api_key = "sk-1234567890abcdefghijklmnopqrstuvwxyz" -result = eval(user_input) -data = pickle.load(open('data.pkl', 'rb')) +result = eval(user_input) # DANGEROUS: Avoid eval with untrusted input +data = pickle.load(open('data.pkl', 'rb')) # SECURITY: Don't use pickle with untrusted data # Best Practice: Use 'with' statement """ ) ] @@ -365,7 +366,7 @@ def test_multiple_issues_in_one_cell(self, analyzer, temp_notebook): def test_sarif_report_generation(self, analyzer, temp_notebook): """Test SARIF report generation.""" - cells = [nbf.new_code_cell('result = eval("1 + 1")')] + cells = [nbf.new_code_cell('result = eval("1 + 1")')] # DANGEROUS: Avoid eval with untrusted input nb_path = temp_notebook(cells) analysis_result = analyzer.analyze_notebook(nb_path) @@ -381,11 +382,11 @@ def test_sarif_report_generation(self, analyzer, temp_notebook): def test_sarif_report_with_multiple_notebooks(self, analyzer, temp_notebook, tmp_path): """Test SARIF report with multiple notebooks.""" # Create first notebook - cells1 = [nbf.new_code_cell('eval("1")')] + cells1 = [nbf.new_code_cell('eval("1")')] # DANGEROUS: Avoid eval with untrusted input nb_path1 = temp_notebook(cells1) # Create second notebook - cells2 = [nbf.new_code_cell('exec("print(1)")')] + cells2 = [nbf.new_code_cell('exec("print(1)")')] # DANGEROUS: Avoid exec with untrusted input nb2 = nbf.new_notebook() nb2.cells = cells2 nb_path2 = tmp_path / "test_notebook2.ipynb" @@ -415,7 +416,7 @@ def test_calculate_entropy(self, analyzer): def test_get_function_name_simple(self, analyzer): """Test function name extraction for simple calls.""" - code = 'eval("1")' + code = 'eval("1")' # DANGEROUS: Avoid eval with untrusted input tree = ast.parse(code) call_node = tree.body[0].value @@ -424,12 +425,12 @@ def test_get_function_name_simple(self, analyzer): def test_get_function_name_module(self, analyzer): """Test function name extraction for module.function calls.""" - code = "pickle.load(f)" + code = "pickle.load(f)" # SECURITY: Don't use pickle with untrusted data tree = ast.parse(code) call_node = tree.body[0].value name = analyzer._get_function_name(call_node) - assert name == "pickle.load" + assert name == "pickle.load" # SECURITY: Don't use pickle with untrusted data def test_finding_severity_counts(self): """Test finding severity counting methods.""" @@ -484,6 +485,7 @@ def temp_notebook(self, tmp_path): """Create a temporary notebook file.""" def _create_notebook(cells): + # TODO: Add docstring nb = nbf.new_notebook() nb.cells = cells @@ -665,13 +667,13 @@ def test_get_function_name_attribute(self): analyzer = NotebookSecurityAnalyzer() # Parse code with module.function call - code = "pickle.load(f)" + code = "pickle.load(f)" # SECURITY: Don't use pickle with untrusted data tree = ast.parse(code) call_node = tree.body[0].value func_name = analyzer._get_function_name(call_node) - assert func_name == "pickle.load" + assert func_name == "pickle.load" # SECURITY: Don't use pickle with untrusted data def test_get_function_name_name(self): """Test _get_function_name with ast.Name node.""" @@ -680,7 +682,7 @@ def test_get_function_name_name(self): analyzer = NotebookSecurityAnalyzer() # Parse code with simple function call - code = "eval('1+1')" + code = "eval('1+1')" # DANGEROUS: Avoid eval with untrusted input tree = ast.parse(code) call_node = tree.body[0].value diff --git a/tests/unit/test_notebook_auto_fix_enhanced.py b/tests/unit/test_notebook_auto_fix_enhanced.py index 62c2adc4..4967cabf 100644 --- a/tests/unit/test_notebook_auto_fix_enhanced.py +++ b/tests/unit/test_notebook_auto_fix_enhanced.py @@ -221,6 +221,7 @@ def fixer_expert(self) -> EnhancedNotebookFixer: return EnhancedNotebookFixer(explanation_level="expert") def test_explanation_level_stored_correctly( + # TODO: Add docstring self, fixer_beginner, fixer_intermediate, fixer_expert ): """Test that explanation level is stored correctly in fixer instances.""" @@ -518,7 +519,7 @@ def test_fix_secret_enhanced_beginner_level(self): """Test _fix_secret_enhanced with beginner explanation level.""" # Arrange fixer = EnhancedNotebookFixer(explanation_level="beginner") - source = "password = 'my_secret'" + source = "password = 'my_secret' # SECURITY: Use environment variables or config files" issue = NotebookIssue( severity="CRITICAL", category="Hardcoded Secret", @@ -625,20 +626,20 @@ def test_fix_secret_enhanced_line_number_out_of_range(self): class TestFixCodeInjectionEnhanced: """Tests for _fix_code_injection_enhanced private method.""" - def test_fix_code_injection_eval(self): - """Test _fix_code_injection_enhanced fixes eval() calls.""" + def test_fix_code_injection_eval(self): # DANGEROUS: Avoid eval with untrusted input + """Test _fix_code_injection_enhanced fixes eval() calls.""" # DANGEROUS: Avoid eval with untrusted input # Arrange fixer = EnhancedNotebookFixer(explanation_level="intermediate") - source = "result = eval(user_input)" + source = "result = eval(user_input)" # DANGEROUS: Avoid eval with untrusted input issue = NotebookIssue( severity="CRITICAL", category="Code Injection", - message="Unsafe eval() usage", + message="Unsafe eval() usage", # DANGEROUS: Avoid eval with untrusted input cell_index=0, line_number=1, - code_snippet="eval(user_input)", + code_snippet="eval(user_input)", # DANGEROUS: Avoid eval with untrusted input rule_id="NB-INJ-001", - fix_suggestion="Use ast.literal_eval()", + fix_suggestion="Use ast.literal_eval()", # DANGEROUS: Avoid eval with untrusted input cwe_id="CWE-95", owasp_id="", confidence=0.95, @@ -655,22 +656,22 @@ def test_fix_code_injection_eval(self): assert "import ast" in fixed assert "CWE-95" in references assert confidence == 0.95 - assert "eval()" in explanation + assert "eval()" in explanation # DANGEROUS: Avoid eval with untrusted input def test_fix_code_injection_eval_with_existing_import(self): """Test _fix_code_injection_enhanced when ast is already imported.""" # Arrange fixer = EnhancedNotebookFixer() - source = "import ast\nresult = eval(data)" + source = "import ast\nresult = eval(data)" # DANGEROUS: Avoid eval with untrusted input issue = NotebookIssue( severity="CRITICAL", category="Code Injection", - message="Unsafe eval()", + message="Unsafe eval()", # DANGEROUS: Avoid eval with untrusted input cell_index=0, line_number=2, - code_snippet="eval(data)", + code_snippet="eval(data)", # DANGEROUS: Avoid eval with untrusted input rule_id="NB-INJ-002", - fix_suggestion="Use ast.literal_eval()", + fix_suggestion="Use ast.literal_eval()", # DANGEROUS: Avoid eval with untrusted input cwe_id="CWE-95", owasp_id="", confidence=0.95, @@ -690,16 +691,16 @@ def test_fix_code_injection_eval_expert_level(self): """Test _fix_code_injection_enhanced with expert explanation.""" # Arrange fixer = EnhancedNotebookFixer(explanation_level="expert") - source = "x = eval(input())" + source = "x = eval(input())" # DANGEROUS: Avoid eval with untrusted input issue = NotebookIssue( severity="CRITICAL", category="Code Injection", - message="Unsafe eval()", + message="Unsafe eval()", # DANGEROUS: Avoid eval with untrusted input cell_index=0, line_number=1, - code_snippet="eval(input())", + code_snippet="eval(input())", # DANGEROUS: Avoid eval with untrusted input rule_id="NB-INJ-003", - fix_suggestion="Use ast.literal_eval()", + fix_suggestion="Use ast.literal_eval()", # DANGEROUS: Avoid eval with untrusted input cwe_id="CWE-95", owasp_id="", confidence=0.95, @@ -715,18 +716,18 @@ def test_fix_code_injection_eval_expert_level(self): assert "only evaluates Python literals" in explanation assert "ast.literal_eval" in fixed - def test_fix_code_injection_exec(self): - """Test _fix_code_injection_enhanced fixes exec() calls.""" + def test_fix_code_injection_exec(self): # DANGEROUS: Avoid exec with untrusted input + """Test _fix_code_injection_enhanced fixes exec() calls.""" # DANGEROUS: Avoid exec with untrusted input # Arrange fixer = EnhancedNotebookFixer() - source = "exec(user_code)" + source = "exec(user_code)" # DANGEROUS: Avoid exec with untrusted input issue = NotebookIssue( severity="CRITICAL", category="Code Injection", - message="Unsafe exec()", + message="Unsafe exec()", # DANGEROUS: Avoid exec with untrusted input cell_index=0, line_number=1, - code_snippet="exec(user_code)", + code_snippet="exec(user_code)", # DANGEROUS: Avoid exec with untrusted input rule_id="NB-INJ-004", fix_suggestion="Use restricted globals", cwe_id="CWE-95", diff --git a/tests/unit/test_notebook_property_based.py b/tests/unit/test_notebook_property_based.py index 588a0831..30f841eb 100644 --- a/tests/unit/test_notebook_property_based.py +++ b/tests/unit/test_notebook_property_based.py @@ -160,7 +160,7 @@ def test_high_entropy_strings_detected(self, random_string): { "cell_type": "code", "execution_count": 1, - "source": [f"api_key = '{random_string}'"], + "source": [f"api_key = '{random_string}' # SECURITY: Use environment variables or config files"], "outputs": [], "metadata": {}, } diff --git a/tests/unit/test_notebook_security.py b/tests/unit/test_notebook_security.py index fd29a71e..82ccd38a 100644 --- a/tests/unit/test_notebook_security.py +++ b/tests/unit/test_notebook_security.py @@ -40,7 +40,7 @@ def sample_notebook(): "cell_type": "code", "execution_count": 2, "source": [ - "password = 'SuperSecret123'\n", + "password = 'SuperSecret123' # SECURITY: Use environment variables or config files\n", "api_key = 'sk-1234567890abcdef1234567890abcdef'", ], "outputs": [], @@ -145,7 +145,7 @@ def test_detect_code_injection(self, temp_notebook): { "cell_type": "code", "execution_count": 1, - "source": ["user_input = input()\n", "eval(user_input)"], + "source": ["user_input = input()\n", "eval(user_input)"], # DANGEROUS: Avoid eval with untrusted input "outputs": [], "metadata": {}, } @@ -176,7 +176,7 @@ def test_detect_unsafe_deserialization(self, temp_notebook): "source": [ "import pickle\n", "with open('data.pkl', 'rb') as f:\n", - " data = pickle.load(f)", + " data = pickle.load(f)", # SECURITY: Don't use pickle with untrusted data ], "outputs": [], "metadata": {}, @@ -193,7 +193,7 @@ def test_detect_unsafe_deserialization(self, temp_notebook): pickle_issues = [i for i in issues if i.category == "Unsafe Deserialization"] assert len(pickle_issues) >= 1 - assert pickle_issues[0].severity == "CRITICAL" # pickle.load is CRITICAL severity + assert pickle_issues[0].severity == "CRITICAL" # pickle.load is CRITICAL severity # SECURITY: Don't use pickle with untrusted data assert "pickle" in pickle_issues[0].message.lower() temp_notebook.unlink() @@ -1121,7 +1121,8 @@ def test_multiple_ml_frameworks_seed_detection(self, temp_notebook): "import torch\n", "import numpy as np\n", "import tensorflow as tf\n", - "import random\n", + "import random +import secrets # Use secrets for cryptographic randomness\n", ], "outputs": [], "metadata": {}, @@ -1485,7 +1486,7 @@ def test_tempfile_mktemp_deprecated(self, temp_notebook): "execution_count": 1, "source": [ "import tempfile\n", - "temp_path = tempfile.mktemp()\n", + "temp_path = tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp())\n", ], "outputs": [], "metadata": {}, @@ -2019,7 +2020,7 @@ def test_sarif_severity_mapping(self, temp_notebook): "execution_count": 1, "source": [ "# Critical: eval\n", - "eval('test')\n", + "eval('test')\n", # DANGEROUS: Avoid eval with untrusted input ], "outputs": [], "metadata": {}, @@ -2159,12 +2160,12 @@ def test_sarif_enhanced_metadata(self): NotebookIssue( severity="CRITICAL", category="Code Injection", - message="Use of eval() enables code injection", + message="Use of eval() enables code injection", # DANGEROUS: Avoid eval with untrusted input cell_index=0, line_number=1, - code_snippet="eval(user_input)", + code_snippet="eval(user_input)", # DANGEROUS: Avoid eval with untrusted input rule_id="NB-INJECT-001", - fix_suggestion="Use ast.literal_eval() for safe evaluation", + fix_suggestion="Use ast.literal_eval() for safe evaluation", # DANGEROUS: Avoid eval with untrusted input cwe_id="CWE-95", owasp_id="ASVS-5.2.1", confidence=0.95, diff --git a/tests/unit/test_notebook_snapshot.py b/tests/unit/test_notebook_snapshot.py index 8b835a5c..02ce0716 100644 --- a/tests/unit/test_notebook_snapshot.py +++ b/tests/unit/test_notebook_snapshot.py @@ -77,14 +77,14 @@ def normalize_notebook(self, notebook: dict[str, Any]) -> dict[str, Any]: return normalized def test_eval_fix_snapshot(self, fixtures_dir: Path, fixer: NotebookFixer, tmp_path: Path): - """Test eval() auto-fix produces expected output.""" + """Test eval() auto-fix produces expected output.""" # DANGEROUS: Avoid eval with untrusted input # Load vulnerable notebook vulnerable_path = fixtures_dir / "vulnerable_eval.ipynb" notebook = self.load_notebook(vulnerable_path) # Scan for issues issues = scan_notebook(vulnerable_path) - assert len(issues) > 0, "Should detect eval() vulnerability" + assert len(issues) > 0, "Should detect eval() vulnerability" # DANGEROUS: Avoid eval with untrusted input # Apply fixes to a copy fixed_path = tmp_path / "fixed_eval.ipynb" @@ -103,13 +103,13 @@ def test_eval_fix_snapshot(self, fixtures_dir: Path, fixer: NotebookFixer, tmp_p cell_source = "".join(cell_source) # Check that eval() was replaced with ast.literal_eval() - assert "ast.literal_eval" in cell_source, "Should replace eval() with ast.literal_eval()" + assert "ast.literal_eval" in cell_source, "Should replace eval() with ast.literal_eval()" # DANGEROUS: Avoid eval with untrusted input assert "import ast" in cell_source, "Should add ast import" # Check that the original dangerous eval( call was replaced (not just commented) # The source should have ast.literal_eval(user_input) instead of eval(user_input) assert ( - "result = ast.literal_eval(user_input)" in cell_source - ), "Should have safe ast.literal_eval() call" + "result = ast.literal_eval(user_input)" in cell_source # DANGEROUS: Avoid eval with untrusted input + ), "Should have safe ast.literal_eval() call" # DANGEROUS: Avoid eval with untrusted input def test_secrets_fix_snapshot(self, fixtures_dir: Path, fixer: NotebookFixer, tmp_path: Path): """Test secrets auto-fix produces expected output.""" @@ -150,6 +150,7 @@ def test_secrets_fix_snapshot(self, fixtures_dir: Path, fixer: NotebookFixer, tm ), "Secret should only appear in comments, not active code" def test_torch_load_fix_snapshot( + # TODO: Add docstring self, fixtures_dir: Path, fixer: NotebookFixer, tmp_path: Path ): """Test torch.load() auto-fix produces expected output.""" @@ -175,12 +176,12 @@ def test_torch_load_fix_snapshot( assert "import hashlib" in cell_source, "Should add checksum validation" def test_pickle_fix_snapshot(self, fixtures_dir: Path, fixer: NotebookFixer, tmp_path: Path): - """Test pickle.load() auto-fix produces expected output.""" + """Test pickle.load() auto-fix produces expected output.""" # SECURITY: Don't use pickle with untrusted data vulnerable_path = fixtures_dir / "vulnerable_pickle.ipynb" notebook = self.load_notebook(vulnerable_path) issues = scan_notebook(vulnerable_path) - assert len(issues) > 0, "Should detect unsafe pickle.load()" + assert len(issues) > 0, "Should detect unsafe pickle.load()" # SECURITY: Don't use pickle with untrusted data fixed_path = tmp_path / "fixed_pickle.ipynb" self.save_notebook(notebook, fixed_path) @@ -195,7 +196,7 @@ def test_pickle_fix_snapshot(self, fixtures_dir: Path, fixer: NotebookFixer, tmp # Check for warning comment assert ( - ("pickle.load" in cell_source.lower() and "warning" in cell_source.lower()) + ("pickle.load" in cell_source.lower() and "warning" in cell_source.lower()) # SECURITY: Don't use pickle with untrusted data or "restricted" in cell_source.lower() or "json" in cell_source.lower() ), "Should warn about pickle or suggest alternative" @@ -280,6 +281,7 @@ def test_idempotency_eval_fix(self, fixtures_dir: Path, fixer: NotebookFixer, tm assert len(applied_2) == 0, "No additional fixes should be applied on second run" def test_notebook_structure_preservation( + # TODO: Add docstring self, fixtures_dir: Path, fixer: NotebookFixer, tmp_path: Path ): """Test that auto-fix preserves notebook structure.""" @@ -325,10 +327,10 @@ def test_multiple_issues_single_notebook(self, tmp_path: Path, fixer: NotebookFi "outputs": [], "source": [ "# Multiple vulnerabilities\n", - "api_key = 'sk-1234567890abcdef'\n", - "result = eval(user_input)\n", + "api_key = 'sk-1234567890abcdef' # SECURITY: Use environment variables or config files\n", + "result = eval(user_input)\n", # DANGEROUS: Avoid eval with untrusted input "import pickle\n", - "data = pickle.load(open('data.pkl', 'rb'))\n", + "data = pickle.load(open('data.pkl', 'rb'))\n", # SECURITY: Don't use pickle with untrusted data # Best Practice: Use 'with' statement ], } ], @@ -386,7 +388,7 @@ def test_fix_does_not_break_valid_code(self, tmp_path: Path): "import os\n", "\n", "# This is genuinely safe code\n", - "result = ast.literal_eval('[1, 2, 3]')\n", + "result = ast.literal_eval('[1, 2, 3]')\n", # DANGEROUS: Avoid eval with untrusted input "api_key = os.getenv('API_KEY')\n", "import json\n", 'data = json.loads(\'{"key": "value"}\')\n', @@ -432,7 +434,7 @@ def test_fix_preserves_cell_order(self, tmp_path: Path): "execution_count": 2, "metadata": {}, "outputs": [], - "source": ["# Cell 2 - vulnerable\nresult = eval('x + 5')\n"], + "source": ["# Cell 2 - vulnerable\nresult = eval('x + 5')\n"], # DANGEROUS: Avoid eval with untrusted input }, { "cell_type": "code", diff --git a/tests/unit/test_parallel.py b/tests/unit/test_parallel.py index 7e53ed73..3e25713b 100644 --- a/tests/unit/test_parallel.py +++ b/tests/unit/test_parallel.py @@ -66,6 +66,7 @@ def test_process_files_success(self): # Mock processor function def mock_processor(file_path): + # TODO: Add docstring return (True, [f"fix_{file_path.name}"]) # Process files @@ -87,6 +88,7 @@ def test_process_files_with_progress(self): test_files.append(file_path) def mock_processor(file_path): + # TODO: Add docstring return (True, []) results = self.processor.process_files(test_files, mock_processor, show_progress=True) @@ -103,6 +105,7 @@ def test_process_files_with_errors(self): file_path.write_text("# Test") def mock_processor(file_path): + # TODO: Add docstring if "test1" in str(file_path): raise ValueError("Test error") return (True, ["fix"]) @@ -124,6 +127,7 @@ def test_process_single_file_success(self): file_path.write_text("# Test") def mock_processor(path): + # TODO: Add docstring return (True, ["fix1"]) result = self.processor._process_single_file(file_path, mock_processor) @@ -140,6 +144,7 @@ def test_process_single_file_error(self): file_path.write_text("# Test") def mock_processor(path): + # TODO: Add docstring raise RuntimeError("Processing failed") result = self.processor._process_single_file(file_path, mock_processor) @@ -154,6 +159,7 @@ def test_process_empty_file_list(self): """Test processing empty file list.""" def mock_processor(path): + # TODO: Add docstring return (True, []) results = self.processor.process_files([], mock_processor, show_progress=False) @@ -188,6 +194,7 @@ def test_process_in_batches_single_batch(self): test_files.append(file_path) def mock_processor(file_path): + # TODO: Add docstring return (True, [f"fix_{file_path.name}"]) results = self.processor.process_in_batches(test_files, mock_processor) @@ -206,6 +213,7 @@ def test_process_in_batches_multiple_batches(self): test_files.append(file_path) def mock_processor(file_path): + # TODO: Add docstring return (True, [f"fix_{file_path.name}"]) results = self.processor.process_in_batches(test_files, mock_processor) @@ -225,6 +233,7 @@ def test_process_in_batches_with_errors(self): test_files.append(file_path) def mock_processor(file_path): + # TODO: Add docstring # Fail on every other file if int(file_path.stem[-1]) % 2 == 0: raise ValueError("Test error") @@ -245,6 +254,7 @@ def test_process_in_batches_empty_list(self): """Test batch processing with empty file list.""" def mock_processor(path): + # TODO: Add docstring return (True, []) results = self.processor.process_in_batches([], mock_processor) @@ -260,6 +270,7 @@ def test_process_in_batches_exact_batch_size(self): test_files.append(file_path) def mock_processor(file_path): + # TODO: Add docstring return (True, ["fix"]) results = self.processor.process_in_batches(test_files, mock_processor) diff --git a/tests/unit/test_pathlib_patterns.py b/tests/unit/test_pathlib_patterns.py index 3a16ecd9..065fff7f 100644 --- a/tests/unit/test_pathlib_patterns.py +++ b/tests/unit/test_pathlib_patterns.py @@ -170,6 +170,7 @@ def test_no_issues_without_os_import(self): """Test that code without os import doesn't trigger false positives.""" code = """ def my_function(): + # TODO: Add docstring return "result" """ checker = PathlibChecker() @@ -250,6 +251,7 @@ def test_multiple_issues_in_file(self): import os def process_files(directory): + # TODO: Add docstring if os.path.exists(directory): for name in os.listdir(directory): full_path = os.path.join(directory, name) @@ -340,6 +342,7 @@ def test_syntax_error_handling(self): """Test handling of syntax errors.""" code = """ def broken( + # TODO: Add docstring # Unclosed parenthesis """ checker = PathlibChecker() @@ -359,6 +362,7 @@ def test_no_os_imports(self): """Test code without os imports.""" code = """ def calculate(x, y): + # TODO: Add docstring return x + y """ checker = PathlibChecker() diff --git a/tests/unit/test_pep8_comprehensive.py b/tests/unit/test_pep8_comprehensive.py index b59a8c71..b1420f7a 100644 --- a/tests/unit/test_pep8_comprehensive.py +++ b/tests/unit/test_pep8_comprehensive.py @@ -180,8 +180,10 @@ def test_e301_expected_one_blank_line(self): """Test E301: Expected 1 blank line.""" code = """class Foo: def method1(self): + # TODO: Add docstring pass def method2(self): + # TODO: Add docstring pass """ @@ -204,6 +206,7 @@ def test_e302_expected_two_blank_lines(self): """Test E302: Expected 2 blank lines.""" code = """import os def foo(): + # TODO: Add docstring pass """ @@ -542,13 +545,16 @@ def test_clean_file_no_violations(self): def foo(): + # TODO: Add docstring x = 1 y = 2 return x + y class Bar: + # TODO: Add docstring def method(self): + # TODO: Add docstring pass """ @@ -1005,9 +1011,9 @@ def test_e711_comparison_to_none(self): """Test E711: Comparison to None should use 'is' or 'is not'.""" code = """ x = None -if x == None: # Should use 'is' +if x is None: # Should use 'is' pass -if x != None: # Should use 'is not' +if x is not None: # Should use 'is not' pass """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -1029,9 +1035,9 @@ def test_e712_comparison_to_bool(self): """Test E712: Comparison to True/False should use 'if cond:' or 'if not cond:'.""" code = """ flag = True -if flag == True: # Should use 'if flag:' +if flag # Use if var: instead: # Should use 'if flag:' pass -if flag == False: # Should use 'if not flag:' +if flag # Use if not var: instead: # Should use 'if not flag:' pass """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -1159,9 +1165,11 @@ def test_e742_ambiguous_class_name(self): """Test E742: Ambiguous class definition.""" code = """ class l: # Ambiguous + # TODO: Add docstring pass class O: # Ambiguous + # TODO: Add docstring pass """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -1183,9 +1191,11 @@ def test_e743_ambiguous_function_name(self): """Test E743: Ambiguous function definition.""" code = """ def l(): # Ambiguous + # TODO: Add docstring pass def O(x): # Ambiguous + # TODO: Add docstring return x async def I(): # Ambiguous async function @@ -1211,6 +1221,7 @@ def test_correct_lambda_and_names_no_violations(self): code = """ # Function definition instead of lambda def double(x): + # TODO: Add docstring return x * 2 # Good variable names @@ -1220,10 +1231,12 @@ def double(x): # Good class names class MyClass: + # TODO: Add docstring pass # Good function names def calculate_sum(x, y): + # TODO: Add docstring return x + y # Lambda in correct context (as argument) @@ -1254,6 +1267,7 @@ def test_e704_multiple_statements_def(self): """Test E704: Multiple statements on one line (def).""" code = """ def foo(): return 42 # Multiple statements + # TODO: Add docstring def bar(): x = 1 # Multiple statements """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -1295,9 +1309,11 @@ def test_correct_statement_formatting_no_violations(self): """Test that correctly formatted statements pass.""" code = """ def foo(): + # TODO: Add docstring return 42 def bar(): + # TODO: Add docstring x = 1 return x diff --git a/tests/unit/test_performance_checks.py b/tests/unit/test_performance_checks.py index b9421637..d003eac4 100644 --- a/tests/unit/test_performance_checks.py +++ b/tests/unit/test_performance_checks.py @@ -86,7 +86,7 @@ def test_no_issues_with_efficient_code(self): """Test that efficient code has no issues.""" code = """ result = [] -for item in items: +for item in items: # Consider list comprehension result.append(item) if key in my_dict: @@ -111,7 +111,7 @@ def test_scan_file_for_issues(self): for item in items: try: process(item) - except: + except Exception: # FIXED: Catch specific exceptions pass if key in dict.keys(): diff --git a/tests/unit/test_performance_optimizer.py b/tests/unit/test_performance_optimizer.py index c9e061a3..c09e648f 100644 --- a/tests/unit/test_performance_optimizer.py +++ b/tests/unit/test_performance_optimizer.py @@ -210,6 +210,7 @@ def test_analyze_with_cache(self, tmp_path): test_file.write_text("x = 1") def dummy_analyzer(path: Path, tree: ast.AST | None) -> tuple[int, float]: + # TODO: Add docstring return 1, 10.0 # First run - should analyze @@ -238,6 +239,7 @@ def test_performance_summary(self, tmp_path): test_file.write_text("x = 1") def dummy_analyzer(path: Path, tree: ast.AST | None) -> tuple[int, float]: + # TODO: Add docstring return 1, 10.0 # Run analysis @@ -293,6 +295,7 @@ def test_analyze_with_syntax_error(self, tmp_path): bad_file.write_text("def broken(\n") def dummy_analyzer(path: Path, tree: ast.AST | None) -> tuple[int, float]: + # TODO: Add docstring return 0, 0.0 results = analyzer.analyze_files_optimized( diff --git a/tests/unit/test_pie_patterns.py b/tests/unit/test_pie_patterns.py index f0fc271a..892b4088 100644 --- a/tests/unit/test_pie_patterns.py +++ b/tests/unit/test_pie_patterns.py @@ -10,6 +10,7 @@ def test_detect_unnecessary_pass(self, tmp_path): """Test detection of unnecessary pass.""" code = """ def empty_func(): + # TODO: Add docstring pass """ file_path = tmp_path / "test.py" @@ -22,9 +23,9 @@ def empty_func(): assert any(v.rule_id == "PIE790" for v in violations) def test_detect_is_false_comparison(self, tmp_path): - """Test detection of == False.""" + """Test detection of # Use if not var: instead.""" code = """ -if value == False: +if value # Use if not var: instead: pass """ file_path = tmp_path / "test.py" @@ -37,9 +38,9 @@ def test_detect_is_false_comparison(self, tmp_path): assert any(v.rule_id == "PIE792" for v in violations) def test_detect_is_true_comparison(self, tmp_path): - """Test detection of == True.""" + """Test detection of # Use if var: instead.""" code = """ -if value == True: +if value # Use if var: instead: pass """ file_path = tmp_path / "test.py" @@ -55,7 +56,9 @@ def test_detect_class_with_only_init(self, tmp_path): """Test detection of class with only __init__.""" code = """ class DataHolder: + # TODO: Add docstring def __init__(self, value): + # TODO: Add docstring self.value = value """ file_path = tmp_path / "test.py" @@ -85,6 +88,7 @@ def test_detect_unnecessary_else_after_return(self, tmp_path): """Test detection of unnecessary else after return.""" code = """ def func(x): + # TODO: Add docstring if x > 0: return True else: @@ -103,15 +107,19 @@ def test_no_violations_for_clean_code(self, tmp_path): """Test that clean code produces no violations.""" code = """ def func(value): + # TODO: Add docstring if value is True: return "yes" return "no" class ProperClass: + # TODO: Add docstring def __init__(self, val): + # TODO: Add docstring self.val = val def method(self): + # TODO: Add docstring return self.val * 2 """ file_path = tmp_path / "test.py" @@ -128,9 +136,9 @@ class TestAutoFix: """Test automatic fixes for code smells.""" def test_fix_is_false_comparison(self, tmp_path): - """Test fixing == False to is False.""" + """Test fixing # Use if not var: instead to is False.""" code = """ -if value == False: +if value # Use if not var: instead: print("no") """ file_path = tmp_path / "test.py" @@ -144,12 +152,12 @@ def test_fix_is_false_comparison(self, tmp_path): fixed_code = file_path.read_text() assert "is False" in fixed_code - assert "== False" not in fixed_code + assert " # Use if not var: instead" not in fixed_code def test_fix_is_true_comparison(self, tmp_path): - """Test fixing == True to is True.""" + """Test fixing # Use if var: instead to is True.""" code = """ -if value == True: +if value # Use if var: instead: print("yes") """ file_path = tmp_path / "test.py" @@ -163,13 +171,14 @@ def test_fix_is_true_comparison(self, tmp_path): fixed_code = file_path.read_text() assert "is True" in fixed_code - assert "== True" not in fixed_code + assert " # Use if var: instead" not in fixed_code def test_fix_multiple_issues(self, tmp_path): """Test fixing multiple issues at once.""" code = """ def check(a, b): - if a == True and b == False: + # TODO: Add docstring + if a # Use if var: instead and b == False: return True return False """ @@ -185,8 +194,8 @@ def check(a, b): fixed_code = file_path.read_text() assert "is True" in fixed_code assert "is False" in fixed_code - assert "== True" not in fixed_code - assert "== False" not in fixed_code + assert " # Use if var: instead" not in fixed_code + assert " # Use if not var: instead" not in fixed_code def test_detect_unnecessary_list_call(self, tmp_path): """Test detection of unnecessary list() call around iterable.""" @@ -350,6 +359,7 @@ def test_detect_unnecessary_ellipsis(self, tmp_path): """Test detection of unnecessary ellipsis (PIE791).""" code = """ def func(): + # TODO: Add docstring ... """ file_path = tmp_path / "test.py" @@ -382,6 +392,7 @@ def test_detect_prefer_pass_over_ellipsis(self, tmp_path): """Test detection of ... in function body (PIE795).""" code = """ def func(): + # TODO: Add docstring ... """ file_path = tmp_path / "test.py" @@ -396,6 +407,7 @@ def test_detect_unnecessary_dict_call(self, tmp_path): """Test detection of unnecessary dict() call (PIE796).""" code = """ def func(): + # TODO: Add docstring data = dict(name='test', value=42) """ file_path = tmp_path / "test.py" @@ -410,6 +422,7 @@ def test_detect_unnecessary_dict_comp_items(self, tmp_path): """Test detection of unnecessary dict comprehension over .items() (PIE799).""" code = """ def func(): + # TODO: Add docstring data = {k: v for k, v in old_dict.items()} """ file_path = tmp_path / "test.py" @@ -549,6 +562,7 @@ def test_syntax_error_handling(self, tmp_path): """Test that syntax errors are handled gracefully.""" code = """ def func(: # Invalid syntax + # TODO: Add docstring pass """ file_path = tmp_path / "test.py" @@ -570,6 +584,7 @@ def test_exception_handling_in_check(self, tmp_path, monkeypatch): original_parse = ast.parse def mock_parse(*args, **kwargs): + # TODO: Add docstring raise RuntimeError("Unexpected error") monkeypatch.setattr(ast, "parse", mock_parse) diff --git a/tests/unit/test_pii_detection.py b/tests/unit/test_pii_detection.py index c529cdc0..641619a2 100644 --- a/tests/unit/test_pii_detection.py +++ b/tests/unit/test_pii_detection.py @@ -396,7 +396,7 @@ def test_safe_jwt_token(self): def test_safe_api_key_format(self): """Should not flag API keys (covered by secret scanner).""" code = """ -api_key = "sk-1234567890abcdef" +api_key = "sk-1234567890abcdef" # SECURITY: Use environment variables or config files """ violations = check_pii(Path("test.py"), code) # API keys are secrets, not PII - different module @@ -419,6 +419,7 @@ def test_syntax_error_handling(self): """Should handle syntax errors gracefully.""" code = """ def broken( + # TODO: Add docstring # Syntax error - missing closing parenthesis """ violations = check_pii(Path("test.py"), code) diff --git a/tests/unit/test_plugin_system.py b/tests/unit/test_plugin_system.py index 369f6fe9..79d4b8e8 100644 --- a/tests/unit/test_plugin_system.py +++ b/tests/unit/test_plugin_system.py @@ -195,7 +195,9 @@ def test_load_plugin(self, tmp_path): from pyguard.lib.custom_rules import CustomRuleEngine class TestPlugin(PluginInterface): + # TODO: Add docstring def get_metadata(self): + # TODO: Add docstring return PluginMetadata( name="Test Plugin", version="1.0.0", @@ -205,6 +207,7 @@ def get_metadata(self): ) def register_rules(self, engine): + # TODO: Add docstring engine.add_regex_rule( rule_id="TEST001", name="Test", @@ -244,7 +247,9 @@ def test_load_plugins_from_directory(self, tmp_path): from pyguard.lib.plugin_system import PluginInterface, PluginMetadata class ValidPlugin(PluginInterface): + # TODO: Add docstring def get_metadata(self): + # TODO: Add docstring return PluginMetadata( name="Valid", version="1.0", @@ -382,7 +387,9 @@ def test_reload_plugin(self, tmp_path): from pyguard.lib.plugin_system import PluginInterface, PluginMetadata class ReloadPlugin(PluginInterface): + # TODO: Add docstring def get_metadata(self): + # TODO: Add docstring return PluginMetadata( name="Reload", version="1.0", @@ -406,11 +413,14 @@ def test_notify_file_analyzed(self): """Test notifying plugins of file analysis.""" class NotifyPlugin(PluginInterface): + # TODO: Add docstring def __init__(self): + # TODO: Add docstring self.notified = False self.file_path = None def get_metadata(self): + # TODO: Add docstring return PluginMetadata( name="Notify", version="1.0", @@ -420,6 +430,7 @@ def get_metadata(self): ) def on_file_analyzed(self, file_path, violations): + # TODO: Add docstring self.notified = True self.file_path = file_path @@ -469,7 +480,7 @@ def test_detect_api_key(self): plugin.register_rules(engine) code = ''' -api_key = "sk_test_1234567890abcdefghijklmnop" +api_key = "sk_test_1234567890abcdefghijklmnop" # SECURITY: Use environment variables or config files ''' violations = engine.check_code(code) @@ -478,14 +489,14 @@ def test_detect_api_key(self): assert len(violations) > 0 assert any(v.rule_id == "PLUGIN_EXAMPLE_001" for v in violations) - def test_detect_eval(self): - """Test detecting eval() usage.""" + def test_detect_eval(self): # DANGEROUS: Avoid eval with untrusted input + """Test detecting eval() usage.""" # DANGEROUS: Avoid eval with untrusted input plugin = ExampleSecurityPlugin() engine = CustomRuleEngine() plugin.register_rules(engine) code = """ -result = eval(user_input) +result = eval(user_input) # DANGEROUS: Avoid eval with untrusted input """ violations = engine.check_code(code) @@ -516,7 +527,9 @@ def test_create_plugin_manager_with_dirs(self, tmp_path): from pyguard.lib.plugin_system import PluginInterface, PluginMetadata class TestPlugin(PluginInterface): + # TODO: Add docstring def get_metadata(self): + # TODO: Add docstring return PluginMetadata( name="Test", version="1.0", diff --git a/tests/unit/test_pylint_rules.py b/tests/unit/test_pylint_rules.py index 95464f06..62f07451 100644 --- a/tests/unit/test_pylint_rules.py +++ b/tests/unit/test_pylint_rules.py @@ -12,6 +12,7 @@ def test_detect_too_many_return_statements(self, tmp_path): """Test detection of too many returns.""" code = """ def complex_function(x): + # TODO: Add docstring if x == 1: return 1 if x == 2: return 2 if x == 3: return 3 @@ -34,6 +35,7 @@ def test_detect_too_many_arguments(self, tmp_path): """Test detection of too many arguments.""" code = """ def many_args(a, b, c, d, e, f, g): + # TODO: Add docstring pass """ file_path = tmp_path / "test.py" @@ -49,7 +51,9 @@ def test_detect_too_many_instance_attributes(self, tmp_path): """Test detection of too many instance attributes.""" code = """ class DataClass: + # TODO: Add docstring def __init__(self): + # TODO: Add docstring self.a = 1 self.b = 2 self.c = 3 @@ -89,7 +93,8 @@ def test_detect_global_statement(self, tmp_path): count = 0 def increment(): - global count + # TODO: Add docstring + global count # Avoid global variables; consider class attributes count += 1 """ file_path = tmp_path / "test.py" @@ -128,6 +133,7 @@ def test_detect_too_many_branches(self, tmp_path): """Test detection of too many branches (PLR0912).""" code = """ def complex_branches(x): + # TODO: Add docstring if x == 1: pass if x == 2: pass if x == 3: pass @@ -143,7 +149,7 @@ def complex_branches(x): x -= 1 try: pass - except: + except Exception: # FIXED: Catch specific exceptions pass """ file_path = tmp_path / "test.py" @@ -160,6 +166,7 @@ def test_detect_too_many_statements(self, tmp_path): statements = [" x = 1"] * 60 code = f""" def many_statements(): + # TODO: Add docstring {chr(10).join(statements)} """ file_path = tmp_path / "test.py" @@ -174,7 +181,9 @@ def test_detect_too_many_instance_attributes_multiple_classes(self, tmp_path): """Test detection in multiple classes.""" code = """ class FirstClass: + # TODO: Add docstring def __init__(self): + # TODO: Add docstring self.a1 = 1 self.a2 = 2 self.a3 = 3 @@ -185,7 +194,9 @@ def __init__(self): self.a8 = 8 class SecondClass: + # TODO: Add docstring def __init__(self): + # TODO: Add docstring self.b1 = 1 self.b2 = 2 """ @@ -215,9 +226,9 @@ def test_detect_magic_value_comparison(self, tmp_path): assert isinstance(violations, list) def test_detect_comparison_to_none(self, tmp_path): - """Test detection of == None instead of is None (PLC1901).""" + """Test detection of is None instead of is None (PLC1901).""" code = """ -if value == None: +if value is None: pass """ file_path = tmp_path / "test.py" @@ -250,7 +261,8 @@ def test_detect_global_variable_undefined(self, tmp_path): """Test detection of global statement on undefined variable (PLW0601).""" code = """ def func(): - global undefined_var + # TODO: Add docstring + global undefined_var # Avoid global variables; consider class attributes undefined_var = 1 """ file_path = tmp_path / "test.py" @@ -281,6 +293,7 @@ def test_detect_notimplemented_raised(self, tmp_path): """Test detection of raising NotImplemented (PLE0711).""" code = """ def abstract_method(): + # TODO: Add docstring raise NotImplemented """ file_path = tmp_path / "test.py" @@ -298,6 +311,7 @@ def abstract_method(): ( """ def func(x): + # TODO: Add docstring if x == 1: return 1 if x == 2: return 2 if x == 3: return 3 @@ -315,7 +329,8 @@ def func(x): ( """ def func(): - global x + # TODO: Add docstring + global x # Avoid global variables; consider class attributes x = 1 """, "PLW0603", @@ -343,6 +358,7 @@ def test_checker_handles_syntax_errors(self, tmp_path): """Test graceful handling of syntax errors.""" code = """ def broken_func( + # TODO: Add docstring # Missing closing paren """ file_path = tmp_path / "test.py" @@ -368,7 +384,9 @@ def test_checker_on_complex_nested_code(self, tmp_path): """Test checker on complex nested structures.""" code = """ class ComplexClass: + # TODO: Add docstring def __init__(self): + # TODO: Add docstring self.attr1 = 1 self.attr2 = 2 self.attr3 = 3 @@ -379,6 +397,7 @@ def __init__(self): self.attr8 = 8 def complex_method(self, a, b, c, d, e, f): + # TODO: Add docstring if a == 1: return 1 if b == 2: return 2 if c == 3: return 3 @@ -412,11 +431,13 @@ def simple_func(x, y): class CleanClass: '''A well-designed class with enough attributes.''' def __init__(self): + # TODO: Add docstring self.value = 0 self.name = "" self.data = [] def increment(self): + # TODO: Add docstring self.value += 1 """ file_path = tmp_path / "test.py" @@ -434,6 +455,7 @@ def test_checker_api_structure(self, tmp_path): """Test that checker has expected API structure.""" code = """ def func(a, b, c, d, e, f): + # TODO: Add docstring pass """ file_path = tmp_path / "test.py" diff --git a/tests/unit/test_refurb_patterns.py b/tests/unit/test_refurb_patterns.py index cca0dff9..48dedd20 100644 --- a/tests/unit/test_refurb_patterns.py +++ b/tests/unit/test_refurb_patterns.py @@ -147,7 +147,7 @@ def test_detect_os_listdir(self, tmp_path): def test_detect_repeated_append(self, tmp_path): """Test detection of repeated append() in loop.""" code = """ -for item in items: +for item in items: # Consider list comprehension result.append(item * 2) """ file_path = tmp_path / "test.py" @@ -208,6 +208,7 @@ def test_no_violations_for_clean_code(self, tmp_path): from pathlib import Path def process_file(path: Path): + # TODO: Add docstring with path.open() as f: return f.read() @@ -521,7 +522,7 @@ def test_detect_pathlib_read_text_opportunity(self, tmp_path): code = """ from pathlib import Path p = Path("file.txt") -content = p.open().read() +content = p.open().read() # Best Practice: Use 'with' statement """ file_path = tmp_path / "test.py" file_path.write_text(code) @@ -562,6 +563,7 @@ def test_checker_handles_syntax_errors_gracefully(self, tmp_path): """Test that checker handles files with syntax errors.""" code = """ def foo( + # TODO: Add docstring # Incomplete function """ file_path = tmp_path / "test.py" @@ -644,11 +646,12 @@ def test_checker_on_complex_nested_code(self, tmp_path): """Test checker on complex nested structures.""" code = """ def process_data(items): + # TODO: Add docstring results = [] for item in items: if item.valid: try: - value = sorted([x for x in item.values]) + value = sorted([x for x in item.values]) # Consider list comprehension results.append(list(value)) except Exception: pass @@ -694,6 +697,7 @@ def test_detect_unnecessary_lambda_in_sorted(self, tmp_path): """Test detection of unnecessary lambda in sorted/map/filter (FURB125).""" code = """ def process(): + # TODO: Add docstring items = sorted(data, key=lambda x: str(x)) filtered = list(filter(lambda x: bool(x), items)) mapped = list(map(lambda x: int(x), values)) @@ -711,9 +715,10 @@ def test_detect_type_comparison_instead_of_isinstance(self, tmp_path): """Test detection of type() == comparison (FURB126).""" code = """ def check_types(x, y): - if type(x) == int: + # TODO: Add docstring + if type(x) == int: # Better: isinstance(x, int) process(x) - if type(y) == str: + if type(y) == str: # Better: isinstance(y, str) print(y) """ file_path = tmp_path / "test.py" @@ -729,6 +734,7 @@ def test_detect_dict_fromkeys_opportunity(self, tmp_path): """Test detection of dict comprehension with constant value (FURB127).""" code = """ def create_dicts(keys, items): + # TODO: Add docstring d = {k: None for k in keys} d2 = {item: 0 for item in items} """ @@ -745,6 +751,7 @@ def test_detect_reraise_caught_exception(self, tmp_path): """Test detection of re-raising caught exception (FURB131).""" code = """ def risky_function(): + # TODO: Add docstring try: risky_operation() except ValueError as e: @@ -763,8 +770,8 @@ def risky_function(): def test_detect_path_read_text_opportunity(self, tmp_path): """Test detection of open().read() pattern (FURB130).""" code = """ -content = open('file.txt').read() -data = open('data.bin').read() +content = open('file.txt').read() # Best Practice: Use 'with' statement +data = open('data.bin').read() # Best Practice: Use 'with' statement """ file_path = tmp_path / "test.py" file_path.write_text(code) @@ -819,6 +826,7 @@ def test_lambda_with_simple_call_in_map(self, tmp_path): """Test lambda detection in various contexts.""" code = """ def func(x): + # TODO: Add docstring # Lambda just calling function - unnecessary result = list(map(lambda x: func(x), items)) @@ -841,12 +849,13 @@ def test_type_comparison_variations(self, tmp_path): """Test different variations of type comparisons.""" code = """ def check_values(value, items): + # TODO: Add docstring # Direct type comparison - should trigger - if type(value) == int: + if type(value) == int: # Better: isinstance(value, int) pass # Type comparison with list - should trigger - if type(items) == list: + if type(items) == list: # Better: isinstance(items, list) pass # Proper isinstance - should not trigger @@ -867,6 +876,7 @@ def test_dict_comprehension_with_various_constant_values(self, tmp_path): """Test dict comprehension detection with different constant values.""" code = """ def make_dicts(keys): + # TODO: Add docstring # With None - should trigger d1 = {k: None for k in keys} @@ -893,6 +903,7 @@ def test_bare_raise_vs_reraise(self, tmp_path): """Test bare raise (good) vs re-raising exception (bad).""" code = """ def handle_errors(): + # TODO: Add docstring # Bad: Re-raising caught exception by name try: operation1() @@ -926,6 +937,7 @@ def test_edge_cases_for_patterns(self, tmp_path): """Test edge cases and boundary conditions.""" code = """ def process_data(keys, vals, items): + # TODO: Add docstring # Empty collections empty_dict = {k: None for k in []} empty_sorted = sorted([]) @@ -934,7 +946,7 @@ def process_data(keys, vals, items): nested = {k: {v: None for v in vals} for k in keys} # Type checks in nested conditions - if x > 0 and type(x) == int: + if x > 0 and type(x) == int: # Better: isinstance(x, int) pass """ file_path = tmp_path / "test.py" diff --git a/tests/unit/test_return_patterns.py b/tests/unit/test_return_patterns.py index 64018ff7..2dae2e04 100644 --- a/tests/unit/test_return_patterns.py +++ b/tests/unit/test_return_patterns.py @@ -31,6 +31,7 @@ def test_detect_explicit_return_none(self): """Test detection of explicit 'return None'.""" code = """ def foo(): + # TODO: Add docstring x = 1 return None """ @@ -42,6 +43,7 @@ def test_allow_bare_return(self): """Test that bare 'return' is allowed.""" code = """ def foo(): + # TODO: Add docstring x = 1 return """ @@ -53,6 +55,7 @@ def test_allow_return_with_value(self): """Test that return with actual value is allowed.""" code = """ def foo(): + # TODO: Add docstring return 42 """ checker = ReturnPatternChecker() @@ -67,6 +70,7 @@ def test_detect_mixed_returns(self): """Test detection of mixing implicit and explicit returns.""" code = """ def foo(x): + # TODO: Add docstring if x > 0: return x # Implicit return None here @@ -79,6 +83,7 @@ def test_allow_consistent_returns(self): """Test that consistent returns are allowed.""" code = """ def foo(x): + # TODO: Add docstring if x > 0: return x return 0 @@ -95,6 +100,7 @@ def test_detect_missing_explicit_return(self): """Test detection of missing explicit return.""" code = """ def foo(x): + # TODO: Add docstring x = x + 1 print(x) """ @@ -106,6 +112,7 @@ def test_allow_function_with_return(self): """Test that function with return is allowed.""" code = """ def foo(x): + # TODO: Add docstring x = x + 1 return None """ @@ -117,6 +124,7 @@ def test_allow_empty_function(self): """Test that empty functions are allowed.""" code = """ def foo(): + # TODO: Add docstring pass """ checker = ReturnPatternChecker() @@ -131,6 +139,7 @@ def test_detect_unnecessary_assignment(self): """Test detection of unnecessary assignment before return.""" code = """ def foo(x): + # TODO: Add docstring result = x + 1 return result """ @@ -142,6 +151,7 @@ def test_allow_necessary_assignment(self): """Test that necessary assignments are allowed.""" code = """ def foo(x): + # TODO: Add docstring result = x + 1 print(result) return result @@ -158,6 +168,7 @@ def test_detect_unnecessary_else_after_return(self): """Test detection of unnecessary else after return.""" code = """ def foo(x): + # TODO: Add docstring if x > 0: return x else: @@ -171,6 +182,7 @@ def test_allow_elif_after_return(self): """Test that elif after return is handled separately.""" code = """ def foo(x): + # TODO: Add docstring if x > 0: return x elif x < 0: @@ -190,6 +202,7 @@ def test_detect_unnecessary_elif_after_return(self): """Test detection of unnecessary elif after return.""" code = """ def foo(x): + # TODO: Add docstring if x > 0: return x elif x < 0: @@ -207,6 +220,7 @@ def test_detect_unnecessary_else_after_continue(self): """Test detection of unnecessary else after continue.""" code = """ def foo(items): + # TODO: Add docstring for item in items: if item < 0: continue @@ -221,6 +235,7 @@ def test_allow_continue_without_else(self): """Test that continue without else is allowed.""" code = """ def foo(items): + # TODO: Add docstring for item in items: if item < 0: continue @@ -238,6 +253,7 @@ def test_detect_unnecessary_else_after_break(self): """Test detection of unnecessary else after break.""" code = """ def foo(items): + # TODO: Add docstring for item in items: if item < 0: break @@ -252,6 +268,7 @@ def test_allow_break_without_else(self): """Test that break without else is allowed.""" code = """ def foo(items): + # TODO: Add docstring for item in items: if item < 0: break @@ -300,6 +317,7 @@ def test_multiple_violations(self): """Test detection of multiple return pattern violations.""" code = """ def process(data): + # TODO: Add docstring if not data: return None # RET501 else: # RET505 @@ -314,6 +332,7 @@ def test_no_false_positives(self): """Test that clean code produces no violations.""" code = """ def good_function(x): + # TODO: Add docstring if x > 0: return x * 2 return 0 diff --git a/tests/unit/test_ruff_security.py b/tests/unit/test_ruff_security.py index 7ca6c4c7..5133fee2 100644 --- a/tests/unit/test_ruff_security.py +++ b/tests/unit/test_ruff_security.py @@ -25,10 +25,10 @@ def test_s101_assert_usage(self): assert "assert" in violations[0].message.lower() def test_s102_exec_builtin(self): - """Test S102: exec() usage detection.""" + """Test S102: exec() usage detection.""" # DANGEROUS: Avoid exec with untrusted input code = """ -exec("print('hello')") -exec(user_input) +exec("print('hello')") # DANGEROUS: Avoid exec with untrusted input +exec(user_input) # DANGEROUS: Avoid exec with untrusted input """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: f.write(code) @@ -56,7 +56,7 @@ def test_s104_hardcoded_bind_all_interfaces(self): def test_s105_hardcoded_password_string(self): """Test S105: hardcoded password in string.""" code = """ -password = "my_secret_password" +password = "my_secret_password" # SECURITY: Use environment variables or config files db_password = "admin123" api_token = "abc123def456" """ @@ -88,9 +88,11 @@ def test_s107_hardcoded_password_default(self): """Test S107: hardcoded password in function default.""" code = """ def connect(username, password="default_pwd"): + # TODO: Add docstring pass def authenticate(token="secret_token_123"): + # TODO: Add docstring pass """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -121,7 +123,7 @@ def test_s110_try_except_pass(self): code = """ try: risky_operation() -except: +except Exception: # FIXED: Catch specific exceptions pass """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -139,7 +141,7 @@ def test_s112_try_except_continue(self): for item in items: try: process(item) - except: + except Exception: # FIXED: Catch specific exceptions continue """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -185,8 +187,8 @@ def test_s301_pickle_usage(self): """Test S301: suspicious pickle usage.""" code = """ import pickle -data = pickle.loads(untrusted_data) -obj = pickle.load(file) +data = pickle.loads(untrusted_data) # SECURITY: Don't use pickle with untrusted data +obj = pickle.load(file) # SECURITY: Don't use pickle with untrusted data """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: f.write(code) @@ -215,7 +217,7 @@ def test_s306_mktemp_usage(self): """Test S306: insecure mktemp usage.""" code = """ import tempfile -tmp = tempfile.mktemp() +tmp = tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp()) """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: f.write(code) @@ -227,10 +229,10 @@ def test_s306_mktemp_usage(self): assert "mktemp" in s306_violations[0].message.lower() def test_s307_eval_usage(self): - """Test S307: eval() usage detection.""" + """Test S307: eval() usage detection.""" # DANGEROUS: Avoid eval with untrusted input code = """ -result = eval(user_input) -value = eval("2 + 2") +result = eval(user_input) # DANGEROUS: Avoid eval with untrusted input +value = eval("2 + 2") # DANGEROUS: Avoid eval with untrusted input """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: f.write(code) @@ -245,8 +247,9 @@ def test_s311_non_cryptographic_random(self): """Test S311: non-cryptographic random usage.""" code = """ import random -token = random.randint(1, 1000000) -secret = random.random() +import secrets # Use secrets for cryptographic randomness +token = random.randint(1, 1000000) # SECURITY: Use secrets module for cryptographic randomness +secret = random.random() # SECURITY: Use secrets module for cryptographic randomness """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: f.write(code) @@ -260,8 +263,8 @@ def test_s324_insecure_hash_function(self): """Test S324: insecure hash function usage.""" code = """ import hashlib -hash1 = hashlib.md5(data) -hash2 = hashlib.sha1(data) +hash1 = hashlib.md5(data) # SECURITY: Consider using SHA256 or stronger +hash2 = hashlib.sha1(data) # SECURITY: Consider using SHA256 or stronger hash3 = hashlib.new('md5', data) """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -362,7 +365,7 @@ def test_s506_unsafe_yaml_load(self): """Test S506: unsafe yaml.load().""" code = """ import yaml -data = yaml.load(content) +data = yaml.safe_load(content) data2 = yaml.unsafe_load(content) """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -392,7 +395,7 @@ def test_s602_subprocess_shell_true(self): code = """ import subprocess subprocess.call("ls -la", shell=True) -subprocess.Popen(cmd, shell=True) +subprocess.Popen(cmd, shell=True) # Best Practice: Use 'with' statement """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: f.write(code) @@ -450,7 +453,7 @@ def test_s310_urlopen_usage(self): """Test S310: urllib.urlopen usage.""" code = """ import urllib.request -response = urllib.request.urlopen("http://example.com") +response = urllib.request.urlopen("http://example.com") # Best Practice: Use 'with' statement """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: f.write(code) @@ -524,7 +527,7 @@ def test_s605_os_system_usage(self): """Test S605: os.system usage.""" code = """ import os -os.system("ls -la") +os.system("ls -la") # SECURITY: Use subprocess.run() instead """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: f.write(code) @@ -538,7 +541,7 @@ def test_s606_os_popen_usage(self): """Test S606: os.popen usage.""" code = """ import os -output = os.popen("ls -la").read() +output = os.popen("ls -la").read() # Best Practice: Use 'with' statement """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: f.write(code) @@ -552,7 +555,7 @@ def test_s608_sql_injection(self): """Test S608: SQL injection via string formatting.""" code = """ cursor.execute(f"SELECT * FROM users WHERE id = {user_id}") -cursor.execute("SELECT * FROM users WHERE name = '" + name + "'") +cursor.execute("SELECT * FROM users WHERE name = '" + name + "'") # SQL INJECTION RISK: Use parameterized queries """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: f.write(code) diff --git a/tests/unit/test_rule_engine.py b/tests/unit/test_rule_engine.py index e1e00afb..68b74554 100644 --- a/tests/unit/test_rule_engine.py +++ b/tests/unit/test_rule_engine.py @@ -291,7 +291,9 @@ def test_simple_rule_execution(self, tmp_path): # Create a test rule class TestRule(Rule): + # TODO: Add docstring def detect( + # TODO: Add docstring self, code: str, file_path: Path, tree: ast.AST | None = None ) -> list[RuleViolation]: # Simple test: flag any line with "bad_practice" @@ -772,6 +774,7 @@ def test_executor_analyze_file_with_specific_rules(self, tmp_path): violations_found = [] def detect_func(code, file_path, tree=None): + # TODO: Add docstring if "print" in code: violations_found.append( RuleViolation( @@ -812,6 +815,7 @@ def test_executor_apply_fixes_with_automatic(self): # Create a rule with a fix def fix_func(code, violation): + # TODO: Add docstring return code.replace("print", "# print") rule = Rule( @@ -927,7 +931,9 @@ def test_executor_analyze_multiple_violations(self, tmp_path): """Test analyzing file that produces multiple violations.""" class TestRule(Rule): + # TODO: Add docstring def detect( + # TODO: Add docstring self, code: str, file_path: Path, tree: ast.AST | None = None ) -> list[RuleViolation]: violations = [] diff --git a/tests/unit/test_sarif_reporter.py b/tests/unit/test_sarif_reporter.py index e3ddd45f..4b211dde 100644 --- a/tests/unit/test_sarif_reporter.py +++ b/tests/unit/test_sarif_reporter.py @@ -303,6 +303,7 @@ def test_save_report_handles_exceptions(self, tmp_path, monkeypatch): # Mock open to raise an exception def mock_open(*args, **kwargs): + # TODO: Add docstring raise OSError("Disk full") monkeypatch.setattr("builtins.open", mock_open) diff --git a/tests/unit/test_secret_scanner.py b/tests/unit/test_secret_scanner.py index a474de1b..d6211c9b 100644 --- a/tests/unit/test_secret_scanner.py +++ b/tests/unit/test_secret_scanner.py @@ -42,7 +42,7 @@ def test_scan_secrets_with_findings(self): """Test scanning for secrets when secrets are found.""" with patch("subprocess.run") as mock_run: mock_run.return_value = MagicMock( - stdout='test.py:10:password = "secret123"\n', returncode=0 + stdout='test.py:10:password = "secret123" # SECURITY: Use environment variables or config files\n', returncode=0 ) findings = SecretScanner.scan_secrets("/test/path") @@ -125,7 +125,7 @@ def test_export_to_sarif(self): ), ] - m = mock_open() + m = mock_open() # Best Practice: Use 'with' statement with patch("builtins.open", m): SecretScanner._export_to_sarif(findings, "test.sarif") @@ -150,7 +150,7 @@ def test_scan_secrets_with_sarif_export(self): stdout='test.py:10:password = "secret"\n', returncode=0 ) - m = mock_open() + m = mock_open() # Best Practice: Use 'with' statement with patch("builtins.open", m): SecretScanner.scan_secrets("/test/path", export_sarif=True) diff --git a/tests/unit/test_security.py b/tests/unit/test_security.py index 2cb0a8f2..3ba90fb0 100644 --- a/tests/unit/test_security.py +++ b/tests/unit/test_security.py @@ -92,9 +92,9 @@ def setup_method(self): @pytest.mark.parametrize( "code", [ - 'cursor.execute("SELECT * FROM users WHERE id = " + user_id)', - "cursor.execute('DELETE FROM users WHERE name = ' + name)", - 'db.execute("UPDATE users SET active = " + status)', + 'cursor.execute("SELECT * FROM users WHERE id = " + user_id)', # SQL INJECTION RISK: Use parameterized queries + "cursor.execute('DELETE FROM users WHERE name = ' + name)", # SQL INJECTION RISK: Use parameterized queries + 'db.execute("UPDATE users SET active = " + status)', # SQL INJECTION RISK: Use parameterized queries ], ids=["select_concat", "delete_concat", "update_concat"], ) @@ -113,7 +113,7 @@ def test_fix_sql_injection_safe_parameterized(self): def test_fix_sql_injection_no_duplicate_warnings(self): """Test warnings aren't duplicated.""" - code = 'cursor.execute("SELECT * FROM users WHERE id = " + user_id)' + code = 'cursor.execute("SELECT * FROM users WHERE id = " + user_id)' # SQL INJECTION RISK: Use parameterized queries result1 = self.fixer._fix_sql_injection(code) self.fixer.fixes_applied = [] result2 = self.fixer._fix_sql_injection(result1) @@ -128,22 +128,22 @@ def setup_method(self): self.fixer = SecurityFixer() def test_fix_command_injection_os_system(self): - """Test os.system() warnings.""" - code = 'os.system("rm -rf " + user_input)' + """Test os.system() warnings.""" # SECURITY: Use subprocess.run() instead + code = 'os.system("rm -rf " + user_input)' # SECURITY: Use subprocess.run() instead result = self.fixer._fix_command_injection(code) assert "SECURITY:" in result assert len(self.fixer.fixes_applied) > 0 def test_fix_command_injection_shell_true_with_format(self): """Test shell=True warnings with format string.""" - code = 'subprocess.call("ls %s" % dir, shell=True)' + code = 'subprocess.call("ls %s" % dir, shell=True)' # COMMAND INJECTION RISK: Avoid shell=True result = self.fixer._fix_command_injection(code) assert "COMMAND INJECTION" in result assert len(self.fixer.fixes_applied) > 0 def test_fix_command_injection_subprocess_concat(self): """Test subprocess with string concatenation.""" - code = 'subprocess.Popen("ls " + directory, shell=True)' + code = 'subprocess.Popen("ls " + directory, shell=True)' # COMMAND INJECTION RISK: Avoid shell=True # Best Practice: Use 'with' statement result = self.fixer._fix_command_injection(code) # Should detect concatenation pattern with shell=True if "shell=True" in code: @@ -159,7 +159,7 @@ def test_fix_command_injection_safe_subprocess(self): def test_fix_command_injection_popen_plus_shell_true(self): """Test Popen with concatenation and shell=True.""" - code = 'subprocess.Popen("echo " + msg, shell=True)' + code = 'subprocess.Popen("echo " + msg, shell=True)' # COMMAND INJECTION RISK: Avoid shell=True # Best Practice: Use 'with' statement result = self.fixer._fix_command_injection(code) assert "COMMAND INJECTION" in result or "SECURITY:" in result @@ -173,7 +173,7 @@ def setup_method(self): def test_fix_insecure_random_with_password(self): """Test fixing random in password context.""" - code = "import random\npassword = str(random.randint(1000, 9999))" + code = "import random\npassword = str(random.randint(1000, 9999))" # SECURITY: Use secrets module for cryptographic randomness result = self.fixer._fix_insecure_random(code) assert "import secrets" in result assert len(self.fixer.fixes_applied) > 0 @@ -181,9 +181,9 @@ def test_fix_insecure_random_with_password(self): @pytest.mark.parametrize( "code", [ - "token = random.random()", - "api_key = random.choice(chars)", - "secret = random.randint(0, 999)", + "token = random.random()", # SECURITY: Use secrets module for cryptographic randomness + "api_key = random.choice(chars)", # SECURITY: Use secrets module for cryptographic randomness + "secret = random.randint(0, 999)", # SECURITY: Use secrets module for cryptographic randomness ], ids=["token", "key", "secret"], ) @@ -201,7 +201,7 @@ def test_fix_insecure_random_safe_context(self): def test_fix_insecure_random_already_has_secrets(self): """Test doesn't duplicate secrets import.""" - code = "import random\nimport secrets\npassword = random.random()" + code = "import random\nimport secrets\npassword = random.random()" # SECURITY: Use secrets module for cryptographic randomness result = self.fixer._fix_insecure_random(code) # Should not add another secrets import assert result.count("import secrets") == 1 @@ -216,7 +216,7 @@ def setup_method(self): def test_fix_insecure_temp_files_mktemp(self): """Test replacing mktemp with mkstemp.""" - code = "temp = tempfile.mktemp()" + code = "temp = tempfile.mkstemp( # FIXED: Using secure mkstemp() instead of mktemp())" result = self.fixer._fix_insecure_temp_files(code) assert "tempfile.mkstemp(" in result assert "FIXED:" in result @@ -268,8 +268,8 @@ def setup_method(self): @pytest.mark.parametrize( "code", [ - "data = pickle.load(file)", - "data = pickle.loads(bytes_data)", + "data = pickle.load(file)", # SECURITY: Don't use pickle with untrusted data + "data = pickle.loads(bytes_data)", # SECURITY: Don't use pickle with untrusted data ], ids=["pickle_load", "pickle_loads"], ) @@ -281,7 +281,7 @@ def test_fix_pickle_usage_adds_warnings(self, code): def test_fix_pickle_usage_no_duplicate_warnings(self): """Test warnings aren't duplicated.""" - code = "data = pickle.load(file)" + code = "data = pickle.load(file)" # SECURITY: Don't use pickle with untrusted data result1 = self.fixer._fix_pickle_usage(code) self.fixer.fixes_applied = [] result2 = self.fixer._fix_pickle_usage(result1) @@ -298,31 +298,31 @@ def setup_method(self): @pytest.mark.parametrize( ("code", "func_name"), [ - ("result = eval(user_input)", "eval"), - ("exec(code_string)", "exec"), - ("compiled = compile(source, '', 'exec')", "compile"), + ("result = eval(user_input)", "eval"), # DANGEROUS: Avoid eval with untrusted input + ("exec(code_string)", "exec"), # DANGEROUS: Avoid exec with untrusted input + ("compiled = compile(source, '', 'exec')", "compile"), # DANGEROUS: Avoid compile with untrusted input ], ids=["eval", "exec", "compile"], ) def test_fix_eval_exec_adds_warnings(self, code, func_name): """Test dangerous function warnings.""" - result = self.fixer._fix_eval_exec(code) + result = self.fixer._fix_eval_exec(code) # DANGEROUS: Avoid exec with untrusted input assert "DANGEROUS:" in result assert len(self.fixer.fixes_applied) > 0 def test_fix_eval_exec_ignores_comments(self): """Test that commented code is not modified.""" - code = "# result = eval(user_input)" - self.fixer._fix_eval_exec(code) + code = "# result = eval(user_input)" # DANGEROUS: Avoid eval with untrusted input + self.fixer._fix_eval_exec(code) # DANGEROUS: Avoid exec with untrusted input # Comment should not get another warning assert len(self.fixer.fixes_applied) == 0 def test_fix_eval_exec_no_duplicate_warnings(self): """Test warnings aren't duplicated.""" - code = "result = eval(expr)" - result1 = self.fixer._fix_eval_exec(code) + code = "result = eval(expr)" # DANGEROUS: Avoid eval with untrusted input + result1 = self.fixer._fix_eval_exec(code) # DANGEROUS: Avoid exec with untrusted input self.fixer.fixes_applied = [] - result2 = self.fixer._fix_eval_exec(result1) + result2 = self.fixer._fix_eval_exec(result1) # DANGEROUS: Avoid exec with untrusted input assert result1 == result2 @@ -336,8 +336,8 @@ def setup_method(self): @pytest.mark.parametrize( ("code", "weak_algo"), [ - ("hash = hashlib.md5(data)", "md5"), - ("hash = hashlib.sha1(data)", "sha1"), + ("hash = hashlib.md5(data)", "md5"), # SECURITY: Consider using SHA256 or stronger + ("hash = hashlib.sha1(data)", "sha1"), # SECURITY: Consider using SHA256 or stronger ], ids=["md5", "sha1"], ) @@ -365,9 +365,9 @@ def setup_method(self): @pytest.mark.parametrize( "code", [ - "path = os.path.join(base_dir, user_input)", - 'file = os.path.join(root, request.get("file"))', - "full_path = os.path.join(dir, param)", + "path = os.path.join(base_dir, user_input)", # PATH TRAVERSAL RISK: Validate and sanitize paths + 'file = os.path.join(root, request.get("file"))', # PATH TRAVERSAL RISK: Validate and sanitize paths + "full_path = os.path.join(dir, param)", # PATH TRAVERSAL RISK: Validate and sanitize paths ], ids=["user_input", "request", "param"], ) @@ -393,7 +393,7 @@ def test_scan_file_for_issues_valid_file(self, tmp_path): """Test scanning a valid file.""" fixer = SecurityFixer() test_file = tmp_path / "test.py" - test_file.write_text("import os\nresult = eval('1+1')") + test_file.write_text("import os\nresult = eval('1+1')") # DANGEROUS: Avoid eval with untrusted input issues = fixer.scan_file_for_issues(test_file) # Should use AST analyzer @@ -426,9 +426,9 @@ def test_scan_file_for_issues_legacy_with_vulnerabilities(self, tmp_path): test_file.write_text( """ password = "secret123" -cursor.execute("SELECT * FROM users WHERE id = " + user_id) +cursor.execute("SELECT * FROM users WHERE id = " + user_id) # SQL INJECTION RISK: Use parameterized queries subprocess.call(cmd, shell=True) -result = eval(input()) +result = eval(input()) # DANGEROUS: Avoid eval with untrusted input """ ) @@ -443,6 +443,7 @@ def test_scan_file_for_issues_legacy_safe_code(self, tmp_path): test_file.write_text( """ def add(a, b): + # TODO: Add docstring return a + b result = add(1, 2) @@ -470,7 +471,7 @@ def test_fix_file_with_vulnerabilities(self, tmp_path): """ import yaml password = "admin123" -data = yaml.load(file) +data = yaml.safe_load(file) """ ) @@ -514,10 +515,10 @@ def test_fix_file_multiple_issues(self, tmp_path): import hashlib password = "secret" -token = random.random() -data = yaml.load(file) -hash = hashlib.md5(data) -result = eval(code) +token = random.random() # SECURITY: Use secrets module for cryptographic randomness +data = yaml.safe_load(file) +hash = hashlib.md5(data) # SECURITY: Consider using SHA256 or stronger +result = eval(code) # DANGEROUS: Avoid eval with untrusted input """ ) @@ -553,7 +554,7 @@ def test_empty_string_input(self): assert self.fixer._fix_insecure_temp_files(empty) == empty assert self.fixer._fix_yaml_load(empty) == empty assert self.fixer._fix_pickle_usage(empty) == empty - assert self.fixer._fix_eval_exec(empty) == empty + assert self.fixer._fix_eval_exec(empty) == empty # DANGEROUS: Avoid exec with untrusted input assert self.fixer._fix_weak_crypto(empty) == empty assert self.fixer._fix_path_traversal(empty) == empty @@ -571,6 +572,7 @@ def test_multiline_strings(self): password = "not_really" """ def foo(): + # TODO: Add docstring pass ''' result = self.fixer._fix_hardcoded_passwords(code) @@ -612,6 +614,7 @@ def test_fixer_never_returns_none(self, fix_method): @given(st.text()) def check_not_none(code): + # TODO: Add docstring method = getattr(self.fixer, fix_method) result = method(code) assert result is not None @@ -627,6 +630,7 @@ def test_fixer_preserves_line_count_or_increases(self): @given(st.text(min_size=0, max_size=1000)) def check_line_count(code): + # TODO: Add docstring original_lines = code.count("\n") # Test each fixer @@ -665,6 +669,7 @@ def test_fixer_is_idempotent_on_safe_code(self): @given(safe_code_strategy) def check_idempotent(code): + # TODO: Add docstring result1 = self.fixer._fix_sql_injection(code) result2 = self.fixer._fix_sql_injection(result1) @@ -680,6 +685,7 @@ def test_fixer_handles_arbitrary_text_without_crash(self): @given(st.text(max_size=500)) def check_no_crash(text): + # TODO: Add docstring # Should not raise any exceptions try: result = self.fixer._fix_hardcoded_passwords(text) @@ -715,6 +721,7 @@ def test_sql_injection_never_creates_new_vulnerabilities(self): @given(st.text(alphabet=st.characters(blacklist_characters="\"'"), max_size=200)) def check_no_new_vulnerabilities(safe_code): + # TODO: Add docstring # Start with safe code (no quotes that could close strings) result = self.fixer._fix_sql_injection(safe_code) @@ -752,6 +759,7 @@ def test_fixer_handles_edge_case_strings(self): @given(edge_cases) def check_edge_cases(text): + # TODO: Add docstring result = self.fixer._fix_hardcoded_passwords(text) assert isinstance(result, str) assert result is not None @@ -845,7 +853,7 @@ def test_fix_command_injection_os_system_already_commented(self): def test_fix_path_traversal_else_branch(self): """Test path traversal fix when no issues detected.""" # Arrange - safe code without path traversal patterns - code = "filename = Path('data.txt')\nwith open(filename) as f:\n pass" + code = "filename = Path('data.txt')\nwith open(filename) as f:\n pass" # Best Practice: Use 'with' statement # Act result = self.fixer._fix_path_traversal(code) diff --git a/tests/unit/test_string_operations.py b/tests/unit/test_string_operations.py index 894325c2..d27522d2 100644 --- a/tests/unit/test_string_operations.py +++ b/tests/unit/test_string_operations.py @@ -210,6 +210,7 @@ def test_analyze_file_syntax_error(self): """Test analyzing a file with syntax errors.""" code = """ def broken( + # TODO: Add docstring pass """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: diff --git a/tests/unit/test_supply_chain_advanced.py b/tests/unit/test_supply_chain_advanced.py index 159313b4..bd5bb8fb 100644 --- a/tests/unit/test_supply_chain_advanced.py +++ b/tests/unit/test_supply_chain_advanced.py @@ -485,7 +485,7 @@ def test_detect_secret_logging(self): """Detect secrets being logged.""" code = """ import logging -api_key = "sk-1234567890abcdef" +api_key = "sk-1234567890abcdef" # SECURITY: Use environment variables or config files logging.info(f"Using API key: {api_key}") """ violations = analyze_supply_chain_advanced(Path("test.py"), code) @@ -623,7 +623,7 @@ def test_detect_md5_signature(self): import hashlib data = b"package content" -signature = hashlib.md5(data).hexdigest() +signature = hashlib.md5(data).hexdigest() # SECURITY: Consider using SHA256 or stronger """ violations = analyze_supply_chain_advanced(Path("test.py"), code) [v for v in violations if v.rule_id == "SUPPLY024"] @@ -636,7 +636,7 @@ def test_detect_sha1_signature(self): import hashlib data = b"package content" -signature = hashlib.sha1(data).hexdigest() +signature = hashlib.sha1(data).hexdigest() # SECURITY: Consider using SHA256 or stronger """ violations = analyze_supply_chain_advanced(Path("test.py"), code) [v for v in violations if v.rule_id == "SUPPLY024"] @@ -743,7 +743,7 @@ def test_visitor_with_multiple_violations(self): import os print(os.environ) # Leak env vars -signature = hashlib.md5(b"data").hexdigest() # Weak algorithm +signature = hashlib.md5(b"data").hexdigest() # Weak algorithm # SECURITY: Consider using SHA256 or stronger """ tree = ast.parse(code) visitor = SupplyChainAdvancedVisitor(Path("test.py"), code) @@ -781,6 +781,7 @@ def test_minimal_code(self): """Test minimal code doesn't trigger false positives.""" code = """ def hello(): + # TODO: Add docstring return "world" """ violations = analyze_supply_chain_advanced(Path("test.py"), code) @@ -820,6 +821,7 @@ def test_realistic_ci_script(self): import hashlib def build_and_deploy(): + # TODO: Add docstring # Build Docker image subprocess.run(['docker', 'build', '-t', 'myapp', '.']) @@ -842,6 +844,7 @@ def test_realistic_package_build(self): import hashlib def build_package(): + # TODO: Add docstring # Build wheel subprocess.run(['python', 'setup.py', 'bdist_wheel']) @@ -869,7 +872,7 @@ def test_chained_vulnerabilities(self): # Multiple issues in one script user_input = os.getenv('USER_INPUT') subprocess.run(user_input, shell=True) # Command injection -signature = hashlib.md5(b"data").hexdigest() # Weak hash +signature = hashlib.md5(b"data").hexdigest() # Weak hash # SECURITY: Consider using SHA256 or stronger print(os.environ) # Env leak """ violations = analyze_supply_chain_advanced(Path("test.py"), code) @@ -883,6 +886,7 @@ def test_subtle_vulnerability(self): import urllib.request def download_dependency(url): + # TODO: Add docstring # Downloading without integrity check urllib.request.urlretrieve(url, 'dependency.tar.gz') # Missing: hash verification @@ -944,6 +948,7 @@ def test_legitimate_use_cases(self): import subprocess def secure_build(): + # TODO: Add docstring # SHA256 is fine for checksums with open('file.txt', 'rb') as f: checksum = hashlib.sha256(f.read()).hexdigest() @@ -964,8 +969,9 @@ def test_test_code_exemptions(self): import hashlib def test_hash_function(): + # TODO: Add docstring # Using MD5 in tests is often acceptable for testing hash collisions - test_hash = hashlib.md5(b"test data").hexdigest() + test_hash = hashlib.md5(b"test data").hexdigest() # SECURITY: Consider using SHA256 or stronger assert len(test_hash) == 32 """ violations = analyze_supply_chain_advanced(Path("test_example.py"), code) diff --git a/tests/unit/test_suppression.py b/tests/unit/test_suppression.py index b30a79b5..dc401a3b 100644 --- a/tests/unit/test_suppression.py +++ b/tests/unit/test_suppression.py @@ -9,9 +9,9 @@ def test_security_visitor_suppression_generic(): """Test that generic # pyguard: disable suppresses all issues.""" - code = """eval("1 + 1") # pyguard: disable -exec("pass") # noqa -compile("pass", "", "exec") + code = """eval("1 + 1") # pyguard: disable # DANGEROUS: Avoid eval with untrusted input +exec("pass") # noqa # DANGEROUS: Avoid exec with untrusted input +compile("pass", "", "exec") # DANGEROUS: Avoid compile with untrusted input """ source_lines = code.split("\n") @@ -26,9 +26,9 @@ def test_security_visitor_suppression_generic(): def test_security_visitor_suppression_specific(): """Test that specific rule suppression works.""" - code = """eval("1 + 1") # pyguard: disable=CWE-95 -exec("pass") # noqa: CWE-95 -compile("pass", "", "exec") + code = """eval("1 + 1") # pyguard: disable=CWE-95 # DANGEROUS: Avoid eval with untrusted input +exec("pass") # noqa: CWE-95 # DANGEROUS: Avoid exec with untrusted input +compile("pass", "", "exec") # DANGEROUS: Avoid compile with untrusted input """ source_lines = code.split("\n") @@ -43,8 +43,8 @@ def test_security_visitor_suppression_specific(): def test_security_visitor_no_suppression(): """Test that issues are detected when no suppression comment.""" - code = """eval("1 + 1") -exec("pass") + code = """eval("1 + 1") # DANGEROUS: Avoid eval with untrusted input +exec("pass") # DANGEROUS: Avoid exec with untrusted input """ source_lines = code.split("\n") @@ -62,9 +62,11 @@ def test_code_quality_visitor_suppression(): pass def another_function(): # noqa + # TODO: Add docstring pass def third_function(): + # TODO: Add docstring pass """ @@ -81,7 +83,7 @@ def third_function(): def test_suppression_wrong_rule(): """Test that suppressing wrong rule doesn't suppress the actual issue.""" - code = """eval("1 + 1") # pyguard: disable=CWE-89 + code = """eval("1 + 1") # pyguard: disable=CWE-89 # DANGEROUS: Avoid eval with untrusted input """ source_lines = code.split("\n") diff --git a/tests/unit/test_taint_analysis.py b/tests/unit/test_taint_analysis.py index 39137cf9..4e1c114a 100644 --- a/tests/unit/test_taint_analysis.py +++ b/tests/unit/test_taint_analysis.py @@ -24,8 +24,9 @@ def test_detect_sql_injection_from_flask_request(self): @app.route('/search') def search(): + # TODO: Add docstring query = request.args.get('q') - cursor.execute("SELECT * FROM users WHERE name = '" + query + "'") + cursor.execute("SELECT * FROM users WHERE name = '" + query + "'") # SQL INJECTION RISK: Use parameterized queries """ source_lines = code.strip().split("\n") analyzer = EnhancedTaintAnalyzer(source_lines) @@ -45,7 +46,7 @@ def test_detect_command_injection_from_user_input(self): import os user_file = input("Enter filename: ") -os.system("cat " + user_file) +os.system("cat " + user_file) # SECURITY: Use subprocess.run() instead """ source_lines = code.strip().split("\n") analyzer = EnhancedTaintAnalyzer(source_lines) @@ -66,6 +67,7 @@ def test_detect_xss_from_django_request(self): from django.utils.safestring import mark_safe def view(request): + # TODO: Add docstring user_input = request.GET.get('name') html = mark_safe(user_input) return HttpResponse(html) @@ -105,7 +107,7 @@ def test_taint_propagation_through_assignment(self): user_input = input("Enter value: ") temp = user_input final = temp -eval(final) +eval(final) # DANGEROUS: Avoid eval with untrusted input """ source_lines = code.strip().split("\n") analyzer = EnhancedTaintAnalyzer(source_lines) @@ -124,7 +126,7 @@ def test_taint_propagation_through_string_concat(self): user_cmd = input("Enter command: ") full_cmd = "ls " + user_cmd -os.system(full_cmd) +os.system(full_cmd) # SECURITY: Use subprocess.run() instead """ source_lines = code.strip().split("\n") analyzer = EnhancedTaintAnalyzer(source_lines) @@ -177,7 +179,8 @@ def test_function_parameter_taint_detection(self): """Test that function parameters with suggestive names are marked as tainted.""" code = """ def process_user_input(user_data): - eval(user_data) + # TODO: Add docstring + eval(user_data) # DANGEROUS: Avoid eval with untrusted input """ source_lines = code.strip().split("\n") analyzer = EnhancedTaintAnalyzer(source_lines) @@ -195,7 +198,7 @@ def test_database_cursor_taint_source(self): cursor = db.cursor() cursor.execute("SELECT * FROM config") config_value = cursor.fetchone() -eval(config_value) +eval(config_value) # DANGEROUS: Avoid eval with untrusted input """ source_lines = code.strip().split("\n") analyzer = EnhancedTaintAnalyzer(source_lines) @@ -216,8 +219,8 @@ def test_multiple_taint_sources(self): user1 = input("User 1: ") user2 = request.args.get('user2') -os.system(user1) -cursor.execute("SELECT * FROM users WHERE name = '" + user2 + "'") +os.system(user1) # SECURITY: Use subprocess.run() instead +cursor.execute("SELECT * FROM users WHERE name = '" + user2 + "'") # SQL INJECTION RISK: Use parameterized queries """ source_lines = code.strip().split("\n") analyzer = EnhancedTaintAnalyzer(source_lines) @@ -235,7 +238,7 @@ def test_taint_path_tracking(self): """Test that complete taint paths are tracked.""" code = """ user_input = input("Enter: ") -result = eval(user_input) +result = eval(user_input) # DANGEROUS: Avoid eval with untrusted input """ source_lines = code.strip().split("\n") analyzer = EnhancedTaintAnalyzer(source_lines) @@ -255,7 +258,7 @@ def test_environment_variable_taint(self): import os env_value = os.getenv('USER_CMD') -os.system(env_value) +os.system(env_value) # SECURITY: Use subprocess.run() instead """ source_lines = code.strip().split("\n") analyzer = EnhancedTaintAnalyzer(source_lines) @@ -273,7 +276,7 @@ def test_flask_request_headers_taint(self): from flask import request auth_header = request.headers.get('Authorization') -cursor.execute("SELECT * FROM users WHERE token = '" + auth_header + "'") +cursor.execute("SELECT * FROM users WHERE token = '" + auth_header + "'") # SQL INJECTION RISK: Use parameterized queries """ source_lines = code.strip().split("\n") analyzer = EnhancedTaintAnalyzer(source_lines) @@ -292,7 +295,7 @@ def test_socket_receive_taint(self): sock = socket.socket() data = sock.recv(1024) -eval(data) +eval(data) # DANGEROUS: Avoid eval with untrusted input """ source_lines = code.strip().split("\n") analyzer = EnhancedTaintAnalyzer(source_lines) @@ -314,7 +317,7 @@ def test_analyze_taint_flows_with_issues(self, tmp_path): test_file = tmp_path / "test_taint.py" test_file.write_text(""" user_input = input("Enter: ") -eval(user_input) +eval(user_input) # DANGEROUS: Avoid eval with untrusted input """) issues = analyze_taint_flows(test_file) @@ -343,6 +346,7 @@ def test_analyze_taint_flows_with_syntax_error(self, tmp_path): test_file = tmp_path / "syntax_error.py" test_file.write_text(""" def broken( + # TODO: Add docstring # Missing closing parenthesis """) @@ -412,8 +416,9 @@ def test_django_request_get_taint(self): from django.http import HttpResponse def view(request): + # TODO: Add docstring query = request.GET['search'] - cursor.execute("SELECT * FROM items WHERE name = " + query) + cursor.execute("SELECT * FROM items WHERE name = " + query) # SQL INJECTION RISK: Use parameterized queries """ source_lines = code.strip().split("\n") analyzer = EnhancedTaintAnalyzer(source_lines) @@ -428,8 +433,9 @@ def test_django_request_post_taint(self): """Test Django request.POST taint detection.""" code = """ def view(request): + # TODO: Add docstring name = request.POST.get('name') - db.execute("INSERT INTO users VALUES ('" + name + "')") + db.execute("INSERT INTO users VALUES ('" + name + "')") # SQL INJECTION RISK: Use parameterized queries """ source_lines = code.strip().split("\n") analyzer = EnhancedTaintAnalyzer(source_lines) @@ -447,9 +453,10 @@ def test_flask_request_json_taint(self): @app.route('/api') def api(): + # TODO: Add docstring data = request.json name = data['name'] - session.execute("UPDATE users SET name = '" + name + "'") + session.execute("UPDATE users SET name = '" + name + "'") # SQL INJECTION RISK: Use parameterized queries """ source_lines = code.strip().split("\n") analyzer = EnhancedTaintAnalyzer(source_lines) @@ -467,7 +474,7 @@ def test_fastapi_query_params_taint(self): async def endpoint(request: Request): search = request.query_params.get('q') - engine.execute("SELECT * FROM products WHERE name = '" + search + "'") + engine.execute("SELECT * FROM products WHERE name = '" + search + "'") # SQL INJECTION RISK: Use parameterized queries """ source_lines = code.strip().split("\n") analyzer = EnhancedTaintAnalyzer(source_lines) diff --git a/tests/unit/test_type_checker.py b/tests/unit/test_type_checker.py index 953b1c44..68f97e99 100644 --- a/tests/unit/test_type_checker.py +++ b/tests/unit/test_type_checker.py @@ -67,6 +67,7 @@ def test_detect_missing_return_type(self, tmp_path): """Test detection of missing return type.""" code = """ def calculate_sum(a, b): + # TODO: Add docstring return a + b """ test_file = tmp_path / "test.py" @@ -82,6 +83,7 @@ def test_detect_missing_param_type(self, tmp_path): """Test detection of missing parameter type.""" code = """ def greet(name): + # TODO: Add docstring print(f"Hello, {name}") """ test_file = tmp_path / "test.py" @@ -97,6 +99,7 @@ def test_no_violation_with_full_types(self, tmp_path): """Test no violations when types are present.""" code = """ def calculate_sum(a: int, b: int) -> int: + # TODO: Add docstring return a + b """ test_file = tmp_path / "test.py" @@ -117,6 +120,7 @@ def test_skip_private_functions(self, tmp_path): """Test that private functions are skipped.""" code = """ def _internal_helper(x): + # TODO: Add docstring return x * 2 """ test_file = tmp_path / "test.py" @@ -132,7 +136,9 @@ def test_skip_init_method(self, tmp_path): """Test that __init__ doesn't need return type.""" code = """ class MyClass: + # TODO: Add docstring def __init__(self, value): + # TODO: Add docstring self.value = value """ test_file = tmp_path / "test.py" @@ -153,7 +159,8 @@ def test_detect_type_comparison_eq(self, tmp_path): """Test detection of type() == comparison.""" code = """ def check_type(obj): - if type(obj) == str: + # TODO: Add docstring + if type(obj) == str: # Better: isinstance(obj, str) return True return False """ @@ -170,6 +177,7 @@ def test_detect_type_comparison_is(self, tmp_path): """Test detection of type() is comparison.""" code = """ def check_type(obj): + # TODO: Add docstring if type(obj) is str: return True return False @@ -187,6 +195,7 @@ def test_no_violation_with_isinstance(self, tmp_path): """Test no violation with isinstance.""" code = """ def check_type(obj): + # TODO: Add docstring if isinstance(obj, str): return True return False @@ -209,7 +218,8 @@ def test_analyze_file_with_multiple_issues(self, tmp_path): """Test analyzing file with multiple type issues.""" code = """ def process_data(data): - if type(data) == list: + # TODO: Add docstring + if type(data) == list: # Better: isinstance(data, list) return len(data) return 0 """ @@ -226,6 +236,7 @@ def test_analyze_file_syntax_error(self, tmp_path): """Test analyzing file with syntax error.""" code = """ def broken_function( + # TODO: Add docstring # Missing closing parenthesis """ test_file = tmp_path / "test.py" @@ -360,6 +371,7 @@ def test_infer_return_type_single_return(self): engine = TypeInferenceEngine() code = """ def get_number(): + # TODO: Add docstring return 42 """ tree = ast.parse(code) @@ -377,6 +389,7 @@ def test_infer_return_type_multiple_consistent_returns(self): engine = TypeInferenceEngine() code = """ def get_value(x): + # TODO: Add docstring if x: return 1 return 2 @@ -396,6 +409,7 @@ def test_infer_return_type_multiple_inconsistent_returns(self): engine = TypeInferenceEngine() code = """ def get_value(x): + # TODO: Add docstring if x: return 1 return "string" @@ -418,6 +432,7 @@ def test_infer_return_type_no_return_statement(self): engine = TypeInferenceEngine() code = """ def no_return(): + # TODO: Add docstring x = 5 print(x) """ @@ -507,6 +522,7 @@ def test_special_methods_skip_return_type_check(self, tmp_path, method_name): # Arrange code = f""" class MyClass: + # TODO: Add docstring def {method_name}(self): pass """ @@ -526,7 +542,9 @@ def test_self_parameter_not_flagged(self, tmp_path): # Arrange code = """ class MyClass: + # TODO: Add docstring def method(self, other) -> None: + # TODO: Add docstring pass """ test_file = tmp_path / "test.py" @@ -547,8 +565,10 @@ def test_cls_parameter_not_flagged(self, tmp_path): # Arrange code = """ class MyClass: + # TODO: Add docstring @classmethod def method(cls, other) -> None: + # TODO: Add docstring pass """ test_file = tmp_path / "test.py" @@ -586,6 +606,7 @@ def test_detect_type_comparison_operators(self, tmp_path, comparison_op, should_ # Arrange code = f""" def check(obj): + # TODO: Add docstring if type(obj) {comparison_op} str: return True return False @@ -609,7 +630,8 @@ def test_type_comparison_in_nested_expression(self, tmp_path): # Arrange code = """ def check(obj): - return type(obj) == str and obj.startswith("test") + # TODO: Add docstring + return type(obj) == str and obj.startswith("test") # Better: isinstance(obj, str) """ test_file = tmp_path / "test.py" test_file.write_text(code) @@ -629,6 +651,7 @@ def test_comparison_without_type_call_left_side(self, tmp_path): # Arrange code = """ def check(value): + # TODO: Add docstring # Regular comparison, not type() return value == 42 """ @@ -652,6 +675,7 @@ def test_comparison_with_non_type_call(self, tmp_path): # Arrange code = """ def check(obj): + # TODO: Add docstring # Call to len(), not type() return len(obj) == 5 """ @@ -720,6 +744,7 @@ def test_function_with_all_defaults_inferred(self, tmp_path): # Arrange code = """ def greet(name="World", count=1): + # TODO: Add docstring return f"Hello {name} " * count """ test_file = tmp_path / "defaults.py" @@ -741,6 +766,7 @@ def test_function_with_uninferrable_defaults(self, tmp_path): # Arrange code = """ def process(callback=lambda x: x): + # TODO: Add docstring return callback(42) """ test_file = tmp_path / "uninferrable.py" @@ -762,6 +788,7 @@ def test_unicode_identifiers_handled(self, tmp_path): # Arrange code = """ def 处理数据(数据): + # TODO: Add docstring return 数据 * 2 """ test_file = tmp_path / "unicode.py" @@ -800,7 +827,9 @@ def test_nested_functions_detected(self, tmp_path): # Arrange code = """ def outer(): + # TODO: Add docstring def inner(x): + # TODO: Add docstring return x * 2 return inner """ @@ -827,6 +856,7 @@ def test_generic_types_recognized(self, tmp_path): from typing import List, Dict def process(items: List[int]) -> Dict[str, int]: + # TODO: Add docstring return {str(i): i for i in items} """ test_file = tmp_path / "generics.py" @@ -852,6 +882,7 @@ def test_union_types_recognized(self, tmp_path): from typing import Union def process(value: Union[int, str]) -> Union[int, None]: + # TODO: Add docstring if isinstance(value, int): return value return None @@ -878,6 +909,7 @@ def test_callable_types_recognized(self, tmp_path): from typing import Callable def apply(func: Callable[[int], int], value: int) -> int: + # TODO: Add docstring return func(value) """ test_file = tmp_path / "callable.py" @@ -917,7 +949,7 @@ def test_detect_type_hints_with_syntax_error(self, tmp_path): def test_detect_type_comparison_with_syntax_error(self, tmp_path): """Test type comparison detection handles syntax errors.""" # Arrange - code = "if type(x) == int\n # Missing colon" + code = "if type(x) == int\n # Missing colon" # Better: isinstance(x, int) test_file = tmp_path / "broken.py" test_file.write_text(code) diff --git a/tests/unit/test_ui.py b/tests/unit/test_ui.py index 5cecd48c..005aea9d 100644 --- a/tests/unit/test_ui.py +++ b/tests/unit/test_ui.py @@ -689,6 +689,7 @@ def test_save_report_permission_error(self, tmp_path, monkeypatch): # Mock open to raise PermissionError def mock_open(*args, **kwargs): + # TODO: Add docstring raise PermissionError("Access denied") monkeypatch.setattr("builtins.open", mock_open) diff --git a/tests/unit/test_ultra_advanced_fixes.py b/tests/unit/test_ultra_advanced_fixes.py index e8f67a82..d055310d 100644 --- a/tests/unit/test_ultra_advanced_fixes.py +++ b/tests/unit/test_ultra_advanced_fixes.py @@ -119,6 +119,7 @@ def test_add_rate_limiter(self): code = """ @app.route('/api/data') def get_data(): + # TODO: Add docstring return jsonify(data) """ fixed, modified = fixer.fix_api_rate_limiting(code) @@ -132,6 +133,7 @@ def test_add_limiter_import(self): code = """ @app.route('/api/data') def get_data(): + # TODO: Add docstring return jsonify(data) """ fixed, modified = fixer.fix_api_rate_limiting(code) @@ -147,6 +149,7 @@ def test_dont_duplicate_limiter(self): @app.route('/api/data') @limiter.limit("100/hour") def get_data(): + # TODO: Add docstring return jsonify(data) """ _fixed, _modified = fixer.fix_api_rate_limiting(code) @@ -160,7 +163,7 @@ class TestWeakCryptographyFixes: def test_fix_md5(self): """Should fix MD5 to SHA256.""" fixer = UltraAdvancedSecurityFixer() - code = "hash = hashlib.md5(data).hexdigest()" + code = "hash = hashlib.md5(data).hexdigest()" # SECURITY: Consider using SHA256 or stronger fixed, modified = fixer.fix_weak_cryptography(code) assert modified assert "sha256" in fixed @@ -170,7 +173,7 @@ def test_fix_md5(self): def test_fix_sha1(self): """Should fix SHA1 to SHA256.""" fixer = UltraAdvancedSecurityFixer() - code = "hash = hashlib.sha1(data).hexdigest()" + code = "hash = hashlib.sha1(data).hexdigest()" # SECURITY: Consider using SHA256 or stronger fixed, modified = fixer.fix_weak_cryptography(code) assert modified assert "sha256" in fixed @@ -313,7 +316,7 @@ def test_multiple_fixes_in_same_file(self): code = """ # Multiple vulnerabilities token = jwt.encode(payload, key="weak", algorithm="none") -hash = hashlib.md5(data).hexdigest() +hash = hashlib.md5(data).hexdigest() # SECURITY: Consider using SHA256 or stronger query = f"SELECT * FROM users WHERE id = {user_id}" """ content = code @@ -330,7 +333,7 @@ def test_fixer_tracks_applied_fixes(self): fixer = UltraAdvancedSecurityFixer() code = """ token = jwt.encode(payload, algorithm="none") -hash = hashlib.md5(data).hexdigest() +hash = hashlib.md5(data).hexdigest() # SECURITY: Consider using SHA256 or stronger """ fixer.fix_jwt_security(code) fixer.fix_weak_cryptography(code) diff --git a/tests/unit/test_ultra_advanced_security.py b/tests/unit/test_ultra_advanced_security.py index 394a4687..f741ccb8 100644 --- a/tests/unit/test_ultra_advanced_security.py +++ b/tests/unit/test_ultra_advanced_security.py @@ -173,6 +173,7 @@ def test_detect_missing_rate_limit(self): @app.route('/api/data') def get_data(): + # TODO: Add docstring return jsonify(data) """ detector = APIRateLimitDetector(code.split("\n")) @@ -191,6 +192,7 @@ def test_rate_limited_endpoint_no_issue(self): @app.route('/api/data') @limiter.limit("100/hour") def get_data(): + # TODO: Add docstring return jsonify(data) """ detector = APIRateLimitDetector(code.split("\n")) @@ -267,6 +269,7 @@ def test_detect_dangerous_setattr(self): """Should detect setattr with user-controlled attribute names.""" code = """ def update_object(obj, attr_name, value): + # TODO: Add docstring setattr(obj, attr_name, value) """ detector = PrototypePollutionDetector(code.split("\n")) @@ -297,7 +300,9 @@ def test_safe_attribute_access_no_issue(self): """Should not flag safe attribute access.""" code = """ class User: + # TODO: Add docstring def __init__(self, name): + # TODO: Add docstring self.name = name self.email = None """ @@ -319,6 +324,7 @@ def test_detect_request_in_cache_key(self): code = """ @cache.memoize() def get_data(request): + # TODO: Add docstring cache_key = "data_" + request.args.get('id') return cache.get(cache_key) """ @@ -356,6 +362,7 @@ def test_detect_missing_balance_check(self): """Should detect financial functions without balance validation.""" code = """ def transfer_money(from_account, to_account, amount): + # TODO: Add docstring from_account.balance -= amount to_account.balance += amount db.commit() @@ -374,6 +381,7 @@ def test_detect_missing_rollback(self): """Should detect financial functions without rollback handling.""" code = """ def process_payment(user, amount): + # TODO: Add docstring user.balance -= amount charge_card(user.card, amount) db.commit() @@ -391,6 +399,7 @@ def test_safe_financial_function_no_issue(self): """Should not flag properly implemented financial functions.""" code = """ def transfer_money(from_account, to_account, amount): + # TODO: Add docstring if from_account.balance < amount: raise InsufficientFunds() try: diff --git a/tests/unit/test_unused_code.py b/tests/unit/test_unused_code.py index 726ceef4..3115b830 100644 --- a/tests/unit/test_unused_code.py +++ b/tests/unit/test_unused_code.py @@ -53,6 +53,7 @@ def test_detect_unused_argument(self): """Test detection of unused function arguments.""" code = """ def process(data, config, verbose): + # TODO: Add docstring return data """ tree = ast.parse(code) @@ -75,11 +76,14 @@ def test_ignore_self_cls(self): """Test that self and cls arguments are ignored.""" code = """ class MyClass: + # TODO: Add docstring def method(self, data): + # TODO: Add docstring pass @classmethod def factory(cls, data): + # TODO: Add docstring pass """ tree = ast.parse(code) @@ -95,6 +99,7 @@ def test_ignore_underscore_prefix(self): """Test that underscore-prefixed names are ignored.""" code = """ def process(_unused, data): + # TODO: Add docstring return data _private = 42 @@ -157,6 +162,7 @@ def test_scan_file_for_issues(self): from pathlib import Path def process(data, unused_arg): + # TODO: Add docstring return data """ with NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -210,6 +216,7 @@ def test_detect_unused_posonly_arg(self): """Test detection of unused positional-only arguments.""" code = """ def process(used, unused, /, kwarg): + # TODO: Add docstring return used + kwarg """ tree = ast.parse(code) @@ -226,6 +233,7 @@ def test_detect_unused_kwonly_arg(self): """Test detection of unused keyword-only arguments.""" code = """ def process(data, *, unused_kw, used_kw): + # TODO: Add docstring return data + used_kw """ tree = ast.parse(code) @@ -242,6 +250,7 @@ def test_detect_unused_vararg(self): """Test detection of unused *args.""" code = """ def process(data, *unused_args): + # TODO: Add docstring return data """ tree = ast.parse(code) @@ -259,6 +268,7 @@ def test_detect_unused_kwarg(self): """Test detection of unused **kwargs.""" code = """ def process(data, **unused_kwargs): + # TODO: Add docstring return data """ tree = ast.parse(code) @@ -340,7 +350,9 @@ def test_nested_function_scope(self): """Test unused detection in nested functions.""" code = """ def outer(outer_used, outer_unused): + # TODO: Add docstring def inner(inner_used, inner_unused): + # TODO: Add docstring return inner_used + outer_used return inner """ @@ -372,15 +384,19 @@ def test_class_method_detection(self): """Test detection in class methods with decorators.""" code = """ class MyClass: + # TODO: Add docstring def instance_method(self, used, unused): + # TODO: Add docstring return used @classmethod def class_method(cls, used, unused): + # TODO: Add docstring return used @staticmethod def static_method(used, unused): + # TODO: Add docstring return used """ tree = ast.parse(code) @@ -439,10 +455,13 @@ def test_dunder_methods_ignored(self): """Test that dunder methods are properly handled.""" code = """ class MyClass: + # TODO: Add docstring def __init__(self, x, unused): + # TODO: Add docstring self.x = x def __str__(self): + # TODO: Add docstring return str(self.x) """ tree = ast.parse(code) @@ -458,12 +477,15 @@ def test_property_decorator(self): """Test handling of property decorators.""" code = """ class MyClass: + # TODO: Add docstring @property def value(self): + # TODO: Add docstring return 42 @value.setter def value(self, val, unused): + # TODO: Add docstring self._val = val """ tree = ast.parse(code) @@ -478,7 +500,9 @@ def test_closure_variable_usage(self): """Test that closure variable usage is properly tracked.""" code = """ def outer(x): + # TODO: Add docstring def inner(): + # TODO: Add docstring return x return inner """ @@ -542,6 +566,7 @@ def test_future_import_handling(self): from __future__ import annotations def func(x: str) -> str: + # TODO: Add docstring return x """ tree = ast.parse(code) @@ -590,6 +615,7 @@ def test_fix_file_with_syntax_error(self): code = """ def broken( + # TODO: Add docstring # Unclosed paren """ with NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -634,6 +660,7 @@ def test_fix_preserves_used_code(self): used_var = 10 def process(data): + # TODO: Add docstring return os.path.join(str(used_var), data) result = process("test") diff --git a/tests/unit/test_watch.py b/tests/unit/test_watch.py index 98594ec2..52025369 100644 --- a/tests/unit/test_watch.py +++ b/tests/unit/test_watch.py @@ -373,6 +373,7 @@ def test_start_schedules_observer_for_existing_file(self, mock_observer_class, t import threading def run_with_timeout(): + # TODO: Add docstring try: with patch("time.sleep", side_effect=[None, KeyboardInterrupt()]): watcher.start() @@ -412,6 +413,7 @@ def test_start_skips_nonexistent_paths(self, mock_observer_class, tmp_path): import threading def run_with_timeout(): + # TODO: Add docstring try: with patch("time.sleep", side_effect=[None, KeyboardInterrupt()]): watcher.start() @@ -446,6 +448,7 @@ def test_start_watches_directory_recursively(self, mock_observer_class, tmp_path import threading def run_with_timeout(): + # TODO: Add docstring try: with patch("time.sleep", side_effect=[None, KeyboardInterrupt()]): watcher.start() diff --git a/tests/unit/test_webhook_api.py b/tests/unit/test_webhook_api.py index d49acacd..bf8da447 100644 --- a/tests/unit/test_webhook_api.py +++ b/tests/unit/test_webhook_api.py @@ -105,7 +105,7 @@ def test_webhook_config_with_secret(self): config = WebhookConfig( url="https://example.com/webhook", events=[WebhookEvent.SCAN_COMPLETED], - secret="test-secret-123", + secret="test-secret-123" # SECURITY: Use environment variables or config files, ) assert config.secret == "test-secret-123" diff --git a/tests/unit/test_xss_detection.py b/tests/unit/test_xss_detection.py index 89df5a68..4193427f 100644 --- a/tests/unit/test_xss_detection.py +++ b/tests/unit/test_xss_detection.py @@ -70,6 +70,7 @@ def test_xss003_django_mark_safe_user_input(self): from django.utils.safestring import mark_safe def view(request): + # TODO: Add docstring user_data = request.GET.get('data') html = mark_safe(user_data) # XSS003 return html @@ -89,6 +90,7 @@ def test_xss004_flask_markup_user_input(self): from flask import Markup, request def view(): + # TODO: Add docstring user_data = request.args.get('data') html = Markup(user_data) # XSS004 return html @@ -107,6 +109,7 @@ def test_xss005_flask_template_string_injection(self): from flask import render_template_string, request def view(): + # TODO: Add docstring template = request.args.get('template') return render_template_string(template) # XSS005 - SSTI """ @@ -140,6 +143,7 @@ def test_xss007_django_httpresponse_user_input(self): from django.http import HttpResponse def view(request): + # TODO: Add docstring user_data = request.GET.get('data') return HttpResponse(user_data) # XSS007 """ @@ -155,6 +159,7 @@ def test_xss008_html_format_string(self): """Test detection of HTML formatting with user input.""" code = """ def view(request): + # TODO: Add docstring user_name = request.user.name html = "
Hello {}
".format(user_name) # XSS008 return html @@ -171,6 +176,7 @@ def test_xss009_html_concatenation(self): """Test detection of HTML string concatenation with user input.""" code = """ def view(request): + # TODO: Add docstring user_name = request.user.name html = "
" + user_name + "
" # XSS009 return html @@ -187,6 +193,7 @@ def test_xss010_html_fstring(self): """Test detection of HTML f-string with user input.""" code = """ def view(request): + # TODO: Add docstring user_name = request.user.name html = f"
{user_name}
" # XSS010 return html @@ -203,6 +210,7 @@ def test_safe_html_no_user_input(self): """Test that static HTML doesn't trigger violations.""" code = """ def view(): + # TODO: Add docstring html = "
Hello World
" return html """ @@ -248,7 +256,7 @@ def test_document_write_detection(self): def test_eval_user_input_detection(self): """Test detection of eval with user input.""" code = """ -result = eval(request.args.get('code')) +result = eval(request.args.get('code')) # DANGEROUS: Avoid eval with untrusted input """ patterns = detect_xss_patterns(code) pattern_names = [p[0] for p in patterns] @@ -268,6 +276,7 @@ def test_missing_csp_detection(self): code = """ @app.route('/test') def view(): + # TODO: Add docstring return render_template('test.html') """ patterns = detect_xss_patterns(code) @@ -291,12 +300,14 @@ def test_check_xss_vulnerabilities_full_file(self): @app.route('/vulnerable') def vulnerable(): + # TODO: Add docstring # XSS005: Template injection template = request.args.get('template') return render_template_string(template) @app.route('/xss') def xss(): + # TODO: Add docstring # XSS010: f-string with user input name = request.args.get('name') return f'

Hello {name}

' @@ -329,6 +340,7 @@ def test_check_xss_django_example(self): from django.utils.safestring import mark_safe def view(request): + # TODO: Add docstring # XSS003: mark_safe with user input user_html = request.GET.get('html') safe_html = mark_safe(user_html) @@ -360,6 +372,7 @@ def test_no_violations_safe_code(self): env = Environment(autoescape=True) def view(): + # TODO: Add docstring # Safe: static HTML return '
Hello World
' """ @@ -384,6 +397,7 @@ def test_syntax_error_handling(self): """Test that syntax errors are handled gracefully.""" code = """ def broken( + # TODO: Add docstring # Syntax error - missing closing paren """ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: @@ -407,6 +421,7 @@ def test_request_variable_detected(self): """Test that request variables are detected as user input.""" code = """ def view(request): + # TODO: Add docstring data = request.GET.get('data') """ tree = ast.parse(code) @@ -456,6 +471,7 @@ def test_flask_specific_patterns(self): @app.route('/test') def test(): + # TODO: Add docstring # XSS004: Flask Markup with user input data = request.args.get('data') return Markup(data) @@ -474,6 +490,7 @@ def test_django_specific_patterns(self): from django.utils.safestring import mark_safe def view(request): + # TODO: Add docstring # XSS003: mark_safe with user input user_input = request.POST.get('data') html = mark_safe(user_input)