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.
@@ -0,0 +1,3 @@
1
+ from .analyzer import PythonAnalyzer
2
+
3
+ __all__ = ["PythonAnalyzer"]
@@ -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