attackmap-analyzer-python 0.2.0__py3-none-any.whl
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.
- attackmap_analyzer_python/__init__.py +3 -0
- attackmap_analyzer_python/_pyast.py +90 -0
- attackmap_analyzer_python/analyzer.py +783 -0
- attackmap_analyzer_python/django_urls.py +667 -0
- attackmap_analyzer_python/litestar_routes.py +358 -0
- attackmap_analyzer_python/sanic_routes.py +77 -0
- attackmap_analyzer_python/tornado_routes.py +182 -0
- attackmap_analyzer_python-0.2.0.dist-info/METADATA +134 -0
- attackmap_analyzer_python-0.2.0.dist-info/RECORD +13 -0
- attackmap_analyzer_python-0.2.0.dist-info/WHEEL +5 -0
- attackmap_analyzer_python-0.2.0.dist-info/entry_points.txt +2 -0
- attackmap_analyzer_python-0.2.0.dist-info/licenses/LICENSE +21 -0
- attackmap_analyzer_python-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Small, defensive helpers around :mod:`ast` for the route extractors.
|
|
2
|
+
|
|
3
|
+
The analyzed repository is untrusted: files can be huge, truncated, Python 2,
|
|
4
|
+
or deliberately pathological. Nothing here executes code — ``ast.parse`` only
|
|
5
|
+
builds a syntax tree — but parsing still has to be bounded and must never
|
|
6
|
+
raise out of ``analyze()``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
|
|
13
|
+
# Files above this size are left to the regex extractors. Real URLconfs,
|
|
14
|
+
# controllers and handler modules are far smaller; this only bounds the cost of
|
|
15
|
+
# a hostile or generated file.
|
|
16
|
+
MAX_PARSE_CHARS = 1_000_000
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def safe_parse(content: str) -> ast.Module | None:
|
|
20
|
+
"""Parse ``content`` or return ``None`` (too big, not Python 3, hostile)."""
|
|
21
|
+
if len(content) > MAX_PARSE_CHARS:
|
|
22
|
+
return None
|
|
23
|
+
try:
|
|
24
|
+
return ast.parse(content)
|
|
25
|
+
except (SyntaxError, ValueError, RecursionError, MemoryError, OverflowError):
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def call_name(node: ast.AST) -> str | None:
|
|
30
|
+
"""The trailing name of a call target: ``path`` for ``path(...)`` and
|
|
31
|
+
``urls.path(...)``, ``DefaultRouter`` for ``routers.DefaultRouter()``."""
|
|
32
|
+
if isinstance(node, ast.Call):
|
|
33
|
+
node = node.func
|
|
34
|
+
if isinstance(node, ast.Name):
|
|
35
|
+
return node.id
|
|
36
|
+
if isinstance(node, ast.Attribute):
|
|
37
|
+
return node.attr
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def dotted(node: ast.AST, *, max_parts: int = 8) -> str | None:
|
|
42
|
+
"""``a.b.c`` for a Name/Attribute chain, else ``None``."""
|
|
43
|
+
parts: list[str] = []
|
|
44
|
+
while isinstance(node, ast.Attribute) and len(parts) < max_parts:
|
|
45
|
+
parts.append(node.attr)
|
|
46
|
+
node = node.value
|
|
47
|
+
if not isinstance(node, ast.Name) or len(parts) >= max_parts:
|
|
48
|
+
return None
|
|
49
|
+
parts.append(node.id)
|
|
50
|
+
return ".".join(reversed(parts))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def str_const(node: ast.AST | None) -> str | None:
|
|
54
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
55
|
+
return node.value
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def keyword(call: ast.Call, name: str) -> ast.expr | None:
|
|
60
|
+
for kw in call.keywords:
|
|
61
|
+
if kw.arg == name:
|
|
62
|
+
return kw.value
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def str_list(node: ast.AST | None, *, limit: int = 64) -> list[str] | None:
|
|
67
|
+
"""String elements of a list/tuple/set literal (non-strings skipped), or
|
|
68
|
+
``None`` when ``node`` isn't such a literal."""
|
|
69
|
+
if not isinstance(node, (ast.List, ast.Tuple, ast.Set)):
|
|
70
|
+
return None
|
|
71
|
+
out: list[str] = []
|
|
72
|
+
for elt in node.elts[:limit]:
|
|
73
|
+
value = str_const(elt)
|
|
74
|
+
if value is not None:
|
|
75
|
+
out.append(value)
|
|
76
|
+
return out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def expr_names(node: ast.AST | None, *, limit: int = 16) -> list[str]:
|
|
80
|
+
"""Readable names of the elements of a list/tuple literal (``[a, b.c]`` →
|
|
81
|
+
``["a", "b.c"]``; calls give their callee)."""
|
|
82
|
+
if not isinstance(node, (ast.List, ast.Tuple, ast.Set)):
|
|
83
|
+
return []
|
|
84
|
+
out: list[str] = []
|
|
85
|
+
for elt in node.elts[:limit]:
|
|
86
|
+
target = elt.func if isinstance(elt, ast.Call) else elt
|
|
87
|
+
name = dotted(target)
|
|
88
|
+
if name:
|
|
89
|
+
out.append(name)
|
|
90
|
+
return out
|