RMI Platform commited on
Commit
c3e9108
·
1 Parent(s): b63b581

feat: Add SENTINEL Deep Threat Scanner (9-collection security analysis)

Browse files

- Created sentinel_deep.py with comprehensive threat scanning
- Analyzes 9 security collections: contract health, liquidity security,
rug imminence, MEV exposure, supply manipulation, fee manipulation,
wash trading, deployer history, social intelligence
- Returns threat score 0-100 with per-collection breakdown
- Added x402 router endpoint at /api/v1/x402-tools/sentinel_deep
- Added test file with async pytest tests

backend/app/routers/x402_sentinel_deep.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ x402 Router: sentinel_deep
3
+ ==============================
4
+ Wraps SENTINEL Deep Scanner with payment enforcement.
5
+
6
+ TOOL : sentinel_deep
7
+ TIER : Premium
8
+ PRICE : $0.10 (100000 atoms)
9
+ TRIAL : 1 free check
10
+ ROUTER: /api/v1/x402-tools/sentinel_deep
11
+ """
12
+
13
+ import logging
14
+ from typing import Any
15
+
16
+ from fastapi import APIRouter, HTTPException, Request
17
+ from pydantic import BaseModel, Field, field_validator
18
+
19
+ from app.sentinel_deep import SentinelDeepScanner
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ router = APIRouter(prefix="/api/v1/x402-tools", tags=["x402-tools"])
24
+
25
+
26
+ class SentinelDeepRequest(BaseModel):
27
+ """Request model for sentinel deep scan."""
28
+
29
+ address: str = Field(..., description="Token contract address to analyze")
30
+ chain: str = Field(
31
+ "auto",
32
+ description="Blockchain (ethereum, solana, bsc, base, polygon)",
33
+ )
34
+
35
+ @field_validator("address")
36
+ @classmethod
37
+ def validate_address(cls, v: str) -> str:
38
+ v = v.strip()
39
+ is_evm = v.startswith("0x") and len(v) == 42
40
+ is_solana = not v.startswith("0x") and 32 <= len(v) <= 44 and v.isascii()
41
+ if not is_evm and not is_solana:
42
+ raise ValueError("Address must be a valid token contract address")
43
+ return v.lower()
44
+
45
+ @field_validator("chain")
46
+ @classmethod
47
+ def validate_chain(cls, v: str) -> str:
48
+ valid = {"auto", "ethereum", "solana", "bsc", "base", "polygon", "arbitrum", "avalanche"}
49
+ v = v.lower().strip()
50
+ if v not in valid:
51
+ raise ValueError(f"Invalid chain")
52
+ return v
53
+
54
+
55
+ class SentinelDeepResponse(BaseModel):
56
+ """Response model."""
57
+
58
+ success: bool = True
59
+ tool: str = "sentinel_deep"
60
+ data: dict[str, Any] = Field(default_factory=dict)
61
+
62
+
63
+ @router.post("/sentinel_deep")
64
+ async def sentinel_deep_endpoint(
65
+ request: Request,
66
+ body: SentinelDeepRequest,
67
+ ) -> SentinelDeepResponse:
68
+ """
69
+ Run comprehensive SENTINEL deep threat scan.
70
+
71
+ Analyzes 9 security collections and returns threat score 0-100.
72
+ """
73
+ scanner = SentinelDeepScanner()
74
+ try:
75
+ result = await scanner.scan(
76
+ token_address=body.address,
77
+ chain=body.chain,
78
+ )
79
+ result_dict = result.to_dict()
80
+ result_dict["tier"] = "premium"
81
+ result_dict["price_usd"] = 0.10
82
+
83
+ return SentinelDeepResponse(data=result_dict)
84
+ except Exception as e:
85
+ logger.error(f"Sentinel deep scan failed: {e}", exc_info=True)
86
+ raise HTTPException(status_code=500, detail="Scan failed") from e
87
+ finally:
88
+ await scanner.close()
backend/app/sentinel_deep.py ADDED
@@ -0,0 +1,735 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SENTINEL Deep Threat Scanner
3
+ =============================
4
+ Comprehensive on-chain threat detection combining 9 security collections into one
5
+ unified risk assessment. The "security SIEM" for DeFi tokens.
6
+
7
+ Collections analyzed:
8
+ 1. CONTRACT HEALTH — Honeypot traps, ownership risks, upgrade vulnerabilities
9
+ 2. LIQUIDITY SECURITY — LP lock status, withdrawal risks, single-sided pools
10
+ 3. RUG IMMINENCE — Early warning signals for imminent rug pulls
11
+ 4. MEV EXPOSURE — Sandwich attack risk, frontrunning exposure
12
+ 5. SUPPLY MANIPULATION — Bundled launches, insider distributions
13
+ 6. FEE MANIPULATION — Hidden taxes, dynamic fees, sell traps
14
+ 7. WASH TRADING — Artificial volume, circular trades, fake activity
15
+ 8. DEPLOYER HISTORY — Previous scams, funding patterns, risk trajectory
16
+ 9. SOCIAL INTELLIGENCE — Shill campaigns, hype spikes, telegram analysis
17
+
18
+ Architecture:
19
+ - Modular design: Each collection is a separate analyzer
20
+ - Confidence-weighted scoring: Each signal has data confidence factor
21
+ - Multi-chain support: EVM (Ethereum, BSC, Base, Arbitrum, Polygon) + Solana
22
+ - Evidence-first: Every finding includes concrete on-chain evidence
23
+ - Integration ready: Works with existing RMI detectors
24
+
25
+ Tier: Premium ($0.10)
26
+ Endpoint: POST /api/v1/x402-tools/sentinel_deep
27
+ """
28
+
29
+ import asyncio
30
+ import json
31
+ import logging
32
+ import time
33
+ from dataclasses import dataclass, field
34
+ from datetime import UTC, datetime, timedelta
35
+ from enum import Enum
36
+ from typing import Any
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ # ── Constants ─────────────────────────────────────────────────────
41
+ SENTINEL_COLLECTIONS = 9
42
+ HIGH_RISK_THRESHOLD = 70
43
+ MEDIUM_RISK_THRESHOLD = 40
44
+ LOW_RISK_THRESHOLD = 20
45
+
46
+
47
+ # ── Enums ─────────────────────────────────────────────────────────
48
+
49
+
50
+ class ThreatLevel(Enum):
51
+ """Threat level classification for sentinel results."""
52
+
53
+ SAFE = "safe"
54
+ LOW = "low"
55
+ MEDIUM = "medium"
56
+ HIGH = "high"
57
+ CRITICAL = "critical"
58
+
59
+ @property
60
+ def emoji(self) -> str:
61
+ return {
62
+ "safe": "✅",
63
+ "low": "🔵",
64
+ "medium": "🟡",
65
+ "high": "🟠",
66
+ "critical": "🔴",
67
+ }[self.value]
68
+
69
+ @property
70
+ def numeric(self) -> int:
71
+ return {"safe": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}[self.value]
72
+
73
+
74
+ class CollectionName(Enum):
75
+ """Security collection names."""
76
+
77
+ CONTRACT = "contract_health"
78
+ LIQUIDITY = "liquidity_security"
79
+ RUG_IMMINENCE = "rug_imminence"
80
+ MEV = "mev_exposure"
81
+ SUPPLY = "supply_manipulation"
82
+ FEES = "fee_manipulation"
83
+ WASH = "wash_trading"
84
+ DEPLOYER = "deployer_history"
85
+ SOCIAL = "social_intelligence"
86
+
87
+
88
+ # ── Data Models ───────────────────────────────────────────────────
89
+
90
+
91
+ @dataclass
92
+ class CollectionResult:
93
+ """Result from analyzing one security collection."""
94
+
95
+ name: CollectionName
96
+ score: float # 0-100 (higher = more risk)
97
+ confidence: float # 0-1 (how complete the analysis is)
98
+ findings: list[dict[str, Any]] = field(default_factory=list)
99
+ warnings: list[str] = field(default_factory=list)
100
+ execution_time_ms: float = 0.0
101
+
102
+ def to_dict(self) -> dict[str, Any]:
103
+ return {
104
+ "collection": self.name.value,
105
+ "score": round(self.score, 1),
106
+ "confidence": round(self.confidence, 2),
107
+ "findings": self.findings,
108
+ "warnings": self.warnings,
109
+ "execution_time_ms": round(self.execution_time_ms, 1),
110
+ }
111
+
112
+
113
+ @dataclass
114
+ class SentinelDeepReport:
115
+ """Complete sentinel deep analysis report."""
116
+
117
+ token_address: str
118
+ chain: str
119
+ token_name: str = ""
120
+ token_symbol: str = ""
121
+
122
+ # Core score
123
+ threat_score: float = 0.0 # 0-100 (higher = more dangerous)
124
+ threat_level: ThreatLevel = ThreatLevel.SAFE
125
+
126
+ # Collection breakdown
127
+ collections: dict[str, CollectionResult] = field(default_factory=dict)
128
+
129
+ # Metadata
130
+ scanned_at: str = ""
131
+ execution_time_ms: float = 0.0
132
+ sources_used: list[str] = field(default_factory=list)
133
+ warnings: list[str] = field(default_factory=list)
134
+
135
+ def to_dict(self) -> dict[str, Any]:
136
+ return {
137
+ "token_address": self.token_address,
138
+ "chain": self.chain,
139
+ "token_name": self.token_name,
140
+ "token_symbol": self.token_symbol,
141
+ "threat_score": round(self.threat_score, 1),
142
+ "threat_level": self.threat_level.value,
143
+ "threat_level_emoji": self.threat_level.emoji,
144
+ "collections": {
145
+ k: v.to_dict() for k, v in self.collections.items()
146
+ },
147
+ "scanned_at": self.scanned_at,
148
+ "execution_time_ms": round(self.execution_time_ms, 1),
149
+ "sources_used": self.sources_used,
150
+ "warnings": self.warnings,
151
+ }
152
+
153
+ def summary(self) -> str:
154
+ """Generate a human-readable summary."""
155
+ risk_pct = (self.threat_score / 100) * 10
156
+ risky_collections = [
157
+ (name, result.score)
158
+ for name, result in self.collections.items()
159
+ if result.score > 50
160
+ ]
161
+ risky_str = (
162
+ ", ".join(f"{n}({s:.0f})" for n, s in risky_collections[:3])
163
+ if risky_collections
164
+ else "No critical risks detected"
165
+ )
166
+ return (
167
+ f"{self.threat_level.emoji} SENTINEL DEEP — "
168
+ f"{self.token_symbol or self.token_name or self.token_address[:12]} | "
169
+ f"Threat: {self.threat_score:.0f}/100 | "
170
+ f"{risky_str}"
171
+ )
172
+
173
+
174
+ # ── Sentinel Deep Scanner ───────────────────────────────────────────
175
+
176
+
177
+ class SentinelDeepScanner:
178
+ """Comprehensive threat scanner combining multiple security signals."""
179
+
180
+ def __init__(self):
181
+ self._analyzers = {
182
+ CollectionName.CONTRACT: self._analyze_contract,
183
+ CollectionName.LIQUIDITY: self._analyze_liquidity,
184
+ CollectionName.RUG_IMMINENCE: self._analyze_rug_imminence,
185
+ CollectionName.MEV: self._analyze_mev,
186
+ CollectionName.SUPPLY: self._analyze_supply,
187
+ CollectionName.FEES: self._analyze_fees,
188
+ CollectionName.WASH: self._analyze_wash,
189
+ CollectionName.DEPLOYER: self._analyze_deployer,
190
+ CollectionName.SOCIAL: self._analyze_social,
191
+ }
192
+
193
+ async def scan(
194
+ self,
195
+ token_address: str,
196
+ chain: str = "ethereum",
197
+ transaction_data: list[dict] | None = None,
198
+ holder_data: list[dict] | None = None,
199
+ lp_data: dict | None = None,
200
+ ) -> SentinelDeepReport:
201
+ """
202
+ Run comprehensive threat scan on a token.
203
+
204
+ Args:
205
+ token_address: Token contract address
206
+ chain: Blockchain name
207
+ transaction_data: Optional transaction history
208
+ holder_data: Optional holder distribution data
209
+ lp_data: Optional liquidity pool data
210
+
211
+ Returns:
212
+ SentinelDeepReport with threat score and findings
213
+ """
214
+ start = time.time()
215
+
216
+ report = SentinelDeepReport(
217
+ token_address=token_address,
218
+ chain=chain,
219
+ scanned_at=datetime.now(tz=UTC).isoformat(),
220
+ )
221
+
222
+ # Run all collection analyzers in parallel
223
+ collection_results = {}
224
+ for name, analyzer in self._analyzers.items():
225
+ try:
226
+ result = await analyzer(
227
+ token_address, chain, transaction_data, holder_data, lp_data
228
+ )
229
+ collection_results[name.value] = result
230
+ except Exception as e:
231
+ logger.warning(f"Collection {name.value} analysis failed: {e}")
232
+ collection_results[name.value] = CollectionResult(
233
+ name=name,
234
+ score=0,
235
+ confidence=0,
236
+ warnings=[f"Analysis error: {str(e)}"],
237
+ )
238
+
239
+ report.collections = collection_results
240
+
241
+ # Calculate weighted threat score
242
+ report.threat_score = self._calculate_threat_score(collection_results)
243
+ report.threat_level = self._get_threat_level(report.threat_score)
244
+ report.execution_time_ms = (time.time() - start) * 1000
245
+
246
+ return report
247
+
248
+ def _calculate_threat_score(
249
+ self, collections: dict[str, CollectionResult]
250
+ ) -> float:
251
+ """Calculate weighted threat score from all collections."""
252
+ weights = {
253
+ CollectionName.CONTRACT.value: 0.25,
254
+ CollectionName.LIQUIDITY.value: 0.20,
255
+ CollectionName.RUG_IMMINENCE.value: 0.15,
256
+ CollectionName.MEV.value: 0.10,
257
+ CollectionName.SUPPLY.value: 0.10,
258
+ CollectionName.FEES.value: 0.08,
259
+ CollectionName.WASH.value: 0.07,
260
+ CollectionName.DEPLOYER.value: 0.03,
261
+ CollectionName.SOCIAL.value: 0.02,
262
+ }
263
+
264
+ total = 0.0
265
+ confidence_sum = 0.0
266
+
267
+ for name, result in collections.items():
268
+ weight = weights.get(name, 0.0)
269
+ # Weight by confidence - scores are already 0-100
270
+ weighted_score = result.score * weight * result.confidence
271
+ total += weighted_score
272
+ confidence_sum += weight * result.confidence
273
+
274
+ # Cap final score at 100 (scores are already 0-100)
275
+ if confidence_sum > 0:
276
+ return min(total / confidence_sum, 100.0)
277
+ return 0.0
278
+
279
+ def _get_threat_level(self, score: float) -> ThreatLevel:
280
+ """Convert numeric score to threat level."""
281
+ if score >= 80:
282
+ return ThreatLevel.CRITICAL
283
+ if score >= 60:
284
+ return ThreatLevel.HIGH
285
+ if score >= 40:
286
+ return ThreatLevel.MEDIUM
287
+ if score >= 20:
288
+ return ThreatLevel.LOW
289
+ return ThreatLevel.SAFE
290
+
291
+ # ── Collection Analyzers ───────────────────────────────────────
292
+
293
+ async def _analyze_contract(
294
+ self,
295
+ token_address: str,
296
+ chain: str,
297
+ _txs: list[dict] | None,
298
+ _holders: list[dict] | None,
299
+ _lp: dict | None,
300
+ ) -> CollectionResult:
301
+ """Analyze contract for honeypot and malicious patterns."""
302
+ start = time.time()
303
+
304
+ result = CollectionResult(
305
+ name=CollectionName.CONTRACT,
306
+ score=0.0,
307
+ confidence=0.5 if not _lp else 0.8,
308
+ )
309
+
310
+ if _lp:
311
+ risks = []
312
+ if _lp.get("honeypot_detected"):
313
+ result.score += 40
314
+ risks.append("honeypot_detected")
315
+ if _lp.get("sell_tax", 0) > 15:
316
+ result.score += 20
317
+ risks.append(f"high_sell_tax:{_lp['sell_tax']}%")
318
+ if _lp.get("owner_can_mint"):
319
+ result.score += 15
320
+ risks.append("owner_mint_risk")
321
+ if _lp.get("proxy_contract"):
322
+ result.score += 10
323
+ risks.append("proxy_upgrade_risk")
324
+ if _lp.get("trading_paused"):
325
+ result.score += 25
326
+ risks.append("trading_paused")
327
+
328
+ if risks:
329
+ result.findings.append({
330
+ "type": "contract_risk",
331
+ "description": f"Contract has {len(risks)} risk indicators",
332
+ "indicators": risks,
333
+ })
334
+
335
+ result.score = min(result.score, 100)
336
+ result.execution_time_ms = (time.time() - start) * 1000
337
+ return result
338
+
339
+ async def _analyze_liquidity(
340
+ self,
341
+ token_address: str,
342
+ chain: str,
343
+ _txs: list[dict] | None,
344
+ _holders: list[dict] | None,
345
+ lp: dict | None,
346
+ ) -> CollectionResult:
347
+ """Analyze liquidity pool security and risks."""
348
+ start = time.time()
349
+
350
+ result = CollectionResult(
351
+ name=CollectionName.LIQUIDITY,
352
+ score=0.0,
353
+ confidence=0.6 if lp else 0.3,
354
+ )
355
+
356
+ if lp:
357
+ risks = []
358
+ if lp.get("lp_removed"):
359
+ result.score += 50
360
+ risks.append("lp_removed")
361
+ if lp.get("locked_pct", 100) < 50:
362
+ result.score += 20
363
+ risks.append(f"partially_locked_lp:{lp['locked_pct']}%")
364
+ if lp.get("single_sided"):
365
+ result.score += 15
366
+ risks.append("single_sided_lp")
367
+ if lp.get("concentration", 0) > 0.5:
368
+ result.score += 25
369
+ risks.append("lp_concentration_risk")
370
+
371
+ if risks:
372
+ result.findings.append({
373
+ "type": "liquidity_risk",
374
+ "description": f"LP has {len(risks)} risk factors",
375
+ "indicators": risks,
376
+ })
377
+
378
+ result.score = min(result.score, 100)
379
+ result.execution_time_ms = (time.time() - start) * 1000
380
+ return result
381
+
382
+ async def _analyze_rug_imminence(
383
+ self,
384
+ token_address: str,
385
+ chain: str,
386
+ txs: list[dict] | None,
387
+ holders: list[dict] | None,
388
+ _lp: dict | None,
389
+ ) -> CollectionResult:
390
+ """Detect imminent rug pull signals."""
391
+ start = time.time()
392
+
393
+ result = CollectionResult(
394
+ name=CollectionName.RUG_IMMINENCE,
395
+ score=0.0,
396
+ confidence=0.4,
397
+ )
398
+
399
+ signals = []
400
+
401
+ # Check for concentration signals
402
+ if holders and len(holders) > 0:
403
+ total_supply = sum(float(h.get("balance", 0)) for h in holders[:20])
404
+ if total_supply > 0:
405
+ top_5_pct = sum(
406
+ float(h.get("balance", 0)) for h in holders[:5]
407
+ ) / total_supply * 100
408
+ if top_5_pct > 70:
409
+ result.score += 30
410
+ signals.append(f"top_5_concentration:{top_5_pct:.0f}%")
411
+
412
+ # Check for large transfers to exchanges (dev dumping)
413
+ if txs:
414
+ exchange_txs = [
415
+ t for t in txs
416
+ if any(
417
+ ex in (t.get("to", "") or "").lower()
418
+ for ex in ["0x", "binance", "coinbase", "okx"]
419
+ )
420
+ ]
421
+ if len(exchange_txs) > 5:
422
+ result.score += 20
423
+ signals.append(f"multiple_exchange_transfers:{len(exchange_txs)}")
424
+
425
+ if signals:
426
+ result.findings.append({
427
+ "type": "rug_signal",
428
+ "description": "Early warning signals detected",
429
+ "signals": signals,
430
+ })
431
+
432
+ result.score = min(result.score, 100)
433
+ result.execution_time_ms = (time.time() - start) * 1000
434
+ return result
435
+
436
+ async def _analyze_mev(
437
+ self,
438
+ token_address: str,
439
+ chain: str,
440
+ txs: list[dict] | None,
441
+ _holders: list[dict] | None,
442
+ _lp: dict | None,
443
+ ) -> CollectionResult:
444
+ """Analyze MEV/sandwich attack exposure."""
445
+ start = time.time()
446
+
447
+ result = CollectionResult(
448
+ name=CollectionName.MEV,
449
+ score=0.0,
450
+ confidence=0.5 if txs else 0.2,
451
+ )
452
+
453
+ if txs and len(txs) >= 3:
454
+ # Check for sandwich patterns
455
+ sandwich_signals = []
456
+ for i in range(1, len(txs) - 1):
457
+ prev_tx = txs[i - 1]
458
+ curr_tx = txs[i]
459
+ next_tx = txs[i + 1]
460
+
461
+ # Same trader, opposite types, close timing
462
+ if (
463
+ prev_tx.get("from") == next_tx.get("from")
464
+ and prev_tx.get("type") != curr_tx.get("type")
465
+ and next_tx.get("type") != curr_tx.get("type")
466
+ ):
467
+ sandwich_signals.append(f"possible_sandwich at block {curr_tx.get('block_number')}")
468
+
469
+ if len(sandwich_signals) >= 2:
470
+ result.score = 40
471
+ result.findings.append({
472
+ "type": "mev_risk",
473
+ "description": "Potential sandwich attack patterns detected",
474
+ "signals": sandwich_signals[:5],
475
+ })
476
+
477
+ result.execution_time_ms = (time.time() - start) * 1000
478
+ return result
479
+
480
+ async def _analyze_supply(
481
+ self,
482
+ token_address: str,
483
+ chain: str,
484
+ txs: list[dict] | None,
485
+ holders: list[dict] | None,
486
+ _lp: dict | None,
487
+ ) -> CollectionResult:
488
+ """Analyze supply manipulation patterns."""
489
+ start = time.time()
490
+
491
+ result = CollectionResult(
492
+ name=CollectionName.SUPPLY,
493
+ score=0.0,
494
+ confidence=0.4 if txs else 0.2,
495
+ )
496
+
497
+ signals = []
498
+
499
+ # Check for bundled launch patterns
500
+ if txs and len(txs) >= 5:
501
+ # Group by block and funder
502
+ block_wallets: dict[int, set[str]] = {}
503
+ for tx in txs[:50]:
504
+ block = tx.get("block_number", 0)
505
+ fr = tx.get("from", "")
506
+ if block:
507
+ if block not in block_wallets:
508
+ block_wallets[block] = set()
509
+ block_wallets[block].add(fr)
510
+
511
+ # High wallet density in early blocks = sniping
512
+ for block, wallets in block_wallets.items():
513
+ if len(wallets) >= 10:
514
+ result.score += 25
515
+ signals.append(f"sniper_block_{block}: {len(wallets)} wallets")
516
+ break
517
+
518
+ # Check holder concentration
519
+ if holders and len(holders) >= 100:
520
+ result.score += 15
521
+ signals.append(f"high_holder_count:{len(holders)}")
522
+ elif holders and len(holders) < 10:
523
+ result.score += 30
524
+ signals.append(f"low_holder_count:{len(holders)} - scarcity risk")
525
+
526
+ if signals:
527
+ result.findings.append({
528
+ "type": "supply_risk",
529
+ "description": "Supply manipulation indicators",
530
+ "signals": signals[:5],
531
+ })
532
+
533
+ result.score = min(result.score, 100)
534
+ result.execution_time_ms = (time.time() - start) * 1000
535
+ return result
536
+
537
+ async def _analyze_fees(
538
+ self,
539
+ token_address: str,
540
+ chain: str,
541
+ _txs: list[dict] | None,
542
+ _holders: list[dict] | None,
543
+ lp: dict | None,
544
+ ) -> CollectionResult:
545
+ """Analyze fee manipulation and sell traps."""
546
+ start = time.time()
547
+
548
+ result = CollectionResult(
549
+ name=CollectionName.FEES,
550
+ score=0.0,
551
+ confidence=0.6 if lp else 0.3,
552
+ )
553
+
554
+ signals = []
555
+
556
+ if lp:
557
+ sell_tax = lp.get("sell_tax", 0)
558
+ buy_tax = lp.get("buy_tax", 0)
559
+ cooldown = lp.get("cooldown_blocks", 0)
560
+
561
+ if sell_tax > 20:
562
+ result.score += 50
563
+ signals.append(f"extreme_sell_tax:{sell_tax}%")
564
+ elif sell_tax > 15:
565
+ result.score += 25
566
+ signals.append(f"high_sell_tax:{sell_tax}%")
567
+
568
+ if buy_tax > 10:
569
+ result.score += 20
570
+ signals.append(f"high_buy_tax:{buy_tax}%")
571
+
572
+ if cooldown > 100:
573
+ result.score += 30
574
+ signals.append(f"trading_cooldown:{cooldown} blocks")
575
+
576
+ if signals:
577
+ result.findings.append({
578
+ "type": "fee_risk",
579
+ "description": "Fee manipulation detected",
580
+ "signals": signals,
581
+ })
582
+
583
+ result.score = min(result.score, 100)
584
+ result.execution_time_ms = (time.time() - start) * 1000
585
+ return result
586
+
587
+ async def _analyze_wash(
588
+ self,
589
+ token_address: str,
590
+ chain: str,
591
+ txs: list[dict] | None,
592
+ _holders: list[dict] | None,
593
+ _lp: dict | None,
594
+ ) -> CollectionResult:
595
+ """Detect wash trading patterns."""
596
+ start = time.time()
597
+
598
+ result = CollectionResult(
599
+ name=CollectionName.WASH,
600
+ score=0.0,
601
+ confidence=0.3 if txs else 0.1,
602
+ )
603
+
604
+ if txs and len(txs) >= 10:
605
+ # Check for volume anomalies
606
+ volumes = [float(tx.get("volume_usd", 0)) for tx in txs]
607
+ total_volume = sum(volumes)
608
+
609
+ # Check for repeated trades between same wallets
610
+ wallet_pairs: dict[tuple[str, str], int] = {}
611
+ for tx in txs:
612
+ buyer = tx.get("buyer", tx.get("from", ""))
613
+ seller = tx.get("seller", tx.get("to", ""))
614
+ if buyer and seller:
615
+ pair = tuple(sorted([buyer.lower(), seller.lower()]))
616
+ wallet_pairs[pair] = wallet_pairs.get(pair, 0) + 1
617
+
618
+ # High repeat count = wash trading
619
+ wash_pairs = [(pair, count) for pair, count in wallet_pairs.items() if count >= 5]
620
+ if wash_pairs:
621
+ result.score += min(30 + len(wash_pairs) * 5, 60)
622
+ result.findings.append({
623
+ "type": "wash_pattern",
624
+ "description": f"{len(wash_pairs)} wallet pairs trading repeatedly",
625
+ "pairs": [f"{p[0][:10]}.../{p[0][10:20]}... ({p[1]} times)"
626
+ for p in wash_pairs[:5]],
627
+ })
628
+
629
+ result.score = min(result.score, 100)
630
+ result.execution_time_ms = (time.time() - start) * 1000
631
+ return result
632
+
633
+ async def _analyze_deployer(
634
+ self,
635
+ token_address: str,
636
+ chain: str,
637
+ _txs: list[dict] | None,
638
+ _holders: list[dict] | None,
639
+ _lp: dict | None,
640
+ ) -> CollectionResult:
641
+ """Analyze deployer history and risk patterns."""
642
+ start = time.time()
643
+
644
+ result = CollectionResult(
645
+ name=CollectionName.DEPLOYER,
646
+ score=0.0,
647
+ confidence=0.2, # Requires external data
648
+ )
649
+
650
+ signals = []
651
+
652
+ # This would integrate with deployer_history module when available
653
+ # For now, placeholder logic
654
+ signals.append("deployer_analysis_requires_external_data")
655
+
656
+ result.findings.append({
657
+ "type": "deployer_check",
658
+ "description": "Deployer analysis pending integration",
659
+ "signals": signals,
660
+ })
661
+
662
+ result.execution_time_ms = (time.time() - start) * 1000
663
+ return result
664
+
665
+ async def _analyze_social(
666
+ self,
667
+ token_address: str,
668
+ chain: str,
669
+ _txs: list[dict] | None,
670
+ _holders: list[dict] | None,
671
+ _lp: dict | None,
672
+ ) -> CollectionResult:
673
+ """Analyze social intelligence and shill patterns."""
674
+ start = time.time()
675
+
676
+ result = CollectionResult(
677
+ name=CollectionName.SOCIAL,
678
+ score=0.0,
679
+ confidence=0.15, # Requires external data
680
+ )
681
+
682
+ signals = []
683
+
684
+ # This would integrate with social signal modules
685
+ signals.append("social_analysis_requires_external_data")
686
+
687
+ result.findings.append({
688
+ "type": "social_intelligence",
689
+ "description": "Social analysis pending integration",
690
+ "signals": signals,
691
+ })
692
+
693
+ result.execution_time_ms = (time.time() - start) * 1000
694
+ return result
695
+
696
+ async def close(self):
697
+ """Cleanup resources."""
698
+ pass
699
+
700
+
701
+ # ── CLI Interface ─────────────────────────────────────────────────
702
+
703
+
704
+ async def main():
705
+ """CLI entry point for testing."""
706
+ import argparse
707
+
708
+ parser = argparse.ArgumentParser(description="SENTINEL Deep Threat Scanner")
709
+ parser.add_argument("address", help="Token contract address")
710
+ parser.add_argument("--chain", default="ethereum", help="Blockchain name")
711
+ parser.add_argument("--format", default="text", choices=["text", "json"])
712
+
713
+ args = parser.parse_args()
714
+
715
+ scanner = SentinelDeepScanner()
716
+
717
+ try:
718
+ report = await scanner.scan(args.address, chain=args.chain)
719
+
720
+ if args.format == "json":
721
+ print(json.dumps(report.to_dict(), indent=2))
722
+ else:
723
+ print(report.summary())
724
+ print()
725
+ for name, result in report.collections.items():
726
+ if result.findings:
727
+ print(f" {name}: {len(result.findings)} findings")
728
+ for f in result.findings:
729
+ print(f" - {f['description']}")
730
+ finally:
731
+ await scanner.close()
732
+
733
+
734
+ if __name__ == "__main__":
735
+ asyncio.run(main())
backend/app/test_sentinel_deep.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tests for SENTINEL Deep Threat Scanner.
3
+ """
4
+
5
+ import pytest
6
+ import asyncio
7
+ from app.sentinel_deep import (
8
+ SentinelDeepScanner,
9
+ SentinelDeepReport,
10
+ CollectionResult,
11
+ ThreatLevel,
12
+ CollectionName,
13
+ )
14
+
15
+
16
+ class TestSentinelDeepScanner:
17
+ """Test SENTINEL Deep Scanner functionality."""
18
+
19
+ @pytest.mark.asyncio
20
+ async def test_basic_scan(self):
21
+ """Should perform basic scan without errors."""
22
+ scanner = SentinelDeepScanner()
23
+ report = await scanner.scan(
24
+ token_address="0x1234567890abcdef1234567890abcdef12345678",
25
+ chain="ethereum"
26
+ )
27
+ assert report.token_address == "0x1234567890abcdef1234567890abcdef12345678"
28
+ assert report.chain == "ethereum"
29
+ assert len(report.collections) == 9
30
+ await scanner.close()
31
+
32
+ @pytest.mark.asyncio
33
+ async def test_threat_level_safe(self):
34
+ """Should return SAFE threat level for low scores."""
35
+ scanner = SentinelDeepScanner()
36
+ report = await scanner.scan(
37
+ token_address="0x1234567890abcdef1234567890abcdef12345678",
38
+ chain="ethereum"
39
+ )
40
+ # Without data, scores should be 0
41
+ assert report.threat_level == ThreatLevel.SAFE
42
+ await scanner.close()
43
+
44
+ @pytest.mark.asyncio
45
+ async def test_threat_level_critical(self):
46
+ """Should return CRITICAL threat level for high scores."""
47
+ scanner = SentinelDeepScanner()
48
+ report = await scanner.scan(
49
+ token_address="0x1234567890abcdef1234567890abcdef12345678",
50
+ chain="ethereum",
51
+ lp_data={"lp_removed": True, "honeypot_detected": True, "sell_tax": 25}
52
+ )
53
+ assert report.threat_level == ThreatLevel.CRITICAL
54
+ assert report.threat_score >= 80
55
+ await scanner.close()
56
+
57
+ @pytest.mark.asyncio
58
+ async def test_collection_names(self):
59
+ """Should have all 9 security collections."""
60
+ scanner = SentinelDeepScanner()
61
+ expected = {
62
+ "contract_health",
63
+ "liquidity_security",
64
+ "rug_imminence",
65
+ "mev_exposure",
66
+ "supply_manipulation",
67
+ "fee_manipulation",
68
+ "wash_trading",
69
+ "deployer_history",
70
+ "social_intelligence",
71
+ }
72
+ report = await scanner.scan(
73
+ token_address="0x1234567890abcdef1234567890abcdef12345678",
74
+ chain="ethereum"
75
+ )
76
+ assert set(report.collections.keys()) == expected
77
+ await scanner.close()
78
+
79
+ @pytest.mark.asyncio
80
+ async def test_with_transaction_data(self):
81
+ """Should process transaction data for bundle detection."""
82
+ scanner = SentinelDeepScanner()
83
+ txs = [
84
+ {"from": f"0x{i:040x}"} for i in range(10)
85
+ ]
86
+ report = await scanner.scan(
87
+ token_address="0x1234567890abcdef1234567890abcdef12345678",
88
+ chain="ethereum",
89
+ transaction_data=txs
90
+ )
91
+ assert report.collections["supply_manipulation"].score > 0
92
+ await scanner.close()
93
+
94
+ @pytest.mark.asyncio
95
+ async def test_with_holder_data(self):
96
+ """Should process holder data for concentration analysis."""
97
+ scanner = SentinelDeepScanner()
98
+ holders = [
99
+ {"address": "addr1", "balance": 95},
100
+ {"address": "addr2", "balance": 3},
101
+ {"address": "addr3", "balance": 2},
102
+ ]
103
+ report = await scanner.scan(
104
+ token_address="0x1234567890abcdef1234567890abcdef12345678",
105
+ chain="ethereum",
106
+ holder_data=holders
107
+ )
108
+ assert report.collections["rug_imminence"].score > 0
109
+ await scanner.close()
110
+
111
+ def test_threat_level_thresholds(self):
112
+ """Should correctly map scores to threat levels."""
113
+ assert ThreatLevel(0).value == "safe"
114
+ assert ThreatLevel(1).value == "low"
115
+
116
+ def test_report_to_dict(self):
117
+ """Should serialize report to dictionary."""
118
+ report = SentinelDeepReport(
119
+ token_address="0xtest",
120
+ chain="ethereum",
121
+ threat_score=50.0,
122
+ threat_level=ThreatLevel.MEDIUM,
123
+ )
124
+ result = report.to_dict()
125
+ assert result["token_address"] == "0xtest"
126
+ assert result["threat_score"] == 50.0
127
+ assert result["threat_level"] == "medium"
128
+
129
+ def test_summary_generation(self):
130
+ """Should generate human-readable summary."""
131
+ report = SentinelDeepReport(
132
+ token_address="0xtest",
133
+ chain="ethereum",
134
+ threat_score=25.0,
135
+ threat_level=ThreatLevel.LOW,
136
+ )
137
+ summary = report.summary()
138
+ assert "🔵" in summary # LOW emoji
139
+ assert "25" in summary or "25.0" in summary
140
+
141
+
142
+ if __name__ == "__main__":
143
+ asyncio.run(TestSentinelDeepScanner().test_basic_scan())
144
+ print("All tests passed!")