attackmap-analyzer-cpp 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_cpp/__init__.py +3 -0
- attackmap_analyzer_cpp/analyzer.py +601 -0
- attackmap_analyzer_cpp/framework_routes.py +255 -0
- attackmap_analyzer_cpp/native_deps.py +524 -0
- attackmap_analyzer_cpp/native_sinks.py +1737 -0
- attackmap_analyzer_cpp-0.2.0.dist-info/METADATA +170 -0
- attackmap_analyzer_cpp-0.2.0.dist-info/RECORD +11 -0
- attackmap_analyzer_cpp-0.2.0.dist-info/WHEEL +5 -0
- attackmap_analyzer_cpp-0.2.0.dist-info/entry_points.txt +2 -0
- attackmap_analyzer_cpp-0.2.0.dist-info/licenses/LICENSE +21 -0
- attackmap_analyzer_cpp-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Route extraction for cpp-httplib, oat++ and restbed (AttackMap#191).
|
|
2
|
+
|
|
3
|
+
- **cpp-httplib**: ``svr.Get("/x", ...)`` / ``Post`` / ``Put`` / ``Patch`` /
|
|
4
|
+
``Delete`` / ``Options`` on a receiver declared in the same file as a
|
|
5
|
+
``httplib::Server`` or ``SSLServer`` (a ``httplib::Client`` uses the same
|
|
6
|
+
method names, so an undeclared receiver is skipped). Regex paths
|
|
7
|
+
(``R"(/numbers/(\\d+))"``) are kept as written.
|
|
8
|
+
- **oat++**: ``ENDPOINT("GET", "users/{id}", name, ...)`` and
|
|
9
|
+
``ENDPOINT_ASYNC``. An ``AUTHORIZATION(...)`` parameter sets
|
|
10
|
+
``Route.auth = "required"``. Paths get a leading ``/``. A router-level
|
|
11
|
+
prefix isn't visible in the controller and isn't applied.
|
|
12
|
+
- **restbed**: ``resource->set_path("/x")`` / ``set_paths({...})`` on a
|
|
13
|
+
``Resource`` variable, joined with
|
|
14
|
+
that resource's ``set_method_handler("GET", ...)`` calls (``ANY`` when it
|
|
15
|
+
has none). ``set_authentication_handler`` on the resource, or on a service
|
|
16
|
+
in the same file, sets ``auth = "required"``.
|
|
17
|
+
|
|
18
|
+
Every scanner is bounded; patterns are module-level and linear.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
|
|
25
|
+
from attackmap.sdk import line_of
|
|
26
|
+
|
|
27
|
+
from attackmap.sdk import Route
|
|
28
|
+
|
|
29
|
+
_MAX_ARGS_SPAN = 2000
|
|
30
|
+
|
|
31
|
+
# ---------- masking ----------
|
|
32
|
+
|
|
33
|
+
_MASK_START_RE = re.compile(r"(?P<line>//)|(?P<block>/\*)|(?P<raw>(?<![\w])(?:u8|[uUL])?R\"(?P<delim>[^()\\\s\"]{0,16})\()|(?P<str>\")|(?P<chr>')")
|
|
34
|
+
_STRING_BODY_RE = re.compile(r"(?:[^\"\\\n]|\\[\s\S])*")
|
|
35
|
+
_CHAR_BODY_RE = re.compile(r"(?:[^'\\\n]|\\[\s\S]){0,8}'")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _blank(text: str) -> str:
|
|
39
|
+
return re.sub(r"[^\n]", " ", text)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _masks(text: str) -> tuple[str, str]:
|
|
43
|
+
"""``(code, bare)``: ``code`` has comments blanked (literals kept, for the
|
|
44
|
+
paths); ``bare`` also has literal contents blanked, to tell whether a
|
|
45
|
+
match starts inside a string. One linear pass; same length as ``text``."""
|
|
46
|
+
code: list[str] = []
|
|
47
|
+
bare: list[str] = []
|
|
48
|
+
pos = 0
|
|
49
|
+
n = len(text)
|
|
50
|
+
while pos < n:
|
|
51
|
+
match = _MASK_START_RE.search(text, pos)
|
|
52
|
+
if match is None:
|
|
53
|
+
break
|
|
54
|
+
start = match.start()
|
|
55
|
+
code.append(text[pos:start])
|
|
56
|
+
bare.append(text[pos:start])
|
|
57
|
+
if match.group("line") is not None:
|
|
58
|
+
newline = text.find("\n", start)
|
|
59
|
+
end = n if newline == -1 else newline
|
|
60
|
+
code.append(_blank(text[start:end]))
|
|
61
|
+
bare.append(_blank(text[start:end]))
|
|
62
|
+
elif match.group("block") is not None:
|
|
63
|
+
close = text.find("*/", match.end())
|
|
64
|
+
end = n if close == -1 else close + 2
|
|
65
|
+
code.append(_blank(text[start:end]))
|
|
66
|
+
bare.append(_blank(text[start:end]))
|
|
67
|
+
else:
|
|
68
|
+
if match.group("raw") is not None:
|
|
69
|
+
terminator = ")" + match.group("delim") + '"'
|
|
70
|
+
close = text.find(terminator, match.end())
|
|
71
|
+
end = n if close == -1 else close + len(terminator)
|
|
72
|
+
elif match.group("str") is not None:
|
|
73
|
+
body = _STRING_BODY_RE.match(text, match.end())
|
|
74
|
+
end = body.end() if body else match.end()
|
|
75
|
+
if end < n and text[end] == '"':
|
|
76
|
+
end += 1
|
|
77
|
+
else:
|
|
78
|
+
body = _CHAR_BODY_RE.match(text, match.end())
|
|
79
|
+
end = body.end() if body else match.end()
|
|
80
|
+
code.append(text[start:end])
|
|
81
|
+
bare.append(text[start : match.end()] + _blank(text[match.end() : end]))
|
|
82
|
+
pos = end
|
|
83
|
+
code.append(text[pos:])
|
|
84
|
+
bare.append(text[pos:])
|
|
85
|
+
return "".join(code), "".join(bare)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _in_code(code: str, bare: str, offset: int) -> bool:
|
|
89
|
+
"""The match at ``offset`` starts in code, not inside a string literal."""
|
|
90
|
+
return offset < len(bare) and bare[offset] == code[offset]
|
|
91
|
+
|
|
92
|
+
# ---------- cpp-httplib ----------
|
|
93
|
+
|
|
94
|
+
_HTTPLIB_FILE_RE = re.compile(r"\bhttplib\b")
|
|
95
|
+
_HTTPLIB_SERVER_DECL_RE = re.compile(
|
|
96
|
+
r"\b(?:httplib::)?(?:SSL)?Server\b\s*(?:[*&]\s*)?([A-Za-z_]\w*)\s*[;({=,)]"
|
|
97
|
+
r"|\b(?:std::)?(?:unique_ptr|shared_ptr)\s*<\s*(?:httplib::)?(?:SSL)?Server\s*>\s*([A-Za-z_]\w*)"
|
|
98
|
+
r"|\b([A-Za-z_]\w*)\s*=\s*(?:std::)?make_(?:unique|shared)\s*<\s*(?:httplib::)?(?:SSL)?Server\s*>"
|
|
99
|
+
)
|
|
100
|
+
_HTTPLIB_ROUTE_RE = re.compile(
|
|
101
|
+
r"\b([A-Za-z_]\w*)\s*(?:\.|->)\s*(Get|Post|Put|Patch|Delete|Options)\s*\(\s*"
|
|
102
|
+
r"(?:R\"(?P<delim>[^()\\\s\"]{0,16})\((?P<raw>[^\n]{0,300}?)\)(?P=delim)\"|\"(?P<plain>(?:[^\"\\\n]|\\.){0,300})\")"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# ---------- oat++ ----------
|
|
106
|
+
|
|
107
|
+
_OATPP_ENDPOINT_RE = re.compile(r"\bENDPOINT(?:_ASYNC)?\s*\(\s*\"([A-Za-z]+)\"\s*,\s*\"([^\"\n]*)\"")
|
|
108
|
+
_OATPP_AUTH_RE = re.compile(r"\bAUTHORIZATION\s*\(")
|
|
109
|
+
|
|
110
|
+
# ---------- restbed ----------
|
|
111
|
+
|
|
112
|
+
_RESTBED_FILE_RE = re.compile(r"\brestbed\b|<\s*Resource\s*>")
|
|
113
|
+
# A Resource variable: `auto r = make_shared< Resource >()`, `Resource r;`,
|
|
114
|
+
# `shared_ptr< Resource > r`. A client `Request` also has set_path().
|
|
115
|
+
_RESTBED_RESOURCE_DECL_RE = re.compile(
|
|
116
|
+
r"\b([A-Za-z_]\w*)\s*=\s*(?:std::)?make_shared\s*<\s*(?:restbed::)?Resource\s*>"
|
|
117
|
+
r"|(?:\bshared_ptr\s*<\s*(?:restbed::)?Resource\s*>|(?<![\w:<])(?:restbed::)?Resource)\s*[&*]?\s*([A-Za-z_]\w*)\s*[;=({,)]"
|
|
118
|
+
)
|
|
119
|
+
_RESTBED_SET_PATH_RE = re.compile(r"\b([A-Za-z_]\w*)\s*(?:->|\.)\s*set_path\s*\(\s*\"([^\"\n]*)\"\s*\)")
|
|
120
|
+
_RESTBED_SET_PATHS_RE = re.compile(r"\b([A-Za-z_]\w*)\s*(?:->|\.)\s*set_paths\s*\(")
|
|
121
|
+
_STRING_LITERAL_RE = re.compile(r"\"([^\"\n]*)\"")
|
|
122
|
+
_RESTBED_METHOD_RE = re.compile(r"\b([A-Za-z_]\w*)\s*(?:->|\.)\s*set_method_handler\s*\(\s*\"([A-Za-z]+)\"")
|
|
123
|
+
_RESTBED_SERVICE_DECL_RE = re.compile(
|
|
124
|
+
r"\b([A-Za-z_]\w*)\s*=\s*(?:std::)?make_shared\s*<\s*(?:restbed::)?Service\s*>"
|
|
125
|
+
r"|(?:\bshared_ptr\s*<\s*(?:restbed::)?Service\s*>|(?<![\w:<])(?:restbed::)?Service)\s*[&*]?\s*([A-Za-z_]\w*)\s*[;=({,)]"
|
|
126
|
+
)
|
|
127
|
+
_RESTBED_AUTH_RE = re.compile(r"\b([A-Za-z_]\w*)\s*(?:->|\.)\s*set_authentication_handler\s*\(")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _paren_args(content: str, open_paren: int) -> str | None:
|
|
131
|
+
depth = 0
|
|
132
|
+
for i in range(open_paren, min(len(content), open_paren + _MAX_ARGS_SPAN)):
|
|
133
|
+
c = content[i]
|
|
134
|
+
if c == "(":
|
|
135
|
+
depth += 1
|
|
136
|
+
elif c == ")":
|
|
137
|
+
depth -= 1
|
|
138
|
+
if depth == 0:
|
|
139
|
+
return content[open_paren + 1 : i]
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _rooted(path: str) -> str:
|
|
144
|
+
"""oat++ and restbed resolve paths against the root: `users` is `/users`."""
|
|
145
|
+
return path if path.startswith("/") else "/" + path
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _snippet(content: str, offset: int) -> str:
|
|
149
|
+
start = content.rfind("\n", 0, offset) + 1
|
|
150
|
+
end = content.find("\n", offset)
|
|
151
|
+
return content[start : end if end != -1 else len(content)].strip()[:160]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def httplib_routes(content: str, relative: str, masks: tuple[str, str] | None = None) -> list[Route]:
|
|
155
|
+
code, bare = masks or _masks(content)
|
|
156
|
+
if _HTTPLIB_FILE_RE.search(content) is None:
|
|
157
|
+
return []
|
|
158
|
+
servers = {
|
|
159
|
+
m.group(1) or m.group(2) or m.group(3)
|
|
160
|
+
for m in _HTTPLIB_SERVER_DECL_RE.finditer(bare)
|
|
161
|
+
}
|
|
162
|
+
servers.discard(None)
|
|
163
|
+
if not servers:
|
|
164
|
+
return []
|
|
165
|
+
routes: list[Route] = []
|
|
166
|
+
for match in _HTTPLIB_ROUTE_RE.finditer(code):
|
|
167
|
+
if match.group(1) not in servers or not _in_code(code, bare, match.start()):
|
|
168
|
+
continue
|
|
169
|
+
raw = match.group("raw")
|
|
170
|
+
path = raw if raw is not None else match.group("plain").replace("\\\\", "\\")
|
|
171
|
+
routes.append(Route(path=path, method=match.group(2).upper(), file=relative, line=line_of(content, match.start())))
|
|
172
|
+
return routes
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def oatpp_routes(content: str, relative: str, masks: tuple[str, str] | None = None) -> list[Route]:
|
|
176
|
+
code, bare = masks or _masks(content)
|
|
177
|
+
routes: list[Route] = []
|
|
178
|
+
for match in _OATPP_ENDPOINT_RE.finditer(code):
|
|
179
|
+
if not _in_code(code, bare, match.start()):
|
|
180
|
+
continue
|
|
181
|
+
path = _rooted(match.group(2))
|
|
182
|
+
open_paren = code.index("(", match.start())
|
|
183
|
+
args = _paren_args(bare, open_paren) or ""
|
|
184
|
+
auth = _OATPP_AUTH_RE.search(args)
|
|
185
|
+
route = Route(path=path, method=match.group(1).upper(), file=relative, line=line_of(content, match.start()))
|
|
186
|
+
if auth is not None:
|
|
187
|
+
route.auth = "required"
|
|
188
|
+
route.guards = ["AUTHORIZATION"]
|
|
189
|
+
route.guard_evidence = _snippet(content, open_paren + 1 + auth.start())
|
|
190
|
+
routes.append(route)
|
|
191
|
+
return routes
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def restbed_routes(content: str, relative: str, masks: tuple[str, str] | None = None) -> list[Route]:
|
|
195
|
+
code, bare = masks or _masks(content)
|
|
196
|
+
if _RESTBED_FILE_RE.search(bare) is None:
|
|
197
|
+
return []
|
|
198
|
+
resources = {m.group(1) or m.group(2) for m in _RESTBED_RESOURCE_DECL_RE.finditer(bare)}
|
|
199
|
+
services = {m.group(1) or m.group(2) for m in _RESTBED_SERVICE_DECL_RE.finditer(bare)}
|
|
200
|
+
paths: dict[str, list[tuple[str, int]]] = {}
|
|
201
|
+
for match in _RESTBED_SET_PATH_RE.finditer(code):
|
|
202
|
+
if match.group(1) in resources and _in_code(code, bare, match.start()):
|
|
203
|
+
paths.setdefault(match.group(1), []).append((_rooted(match.group(2)), match.start()))
|
|
204
|
+
for match in _RESTBED_SET_PATHS_RE.finditer(code):
|
|
205
|
+
if match.group(1) in resources and _in_code(code, bare, match.start()):
|
|
206
|
+
for literal in _STRING_LITERAL_RE.finditer(_paren_args(code, match.end() - 1) or ""):
|
|
207
|
+
paths.setdefault(match.group(1), []).append((_rooted(literal.group(1)), match.start()))
|
|
208
|
+
if not paths:
|
|
209
|
+
return []
|
|
210
|
+
methods: dict[str, list[str]] = {}
|
|
211
|
+
for match in _RESTBED_METHOD_RE.finditer(code):
|
|
212
|
+
if _in_code(code, bare, match.start()):
|
|
213
|
+
methods.setdefault(match.group(1), []).append(match.group(2).upper())
|
|
214
|
+
# A handler on a Resource guards that resource only (even one whose path
|
|
215
|
+
# isn't a literal); on a Service, every resource in the file; on an
|
|
216
|
+
# unknown receiver, nothing (ambiguous: auth stays unknown).
|
|
217
|
+
auth_resources: dict[str, int] = {}
|
|
218
|
+
service_auth: int | None = None
|
|
219
|
+
for match in _RESTBED_AUTH_RE.finditer(bare):
|
|
220
|
+
receiver = match.group(1)
|
|
221
|
+
if receiver in resources:
|
|
222
|
+
auth_resources.setdefault(receiver, match.start())
|
|
223
|
+
elif receiver in services and service_auth is None:
|
|
224
|
+
service_auth = match.start()
|
|
225
|
+
routes: list[Route] = []
|
|
226
|
+
for resource, declared in paths.items():
|
|
227
|
+
guard_at = auth_resources.get(resource, service_auth)
|
|
228
|
+
for path, offset in declared:
|
|
229
|
+
for method in sorted(set(methods.get(resource, ["ANY"]))):
|
|
230
|
+
route = Route(path=path, method=method, file=relative, line=line_of(content, offset))
|
|
231
|
+
if guard_at is not None:
|
|
232
|
+
route.auth = "required"
|
|
233
|
+
route.guards = ["set_authentication_handler"]
|
|
234
|
+
route.guard_evidence = _snippet(content, guard_at)
|
|
235
|
+
routes.append(route)
|
|
236
|
+
return routes
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def framework_routes(content: str, relative: str) -> list[Route]:
|
|
240
|
+
"""Routes from cpp-httplib, oat++ and restbed registrations in one file.
|
|
241
|
+
Commented-out code and text inside string literals don't count."""
|
|
242
|
+
if not _FRAMEWORK_HINT_RE.search(content):
|
|
243
|
+
return []
|
|
244
|
+
masks = _masks(content)
|
|
245
|
+
return [
|
|
246
|
+
*httplib_routes(content, relative, masks),
|
|
247
|
+
*oatpp_routes(content, relative, masks),
|
|
248
|
+
*restbed_routes(content, relative, masks),
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
_FRAMEWORK_HINT_RE = re.compile(r"\bhttplib\b|\bENDPOINT|\bset_paths?\b")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
__all__ = ["framework_routes", "httplib_routes", "oatpp_routes", "restbed_routes"]
|