attackmap-analyzer-c 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_c/__init__.py +3 -0
- attackmap_analyzer_c/analyzer.py +431 -0
- attackmap_analyzer_c/native_deps.py +524 -0
- attackmap_analyzer_c/native_sinks.py +1737 -0
- attackmap_analyzer_c-0.2.0.dist-info/METADATA +167 -0
- attackmap_analyzer_c-0.2.0.dist-info/RECORD +10 -0
- attackmap_analyzer_c-0.2.0.dist-info/WHEEL +5 -0
- attackmap_analyzer_c-0.2.0.dist-info/entry_points.txt +2 -0
- attackmap_analyzer_c-0.2.0.dist-info/licenses/LICENSE +21 -0
- attackmap_analyzer_c-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""C ecosystem analyzer for AttackMap.
|
|
2
|
+
|
|
3
|
+
Coverage (v0.1):
|
|
4
|
+
- Web frameworks: civetweb (mg_set_request_handler routes), libmicrohttpd
|
|
5
|
+
(MHD_start_daemon entrypoint), mongoose (mg_http_listen entrypoint +
|
|
6
|
+
mg_http_match_uri pseudo-routes), libonion (onion_url_add routes)
|
|
7
|
+
- HTTP clients (external calls): libcurl (CURLOPT_URL string literals)
|
|
8
|
+
- Databases: sqlite3 (sqlite3_open*), libpq (PQconnectdb*), MySQL/MariaDB C
|
|
9
|
+
client (mysql_real_connect), hiredis (redisConnect*), MongoDB C driver
|
|
10
|
+
(mongoc_client_new)
|
|
11
|
+
- Auth/crypto: OpenSSL (broad framework hint via EVP_/SSL_/RSA_), mbedTLS,
|
|
12
|
+
libsodium (crypto_pwhash, crypto_secretbox), Argon2 reference impl,
|
|
13
|
+
bcrypt-c, scrypt
|
|
14
|
+
- Secrets: getenv / secure_getenv / getenv_s with secret-shaped names
|
|
15
|
+
- Service hints: project name from CMakeLists.txt
|
|
16
|
+
- Dependencies (AttackMap#191): CMake FetchContent / ExternalProject / CPM /
|
|
17
|
+
find_package, Conan and vcpkg manifests (see ``native_deps``, shared
|
|
18
|
+
verbatim with attackmap-analyzer-cpp)
|
|
19
|
+
- Native sinks (AttackMap#189): unbounded copies, non-constant formats,
|
|
20
|
+
command execution, input-controlled copy lengths and allocation-size
|
|
21
|
+
overflows, emitted as ``CodeWeakness`` records (see ``native_sinks``)
|
|
22
|
+
|
|
23
|
+
C is more fragmented than other ecosystems — there's no single dominant
|
|
24
|
+
HTTP framework, and routing patterns vary widely. We capture what regex
|
|
25
|
+
can reach reliably and conservatively confidence-tag pattern hits.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from attackmap.sdk import (
|
|
34
|
+
DEFAULT_SKIP_DIRS,
|
|
35
|
+
AnalyzerMetadata,
|
|
36
|
+
AuthHint,
|
|
37
|
+
DatabaseHint,
|
|
38
|
+
EntrypointHint,
|
|
39
|
+
ExternalCall,
|
|
40
|
+
FrameworkHint,
|
|
41
|
+
iter_repo_files,
|
|
42
|
+
line_of,
|
|
43
|
+
read_source,
|
|
44
|
+
rel,
|
|
45
|
+
Route,
|
|
46
|
+
ScanResult,
|
|
47
|
+
SecretHint,
|
|
48
|
+
ServiceHint,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
from .native_deps import MANIFEST_NAMES, MANIFEST_SUFFIXES, dedupe_dependencies, is_manifest, manifest_records
|
|
52
|
+
from .native_sinks import find_native_weaknesses
|
|
53
|
+
|
|
54
|
+
CODE_SUFFIXES = {".c", ".h"}
|
|
55
|
+
# C-specific additions to the shared skip list (which already covers build/,
|
|
56
|
+
# out/, vendor/, node_modules/, .git/, ...). Matched against directory names
|
|
57
|
+
# *inside* the repo only.
|
|
58
|
+
SKIP_DIRS = DEFAULT_SKIP_DIRS | {"_deps", "third_party", "external", ".cache"}
|
|
59
|
+
|
|
60
|
+
# ---------- .h ownership (shared rule with attackmap-analyzer-cpp) ----------
|
|
61
|
+
#
|
|
62
|
+
# A ``.h`` header is either C or C++, and only one analyzer may claim it, or a
|
|
63
|
+
# C++ project's headers (e.g. Drogon controllers, whose routes live in the
|
|
64
|
+
# header) get analyzed as C and labelled language ``c``. The rule is decided
|
|
65
|
+
# per repo and is mirrored *verbatim* in attackmap-analyzer-cpp, so both
|
|
66
|
+
# plugins agree whichever of ``-m c`` / ``-m cpp`` is selected:
|
|
67
|
+
#
|
|
68
|
+
# ``.h`` belongs to C++ if the repo has any C++ source/header (the suffixes
|
|
69
|
+
# below) or a CMakeLists.txt that enables CXX (``project(... CXX ...)``,
|
|
70
|
+
# ``enable_language(CXX)`` or ``CMAKE_CXX_STANDARD``); otherwise to C.
|
|
71
|
+
#
|
|
72
|
+
# Keep ``_CXX_MARKER_SUFFIXES``, ``_CMAKE_CXX_PATTERN`` and
|
|
73
|
+
# ``_OWNERSHIP_EXTRA_SKIP_DIRS`` in sync with the cpp plugin.
|
|
74
|
+
_CXX_MARKER_SUFFIXES = {".cpp", ".cc", ".cxx", ".hpp", ".hxx", ".ipp", ".tpp"}
|
|
75
|
+
_CMAKE_CXX_PATTERN = re.compile(
|
|
76
|
+
r"\b(?i:project)\s*\([^)]*\bCXX\b"
|
|
77
|
+
r"|\b(?i:enable_language)\s*\(\s*CXX\b"
|
|
78
|
+
r"|\bCMAKE_CXX_STANDARD\b",
|
|
79
|
+
)
|
|
80
|
+
# The cpp plugin also prunes these; ignore C++ markers under them here too so
|
|
81
|
+
# both plugins see the same marker set.
|
|
82
|
+
_OWNERSHIP_EXTRA_SKIP_DIRS = {"Debug", "Release"}
|
|
83
|
+
_SNIPPET_MAX_CHARS = 160
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---------- Patterns ----------
|
|
87
|
+
|
|
88
|
+
# civetweb: mg_set_request_handler(ctx, "/path", handler, NULL)
|
|
89
|
+
CIVETWEB_ROUTE_PATTERN = re.compile(
|
|
90
|
+
r'\bmg_set_request_handler\s*\(\s*\w+\s*,\s*"([^"]+)"',
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# mongoose: mg_http_match_uri(hm, "/path") inside an event handler — pseudo-routes
|
|
94
|
+
MONGOOSE_MATCH_URI_PATTERN = re.compile(
|
|
95
|
+
r'\bmg_http_match_uri\s*\(\s*\w+\s*,\s*"([^"]+)"',
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# libonion: onion_url_add(url, "^/path/?$", handler, ...)
|
|
99
|
+
ONION_ROUTE_PATTERN = re.compile(
|
|
100
|
+
r'\bonion_url_add(?:_static|_with_data)?\s*\(\s*\w+\s*,\s*"([^"]+)"',
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# libcurl: curl_easy_setopt(handle, CURLOPT_URL, "https://...")
|
|
104
|
+
LIBCURL_URL_PATTERN = re.compile(
|
|
105
|
+
r'\bcurl_easy_setopt\s*\(\s*\w+\s*,\s*CURLOPT_URL\s*,\s*"(https?://[^"]+)"',
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# DBs
|
|
109
|
+
DB_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
110
|
+
(re.compile(r'\bsqlite3_open(?:_v2)?\s*\('), "sqlite"),
|
|
111
|
+
(re.compile(r'\bPQconnectdb(?:Params)?\s*\(|\bPQsetdbLogin\s*\('), "postgresql"),
|
|
112
|
+
(re.compile(r'\bmysql_real_connect\s*\(|\bmariadb_real_connect\s*\('), "mysql"),
|
|
113
|
+
(re.compile(r'\bredisConnect(?:WithTimeout)?\s*\('), "redis"),
|
|
114
|
+
(re.compile(r'\bmongoc_client_new\s*\(|\bmongoc_uri_new\s*\('), "mongodb"),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
# Auth / crypto
|
|
118
|
+
AUTH_PATTERNS: list[tuple[re.Pattern[str], str, float]] = [
|
|
119
|
+
(re.compile(r'\bcrypto_pwhash(?:_argon2(?:i|id))?\s*\(|\bcrypto_pwhash_str\s*\('), "argon2", 0.9),
|
|
120
|
+
(re.compile(r'\bargon2(?:i|d|id)?_hash\w*\s*\('), "argon2", 0.9),
|
|
121
|
+
(re.compile(r'\bbcrypt(?:_hashpw|_checkpw|_gensalt)?\s*\('), "bcrypt", 0.9),
|
|
122
|
+
(re.compile(r'\bscrypt(?:_kdf)?\s*\('), "scrypt", 0.9),
|
|
123
|
+
(re.compile(r'\bcrypto_secretbox\w*\s*\(|\bcrypto_aead_(?:chacha20poly1305|aes256gcm)\w*\s*\('), "libsodium_aead", 0.85),
|
|
124
|
+
(re.compile(r'\bSSL_CTX_new\s*\(|\bSSL_new\s*\(|\bTLS_method\s*\(|\bTLS_(?:client|server)_method\s*\('), "openssl_tls", 0.85),
|
|
125
|
+
(re.compile(r'\bEVP_PKEY_new\s*\(|\bEVP_(?:Encrypt|Decrypt)Init\w*\s*\('), "openssl_evp", 0.8),
|
|
126
|
+
(re.compile(r'\bRAND_bytes\s*\(|\bRAND_priv_bytes\s*\('), "openssl_rand", 0.8),
|
|
127
|
+
(re.compile(r'\bmbedtls_(?:ssl|x509)_\w+\s*\('), "mbedtls", 0.85),
|
|
128
|
+
(re.compile(r'\bjwt_(?:encode|decode|new|verify)\s*\('), "jwt", 0.85),
|
|
129
|
+
(re.compile(r'\bAuthorization\b'), "authorization_header", 0.6),
|
|
130
|
+
(re.compile(r'\bBearer\b'), "bearer_token", 0.6),
|
|
131
|
+
(re.compile(r'\bapi[_-]?key\b', re.IGNORECASE), "api_key", 0.6),
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
FRAMEWORK_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
135
|
+
(re.compile(r'\bMHD_start_daemon\s*\(|\bMHD_create_response\w*\s*\(|#\s*include\s+["<]microhttpd\.h'), "libmicrohttpd"),
|
|
136
|
+
(re.compile(r'\bmg_start\s*\(|\bmg_set_request_handler\s*\(|#\s*include\s+["<]civetweb\.h'), "civetweb"),
|
|
137
|
+
(re.compile(r'\bmg_mgr_init\s*\(|\bmg_http_listen\s*\(|#\s*include\s+["<]mongoose\.h'), "mongoose"),
|
|
138
|
+
(re.compile(r'\bonion_new\s*\(|\bonion_listen\s*\(|#\s*include\s+["<]onion/onion\.h'), "libonion"),
|
|
139
|
+
(re.compile(r'#\s*include\s+["<]openssl/'), "openssl"),
|
|
140
|
+
(re.compile(r'#\s*include\s+["<]mbedtls/'), "mbedtls"),
|
|
141
|
+
(re.compile(r'#\s*include\s+["<]sodium\.h'), "libsodium"),
|
|
142
|
+
(re.compile(r'#\s*include\s+["<]curl/curl\.h'), "libcurl"),
|
|
143
|
+
(re.compile(r'#\s*include\s+["<]sqlite3\.h'), "sqlite3"),
|
|
144
|
+
(re.compile(r'#\s*include\s+["<]libpq-fe\.h'), "libpq"),
|
|
145
|
+
(re.compile(r'#\s*include\s+["<]hiredis/hiredis\.h'), "hiredis"),
|
|
146
|
+
(re.compile(r'#\s*include\s+["<]mongoc/mongoc\.h|#\s*include\s+["<]mongoc\.h'), "mongoc"),
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
ENTRYPOINT_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
150
|
+
(re.compile(r'\bMHD_start_daemon\s*\('), "mhd_start_daemon"),
|
|
151
|
+
(re.compile(r'\bmg_start\s*\('), "civetweb_start"),
|
|
152
|
+
(re.compile(r'\bmg_http_listen\s*\('), "mongoose_http_listen"),
|
|
153
|
+
(re.compile(r'\bonion_listen\s*\('), "onion_listen"),
|
|
154
|
+
(re.compile(r'\baccept\s*\(\s*\w+\s*,\s*\(struct\s+sockaddr\s*\*\)'), "raw_socket_accept"),
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
# Secrets via getenv / secure_getenv / getenv_s
|
|
158
|
+
SECRET_PATTERNS: list[re.Pattern[str]] = [
|
|
159
|
+
re.compile(
|
|
160
|
+
r'\b(?:secure_)?getenv\s*\(\s*"([A-Z0-9_]*(?:SECRET|TOKEN|KEY|PASSWORD|PASS|PWD)[A-Z0-9_]*)"',
|
|
161
|
+
),
|
|
162
|
+
re.compile(
|
|
163
|
+
r'\bgetenv_s\s*\([^,]*,\s*[^,]*,\s*"([A-Z0-9_]*(?:SECRET|TOKEN|KEY|PASSWORD|PASS|PWD)[A-Z0-9_]*)"',
|
|
164
|
+
),
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _line_snippet(content: str, offset: int, *, max_chars: int = _SNIPPET_MAX_CHARS) -> str:
|
|
169
|
+
# Kept local rather than ``attackmap.sdk.line_snippet(content, line_of(...))``:
|
|
170
|
+
# the SDK helper indexes ``str.splitlines()``, which also breaks on form
|
|
171
|
+
# feeds (common in legacy C) and lone ``\r``, so its line numbering can
|
|
172
|
+
# disagree with ``line_of`` (which counts ``\n`` only).
|
|
173
|
+
line_start = content.rfind("\n", 0, offset) + 1
|
|
174
|
+
line_end = content.find("\n", offset)
|
|
175
|
+
if line_end == -1:
|
|
176
|
+
line_end = len(content)
|
|
177
|
+
line = content[line_start:line_end].strip()
|
|
178
|
+
if len(line) > max_chars:
|
|
179
|
+
line = line[: max_chars - 1] + "…"
|
|
180
|
+
return line
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _project_name_from_cmake(text: str) -> str | None:
|
|
184
|
+
match = re.search(r"\bproject\s*\(\s*([A-Za-z0-9_\-]+)", text)
|
|
185
|
+
if match:
|
|
186
|
+
return match.group(1)
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _counts_for_ownership(path: Path, root: Path) -> bool:
|
|
191
|
+
parents = Path(rel(path, root)).parts[:-1]
|
|
192
|
+
return not any(part in _OWNERSHIP_EXTRA_SKIP_DIRS for part in parents)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class CAnalyzer:
|
|
196
|
+
metadata = AnalyzerMetadata(
|
|
197
|
+
name="c",
|
|
198
|
+
display_name="C Analyzer",
|
|
199
|
+
version="0.2.0",
|
|
200
|
+
description="C ecosystem analyzer covering libmicrohttpd, civetweb, mongoose, libonion, libcurl, OpenSSL, libsodium, sqlite3, libpq, mysql, hiredis, mongoc, plus memory-safety and command-exec sinks.",
|
|
201
|
+
scope="C source trees and CMake/Make projects. Detects HTTP server entrypoints, libcurl outbound calls, common DB and crypto libraries, and function-local memory-safety / format-string / command-exec sinks tied to request and input sources.",
|
|
202
|
+
targets=["c", "civetweb", "libmicrohttpd", "mongoose"],
|
|
203
|
+
languages=["c"],
|
|
204
|
+
priority=20,
|
|
205
|
+
experimental=True, # C ecosystem coverage is heuristic and partial; mark experimental.
|
|
206
|
+
enabled_by_default=False, # opt-in via `-m c` while experimental (AttackMap#221)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def name(self) -> str:
|
|
211
|
+
return self.metadata.name
|
|
212
|
+
|
|
213
|
+
# ---------- Public entry points ----------
|
|
214
|
+
|
|
215
|
+
def detect(self, repo_path: str | Path) -> bool:
|
|
216
|
+
root = Path(repo_path).resolve()
|
|
217
|
+
if not root.exists() or not root.is_dir():
|
|
218
|
+
return False
|
|
219
|
+
# One walk; at least one ``.c`` source claims the repo. A lone ``.h``
|
|
220
|
+
# (an ObjC/Swift bridging header, a Python C-extension header) or a
|
|
221
|
+
# CMakeLists.txt/Makefile on its own does not. Suffixes are matched
|
|
222
|
+
# case-sensitively (``.C`` is a C++ convention), so filter after the
|
|
223
|
+
# case-insensitive SDK match.
|
|
224
|
+
for path in iter_repo_files(root, suffixes={".c"}, skip_dirs=SKIP_DIRS):
|
|
225
|
+
if path.suffix == ".c":
|
|
226
|
+
return True
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
def analyze(self, repo_path: str | Path) -> ScanResult:
|
|
230
|
+
root = Path(repo_path).resolve()
|
|
231
|
+
result = ScanResult(root=str(root))
|
|
232
|
+
if not root.exists() or not root.is_dir():
|
|
233
|
+
return result
|
|
234
|
+
|
|
235
|
+
# One walk collects sources, CMake files and the C++ markers that
|
|
236
|
+
# decide .h ownership (see the module-level rule above).
|
|
237
|
+
sources: list[Path] = []
|
|
238
|
+
cxx_repo = False
|
|
239
|
+
for file_path in iter_repo_files(
|
|
240
|
+
root,
|
|
241
|
+
suffixes=CODE_SUFFIXES | _CXX_MARKER_SUFFIXES | MANIFEST_SUFFIXES,
|
|
242
|
+
names={"CMakeLists.txt"} | MANIFEST_NAMES,
|
|
243
|
+
skip_dirs=SKIP_DIRS,
|
|
244
|
+
):
|
|
245
|
+
if is_manifest(file_path):
|
|
246
|
+
# CMake / Conan / vcpkg dependencies and unpinned CMake
|
|
247
|
+
# downloads (AttackMap#191).
|
|
248
|
+
text = read_source(file_path)
|
|
249
|
+
if text is None:
|
|
250
|
+
continue
|
|
251
|
+
dependencies, supply_chain = manifest_records(file_path, text, rel(file_path, root))
|
|
252
|
+
result.dependencies.extend(dependencies)
|
|
253
|
+
result.supply_chain_issues.extend(supply_chain)
|
|
254
|
+
if file_path.name != "CMakeLists.txt":
|
|
255
|
+
continue
|
|
256
|
+
project = _project_name_from_cmake(text)
|
|
257
|
+
if project:
|
|
258
|
+
self._append_unique_service(result, f"project:{project}", rel(file_path, root))
|
|
259
|
+
if _CMAKE_CXX_PATTERN.search(text) and _counts_for_ownership(file_path, root):
|
|
260
|
+
cxx_repo = True
|
|
261
|
+
continue
|
|
262
|
+
if file_path.suffix in _CXX_MARKER_SUFFIXES:
|
|
263
|
+
if _counts_for_ownership(file_path, root):
|
|
264
|
+
cxx_repo = True
|
|
265
|
+
continue
|
|
266
|
+
if file_path.suffix in CODE_SUFFIXES:
|
|
267
|
+
sources.append(file_path)
|
|
268
|
+
|
|
269
|
+
for file_path in sources:
|
|
270
|
+
if file_path.suffix == ".h" and cxx_repo:
|
|
271
|
+
continue # owned by the C++ analyzer
|
|
272
|
+
content = read_source(file_path)
|
|
273
|
+
if content is None:
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
result.files_scanned += 1
|
|
277
|
+
if "c" not in result.languages:
|
|
278
|
+
result.languages.append("c")
|
|
279
|
+
|
|
280
|
+
relative = rel(file_path, root)
|
|
281
|
+
self._extract_routes(content, relative, result)
|
|
282
|
+
self._extract_databases(content, relative, result)
|
|
283
|
+
self._extract_auth(content, relative, result)
|
|
284
|
+
self._extract_secrets(content, relative, result)
|
|
285
|
+
self._extract_external_calls(content, relative, result)
|
|
286
|
+
self._extract_frameworks(content, relative, result)
|
|
287
|
+
self._extract_entrypoints(content, relative, result)
|
|
288
|
+
result.code_weaknesses.extend(find_native_weaknesses(content, relative))
|
|
289
|
+
|
|
290
|
+
result.dependencies = dedupe_dependencies(result.dependencies)
|
|
291
|
+
|
|
292
|
+
result.languages.sort()
|
|
293
|
+
return result
|
|
294
|
+
|
|
295
|
+
# ---------- Extractors ----------
|
|
296
|
+
|
|
297
|
+
def _extract_routes(self, content: str, relative: str, result: ScanResult) -> None:
|
|
298
|
+
for match in CIVETWEB_ROUTE_PATTERN.finditer(content):
|
|
299
|
+
path = match.group(1)
|
|
300
|
+
self._append_unique_route(result, path, "ANY", relative, line_of(content, match.start()))
|
|
301
|
+
for match in MONGOOSE_MATCH_URI_PATTERN.finditer(content):
|
|
302
|
+
path = match.group(1)
|
|
303
|
+
self._append_unique_route(result, path, "ANY", relative, line_of(content, match.start()))
|
|
304
|
+
for match in ONION_ROUTE_PATTERN.finditer(content):
|
|
305
|
+
path = match.group(1)
|
|
306
|
+
self._append_unique_route(result, path, "ANY", relative, line_of(content, match.start()))
|
|
307
|
+
|
|
308
|
+
def _extract_databases(self, content: str, relative: str, result: ScanResult) -> None:
|
|
309
|
+
for pattern, kind in DB_PATTERNS:
|
|
310
|
+
match = pattern.search(content)
|
|
311
|
+
if match is None:
|
|
312
|
+
continue
|
|
313
|
+
self._append_unique_database(
|
|
314
|
+
result, kind, relative,
|
|
315
|
+
line_of(content, match.start()),
|
|
316
|
+
_line_snippet(content, match.start()),
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def _extract_auth(self, content: str, relative: str, result: ScanResult) -> None:
|
|
320
|
+
for pattern, hint, confidence in AUTH_PATTERNS:
|
|
321
|
+
match = pattern.search(content)
|
|
322
|
+
if match is None:
|
|
323
|
+
continue
|
|
324
|
+
self._append_unique_auth(
|
|
325
|
+
result, hint, relative,
|
|
326
|
+
line_of(content, match.start()),
|
|
327
|
+
_line_snippet(content, match.start()),
|
|
328
|
+
confidence,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def _extract_secrets(self, content: str, relative: str, result: ScanResult) -> None:
|
|
332
|
+
for pattern in SECRET_PATTERNS:
|
|
333
|
+
for match in pattern.finditer(content):
|
|
334
|
+
name = match.group(1)
|
|
335
|
+
self._append_unique_secret(
|
|
336
|
+
result, name, relative,
|
|
337
|
+
line_of(content, match.start()),
|
|
338
|
+
_line_snippet(content, match.start()),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def _extract_external_calls(self, content: str, relative: str, result: ScanResult) -> None:
|
|
342
|
+
for match in LIBCURL_URL_PATTERN.finditer(content):
|
|
343
|
+
target = match.group(1)
|
|
344
|
+
self._append_unique_external(
|
|
345
|
+
result, target, relative,
|
|
346
|
+
line_of(content, match.start()),
|
|
347
|
+
_line_snippet(content, match.start()),
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def _extract_frameworks(self, content: str, relative: str, result: ScanResult) -> None:
|
|
351
|
+
for pattern, name in FRAMEWORK_PATTERNS:
|
|
352
|
+
match = pattern.search(content)
|
|
353
|
+
if match is None:
|
|
354
|
+
continue
|
|
355
|
+
self._append_unique_framework(
|
|
356
|
+
result, name, relative,
|
|
357
|
+
line_of(content, match.start()),
|
|
358
|
+
_line_snippet(content, match.start()),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def _extract_entrypoints(self, content: str, relative: str, result: ScanResult) -> None:
|
|
362
|
+
for pattern, hint in ENTRYPOINT_PATTERNS:
|
|
363
|
+
match = pattern.search(content)
|
|
364
|
+
if match is None:
|
|
365
|
+
continue
|
|
366
|
+
self._append_unique_entrypoint(
|
|
367
|
+
result, hint, relative,
|
|
368
|
+
line_of(content, match.start()),
|
|
369
|
+
_line_snippet(content, match.start()),
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# ---------- Append helpers ----------
|
|
373
|
+
|
|
374
|
+
@staticmethod
|
|
375
|
+
def _append_unique_route(result: ScanResult, path: str, method: str, file: str, line: int | None) -> None:
|
|
376
|
+
key = (path, method, file)
|
|
377
|
+
if any((item.path, item.method, item.file) == key for item in result.routes):
|
|
378
|
+
return
|
|
379
|
+
result.routes.append(Route(path=path, method=method, file=file, line=line))
|
|
380
|
+
|
|
381
|
+
@staticmethod
|
|
382
|
+
def _append_unique_database(result: ScanResult, kind: str, file: str, line: int | None, evidence: str | None) -> None:
|
|
383
|
+
key = (kind, file)
|
|
384
|
+
if any((item.kind, item.file) == key for item in result.databases):
|
|
385
|
+
return
|
|
386
|
+
result.databases.append(DatabaseHint(kind=kind, file=file, line=line, evidence_text=evidence))
|
|
387
|
+
|
|
388
|
+
@staticmethod
|
|
389
|
+
def _append_unique_auth(result: ScanResult, hint: str, file: str, line: int | None, evidence: str | None, confidence: float) -> None:
|
|
390
|
+
key = (hint, file)
|
|
391
|
+
if any((item.hint, item.file) == key for item in result.auth_hints):
|
|
392
|
+
return
|
|
393
|
+
result.auth_hints.append(AuthHint(hint=hint, file=file, line=line, evidence_text=evidence, confidence=confidence))
|
|
394
|
+
|
|
395
|
+
@staticmethod
|
|
396
|
+
def _append_unique_secret(result: ScanResult, name: str, file: str, line: int | None, evidence: str | None) -> None:
|
|
397
|
+
key = (name, file)
|
|
398
|
+
if any((item.name, item.file) == key for item in result.secret_hints):
|
|
399
|
+
return
|
|
400
|
+
result.secret_hints.append(SecretHint(name=name, file=file, line=line, evidence_text=evidence, confidence=0.85))
|
|
401
|
+
|
|
402
|
+
@staticmethod
|
|
403
|
+
def _append_unique_external(result: ScanResult, target: str, file: str, line: int | None, evidence: str | None) -> None:
|
|
404
|
+
key = (target, file)
|
|
405
|
+
if any((item.target, item.file) == key for item in result.external_calls):
|
|
406
|
+
return
|
|
407
|
+
result.external_calls.append(ExternalCall(target=target, file=file, line=line, evidence_text=evidence))
|
|
408
|
+
|
|
409
|
+
@staticmethod
|
|
410
|
+
def _append_unique_framework(result: ScanResult, hint: str, file: str, line: int | None, evidence: str | None) -> None:
|
|
411
|
+
key = (hint, file)
|
|
412
|
+
if any((item.hint, item.file) == key for item in result.framework_hints):
|
|
413
|
+
return
|
|
414
|
+
result.framework_hints.append(FrameworkHint(hint=hint, file=file, line=line, evidence_text=evidence))
|
|
415
|
+
|
|
416
|
+
@staticmethod
|
|
417
|
+
def _append_unique_entrypoint(result: ScanResult, hint: str, file: str, line: int | None, evidence: str | None) -> None:
|
|
418
|
+
key = (hint, file)
|
|
419
|
+
if any((item.hint, item.file) == key for item in result.entrypoint_hints):
|
|
420
|
+
return
|
|
421
|
+
result.entrypoint_hints.append(EntrypointHint(hint=hint, file=file, line=line, evidence_text=evidence))
|
|
422
|
+
|
|
423
|
+
@staticmethod
|
|
424
|
+
def _append_unique_service(result: ScanResult, hint: str, file: str) -> None:
|
|
425
|
+
key = (hint, file)
|
|
426
|
+
if any((item.hint, item.file) == key for item in result.service_hints):
|
|
427
|
+
return
|
|
428
|
+
result.service_hints.append(ServiceHint(hint=hint, file=file))
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
__all__ = ["CAnalyzer"]
|