attackmap 0.6.4__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/__init__.py +2 -0
- attackmap/analyzer.py +449 -0
- attackmap/analyzer_contracts.py +194 -0
- attackmap/analyzers.py +1003 -0
- attackmap/anomalies.py +612 -0
- attackmap/asset_model.py +245 -0
- attackmap/attack_taxonomy.py +133 -0
- attackmap/auth_protocol.py +702 -0
- attackmap/authz.py +1695 -0
- attackmap/bench.py +467 -0
- attackmap/cli.py +1474 -0
- attackmap/config_scanner.py +389 -0
- attackmap/context_pack.py +82 -0
- attackmap/contracts.py +201 -0
- attackmap/control_model.py +280 -0
- attackmap/crossrepo.py +356 -0
- attackmap/crypto.py +240 -0
- attackmap/cve.py +717 -0
- attackmap/defensive_review.py +647 -0
- attackmap/detection_opportunities.py +1221 -0
- attackmap/detections/__init__.py +491 -0
- attackmap/detections/bola.yml +57 -0
- attackmap/detections/cloud.yml +609 -0
- attackmap/detections/injection.yml +167 -0
- attackmap/detections/secrets.yml +48 -0
- attackmap/detections/sigma.py +511 -0
- attackmap/detections/ssrf.yml +98 -0
- attackmap/detections/weakness.yml +62 -0
- attackmap/detections/webhook.yml +45 -0
- attackmap/detections/workflow.yml +121 -0
- attackmap/diagrams.py +237 -0
- attackmap/diff.py +301 -0
- attackmap/exploitability.py +493 -0
- attackmap/fingerprint.py +91 -0
- attackmap/fleet.py +370 -0
- attackmap/gitref.py +129 -0
- attackmap/graph.py +26 -0
- attackmap/handler_auth.py +639 -0
- attackmap/handler_span.py +62 -0
- attackmap/hunt_harness.py +505 -0
- attackmap/insights.py +701 -0
- attackmap/llm_review.py +846 -0
- attackmap/lockfiles.py +521 -0
- attackmap/mass_assignment.py +543 -0
- attackmap/md.py +175 -0
- attackmap/merge.py +333 -0
- attackmap/models.py +1284 -0
- attackmap/plugins_lock.py +114 -0
- attackmap/popular_packages.py +193 -0
- attackmap/progress.py +258 -0
- attackmap/recon_models.py +31 -0
- attackmap/recon_to_analysis.py +149 -0
- attackmap/redact.py +220 -0
- attackmap/report.py +424 -0
- attackmap/review_eval.py +335 -0
- attackmap/review_json.py +308 -0
- attackmap/review_prompts.py +796 -0
- attackmap/route_auth_fusion.py +474 -0
- attackmap/safe_fs.py +285 -0
- attackmap/sarif.py +373 -0
- attackmap/sbom.py +418 -0
- attackmap/scanner.py +1379 -0
- attackmap/sdk/__init__.py +278 -0
- attackmap/sdk/contracts.py +17 -0
- attackmap/sdk/fs.py +156 -0
- attackmap/sdk/models.py +59 -0
- attackmap/secrets_history.py +161 -0
- attackmap/security_overlay.py +71 -0
- attackmap/srcpaths.py +301 -0
- attackmap/suggest.py +394 -0
- attackmap/supply_chain.py +688 -0
- attackmap/suppress.py +740 -0
- attackmap/taint.py +1540 -0
- attackmap/taint_flow.py +1419 -0
- attackmap/taint_sinks.py +431 -0
- attackmap/taint_sinks.yaml +308 -0
- attackmap/taint_sources.py +396 -0
- attackmap/taxonomy.py +465 -0
- attackmap/threat_model.py +3441 -0
- attackmap/topology.py +316 -0
- attackmap/triage.py +151 -0
- attackmap/weaknesses.py +365 -0
- attackmap/webhardening.py +479 -0
- attackmap/webhook_verification.py +1067 -0
- attackmap/workflow_scanner.py +964 -0
- attackmap-0.6.4.dist-info/METADATA +226 -0
- attackmap-0.6.4.dist-info/RECORD +91 -0
- attackmap-0.6.4.dist-info/WHEEL +5 -0
- attackmap-0.6.4.dist-info/entry_points.txt +2 -0
- attackmap-0.6.4.dist-info/licenses/LICENSE +21 -0
- attackmap-0.6.4.dist-info/top_level.txt +1 -0
attackmap/weaknesses.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Novel vulnerability-class detectors (#77).
|
|
2
|
+
|
|
3
|
+
Per-file finder (rides the built-in scanner's read loop) for bug classes
|
|
4
|
+
beyond the taint / crypto / web-hardening families. Part 1 covers:
|
|
5
|
+
|
|
6
|
+
prototype_pollution __proto__ writes; deep-merge of a request object
|
|
7
|
+
mass_assignment request object bound wholesale to a model
|
|
8
|
+
jwt_weakness alg=none / signature verification disabled
|
|
9
|
+
xxe XML parser with external entities enabled
|
|
10
|
+
graphql_no_query_limits a GraphQL server built with no depth/complexity
|
|
11
|
+
limit anywhere in the same file (#244)
|
|
12
|
+
graphql_batching Apollo/Yoga HTTP batching switched on (#244)
|
|
13
|
+
|
|
14
|
+
Precision-first: everything anchors on a concrete risky construct (not
|
|
15
|
+
absence), and request-object patterns name the request container
|
|
16
|
+
explicitly. ReDoS, insecure-upload, and GraphQL-exposure classes are
|
|
17
|
+
declared in the model but land in a follow-up.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
|
|
24
|
+
from .srcpaths import line_number
|
|
25
|
+
from .models import CodeWeakness
|
|
26
|
+
from .taint_sinks import call_receiver
|
|
27
|
+
|
|
28
|
+
_REQ = r"(?:req|request|body|query|params|payload)"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _rx(pattern: str) -> re.Pattern[str]:
|
|
32
|
+
return re.compile(pattern, re.IGNORECASE | re.MULTILINE)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_JS_BODY_BIND = _rx(
|
|
36
|
+
rf"(?:\.(?:create|update|updateOne|updateMany|insert|save|build)|Object\.assign\s*\([^,]+,)\s*\(?\s*{_REQ}\.body\b"
|
|
37
|
+
)
|
|
38
|
+
# A Sequelize field allow-list in the same call: `.update(req.body, { fields: [...] })`.
|
|
39
|
+
_FIELD_ALLOWLIST = _rx(r"\bfields\s*:\s*\[")
|
|
40
|
+
# A Node crypto hash/HMAC/cipher (or hashlib/CryptoJS/forge) receiver:
|
|
41
|
+
# `crypto.createHmac(…).update(req.body)` digests the body, it doesn't bind it
|
|
42
|
+
# (#330). Same pattern as the `mongo-filter-request` sink's `receiver_unless`.
|
|
43
|
+
_DIGEST_RECEIVER = re.compile(
|
|
44
|
+
r"(?:\bcreate(?:Hmac|Hash|Cipheriv|Decipheriv|Cipher|Decipher|Sign|Verify)|\bhashlib\.\w{1,40}|\bhmac\.new"
|
|
45
|
+
r"|\bCryptoJS\.[\w.]{1,80}|\bforge\.(?:md|hmac)\.[\w.]{1,80})\(\)(?:\.update\(\)){0,8}$"
|
|
46
|
+
)
|
|
47
|
+
_CALL_SCAN_LIMIT = 2000
|
|
48
|
+
|
|
49
|
+
_PATTERNS: tuple[tuple[str, str, re.Pattern[str]], ...] = (
|
|
50
|
+
# --- prototype pollution (JS/TS) ------------------------------------
|
|
51
|
+
(
|
|
52
|
+
"prototype_pollution",
|
|
53
|
+
"high",
|
|
54
|
+
# Literal __proto__ / constructor.prototype *write* (assignment target).
|
|
55
|
+
# Requiring a trailing `=` (not `==`) avoids flagging prototype-chain
|
|
56
|
+
# *reads* like `x.prototype.__proto__.constructor` (#94).
|
|
57
|
+
_rx(
|
|
58
|
+
r"(?:"
|
|
59
|
+
r"\.__proto__"
|
|
60
|
+
r"|\[\s*['\"]__proto__['\"]\s*\]"
|
|
61
|
+
r"|\[\s*['\"]constructor['\"]\s*\]\s*\[\s*['\"]prototype['\"]\s*\]"
|
|
62
|
+
r"|\.constructor\s*\.\s*prototype"
|
|
63
|
+
r")"
|
|
64
|
+
r"(?:\s*\.\s*\w+|\s*\[[^\]]*\])*" # optional further member/subscript access
|
|
65
|
+
r"\s*=(?!=)"
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
(
|
|
69
|
+
"prototype_pollution",
|
|
70
|
+
"high",
|
|
71
|
+
# Recursive merge of a request object into a target (lodash / deepmerge).
|
|
72
|
+
_rx(rf"(?:_\.(?:merge|mergeWith|defaultsDeep|set|setWith)|deepmerge)\s*\([^)]*\b{_REQ}\b"),
|
|
73
|
+
),
|
|
74
|
+
(
|
|
75
|
+
"prototype_pollution",
|
|
76
|
+
"high",
|
|
77
|
+
# jQuery deep extend of a request object: $.extend(true, target, req.*)
|
|
78
|
+
_rx(rf"\$\.extend\s*\(\s*true\b[^)]*\b{_REQ}\b"),
|
|
79
|
+
),
|
|
80
|
+
# --- mass assignment ------------------------------------------------
|
|
81
|
+
(
|
|
82
|
+
"mass_assignment",
|
|
83
|
+
"high",
|
|
84
|
+
# Python: Model(**request.json) / .objects.create(**request.data) / .update(**req.form)
|
|
85
|
+
_rx(rf"\(\s*\*\*\s*{_REQ}\.(?:json|data|POST|GET|form|args|get_json\s*\(\s*\))"),
|
|
86
|
+
),
|
|
87
|
+
(
|
|
88
|
+
"mass_assignment",
|
|
89
|
+
"high",
|
|
90
|
+
# JS: ORM-style bind of the whole body — .create(req.body) /
|
|
91
|
+
# .update(req.body) / Object.assign(model, req.body). A Sequelize
|
|
92
|
+
# `{ fields: [...] }` allow-list clears it (see find_code_weaknesses).
|
|
93
|
+
_JS_BODY_BIND,
|
|
94
|
+
),
|
|
95
|
+
(
|
|
96
|
+
"mass_assignment",
|
|
97
|
+
"high",
|
|
98
|
+
# Mongoose: the body as the *update document* —
|
|
99
|
+
# findByIdAndUpdate(id, req.body) / updateOne(filter, { $set: req.body }).
|
|
100
|
+
_rx(
|
|
101
|
+
r"\.(?:findByIdAndUpdate|findOneAndUpdate|findOneAndReplace|updateOne|updateMany|replaceOne)"
|
|
102
|
+
rf"\s*\([^(),;\n]*(?:\([^()\n]*\)[^(),;\n]*)?,\s*(?:\{{\s*\$set\s*:\s*)?{_REQ}\.body\b"
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
(
|
|
106
|
+
"mass_assignment",
|
|
107
|
+
"high",
|
|
108
|
+
# JS: `new Model(req.body)` — excluding Web/std builtins that
|
|
109
|
+
# legitimately take a body (Fetch Response/Request, Blob, URL, …).
|
|
110
|
+
_rx(
|
|
111
|
+
rf"new\s+(?!(?:Response|Request|Blob|File|FormData|Headers|URL|URLSearchParams|ReadableStream|Error|Buffer|Uint8Array|TextEncoder|TextDecoder)\b)\w+\s*\(\s*{_REQ}\.body\b"
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
(
|
|
115
|
+
"mass_assignment",
|
|
116
|
+
"high",
|
|
117
|
+
# Rails strong-params bypass.
|
|
118
|
+
_rx(r"\.permit!\B|params\.permit!"),
|
|
119
|
+
),
|
|
120
|
+
# --- JWT weaknesses -------------------------------------------------
|
|
121
|
+
(
|
|
122
|
+
"jwt_weakness",
|
|
123
|
+
"high",
|
|
124
|
+
# alg 'none' in an algorithms list (PyJWT + jsonwebtoken).
|
|
125
|
+
_rx(r"algorithms?\s*[=:]\s*\[[^\]]*['\"]none['\"]"),
|
|
126
|
+
),
|
|
127
|
+
(
|
|
128
|
+
"jwt_weakness",
|
|
129
|
+
"high",
|
|
130
|
+
# signature verification turned off.
|
|
131
|
+
_rx(r"jwt\.decode\s*\([^)]*verify\s*=\s*False|verify_signature['\"]?\s*[:=]\s*(?:false|False)|['\"]alg['\"]?\s*:\s*['\"]none['\"]"),
|
|
132
|
+
),
|
|
133
|
+
# --- XXE ------------------------------------------------------------
|
|
134
|
+
(
|
|
135
|
+
"xxe",
|
|
136
|
+
"high",
|
|
137
|
+
_rx(
|
|
138
|
+
r"resolve_entities\s*=\s*True"
|
|
139
|
+
r"|\bload_dtd\s*=\s*True"
|
|
140
|
+
r"|libxml_disable_entity_loader\s*\(\s*(?:false|0)\s*\)"
|
|
141
|
+
r"|\.setExpandEntityReferences\s*\(\s*true\s*\)"
|
|
142
|
+
r"|disallow-doctype-decl['\"]?\s*,\s*false"
|
|
143
|
+
r"|FEATURE_SECURE_PROCESSING[^;\n]*false"
|
|
144
|
+
),
|
|
145
|
+
),
|
|
146
|
+
# --- ReDoS (catastrophic backtracking) ------------------------------
|
|
147
|
+
# A quantified group whose body also has a quantifier — (a+)+, (.*)*,
|
|
148
|
+
# (\d+)*. Gated to a REGEX context (regex literal, re.* call, or
|
|
149
|
+
# RegExp constructor) so arithmetic like `(x+1)*2` isn't flagged.
|
|
150
|
+
(
|
|
151
|
+
"redos",
|
|
152
|
+
"medium",
|
|
153
|
+
# JS regex literal: /.../ containing a nested quantifier.
|
|
154
|
+
re.compile(r"/[^/\n]*\([^()\n]*[+*][^()\n]*\)[+*][^/\n]*/"),
|
|
155
|
+
),
|
|
156
|
+
(
|
|
157
|
+
"redos",
|
|
158
|
+
"medium",
|
|
159
|
+
# Python re.* with a nested-quantifier pattern string.
|
|
160
|
+
re.compile(
|
|
161
|
+
r"\bre\.(?:compile|match|search|fullmatch|findall|finditer|sub|subn|split)"
|
|
162
|
+
r"\s*\(\s*[rbuRBU]*['\"][^'\"\n]*\([^()\n]*[+*][^()\n]*\)[+*]"
|
|
163
|
+
),
|
|
164
|
+
),
|
|
165
|
+
(
|
|
166
|
+
"redos",
|
|
167
|
+
"medium",
|
|
168
|
+
re.compile(r"\bnew\s+RegExp\s*\(\s*['\"][^'\"\n]*\([^()\n]*[+*][^()\n]*\)[+*]"),
|
|
169
|
+
),
|
|
170
|
+
# --- insecure file upload -------------------------------------------
|
|
171
|
+
(
|
|
172
|
+
"insecure_upload",
|
|
173
|
+
"high",
|
|
174
|
+
# Werkzeug FileStorage saved with the client-supplied filename
|
|
175
|
+
# (path traversal / overwrite), or Multer originalname into a write.
|
|
176
|
+
_rx(
|
|
177
|
+
r"\.save\s*\([^)]*\.filename\b"
|
|
178
|
+
r"|(?:writeFile|writeFileSync|createWriteStream)\s*\([^)]*\.originalname\b"
|
|
179
|
+
r"|path\.join\s*\([^)]*\.originalname\b"
|
|
180
|
+
),
|
|
181
|
+
),
|
|
182
|
+
# --- GraphQL exposure -----------------------------------------------
|
|
183
|
+
(
|
|
184
|
+
"graphql_exposure",
|
|
185
|
+
"low",
|
|
186
|
+
_rx(
|
|
187
|
+
r"\bintrospection\s*:\s*true"
|
|
188
|
+
r"|\bgraphiql\s*[:=]\s*(?:true|True)"
|
|
189
|
+
r"|\bplayground\s*:\s*true"
|
|
190
|
+
),
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# GraphQL server construction (#244). Limits are usually passed in the same
|
|
196
|
+
# call (`validationRules`, `extensions=[...]`) or applied to the server object
|
|
197
|
+
# in the same file (gqlgen `srv.Use(...)`); any limit marker in the file clears
|
|
198
|
+
# the finding.
|
|
199
|
+
_GRAPHQL_SERVER = _rx(
|
|
200
|
+
r"\bnew\s+ApolloServer\s*\("
|
|
201
|
+
r"|\bgraphqlHTTP\s*\("
|
|
202
|
+
r"|\bcreateYoga\s*\("
|
|
203
|
+
r"|\bmercurius\b"
|
|
204
|
+
r"|\bstrawberry\.Schema\s*\("
|
|
205
|
+
r"|\bGraphQLView\.as_view\s*\("
|
|
206
|
+
r"|\bhandler\.NewDefaultServer\s*\("
|
|
207
|
+
)
|
|
208
|
+
_GRAPHQL_LIMITS = _rx(
|
|
209
|
+
r"depthLimit|depth[_-]limit|QueryDepthLimiter|MaxTokensLimiter|MaxAliasesLimiter"
|
|
210
|
+
r"|ComplexityLimit|complexity[_-]?limit|createComplexityRule|queryComplexity|query[_-]complexity"
|
|
211
|
+
r"|graphql-armor|@escape\.tech|costLimit|cost[_-]?analysis|maxDepth|max_depth|maxAliases|maxTokens"
|
|
212
|
+
r"|validationRules|validation_rules|useDepthLimit|queryDepth|query_depth"
|
|
213
|
+
)
|
|
214
|
+
_GRAPHQL_BATCHING = _rx(r"\ballowBatchedHttpRequests\s*:\s*true\b|\bbatching\s*:\s*(?:true\b|\{)")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def find_graphql_weaknesses(content: str, rel_file: str) -> list[CodeWeakness]:
|
|
218
|
+
"""Missing query limits and enabled batching on GraphQL servers built in
|
|
219
|
+
this file (#244)."""
|
|
220
|
+
server = _GRAPHQL_SERVER.search(content)
|
|
221
|
+
if server is None:
|
|
222
|
+
return []
|
|
223
|
+
out: list[CodeWeakness] = []
|
|
224
|
+
|
|
225
|
+
def _add(kind: str, severity: str, offset: int) -> None:
|
|
226
|
+
out.append(
|
|
227
|
+
CodeWeakness(
|
|
228
|
+
kind=kind, # type: ignore[arg-type]
|
|
229
|
+
file=rel_file,
|
|
230
|
+
line=line_number(content, offset),
|
|
231
|
+
evidence_text=_snippet(content, offset),
|
|
232
|
+
severity=severity, # type: ignore[arg-type]
|
|
233
|
+
source_analyzer="weaknesses",
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if _GRAPHQL_LIMITS.search(content) is None:
|
|
238
|
+
_add("graphql_no_query_limits", "low", server.start())
|
|
239
|
+
for match in _GRAPHQL_BATCHING.finditer(content):
|
|
240
|
+
if not _in_line_comment(content, match.start()):
|
|
241
|
+
_add("graphql_batching", "medium", match.start())
|
|
242
|
+
break
|
|
243
|
+
return out
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# Text aimed at an automated reviewer (#233). Unlike the patterns above these
|
|
247
|
+
# live in comments and strings by design, so they skip the comment filter.
|
|
248
|
+
PROMPT_INJECTION_RE = re.compile(
|
|
249
|
+
r"\bignore\s+(?:all\s+|any\s+)?(?:the\s+)?(?:previous|prior|above|earlier)\s+(?:instructions|prompts?|rules|context)"
|
|
250
|
+
r"|\b(?:note|message|instructions?)\s+(?:to|for)\s+(?:the\s+|any\s+|all\s+)?(?:ai|llm|gpt|chatgpt|claude|copilot|codex|assistant|language\s+model)s?\b"
|
|
251
|
+
# Addressing form only — the reviewer noun followed by a colon or comma,
|
|
252
|
+
# as in a salutation. Merely mentioning AI review in a comment is common.
|
|
253
|
+
r"|\b(?:ai|llm)\s+(?:code\s+|security\s+)?(?:reviewers?|auditors?|scanners?|agents?)\s*[:,]"
|
|
254
|
+
r"|\bmark\s+(?:this|these|it|every|all|each)\b[^\n]{0,60}?\b(?:refuted|false[\s-]positive|not\s+vulnerable)\b"
|
|
255
|
+
r"|\bdo\s+not\s+(?:report|flag|mention)\b[^\n]{0,40}?\b(?:this|vulnerabilit\w*|issues?|findings?|backdoor)\b"
|
|
256
|
+
r"|<\|im_(?:start|end)\|>|\[/?INST\]|<</?SYS>>"
|
|
257
|
+
r"|[\u202a-\u202e\u2066-\u2069\u200b\u200c\u2060]",
|
|
258
|
+
re.IGNORECASE,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
_INVISIBLE_RE = re.compile(r"[\u202a-\u202e\u2066-\u2069\u200b\u200c\u2060]")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _injection_evidence(matched: str) -> str:
|
|
266
|
+
if _INVISIBLE_RE.search(matched):
|
|
267
|
+
return "invisible/bidirectional Unicode control character " + matched.encode("unicode_escape").decode("ascii")
|
|
268
|
+
return "text addressed to automated reviewers (content withheld; open the line to review it)"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def find_prompt_injection(content: str, rel_file: str) -> list[CodeWeakness]:
|
|
272
|
+
"""Lines carrying text aimed at an automated reviewer, one weakness per line."""
|
|
273
|
+
out: list[CodeWeakness] = []
|
|
274
|
+
seen: set[int] = set()
|
|
275
|
+
for match in PROMPT_INJECTION_RE.finditer(content):
|
|
276
|
+
line = line_number(content, match.start())
|
|
277
|
+
if line in seen:
|
|
278
|
+
continue
|
|
279
|
+
seen.add(line)
|
|
280
|
+
out.append(
|
|
281
|
+
CodeWeakness(
|
|
282
|
+
kind="prompt_injection_attempt",
|
|
283
|
+
file=rel_file,
|
|
284
|
+
line=line,
|
|
285
|
+
# Never echo the injected text itself: evidence flows into LLM
|
|
286
|
+
# passes. Invisible/bidi characters are shown escaped.
|
|
287
|
+
evidence_text=_injection_evidence(match.group(0)),
|
|
288
|
+
severity="low",
|
|
289
|
+
source_analyzer="weaknesses",
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
return out
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def find_code_weaknesses(content: str, rel_file: str) -> list[CodeWeakness]:
|
|
296
|
+
"""Return novel-class weaknesses in one file's ``content`` (deduped by
|
|
297
|
+
(kind, line))."""
|
|
298
|
+
seen: set[tuple[str, int]] = set()
|
|
299
|
+
out: list[CodeWeakness] = []
|
|
300
|
+
for kind, severity, pattern in _PATTERNS:
|
|
301
|
+
for match in pattern.finditer(content):
|
|
302
|
+
if _in_line_comment(content, match.start()):
|
|
303
|
+
continue # a pattern inside a `//` comment isn't real code (#94)
|
|
304
|
+
if pattern is _JS_BODY_BIND and _FIELD_ALLOWLIST.search(_rest_of_call(content, match.end())):
|
|
305
|
+
continue
|
|
306
|
+
if (
|
|
307
|
+
pattern is _JS_BODY_BIND
|
|
308
|
+
and content.startswith(".", match.start())
|
|
309
|
+
and _DIGEST_RECEIVER.search(call_receiver(content, match.start()))
|
|
310
|
+
):
|
|
311
|
+
continue
|
|
312
|
+
line = line_number(content, match.start())
|
|
313
|
+
key = (kind, line)
|
|
314
|
+
if key in seen:
|
|
315
|
+
continue
|
|
316
|
+
seen.add(key)
|
|
317
|
+
out.append(
|
|
318
|
+
CodeWeakness(
|
|
319
|
+
kind=kind, # type: ignore[arg-type]
|
|
320
|
+
file=rel_file,
|
|
321
|
+
line=line,
|
|
322
|
+
evidence_text=_snippet(content, match.start()),
|
|
323
|
+
severity=severity, # type: ignore[arg-type]
|
|
324
|
+
source_analyzer="weaknesses",
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
out.extend(find_graphql_weaknesses(content, rel_file))
|
|
328
|
+
out.extend(find_prompt_injection(content, rel_file))
|
|
329
|
+
return out
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _rest_of_call(content: str, offset: int) -> str:
|
|
333
|
+
"""The rest of the call ``offset`` sits inside, up to its closing paren
|
|
334
|
+
(bounded, so a hostile file with no closing paren stays cheap)."""
|
|
335
|
+
depth = 1
|
|
336
|
+
end = min(len(content), offset + _CALL_SCAN_LIMIT)
|
|
337
|
+
for i in range(offset, end):
|
|
338
|
+
ch = content[i]
|
|
339
|
+
if ch in "([{":
|
|
340
|
+
depth += 1
|
|
341
|
+
elif ch in ")]}":
|
|
342
|
+
depth -= 1
|
|
343
|
+
if depth == 0:
|
|
344
|
+
return content[offset:i]
|
|
345
|
+
return content[offset:end]
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _in_line_comment(content: str, offset: int) -> bool:
|
|
349
|
+
"""True if ``offset`` falls after a `//` line comment on its line. Cheap
|
|
350
|
+
heuristic (ignores `//` inside strings), used only to drop obvious
|
|
351
|
+
comment-embedded matches like `// x.__proto__.y = z`."""
|
|
352
|
+
line_start = content.rfind("\n", 0, offset) + 1
|
|
353
|
+
return "//" in content[line_start:offset]
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _snippet(content: str, offset: int, radius: int = 120) -> str:
|
|
357
|
+
start = max(0, content.rfind("\n", 0, offset) + 1)
|
|
358
|
+
end = content.find("\n", offset)
|
|
359
|
+
if end == -1:
|
|
360
|
+
end = len(content)
|
|
361
|
+
line = content[start:end].strip()
|
|
362
|
+
return line[:radius] + ("…" if len(line) > radius else "")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
__all__ = ["PROMPT_INJECTION_RE", "find_code_weaknesses", "find_graphql_weaknesses", "find_prompt_injection"]
|