ballpython 2.0.4__tar.gz → 2.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ballpython-2.0.4 → ballpython-2.0.5}/PKG-INFO +2 -2
- {ballpython-2.0.4 → ballpython-2.0.5}/README.md +1 -1
- {ballpython-2.0.4 → ballpython-2.0.5}/ballpython.egg-info/PKG-INFO +2 -2
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/__init__.py +1 -1
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/security_scanner.py +37 -2
- {ballpython-2.0.4 → ballpython-2.0.5}/pyproject.toml +1 -1
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_security_scanner.py +26 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/ballpython/__init__.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/ballpython/__main__.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/ballpython/cli.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/ballpython/py.typed +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/ballpython.egg-info/SOURCES.txt +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/ballpython.egg-info/dependency_links.txt +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/ballpython.egg-info/entry_points.txt +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/ballpython.egg-info/requires.txt +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/ballpython.egg-info/top_level.txt +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/__main__.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/baseline.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/cache.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/cli.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/complexity_analyzer.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/config.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/dead_code_detector.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/dependency_auditor.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/discovery.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/explanations.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/frameworks/__init__.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/frameworks/plugins.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/import_resolver.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/linter_formatter.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/modernizer.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/pipeline.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/py.typed +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/syntax_healer.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/taint_engine.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/test_generator.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/type_checker.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/typeshed_resolver.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/pycleaner/verifier.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/setup.cfg +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_baseline_and_explain.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_cache.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_cli.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_cli_prove.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_complexity_analyzer.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_config.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_dead_code_detector.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_dead_code_fixer.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_dependency_auditor.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_discovery.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_frameworks.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_import_resolver.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_linter_formatter.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_modernizer.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_mutation_test_generator.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_pipeline.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_syntax_healer.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_taint_engine.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_test_generator.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_type_checker.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_verifier.py +0 -0
- {ballpython-2.0.4 → ballpython-2.0.5}/tests/test_verifier_pipeline.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ballpython
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.5
|
|
4
4
|
Summary: The Ultimate Static Python Intelligence, Healing, Type Verification, and Security Suite
|
|
5
5
|
Author: Developer
|
|
6
6
|
License-Expression: MIT
|
|
@@ -78,7 +78,7 @@ Add `ballpython` to your `.pre-commit-config.yaml`:
|
|
|
78
78
|
```yaml
|
|
79
79
|
repos:
|
|
80
80
|
- repo: https://github.com/moonrox420/ball-python
|
|
81
|
-
rev: v2.0.
|
|
81
|
+
rev: v2.0.5
|
|
82
82
|
hooks:
|
|
83
83
|
- id: pycleaner
|
|
84
84
|
args: ["check"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ballpython
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.5
|
|
4
4
|
Summary: The Ultimate Static Python Intelligence, Healing, Type Verification, and Security Suite
|
|
5
5
|
Author: Developer
|
|
6
6
|
License-Expression: MIT
|
|
@@ -78,7 +78,7 @@ Add `ballpython` to your `.pre-commit-config.yaml`:
|
|
|
78
78
|
```yaml
|
|
79
79
|
repos:
|
|
80
80
|
- repo: https://github.com/moonrox420/ball-python
|
|
81
|
-
rev: v2.0.
|
|
81
|
+
rev: v2.0.5
|
|
82
82
|
hooks:
|
|
83
83
|
- id: pycleaner
|
|
84
84
|
args: ["check"]
|
|
@@ -190,6 +190,24 @@ class _DangerousCallDetector(ast.NodeVisitor):
|
|
|
190
190
|
self.filepath = filepath
|
|
191
191
|
self.source_lines = source_lines
|
|
192
192
|
self.findings: list[SecurityFinding] = []
|
|
193
|
+
self._is_test_module: bool = False
|
|
194
|
+
|
|
195
|
+
def check_tree(self, tree: ast.AST) -> None:
|
|
196
|
+
"""Inspect imports to determine if module is a test file."""
|
|
197
|
+
for node in ast.walk(tree):
|
|
198
|
+
if isinstance(node, ast.Import):
|
|
199
|
+
for alias in node.names:
|
|
200
|
+
if alias.name.split(".")[0] in ("pytest", "unittest", "mock"):
|
|
201
|
+
self._is_test_module = True
|
|
202
|
+
return
|
|
203
|
+
elif isinstance(node, ast.ImportFrom):
|
|
204
|
+
if node.module and node.module.split(".")[0] in (
|
|
205
|
+
"pytest",
|
|
206
|
+
"unittest",
|
|
207
|
+
"mock",
|
|
208
|
+
):
|
|
209
|
+
self._is_test_module = True
|
|
210
|
+
return
|
|
193
211
|
|
|
194
212
|
def _get_snippet(self, lineno: int) -> str:
|
|
195
213
|
if 1 <= lineno <= len(self.source_lines):
|
|
@@ -245,8 +263,11 @@ class _DangerousCallDetector(ast.NodeVisitor):
|
|
|
245
263
|
parts = [p.lower() for p in Path(filepath).parts[:-1]]
|
|
246
264
|
return any(p in ("tests", "test", "testing") for p in parts)
|
|
247
265
|
|
|
266
|
+
def _is_test(self) -> bool:
|
|
267
|
+
return self._is_test_module or self._is_test_path(self.filepath)
|
|
268
|
+
|
|
248
269
|
def visit_Assert(self, node: ast.Assert) -> None:
|
|
249
|
-
if self.
|
|
270
|
+
if self._is_test():
|
|
250
271
|
return
|
|
251
272
|
self._add_finding(
|
|
252
273
|
SecurityFinding(
|
|
@@ -429,7 +450,20 @@ class _DangerousCallDetector(ast.NodeVisitor):
|
|
|
429
450
|
if isinstance(arg, ast.BinOp) and isinstance(arg.op, ast.Add):
|
|
430
451
|
return True
|
|
431
452
|
if isinstance(arg, ast.Call):
|
|
432
|
-
|
|
453
|
+
if isinstance(arg.func, ast.Attribute) and arg.func.attr == "format":
|
|
454
|
+
# Check for psycopg safe sql.SQL(...).format(...) query composition
|
|
455
|
+
val = arg.func.value
|
|
456
|
+
if isinstance(val, ast.Call):
|
|
457
|
+
func = val.func
|
|
458
|
+
func_id = ""
|
|
459
|
+
if isinstance(func, ast.Name):
|
|
460
|
+
func_id = func.id
|
|
461
|
+
elif isinstance(func, ast.Attribute):
|
|
462
|
+
func_id = func.attr
|
|
463
|
+
if func_id == "SQL":
|
|
464
|
+
return False
|
|
465
|
+
return True
|
|
466
|
+
return False
|
|
433
467
|
if isinstance(arg, ast.BinOp) and isinstance(arg.op, ast.Mod):
|
|
434
468
|
return isinstance(arg.left, ast.Constant) and isinstance(
|
|
435
469
|
arg.left.value, str
|
|
@@ -483,6 +517,7 @@ class SecurityScanner:
|
|
|
483
517
|
tree = ast.parse(source, filename=filename)
|
|
484
518
|
source_lines = source.splitlines()
|
|
485
519
|
detector = _DangerousCallDetector(filename, source_lines)
|
|
520
|
+
detector.check_tree(tree)
|
|
486
521
|
detector.visit(tree)
|
|
487
522
|
findings.extend(detector.findings)
|
|
488
523
|
except SyntaxError:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ballpython"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.5"
|
|
8
8
|
description = "The Ultimate Static Python Intelligence, Healing, Type Verification, and Security Suite"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -210,3 +210,29 @@ class TestSecurityScanner:
|
|
|
210
210
|
def test_security_report_by_category(self) -> None:
|
|
211
211
|
report = SecurityReport(findings=[])
|
|
212
212
|
assert len(report.by_category("dangerous-eval")) == 0
|
|
213
|
+
|
|
214
|
+
def test_assert_suppressed_in_module_importing_pytest(self) -> None:
|
|
215
|
+
source = textwrap.dedent("""
|
|
216
|
+
import pytest
|
|
217
|
+
|
|
218
|
+
def test_feature():
|
|
219
|
+
assert 1 + 1 == 2
|
|
220
|
+
""")
|
|
221
|
+
scanner = SecurityScanner(severity_threshold="LOW")
|
|
222
|
+
report = scanner.scan_source(source, filename="test_root_file.py")
|
|
223
|
+
categories = {f.category for f in report.findings}
|
|
224
|
+
assert "assert-in-production" not in categories
|
|
225
|
+
|
|
226
|
+
def test_safe_sql_composition_not_flagged(self) -> None:
|
|
227
|
+
source = textwrap.dedent("""
|
|
228
|
+
from psycopg import sql
|
|
229
|
+
|
|
230
|
+
def get_data(conn, tbl):
|
|
231
|
+
with conn.cursor() as cur:
|
|
232
|
+
cur.execute(sql.SQL("SELECT count(*) FROM {}").format(sql.Identifier(tbl)))
|
|
233
|
+
""")
|
|
234
|
+
scanner = SecurityScanner()
|
|
235
|
+
report = scanner.scan_source(source, filename="query.py")
|
|
236
|
+
categories = {f.category for f in report.findings}
|
|
237
|
+
assert "sql-injection" not in categories
|
|
238
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|