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/crypto.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Insecure-cryptography & weak-randomness detection (#70).
|
|
2
|
+
|
|
3
|
+
A cheap, high-signal regex pass over each source file, invoked from the
|
|
4
|
+
built-in scanner (content is already in hand — no extra file walk).
|
|
5
|
+
|
|
6
|
+
Detected families (each → a ``CryptoWeakness``):
|
|
7
|
+
|
|
8
|
+
weak_password_hash MD5/SHA-1 applied to a password-shaped value
|
|
9
|
+
weak_cipher DES / 3DES / RC4 / Blowfish
|
|
10
|
+
ecb_mode ECB block-cipher mode (incl. Java's AES default)
|
|
11
|
+
static_iv_salt hard-coded IV or salt literal
|
|
12
|
+
insecure_random non-CSPRNG used for a security value
|
|
13
|
+
insecure_tls certificate / hostname verification disabled
|
|
14
|
+
|
|
15
|
+
Precision note (SSRF lesson from #68): the two families that would
|
|
16
|
+
otherwise be noisy — ``weak_password_hash`` and ``insecure_random`` —
|
|
17
|
+
are gated on a security-context identifier in the same statement.
|
|
18
|
+
Unambiguous families (DES, RC4, ECB, ``verify=False``) fire on the
|
|
19
|
+
pattern alone.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
|
|
26
|
+
from .srcpaths import line_number
|
|
27
|
+
from .models import CryptoWeakness
|
|
28
|
+
|
|
29
|
+
# Identifiers that mark a value as security-sensitive. Gate the noisy
|
|
30
|
+
# families on one of these appearing in the same call/statement.
|
|
31
|
+
#
|
|
32
|
+
# Matched as a SUBSTRING (no word boundaries) so compound identifiers
|
|
33
|
+
# like `user_password`, `sessionId`, and `reset_token` are caught. We
|
|
34
|
+
# deliberately omit generic words like "hash" that collide with common
|
|
35
|
+
# non-security identifiers (`hashlib`, `hashmap`).
|
|
36
|
+
_SECURITY_CTX = (
|
|
37
|
+
r"(?:password|passwd|pwd|secret|token|api[_-]?key|apikey|"
|
|
38
|
+
r"private[_-]?key|nonce|otp|salt|seed|session|csrf|cred)"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _rx(pattern: str) -> re.Pattern[str]:
|
|
43
|
+
return re.compile(pattern, re.IGNORECASE)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Each entry: (kind, severity, compiled pattern). Order is stable for
|
|
47
|
+
# deterministic output.
|
|
48
|
+
_CRYPTO_PATTERNS: tuple[tuple[str, str, re.Pattern[str]], ...] = (
|
|
49
|
+
# --- weak password hashing (gated on a password-shaped identifier) ---
|
|
50
|
+
(
|
|
51
|
+
"weak_password_hash",
|
|
52
|
+
"high",
|
|
53
|
+
_rx(rf"\b(?:hashlib\.)?(?:md5|sha1)\s*\([^)]*{_SECURITY_CTX}"),
|
|
54
|
+
),
|
|
55
|
+
(
|
|
56
|
+
"weak_password_hash",
|
|
57
|
+
"high",
|
|
58
|
+
# createHash('md5'|'sha1') ... update(password) — Node
|
|
59
|
+
_rx(rf"createHash\s*\(\s*['\"](?:md5|sha1)['\"]\s*\)[^;\n]*{_SECURITY_CTX}"),
|
|
60
|
+
),
|
|
61
|
+
(
|
|
62
|
+
"weak_password_hash",
|
|
63
|
+
"high",
|
|
64
|
+
# Java MessageDigest.getInstance("MD5"|"SHA-1")
|
|
65
|
+
_rx(r"MessageDigest\.getInstance\s*\(\s*['\"](?:MD5|SHA-?1)['\"]\s*\)"),
|
|
66
|
+
),
|
|
67
|
+
# --- weak ciphers ----------------------------------------------------
|
|
68
|
+
# CASE-SENSITIVE on purpose: algorithm tokens are uppercase/PascalCase
|
|
69
|
+
# in code (`DES`, `RC4`, `TripleDES`, `Blowfish`). An IGNORECASE match
|
|
70
|
+
# flags the French word "des", Spanish "rc", etc. in i18n strings —
|
|
71
|
+
# observed as 12 false positives on a real repo before this fix.
|
|
72
|
+
(
|
|
73
|
+
"weak_cipher",
|
|
74
|
+
"high",
|
|
75
|
+
re.compile(r"\b(?:3?DES|DESede|TripleDES|RC4|ARC4|ARCFOUR|RC2)\b|\bBlowfish\w*"),
|
|
76
|
+
),
|
|
77
|
+
(
|
|
78
|
+
"weak_cipher",
|
|
79
|
+
"high",
|
|
80
|
+
# Node/OpenSSL-style lowercase scheme strings: 'des-cbc',
|
|
81
|
+
# 'des-ede3-cbc', 'rc4', 'bf-cbc' (Blowfish). Quoted to avoid prose.
|
|
82
|
+
# `bf` is too short to match alone (a 'bf' label != Blowfish), so it
|
|
83
|
+
# requires a mode suffix; the distinctive names may stand alone.
|
|
84
|
+
_rx(r"['\"](?:3?des(?:-ede3?)?(?:-[a-z0-9]+)*|rc4|arcfour|blowfish(?:-[a-z0-9]+)*|bf-[a-z0-9]+)['\"]"),
|
|
85
|
+
),
|
|
86
|
+
# --- ECB mode --------------------------------------------------------
|
|
87
|
+
# Case-sensitive uppercase tokens; plus a lowercase quoted-scheme pass.
|
|
88
|
+
(
|
|
89
|
+
"ecb_mode",
|
|
90
|
+
"medium",
|
|
91
|
+
re.compile(r"MODE_ECB|['\"/](?:AES|DES)/ECB|\bECB\b"),
|
|
92
|
+
),
|
|
93
|
+
(
|
|
94
|
+
"ecb_mode",
|
|
95
|
+
"medium",
|
|
96
|
+
# lowercase scheme strings like 'aes-256-ecb'
|
|
97
|
+
_rx(r"['\"][a-z0-9]+(?:-[a-z0-9]+)*-ecb['\"]"),
|
|
98
|
+
),
|
|
99
|
+
(
|
|
100
|
+
"ecb_mode",
|
|
101
|
+
"medium",
|
|
102
|
+
# Java Cipher.getInstance("AES") defaults to ECB when no mode given
|
|
103
|
+
_rx(r"Cipher\.getInstance\s*\(\s*['\"]AES['\"]\s*\)"),
|
|
104
|
+
),
|
|
105
|
+
# --- static IV / salt ------------------------------------------------
|
|
106
|
+
(
|
|
107
|
+
"static_iv_salt",
|
|
108
|
+
"medium",
|
|
109
|
+
# iv = b"...." / IV: "...." / salt = '....' (a string/bytes literal).
|
|
110
|
+
# `nonce` is deliberately excluded — hardcoded nonces are mostly
|
|
111
|
+
# HTTP-auth / protocol test data, not cipher-IV misuse.
|
|
112
|
+
_rx(r"\b(?:iv|salt)\s*[:=]\s*(?:b|rb|u)?['\"][^'\"]{4,}['\"]"),
|
|
113
|
+
),
|
|
114
|
+
# --- insecure randomness (gated on security context) -----------------
|
|
115
|
+
(
|
|
116
|
+
"insecure_random",
|
|
117
|
+
"medium",
|
|
118
|
+
_rx(rf"Math\.random\s*\(\s*\)[^;\n]*{_SECURITY_CTX}"),
|
|
119
|
+
),
|
|
120
|
+
(
|
|
121
|
+
"insecure_random",
|
|
122
|
+
"medium",
|
|
123
|
+
_rx(rf"{_SECURITY_CTX}[^;\n]*(?:Math\.random\s*\(|random\.(?:random|randint|choice|randrange)\s*\(|\bmt_rand\s*\(|\brand\s*\()"),
|
|
124
|
+
),
|
|
125
|
+
(
|
|
126
|
+
"insecure_random",
|
|
127
|
+
"medium",
|
|
128
|
+
_rx(rf"(?:random\.(?:random|randint|choice|randrange)\s*\(|\bmt_rand\s*\()[^;\n]*{_SECURITY_CTX}"),
|
|
129
|
+
),
|
|
130
|
+
# --- insecure TLS ----------------------------------------------------
|
|
131
|
+
(
|
|
132
|
+
"insecure_tls",
|
|
133
|
+
"high",
|
|
134
|
+
# requests/httpx verify=False; Python ssl check_hostname=False
|
|
135
|
+
_rx(r"verify\s*=\s*False|check_hostname\s*=\s*False|CERT_NONE"),
|
|
136
|
+
),
|
|
137
|
+
(
|
|
138
|
+
"insecure_tls",
|
|
139
|
+
"high",
|
|
140
|
+
# Node rejectUnauthorized: false ; Go InsecureSkipVerify: true
|
|
141
|
+
_rx(r"rejectUnauthorized\s*:\s*false|InsecureSkipVerify\s*:\s*true"),
|
|
142
|
+
),
|
|
143
|
+
(
|
|
144
|
+
"insecure_tls",
|
|
145
|
+
"high",
|
|
146
|
+
# deprecated protocols
|
|
147
|
+
_rx(r"PROTOCOL_TLSv1(?:_1)?\b|PROTOCOL_SSLv[23]\b|SSLv3|\bTLSv1\.0\b"),
|
|
148
|
+
),
|
|
149
|
+
# --- Go-specific (#102 follow-up) ------------------------------------
|
|
150
|
+
(
|
|
151
|
+
# Go weak hash: md5.New()/md5.Sum(...) / sha1.* over a security value.
|
|
152
|
+
"weak_password_hash",
|
|
153
|
+
"high",
|
|
154
|
+
_rx(rf"\b(?:md5|sha1)\.(?:New|Sum)\s*\([^;\n]*{_SECURITY_CTX}"),
|
|
155
|
+
),
|
|
156
|
+
(
|
|
157
|
+
"weak_password_hash",
|
|
158
|
+
"high",
|
|
159
|
+
_rx(rf"{_SECURITY_CTX}[^;\n]*\b(?:md5|sha1)\.(?:New|Sum)\s*\("),
|
|
160
|
+
),
|
|
161
|
+
(
|
|
162
|
+
# Go weak cipher: des.NewCipher / rc4.NewCipher / crypto/des|rc4 import.
|
|
163
|
+
# `.New…(` / `crypto/` context (not prose) makes lowercase safe.
|
|
164
|
+
"weak_cipher",
|
|
165
|
+
"high",
|
|
166
|
+
re.compile(r"\b(?:des|rc4)\.New(?:Cipher|TripleDESCipher)?\s*\(|\bcrypto/(?:des|rc4)\b"),
|
|
167
|
+
),
|
|
168
|
+
(
|
|
169
|
+
# Go insecure RNG for a security value — math/rand-exclusive funcs only
|
|
170
|
+
# (rand.Int/Read are ambiguous with the secure crypto/rand, so excluded).
|
|
171
|
+
"insecure_random",
|
|
172
|
+
"medium",
|
|
173
|
+
_rx(rf"{_SECURITY_CTX}[^;\n]*\brand\.(?:Intn|Int31n?|Int63n?|Float64|Float32|Perm|Shuffle)\s*\("),
|
|
174
|
+
),
|
|
175
|
+
(
|
|
176
|
+
"insecure_random",
|
|
177
|
+
"medium",
|
|
178
|
+
_rx(rf"\brand\.(?:Intn|Int31n?|Int63n?|Float64|Float32|Perm|Shuffle)\s*\([^;\n]*{_SECURITY_CTX}"),
|
|
179
|
+
),
|
|
180
|
+
# --- PHP-specific (#103 follow-up) -----------------------------------
|
|
181
|
+
(
|
|
182
|
+
# PHP mcrypt weak ciphers (mcrypt is removed/insecure) + DES/RC4/Blowfish
|
|
183
|
+
# constants (which the \bDES\b token misses after the `MCRYPT_` prefix).
|
|
184
|
+
"weak_cipher",
|
|
185
|
+
"high",
|
|
186
|
+
re.compile(
|
|
187
|
+
r"\bMCRYPT_(?:DES|3DES|TRIPLEDES|RC4|RC2|BLOWFISH)\b"
|
|
188
|
+
r"|\bmcrypt_(?:encrypt|decrypt|module_open)\s*\("
|
|
189
|
+
),
|
|
190
|
+
),
|
|
191
|
+
(
|
|
192
|
+
# PHP disabled TLS verification (cURL / stream context).
|
|
193
|
+
"insecure_tls",
|
|
194
|
+
"high",
|
|
195
|
+
_rx(
|
|
196
|
+
r"CURLOPT_SSL_VERIFY(?:PEER|HOST)\s*(?:,|=>)\s*(?:false|0)\b"
|
|
197
|
+
r"|['\"]verify(?:_peer(?:_name)?)?['\"]\s*=>\s*false"
|
|
198
|
+
),
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def find_crypto_weaknesses(content: str, rel_file: str) -> list[CryptoWeakness]:
|
|
204
|
+
"""Return crypto weaknesses found in one file's ``content``.
|
|
205
|
+
|
|
206
|
+
Deduped by (kind, line) within the file so a single risky line isn't
|
|
207
|
+
reported twice by overlapping patterns.
|
|
208
|
+
"""
|
|
209
|
+
seen: set[tuple[str, int]] = set()
|
|
210
|
+
out: list[CryptoWeakness] = []
|
|
211
|
+
for kind, severity, pattern in _CRYPTO_PATTERNS:
|
|
212
|
+
for match in pattern.finditer(content):
|
|
213
|
+
line = line_number(content, match.start())
|
|
214
|
+
key = (kind, line)
|
|
215
|
+
if key in seen:
|
|
216
|
+
continue
|
|
217
|
+
seen.add(key)
|
|
218
|
+
out.append(
|
|
219
|
+
CryptoWeakness(
|
|
220
|
+
kind=kind, # type: ignore[arg-type]
|
|
221
|
+
file=rel_file,
|
|
222
|
+
line=line,
|
|
223
|
+
evidence_text=_snippet(content, match.start()),
|
|
224
|
+
severity=severity, # type: ignore[arg-type]
|
|
225
|
+
source_analyzer="crypto",
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
return out
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _snippet(content: str, offset: int, radius: int = 120) -> str:
|
|
232
|
+
start = max(0, content.rfind("\n", 0, offset) + 1)
|
|
233
|
+
end = content.find("\n", offset)
|
|
234
|
+
if end == -1:
|
|
235
|
+
end = len(content)
|
|
236
|
+
line = content[start:end].strip()
|
|
237
|
+
return line[:radius] + ("…" if len(line) > radius else "")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
__all__ = ["find_crypto_weaknesses"]
|