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.
@@ -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"]