cryptoshield 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,454 @@
1
+ """Phishing URL checker — detect crypto scam websites.
2
+
3
+ Checks against known scam databases and analyzes URL patterns.
4
+ """
5
+
6
+ import re
7
+ from urllib.parse import urlparse
8
+ from .utils import fetch_json, print_header, print_ok, print_warn, print_fail, print_info
9
+
10
+ # Known legitimate domains
11
+ LEGIT_DOMAINS = {
12
+ "uniswap.org", "app.uniswap.org",
13
+ "aave.com", "app.aave.com",
14
+ "opensea.io",
15
+ "blur.io",
16
+ "pancakeswap.finance", "pancakeswap.io",
17
+ "sushi.com",
18
+ "compound.finance",
19
+ "makerdao.com",
20
+ "lido.fi",
21
+ "curve.fi",
22
+ "1inch.io",
23
+ "metamask.io",
24
+ "phantom.app",
25
+ "solflare.com",
26
+ "walletconnect.com",
27
+ "rainbow.me",
28
+ "argent.xyz",
29
+ "rabby.io",
30
+ "paraswap.io",
31
+ "dydx.exchange",
32
+ "gmx.io",
33
+ "arbitrum.io",
34
+ "optimism.io",
35
+ "base.org",
36
+ "zksync.io",
37
+ "starknet.io",
38
+ "layerzero.network",
39
+ "wormhole.com",
40
+ "jito.network",
41
+ "marinade.finance",
42
+ "jup.ag",
43
+ "raydium.io",
44
+ "orca.so",
45
+ "magic-eden.io",
46
+ "tensorhq.xyz",
47
+ "pump.fun",
48
+ "dextools.io",
49
+ "dexscreener.com",
50
+ "etherscan.io",
51
+ "bscscan.com",
52
+ "polygonscan.com",
53
+ "arbiscan.io",
54
+ "basescan.org",
55
+ "solscan.io",
56
+ "coingecko.com",
57
+ "coinmarketcap.com",
58
+ "binance.com",
59
+ "coinbase.com",
60
+ "kraken.com",
61
+ "okx.com",
62
+ "bybit.com",
63
+ "kucoin.com",
64
+ "gate.io",
65
+ "huobi.com",
66
+ "poloniex.com",
67
+ "bitfinex.com",
68
+ "gemini.com",
69
+ "bitstamp.net",
70
+ "crypto.com",
71
+ "eigenlayer.xyz",
72
+ "stargate.finance",
73
+ "jumper.exchange",
74
+ "orbiter.finance",
75
+ "rhino.fi",
76
+ "synapseprotocol.com",
77
+ "multichain.org",
78
+ "celer.network",
79
+ "debridge.finance",
80
+ "li.fi",
81
+ "socket.tech",
82
+ "bungee.exchange",
83
+ "chainlist.org",
84
+ "revoke.cash",
85
+ "debank.com",
86
+ "zapper.fi",
87
+ "zerion.io",
88
+ "matcha.xyz",
89
+ "slingshot.finance",
90
+ "kwenta.io",
91
+ "velodrome.finance",
92
+ "aerodrome.finance",
93
+ "camelot.exchange",
94
+ "traderjoexyz.com",
95
+ "spookyswap.finance",
96
+ "spiritswap.finance",
97
+ "beets.fi",
98
+ "balancer.fi",
99
+ "frax.finance",
100
+ "convexfinance.com",
101
+ "yearn.finance",
102
+ "synthetix.io",
103
+ "perp.com",
104
+ "gains.trade",
105
+ "mux.network",
106
+ "aevo.xyz",
107
+ "premia.finance",
108
+ "lyra.finance",
109
+ "hegic.co",
110
+ "ribbon.finance",
111
+ "opyn.co",
112
+ "squeeth.com",
113
+ "aztec.network",
114
+ "scroll.io",
115
+ "linea.build",
116
+ "mantle.xyz",
117
+ "manta.network",
118
+ "connext.network",
119
+ "across.to",
120
+ "hop.exchange",
121
+ "satellite.axelar.network",
122
+ "squidrouter.com",
123
+ "chainge.finance",
124
+ }
125
+
126
+ # Suspicious keywords in URLs
127
+ SCAM_PATTERNS = [
128
+ r"airdrop",
129
+ r"claim",
130
+ r"free",
131
+ r"bonus",
132
+ r"reward",
133
+ r"gift",
134
+ r"presale",
135
+ r"pre-sale",
136
+ r"whitelist",
137
+ r"mint-free",
138
+ r"connect-wallet",
139
+ r"verify",
140
+ r"restore",
141
+ r"sync",
142
+ r"validate",
143
+ r"update.*wallet",
144
+ r"security.*alert",
145
+ r"suspended",
146
+ r"limited.*time",
147
+ r"act.*now",
148
+ r"urgent",
149
+ r"migration",
150
+ r"migrate",
151
+ r"unlock",
152
+ r"activate",
153
+ r"confirm.*seed",
154
+ r"seed.*phrase",
155
+ r"private.*key",
156
+ r"recovery",
157
+ r"support.*chat",
158
+ r"live.*chat",
159
+ r"help.*desk",
160
+ r"fix.*wallet",
161
+ r"repair",
162
+ r"rectify",
163
+ r"resolve.*issue",
164
+ r"pending.*transaction",
165
+ r"failed.*transaction",
166
+ r"stuck.*funds",
167
+ r"frozen.*account",
168
+ r"kyc.*verify",
169
+ r"identity.*verify",
170
+ r"document.*upload",
171
+ r"passport",
172
+ r"driver.*license",
173
+ r"social.*security",
174
+ r"tax.*refund",
175
+ r"gas.*fee.*refund",
176
+ r"rebate",
177
+ r"cashback",
178
+ r"double.*your",
179
+ r"multiply",
180
+ r"investment.*opportunity",
181
+ r"guaranteed.*profit",
182
+ r"risk.*free",
183
+ r"passive.*income",
184
+ r"financial.*freedom",
185
+ r"early.*access",
186
+ r"beta.*test",
187
+ r"exclusive.*offer",
188
+ r"limited.*supply",
189
+ r"last.*chance",
190
+ r"final.*notice",
191
+ r"action.*required",
192
+ r"immediate.*attention",
193
+ ]
194
+
195
+ # TLDs commonly used by scammers
196
+ SUSPICIOUS_TLDS = [
197
+ ".xyz", ".top", ".club", ".buzz", ".click", ".link",
198
+ ".site", ".online", ".icu", ".work", ".fit", ".monster",
199
+ ".cfd", ".sbs", ".surf", ".rest", ".cam", ".hair",
200
+ ".mom", ".lol", ".bond", ".cyou", ".uno", ".sarl",
201
+ ".mov", ".zip", ".py", ".sh", ".tk", ".ml", ".ga", ".cf", ".gq",
202
+ ]
203
+
204
+ # Known scam domains (community-reported)
205
+ KNOWN_SCAM_DOMAINS = {
206
+ "uniswap-airdrop.com",
207
+ "metamask-airdrop.com",
208
+ "metamask-sync.com",
209
+ "metamask-restore.com",
210
+ "metamask-verify.com",
211
+ "metamask-update.com",
212
+ "phantom-airdrop.com",
213
+ "phantom-sync.com",
214
+ "phantom-restore.com",
215
+ "arbitrum-airdrop.com",
216
+ "arbitrum-claim.com",
217
+ "optimism-airdrop.com",
218
+ "starknet-airdrop.com",
219
+ "zksync-airdrop.com",
220
+ "layerzero-airdrop.com",
221
+ "eigenlayer-airdrop.com",
222
+ "jito-airdrop.com",
223
+ "jupiter-airdrop.com",
224
+ "blur-airdrop.com",
225
+ "pudgypenguins-airdrop.com",
226
+ "apecoin-claim.com",
227
+ "uniswapv3.com",
228
+ "app-uniswap.org",
229
+ "uniswap-app.org",
230
+ "metamask-download.com",
231
+ "metamask-extension.com",
232
+ "metamask-io.com",
233
+ "metamask-wallet.com",
234
+ "pancakeswap-finance.com",
235
+ "aave-protocol.com",
236
+ "opensea-io.com",
237
+ "opensea-nft.com",
238
+ "coinbase-airdrop.com",
239
+ "binance-airdrop.com",
240
+ "crypto-com-airdrop.com",
241
+ }
242
+
243
+
244
+ def check_url(url: str) -> dict:
245
+ """Analyze a URL for phishing indicators."""
246
+ # Normalize URL
247
+ if not url.startswith("http"):
248
+ url = "https://" + url
249
+
250
+ parsed = urlparse(url)
251
+ domain = parsed.netloc.lower()
252
+ if domain.startswith("www."):
253
+ domain = domain[4:]
254
+
255
+ report = {
256
+ "url": url,
257
+ "domain": domain,
258
+ "path": parsed.path,
259
+ "is_legit": False,
260
+ "is_suspicious": False,
261
+ "is_known_scam": False,
262
+ "indicators": [],
263
+ "risk_score": 0,
264
+ }
265
+
266
+ # 0. Check known scam domains FIRST
267
+ base_domain = _get_base_domain(domain)
268
+ if domain in KNOWN_SCAM_DOMAINS or base_domain in KNOWN_SCAM_DOMAINS:
269
+ report["is_known_scam"] = True
270
+ report["is_suspicious"] = True
271
+ report["indicators"].append({
272
+ "type": "FAIL",
273
+ "detail": f"KNOWN SCAM DOMAIN — {domain} is in scam database"
274
+ })
275
+ report["risk_score"] = 90
276
+ return report
277
+
278
+ # 1. Check if it's a known legitimate domain
279
+ if base_domain in LEGIT_DOMAINS or domain in LEGIT_DOMAINS:
280
+ report["is_legit"] = True
281
+ report["indicators"].append({"type": "OK", "detail": f"Known legitimate domain: {base_domain}"})
282
+ report["risk_score"] = 0
283
+ return report
284
+
285
+ report["indicators"].append({"type": "INFO", "detail": f"Unknown domain: {domain}"})
286
+ report["risk_score"] += 10
287
+
288
+ # 2. Check for typosquatting (similar to legit domains)
289
+ for legit in LEGIT_DOMAINS:
290
+ similarity = _domain_similarity(domain, legit)
291
+ if similarity > 0.7 and similarity < 1.0:
292
+ report["is_suspicious"] = True
293
+ report["indicators"].append({
294
+ "type": "FAIL",
295
+ "detail": f"Possible typosquat of {legit} (similarity: {similarity:.0%})"
296
+ })
297
+ report["risk_score"] += 30
298
+
299
+ # 3. Check for scam patterns in URL
300
+ full_url = url.lower()
301
+ matched_patterns = []
302
+ for pattern in SCAM_PATTERNS:
303
+ if re.search(pattern, full_url):
304
+ matched_patterns.append(pattern)
305
+
306
+ if matched_patterns:
307
+ report["is_suspicious"] = True
308
+ if len(matched_patterns) == 1:
309
+ report["indicators"].append({
310
+ "type": "WARN",
311
+ "detail": f"Suspicious pattern: '{matched_patterns[0]}' in URL"
312
+ })
313
+ report["risk_score"] += 10
314
+ else:
315
+ report["indicators"].append({
316
+ "type": "FAIL",
317
+ "detail": f"Multiple suspicious patterns ({len(matched_patterns)}): {', '.join(matched_patterns[:3])}"
318
+ })
319
+ report["risk_score"] += 15 * len(matched_patterns)
320
+
321
+ # 4. Check TLD
322
+ tld = "." + domain.split(".")[-1]
323
+ if tld in SUSPICIOUS_TLDS:
324
+ report["indicators"].append({
325
+ "type": "WARN",
326
+ "detail": f"Suspicious TLD: {tld}"
327
+ })
328
+ report["risk_score"] += 5
329
+
330
+ # 5. Check for IP address instead of domain
331
+ if re.match(r"^\d+\.\d+\.\d+\.\d+", domain):
332
+ report["is_suspicious"] = True
333
+ report["indicators"].append({
334
+ "type": "FAIL",
335
+ "detail": "IP address used instead of domain name"
336
+ })
337
+ report["risk_score"] += 25
338
+
339
+ # 6. Check for excessive subdomains
340
+ subdomain_count = domain.count(".") - 1
341
+ if subdomain_count > 2:
342
+ report["is_suspicious"] = True
343
+ report["indicators"].append({
344
+ "type": "WARN",
345
+ "detail": f"Excessive subdomains ({subdomain_count})"
346
+ })
347
+ report["risk_score"] += 10
348
+
349
+ # 7. Check for punycode (IDN homograph attack)
350
+ if "xn--" in domain:
351
+ report["is_suspicious"] = True
352
+ report["indicators"].append({
353
+ "type": "FAIL",
354
+ "detail": "Punycode domain — possible IDN homograph attack"
355
+ })
356
+ report["risk_score"] += 20
357
+
358
+ # 8. Check domain length
359
+ if len(domain) > 30:
360
+ report["indicators"].append({
361
+ "type": "WARN",
362
+ "detail": f"Very long domain name ({len(domain)} chars)"
363
+ })
364
+ report["risk_score"] += 5
365
+
366
+ # 9. Check for hyphens (common in phishing)
367
+ if domain.count("-") > 2:
368
+ report["indicators"].append({
369
+ "type": "WARN",
370
+ "detail": f"Multiple hyphens in domain ({domain.count('-')} hyphens)"
371
+ })
372
+ report["risk_score"] += 5
373
+
374
+ # 10. Check for brand names in non-brand domains
375
+ brand_domains = ["uniswap", "metamask", "phantom", "aave", "opensea", "blur",
376
+ "pancakeswap", "sushi", "compound", "lido", "curve", "binance",
377
+ "coinbase", "kraken", "okx", "bybit", "arbitrum", "optimism",
378
+ "zksync", "starknet", "eigenlayer", "jito", "jupiter", "raydium"]
379
+ for brand in brand_domains:
380
+ if brand in domain and base_domain not in LEGIT_DOMAINS:
381
+ report["is_suspicious"] = True
382
+ report["indicators"].append({
383
+ "type": "FAIL",
384
+ "detail": f"Brand name '{brand}' in non-official domain"
385
+ })
386
+ report["risk_score"] += 25
387
+ break
388
+
389
+ # Cap score
390
+ report["risk_score"] = min(report["risk_score"], 100)
391
+
392
+ if report["risk_score"] >= 40:
393
+ report["is_suspicious"] = True
394
+
395
+ return report
396
+
397
+
398
+ def print_phishing_report(report: dict):
399
+ """Pretty-print URL check results."""
400
+ print_header(f"PHISHING CHECK — {report['domain']}")
401
+
402
+ if report.get("is_known_scam"):
403
+ print_fail("🚨 KNOWN SCAM DOMAIN — DO NOT VISIT")
404
+ for ind in report["indicators"]:
405
+ print_fail(ind["detail"])
406
+ return
407
+
408
+ if report["is_legit"]:
409
+ print_ok("Known legitimate domain")
410
+ return
411
+
412
+ for ind in report["indicators"]:
413
+ if ind["type"] == "OK":
414
+ print_ok(ind["detail"])
415
+ elif ind["type"] == "WARN":
416
+ print_warn(ind["detail"])
417
+ elif ind["type"] == "FAIL":
418
+ print_fail(ind["detail"])
419
+ else:
420
+ print_info(ind["detail"])
421
+
422
+ print()
423
+ score = report["risk_score"]
424
+ if score <= 20:
425
+ print_ok(f"Risk Score: {score}/100 — Likely safe")
426
+ elif score <= 50:
427
+ print_warn(f"Risk Score: {score}/100 — Suspicious — verify carefully")
428
+ else:
429
+ print_fail(f"Risk Score: {score}/100 — HIGH RISK — likely phishing")
430
+
431
+
432
+ def _get_base_domain(domain: str) -> str:
433
+ """Extract base domain (e.g., app.uniswap.org → uniswap.org)."""
434
+ parts = domain.split(".")
435
+ if len(parts) >= 2:
436
+ return ".".join(parts[-2:])
437
+ return domain
438
+
439
+
440
+ def _domain_similarity(a: str, b: str) -> float:
441
+ """Simple character-level similarity between two domains."""
442
+ a_base = _get_base_domain(a)
443
+ b_base = _get_base_domain(b)
444
+
445
+ if a_base == b_base:
446
+ return 1.0
447
+
448
+ # Levenshtein-like ratio
449
+ longer = max(len(a_base), len(b_base))
450
+ if longer == 0:
451
+ return 1.0
452
+
453
+ matches = sum(1 for x, y in zip(a_base, b_base) if x == y)
454
+ return matches / longer
@@ -0,0 +1,141 @@
1
+ """Rugpull risk scorer — analyze contracts for common rug patterns.
2
+
3
+ Combines on-chain data + heuristics to calculate a risk score.
4
+ """
5
+
6
+ from web3 import Web3
7
+ from .utils import (
8
+ get_web3, checksum, ERC20_ABI, label_address,
9
+ print_header, print_ok, print_warn, print_fail, print_info,
10
+ )
11
+ from .honeypot import check_honeypot
12
+
13
+
14
+ def analyze_rugpull(address: str, chain: str = "eth") -> dict:
15
+ """Analyze a token contract for rugpull risk."""
16
+ w3 = get_web3(chain)
17
+ addr = checksum(address)
18
+
19
+ report = {
20
+ "address": addr,
21
+ "chain": chain,
22
+ "checks": [],
23
+ "score": 0,
24
+ "risk_level": "UNKNOWN",
25
+ }
26
+
27
+ contract = w3.eth.contract(address=addr, abi=ERC20_ABI)
28
+
29
+ # 1. Check if contract is verified (has code)
30
+ code = w3.eth.get_code(addr)
31
+ if code == b"" or code == b"0x":
32
+ report["checks"].append({"name": "Contract Code", "status": "FAIL", "detail": "No contract code — not a contract"})
33
+ report["score"] += 50
34
+ else:
35
+ report["checks"].append({"name": "Contract Code", "status": "OK", "detail": f"{len(code)} bytes deployed"})
36
+
37
+ # 2. Check if owner is renounced
38
+ try:
39
+ owner = contract.functions.owner().call()
40
+ zero = "0x0000000000000000000000000000000000000000"
41
+ dead = "0x000000000000000000000000000000000000dEaD"
42
+ if owner.lower() in (zero, dead):
43
+ report["checks"].append({"name": "Ownership", "status": "OK", "detail": "Owner renounced"})
44
+ else:
45
+ report["checks"].append({"name": "Ownership", "status": "WARN", "detail": f"Owner: {label_address(owner)}"})
46
+ report["score"] += 10
47
+ except Exception:
48
+ report["checks"].append({"name": "Ownership", "status": "INFO", "detail": "No owner function (could be good or bad)"})
49
+
50
+ # 3. Check total supply concentration
51
+ try:
52
+ total_supply = contract.functions.totalSupply().call()
53
+ if total_supply > 0:
54
+ report["checks"].append({"name": "Total Supply", "status": "INFO", "detail": f"{total_supply:,}"})
55
+ else:
56
+ report["checks"].append({"name": "Total Supply", "status": "FAIL", "detail": "Zero supply"})
57
+ report["score"] += 20
58
+ except Exception:
59
+ report["checks"].append({"name": "Total Supply", "status": "FAIL", "detail": "Cannot read supply"})
60
+
61
+ # 4. Check liquidity (look for LP tokens)
62
+ # This is a simplified check — real check would scan DEX pairs
63
+ report["checks"].append({"name": "Liquidity", "status": "INFO", "detail": "Check manually on DEX Screener"})
64
+
65
+ # 5. Get GoPlus data for additional checks
66
+ goplus = check_honeypot(addr, chain)
67
+ if "error" not in goplus:
68
+ if goplus.get("is_honeypot"):
69
+ report["score"] += 40
70
+ report["checks"].append({"name": "Honeypot", "status": "FAIL", "detail": "Token is a honeypot"})
71
+ else:
72
+ report["checks"].append({"name": "Honeypot", "status": "OK", "detail": "Not a honeypot"})
73
+
74
+ if not goplus.get("is_open_source"):
75
+ report["score"] += 15
76
+ report["checks"].append({"name": "Source Code", "status": "FAIL", "detail": "Not verified"})
77
+ else:
78
+ report["checks"].append({"name": "Source Code", "status": "OK", "detail": "Verified on explorer"})
79
+
80
+ if goplus.get("is_proxy"):
81
+ report["score"] += 5
82
+ report["checks"].append({"name": "Proxy", "status": "WARN", "detail": "Proxy contract — logic can change"})
83
+
84
+ if goplus.get("selfdestruct"):
85
+ report["score"] += 15
86
+ report["checks"].append({"name": "Self-Destruct", "status": "FAIL", "detail": "Has selfdestruct function"})
87
+
88
+ if goplus.get("hidden_owner"):
89
+ report["score"] += 10
90
+ report["checks"].append({"name": "Hidden Owner", "status": "FAIL", "detail": "Hidden owner detected"})
91
+
92
+ if goplus.get("owner_can_mint"):
93
+ report["score"] += 10
94
+ report["checks"].append({"name": "Mintable", "status": "WARN", "detail": "Owner can mint new tokens"})
95
+
96
+ holder_count = goplus.get("holder_count", 0)
97
+ if holder_count:
98
+ report["checks"].append({"name": "Holders", "status": "INFO", "detail": f"{holder_count:,} holders"})
99
+
100
+ # Cap score at 100
101
+ report["score"] = min(report["score"], 100)
102
+
103
+ # Risk level
104
+ if report["score"] <= 20:
105
+ report["risk_level"] = "LOW"
106
+ elif report["score"] <= 50:
107
+ report["risk_level"] = "MEDIUM"
108
+ elif report["score"] <= 75:
109
+ report["risk_level"] = "HIGH"
110
+ else:
111
+ report["risk_level"] = "CRITICAL"
112
+
113
+ return report
114
+
115
+
116
+ def print_rugpull_report(report: dict):
117
+ """Pretty-print rugpull analysis."""
118
+ print_header(f"RUGPULL SCORE — {report['address'][:6]}...{report['address'][-4:]}")
119
+
120
+ for check in report["checks"]:
121
+ status = check["status"]
122
+ name = check["name"]
123
+ detail = check["detail"]
124
+ if status == "OK":
125
+ print_ok(f"{name}: {detail}")
126
+ elif status == "WARN":
127
+ print_warn(f"{name}: {detail}")
128
+ elif status == "FAIL":
129
+ print_fail(f"{name}: {detail}")
130
+ else:
131
+ print_info(f"{name}: {detail}")
132
+
133
+ print()
134
+ score = report["score"]
135
+ level = report["risk_level"]
136
+ if level == "LOW":
137
+ print_ok(f"Risk Score: {score}/100 — {level} RISK")
138
+ elif level == "MEDIUM":
139
+ print_warn(f"Risk Score: {score}/100 — {level} RISK")
140
+ else:
141
+ print_fail(f"Risk Score: {score}/100 — {level} RISK")