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.
Files changed (91) hide show
  1. attackmap/__init__.py +2 -0
  2. attackmap/analyzer.py +449 -0
  3. attackmap/analyzer_contracts.py +194 -0
  4. attackmap/analyzers.py +1003 -0
  5. attackmap/anomalies.py +612 -0
  6. attackmap/asset_model.py +245 -0
  7. attackmap/attack_taxonomy.py +133 -0
  8. attackmap/auth_protocol.py +702 -0
  9. attackmap/authz.py +1695 -0
  10. attackmap/bench.py +467 -0
  11. attackmap/cli.py +1474 -0
  12. attackmap/config_scanner.py +389 -0
  13. attackmap/context_pack.py +82 -0
  14. attackmap/contracts.py +201 -0
  15. attackmap/control_model.py +280 -0
  16. attackmap/crossrepo.py +356 -0
  17. attackmap/crypto.py +240 -0
  18. attackmap/cve.py +717 -0
  19. attackmap/defensive_review.py +647 -0
  20. attackmap/detection_opportunities.py +1221 -0
  21. attackmap/detections/__init__.py +491 -0
  22. attackmap/detections/bola.yml +57 -0
  23. attackmap/detections/cloud.yml +609 -0
  24. attackmap/detections/injection.yml +167 -0
  25. attackmap/detections/secrets.yml +48 -0
  26. attackmap/detections/sigma.py +511 -0
  27. attackmap/detections/ssrf.yml +98 -0
  28. attackmap/detections/weakness.yml +62 -0
  29. attackmap/detections/webhook.yml +45 -0
  30. attackmap/detections/workflow.yml +121 -0
  31. attackmap/diagrams.py +237 -0
  32. attackmap/diff.py +301 -0
  33. attackmap/exploitability.py +493 -0
  34. attackmap/fingerprint.py +91 -0
  35. attackmap/fleet.py +370 -0
  36. attackmap/gitref.py +129 -0
  37. attackmap/graph.py +26 -0
  38. attackmap/handler_auth.py +639 -0
  39. attackmap/handler_span.py +62 -0
  40. attackmap/hunt_harness.py +505 -0
  41. attackmap/insights.py +701 -0
  42. attackmap/llm_review.py +846 -0
  43. attackmap/lockfiles.py +521 -0
  44. attackmap/mass_assignment.py +543 -0
  45. attackmap/md.py +175 -0
  46. attackmap/merge.py +333 -0
  47. attackmap/models.py +1284 -0
  48. attackmap/plugins_lock.py +114 -0
  49. attackmap/popular_packages.py +193 -0
  50. attackmap/progress.py +258 -0
  51. attackmap/recon_models.py +31 -0
  52. attackmap/recon_to_analysis.py +149 -0
  53. attackmap/redact.py +220 -0
  54. attackmap/report.py +424 -0
  55. attackmap/review_eval.py +335 -0
  56. attackmap/review_json.py +308 -0
  57. attackmap/review_prompts.py +796 -0
  58. attackmap/route_auth_fusion.py +474 -0
  59. attackmap/safe_fs.py +285 -0
  60. attackmap/sarif.py +373 -0
  61. attackmap/sbom.py +418 -0
  62. attackmap/scanner.py +1379 -0
  63. attackmap/sdk/__init__.py +278 -0
  64. attackmap/sdk/contracts.py +17 -0
  65. attackmap/sdk/fs.py +156 -0
  66. attackmap/sdk/models.py +59 -0
  67. attackmap/secrets_history.py +161 -0
  68. attackmap/security_overlay.py +71 -0
  69. attackmap/srcpaths.py +301 -0
  70. attackmap/suggest.py +394 -0
  71. attackmap/supply_chain.py +688 -0
  72. attackmap/suppress.py +740 -0
  73. attackmap/taint.py +1540 -0
  74. attackmap/taint_flow.py +1419 -0
  75. attackmap/taint_sinks.py +431 -0
  76. attackmap/taint_sinks.yaml +308 -0
  77. attackmap/taint_sources.py +396 -0
  78. attackmap/taxonomy.py +465 -0
  79. attackmap/threat_model.py +3441 -0
  80. attackmap/topology.py +316 -0
  81. attackmap/triage.py +151 -0
  82. attackmap/weaknesses.py +365 -0
  83. attackmap/webhardening.py +479 -0
  84. attackmap/webhook_verification.py +1067 -0
  85. attackmap/workflow_scanner.py +964 -0
  86. attackmap-0.6.4.dist-info/METADATA +226 -0
  87. attackmap-0.6.4.dist-info/RECORD +91 -0
  88. attackmap-0.6.4.dist-info/WHEEL +5 -0
  89. attackmap-0.6.4.dist-info/entry_points.txt +2 -0
  90. attackmap-0.6.4.dist-info/licenses/LICENSE +21 -0
  91. 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"]