evilspider 2.5.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.
_version.py ADDED
@@ -0,0 +1,5 @@
1
+ """Single source of truth for EvilSpider metadata."""
2
+
3
+ __version__ = "2.5.0"
4
+ __author__ = "Baba01hacker666"
5
+ __license__ = "MIT"
analyzer/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Analyzer module for EvilSpider: Secrets, Tech Fingerprinting, Security Auditing, API parsing."""
2
+
3
+ from .api_parser import ApiParser
4
+ from .secrets import SecretHunter
5
+ from .security import SecurityAuditor
6
+ from .tech import TechDetector
7
+
8
+ __all__ = ["SecretHunter", "TechDetector", "SecurityAuditor", "ApiParser"]
analyzer/api_parser.py ADDED
@@ -0,0 +1,188 @@
1
+ """
2
+ EvilSpider - OpenAPI / Swagger & GraphQL API Parser
3
+ Parses API documentation specifications and extracts structured routes and parameters.
4
+ """
5
+
6
+ import json
7
+ from typing import Any, Dict, Optional
8
+
9
+
10
+ class ApiParser:
11
+ """Parses OpenAPI, Swagger, and GraphQL endpoints & schemas."""
12
+
13
+ def is_api_spec(self, content: str) -> bool:
14
+ """Determines if the response text looks like an OpenAPI or Swagger spec."""
15
+ if not content or len(content) < 20:
16
+ return False
17
+ if '"swagger"' in content or '"openapi"' in content or "swagger:" in content or "openapi:" in content:
18
+ return True
19
+ if '"__schema"' in content or '"__type"' in content:
20
+ return True
21
+ return False
22
+
23
+ def _join_api_url(self, server_url: str, path: str) -> str:
24
+ """Joins server base URL and API path properly preserving base path."""
25
+ s = server_url.rstrip("/")
26
+ p = path.lstrip("/")
27
+ return f"{s}/{p}"
28
+
29
+ def parse_spec(self, content: str, base_url: str = "") -> Optional[Dict[str, Any]]:
30
+ """Parses OpenAPI or Swagger content into endpoints and parameters."""
31
+ data = None
32
+ try:
33
+ data = json.loads(content)
34
+ except Exception:
35
+ try:
36
+ import yaml
37
+
38
+ data = yaml.safe_load(content)
39
+ except Exception:
40
+ return None
41
+
42
+ if not isinstance(data, dict):
43
+ return None
44
+
45
+ # Check for GraphQL introspection
46
+ if "data" in data and "__schema" in data["data"]:
47
+ return self._parse_graphql_introspection(data["data"]["__schema"], base_url)
48
+
49
+ is_swagger = "swagger" in data
50
+ is_openapi = "openapi" in data
51
+
52
+ if not (is_swagger or is_openapi):
53
+ return None
54
+
55
+ version = data.get("openapi") or data.get("swagger")
56
+ info = data.get("info", {})
57
+ title = info.get("title", "API")
58
+ api_description = info.get("description", "")
59
+
60
+ servers = []
61
+ if is_openapi and "servers" in data:
62
+ for s in data["servers"]:
63
+ s_url = s.get("url", "")
64
+ if s_url:
65
+ if s_url.startswith(("http://", "https://")):
66
+ servers.append(s_url)
67
+ elif base_url:
68
+ servers.append(self._join_api_url(base_url, s_url))
69
+ elif is_swagger:
70
+ host = data.get("host", "")
71
+ base_path = data.get("basePath", "")
72
+ schemes = data.get("schemes", ["https"])
73
+ scheme = schemes[0] if schemes else "https"
74
+ if host:
75
+ servers.append(f"{scheme}://{host}{base_path}".rstrip("/"))
76
+ elif base_path and base_url:
77
+ servers.append(self._join_api_url(base_url, base_path))
78
+
79
+ if not servers and base_url:
80
+ servers.append(base_url.rstrip("/"))
81
+
82
+ endpoints = []
83
+ paths = data.get("paths", {})
84
+
85
+ for path_str, path_item in paths.items():
86
+ if not isinstance(path_item, dict):
87
+ continue
88
+
89
+ for method in ("get", "post", "put", "delete", "patch", "options", "head"):
90
+ if method in path_item:
91
+ op = path_item[method]
92
+ if not isinstance(op, dict):
93
+ continue
94
+
95
+ summary = op.get("summary", "")
96
+ description = op.get("description", "")
97
+ operation_id = op.get("operationId", "")
98
+ tags = op.get("tags", [])
99
+
100
+ params = []
101
+ all_params = list(path_item.get("parameters", [])) + list(op.get("parameters", []))
102
+ for p in all_params:
103
+ if isinstance(p, dict) and "name" in p:
104
+ params.append(
105
+ {
106
+ "name": p.get("name"),
107
+ "in": p.get("in", "query"),
108
+ "required": p.get("required", False),
109
+ "type": p.get("type")
110
+ or (
111
+ p.get("schema", {}).get("type")
112
+ if isinstance(p.get("schema"), dict)
113
+ else "string"
114
+ ),
115
+ }
116
+ )
117
+
118
+ resolved_urls = [self._join_api_url(s, path_str) for s in servers] if servers else [path_str]
119
+
120
+ endpoints.append(
121
+ {
122
+ "path": path_str,
123
+ "method": method.upper(),
124
+ "summary": summary,
125
+ "description": description,
126
+ "operation_id": operation_id,
127
+ "tags": tags,
128
+ "parameters": params,
129
+ "urls": resolved_urls,
130
+ }
131
+ )
132
+
133
+ return {
134
+ "type": "OpenAPI" if is_openapi else "Swagger",
135
+ "version": str(version),
136
+ "title": title,
137
+ "description": api_description,
138
+ "servers": servers,
139
+ "endpoint_count": len(endpoints),
140
+ "endpoints": endpoints,
141
+ }
142
+
143
+ def _parse_graphql_introspection(self, schema: Dict[str, Any], base_url: str) -> Dict[str, Any]:
144
+ types = schema.get("types", [])
145
+ query_type_name = schema.get("queryType", {}).get("name", "Query") if schema.get("queryType") else "Query"
146
+ mutation_type_name = (
147
+ schema.get("mutationType", {}).get("name", "Mutation") if schema.get("mutationType") else "Mutation"
148
+ )
149
+
150
+ queries = []
151
+ mutations = []
152
+ custom_types = []
153
+
154
+ for t in types:
155
+ name = t.get("name", "")
156
+ if name.startswith("__"):
157
+ continue
158
+
159
+ fields = t.get("fields") or []
160
+ if name == query_type_name:
161
+ for f in fields:
162
+ queries.append(
163
+ {
164
+ "name": f.get("name"),
165
+ "description": f.get("description", ""),
166
+ "args": [a.get("name") for a in (f.get("args") or [])],
167
+ }
168
+ )
169
+ elif name == mutation_type_name:
170
+ for f in fields:
171
+ mutations.append(
172
+ {
173
+ "name": f.get("name"),
174
+ "description": f.get("description", ""),
175
+ "args": [a.get("name") for a in (f.get("args") or [])],
176
+ }
177
+ )
178
+ else:
179
+ custom_types.append(name)
180
+
181
+ return {
182
+ "type": "GraphQL Schema",
183
+ "url": base_url,
184
+ "queries": queries,
185
+ "mutations": mutations,
186
+ "types_count": len(custom_types),
187
+ "types": custom_types[:50],
188
+ }
analyzer/secrets.py ADDED
@@ -0,0 +1,363 @@
1
+ """
2
+ EvilSpider - Sensitive Data & Secret Hunter
3
+ Extracts credentials, API keys, tokens, private keys, and sensitive info from responses.
4
+ """
5
+
6
+ import math
7
+ import re
8
+ from typing import Any, Dict, List, Optional
9
+
10
+
11
+ class SecretRule:
12
+ def __init__(
13
+ self, name: str, severity: str, pattern: str, description: str, category: str = "Credentials", validator=None
14
+ ):
15
+ self.name = name
16
+ self.severity = severity # CRITICAL, HIGH, MEDIUM, LOW, INFO
17
+ self.pattern = re.compile(pattern, re.IGNORECASE if "PRIVATE KEY" not in pattern else 0)
18
+ self.description = description
19
+ self.category = category
20
+ self.validator = validator
21
+
22
+
23
+ def _shannon_entropy(data: str) -> float:
24
+ """Calculate Shannon entropy to filter low-entropy false positives."""
25
+ if not data:
26
+ return 0.0
27
+ entropy = 0.0
28
+ for x in set(data):
29
+ p_x = float(data.count(x)) / len(data)
30
+ if p_x > 0:
31
+ entropy += -p_x * math.log2(p_x)
32
+ return entropy
33
+
34
+
35
+ def _is_dummy_value(val: str) -> bool:
36
+ """Filter out common placeholder dummy strings."""
37
+ low = val.lower()
38
+ dummy_indicators = [
39
+ "example",
40
+ "placeholder",
41
+ "your_key",
42
+ "your_token",
43
+ "your_api",
44
+ "xxxxxx",
45
+ "123456",
46
+ "abcdef",
47
+ "sample",
48
+ "dummy",
49
+ "test_key",
50
+ "mysecret",
51
+ "change_me",
52
+ "insert_",
53
+ "<api_key>",
54
+ "{api_key}",
55
+ "undefined",
56
+ "null",
57
+ "none",
58
+ ]
59
+ return any(d in low for d in dummy_indicators)
60
+
61
+
62
+ SECRET_RULES: List[SecretRule] = [
63
+ # AWS Keys
64
+ SecretRule(
65
+ name="AWS Access Key ID",
66
+ severity="HIGH",
67
+ pattern=r"\b((?:AKIA|ABIA|ACCA|ASIA)[0-9A-Z]{16})\b",
68
+ description="Amazon Web Services Access Key ID",
69
+ category="Cloud",
70
+ validator=lambda m: not _is_dummy_value(m),
71
+ ),
72
+ SecretRule(
73
+ name="AWS Secret Access Key",
74
+ severity="CRITICAL",
75
+ pattern=r"(?i)aws_?(?:secret)?_?(?:access)?_?key(?:_id)?\s*[:=]\s*['\"]([A-Za-z0-9/+=]{40})['\"]",
76
+ description="Amazon Web Services Secret Access Key",
77
+ category="Cloud",
78
+ validator=lambda m: _shannon_entropy(m) > 3.0 and not _is_dummy_value(m),
79
+ ),
80
+ # Google Cloud
81
+ SecretRule(
82
+ name="Google Cloud API Key",
83
+ severity="HIGH",
84
+ pattern=r"\b(AIza[0-9A-Za-z_\-]{35})\b",
85
+ description="Google Cloud / Firebase API Key",
86
+ category="Cloud",
87
+ validator=lambda m: not _is_dummy_value(m),
88
+ ),
89
+ SecretRule(
90
+ name="Google OAuth Client Secret",
91
+ severity="HIGH",
92
+ pattern=r"(?i)client_secret\s*[:=]\s*['\"]([a-zA-Z0-9_\-]{24,36})['\"]",
93
+ description="Google OAuth Client Secret",
94
+ category="OAuth",
95
+ validator=lambda m: _shannon_entropy(m) > 3.2 and not _is_dummy_value(m),
96
+ ),
97
+ # GitHub
98
+ SecretRule(
99
+ name="GitHub Personal Access Token (Classic)",
100
+ severity="CRITICAL",
101
+ pattern=r"\b(ghp_[0-9a-zA-Z]{36})\b",
102
+ description="GitHub Classic Personal Access Token",
103
+ category="VCS",
104
+ ),
105
+ SecretRule(
106
+ name="GitHub Fine-Grained Token",
107
+ severity="CRITICAL",
108
+ pattern=r"\b(github_pat_[0-9a-zA-Z_]{82})\b",
109
+ description="GitHub Fine-Grained Personal Access Token",
110
+ category="VCS",
111
+ ),
112
+ SecretRule(
113
+ name="GitHub OAuth / App Token",
114
+ severity="CRITICAL",
115
+ pattern=r"\b((?:gho|ghu|ghs|ghr)_[0-9a-zA-Z]{36})\b",
116
+ description="GitHub OAuth / App Access Token",
117
+ category="VCS",
118
+ ),
119
+ # GitLab
120
+ SecretRule(
121
+ name="GitLab Personal Access Token",
122
+ severity="CRITICAL",
123
+ pattern=r"\b(glpat-[0-9a-zA-Z_\-]{20,24})\b",
124
+ description="GitLab Personal Access Token",
125
+ category="VCS",
126
+ ),
127
+ # Slack
128
+ SecretRule(
129
+ name="Slack Webhook URL",
130
+ severity="HIGH",
131
+ pattern=r"https://hooks\.slack\.com/services/T[a-zA-Z0-9_]+/B[a-zA-Z0-9_]+/[a-zA-Z0-9_]+",
132
+ description="Slack Incoming Webhook URL",
133
+ category="Messaging",
134
+ ),
135
+ SecretRule(
136
+ name="Slack API Token",
137
+ severity="CRITICAL",
138
+ pattern=r"\b(xox[baprs]-[0-9a-zA-Z\-]{10,72})\b",
139
+ description="Slack API / Bot / User Token",
140
+ category="Messaging",
141
+ ),
142
+ # Stripe
143
+ SecretRule(
144
+ name="Stripe Live Secret Key",
145
+ severity="CRITICAL",
146
+ pattern=r"\b(sk_live_[0-9a-zA-Z]{24,99})\b",
147
+ description="Stripe Live Secret Key",
148
+ category="Payment",
149
+ ),
150
+ SecretRule(
151
+ name="Stripe Live Restricted Key",
152
+ severity="HIGH",
153
+ pattern=r"\b(rk_live_[0-9a-zA-Z]{24,99})\b",
154
+ description="Stripe Live Restricted Key",
155
+ category="Payment",
156
+ ),
157
+ SecretRule(
158
+ name="Stripe Publishable Key",
159
+ severity="INFO",
160
+ pattern=r"\b(pk_live_[0-9a-zA-Z]{24,99})\b",
161
+ description="Stripe Live Publishable Key",
162
+ category="Payment",
163
+ ),
164
+ # JWT Token
165
+ SecretRule(
166
+ name="JSON Web Token (JWT)",
167
+ severity="MEDIUM",
168
+ pattern=r"\b(eyJ[a-zA-Z0-9_\-]{10,}\.eyJ[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,})\b",
169
+ description="JSON Web Token authentication bearer",
170
+ category="Authentication",
171
+ ),
172
+ # Private Keys
173
+ SecretRule(
174
+ name="RSA Private Key",
175
+ severity="CRITICAL",
176
+ pattern=r"-----BEGIN (?:RSA )?PRIVATE KEY-----[^-]+-----END (?:RSA )?PRIVATE KEY-----",
177
+ description="RSA Private Key Block",
178
+ category="Cryptography",
179
+ ),
180
+ SecretRule(
181
+ name="OpenSSH / EC Private Key",
182
+ severity="CRITICAL",
183
+ pattern=r"-----BEGIN (?:OPENSSH|EC|DSA) PRIVATE KEY-----[^-]+-----END (?:OPENSSH|EC|DSA) PRIVATE KEY-----",
184
+ description="OpenSSH or EC/DSA Private Key Block",
185
+ category="Cryptography",
186
+ ),
187
+ SecretRule(
188
+ name="PGP Private Key",
189
+ severity="CRITICAL",
190
+ pattern=r"-----BEGIN PGP PRIVATE KEY BLOCK-----[^-]+-----END PGP PRIVATE KEY BLOCK-----",
191
+ description="PGP Private Key Block",
192
+ category="Cryptography",
193
+ ),
194
+ # SendGrid, Twilio, Mailgun
195
+ SecretRule(
196
+ name="SendGrid API Key",
197
+ severity="HIGH",
198
+ pattern=r"\b(SG\.[a-zA-Z0-9_\-]{22}\.[a-zA-Z0-9_\-]{43})\b",
199
+ description="SendGrid API Key",
200
+ category="Email",
201
+ ),
202
+ SecretRule(
203
+ name="Twilio Account SID",
204
+ severity="MEDIUM",
205
+ pattern=r"\b(AC[a-f0-9]{32})\b",
206
+ description="Twilio Account SID",
207
+ category="Messaging",
208
+ ),
209
+ SecretRule(
210
+ name="Twilio API Secret",
211
+ severity="HIGH",
212
+ pattern=r"\b(SK[a-f0-9]{32})\b",
213
+ description="Twilio API Secret Key",
214
+ category="Messaging",
215
+ ),
216
+ SecretRule(
217
+ name="Mailgun API Key",
218
+ severity="HIGH",
219
+ pattern=r"\b(key-[0-9a-zA-Z]{32})\b",
220
+ description="Mailgun API Key",
221
+ category="Email",
222
+ ),
223
+ # Square & Shopify
224
+ SecretRule(
225
+ name="Square Access Token",
226
+ severity="HIGH",
227
+ pattern=r"\b(sq0atp-[0-9A-Za-z_\-]{22})\b",
228
+ description="Square OAuth Access Token",
229
+ category="Payment",
230
+ ),
231
+ SecretRule(
232
+ name="Shopify Access Token",
233
+ severity="HIGH",
234
+ pattern=r"\b(shpat_[a-fA-F0-9]{32})\b",
235
+ description="Shopify Admin API Access Token",
236
+ category="E-commerce",
237
+ ),
238
+ # Telegram & Discord
239
+ SecretRule(
240
+ name="Telegram Bot Token",
241
+ severity="HIGH",
242
+ pattern=r"\b([0-9]{9,10}:[a-zA-Z0-9_\-]{35})\b",
243
+ description="Telegram Bot API Token",
244
+ category="Messaging",
245
+ ),
246
+ SecretRule(
247
+ name="Discord Webhook URL",
248
+ severity="HIGH",
249
+ pattern=r"https://(?:canary\.|ptb\.)?discord(?:app)?\.com/api/webhooks/[0-9]+/[a-zA-Z0-9_\-]+",
250
+ description="Discord Incoming Webhook URL",
251
+ category="Messaging",
252
+ ),
253
+ SecretRule(
254
+ name="Discord Bot Token",
255
+ severity="CRITICAL",
256
+ pattern=r"\b([MN][A-Za-z\d]{23,26}\.[A-Za-z\d_\-]{6}\.[A-Za-z\d_\-]{27,38})\b",
257
+ description="Discord Bot Token",
258
+ category="Messaging",
259
+ ),
260
+ # Database Connection Strings
261
+ SecretRule(
262
+ name="Database Connection URI",
263
+ severity="CRITICAL",
264
+ pattern=r"(?:mysql|postgres|postgresql|mongodb|redis|amqp|couchdb)://[a-zA-Z0-9_.\-]+:[a-zA-Z0-9_.\-~%!$&'()*+,;=]+@[a-zA-Z0-9_.\-]+(?::\d+)?(?:/[a-zA-Z0-9_.\-]*)?",
265
+ description="Database connection URI with credentials",
266
+ category="Database",
267
+ validator=lambda m: not _is_dummy_value(m) and "@localhost" not in m.lower() and "@127.0.0.1" not in m,
268
+ ),
269
+ # Hardcoded Passwords & Auth Headers in JS/HTML
270
+ SecretRule(
271
+ name="Hardcoded Secret / Password Assignment",
272
+ severity="MEDIUM",
273
+ pattern=r"(?i)(?:password|passwd|api_secret|client_secret|auth_token|access_token|secret_key)\s*[:=]\s*['\"]([a-zA-Z0-9!@#$%^&*()_+={}\[\]|;:<>,.?/~`_\-]{8,64})['\"]",
274
+ description="Hardcoded credential or token assignment",
275
+ category="Credentials",
276
+ validator=lambda m: _shannon_entropy(m) > 2.8 and not _is_dummy_value(m),
277
+ ),
278
+ SecretRule(
279
+ name="Authorization Bearer Token Header",
280
+ severity="HIGH",
281
+ pattern=r"(?i)bearer\s+([a-zA-Z0-9_\-\.]{20,128})",
282
+ description="Authorization Bearer Token leaked in response",
283
+ category="Authentication",
284
+ validator=lambda m: not _is_dummy_value(m) and _shannon_entropy(m) > 3.0,
285
+ ),
286
+ # Internal IPs and Leaked Infrastructure
287
+ SecretRule(
288
+ name="Private IPv4 Address",
289
+ severity="INFO",
290
+ pattern=r"\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b",
291
+ description="Internal RFC 1918 Private IP Address",
292
+ category="Infrastructure",
293
+ validator=lambda m: (
294
+ not m.startswith("10.0.0.1") and not m.startswith("192.168.1.1") and not m.startswith("127.0.0.1")
295
+ ),
296
+ ),
297
+ ]
298
+
299
+
300
+ class SecretHunter:
301
+ """Scans response bodies (HTML, JS, JSON, XML) for secrets, tokens, and credentials."""
302
+
303
+ def __init__(self, custom_rules: Optional[List[SecretRule]] = None):
304
+ self.rules = list(SECRET_RULES)
305
+ if custom_rules:
306
+ self.rules.extend(custom_rules)
307
+
308
+ def mask_secret(self, secret: str) -> str:
309
+ """Mask sensitive value for safe reporting (e.g., AKIA1234...ABCD)."""
310
+ if len(secret) <= 8:
311
+ return "***"
312
+ prefix_len = min(4, len(secret) // 4)
313
+ suffix_len = min(4, len(secret) // 4)
314
+ return f"{secret[:prefix_len]}...{secret[-suffix_len:]}"
315
+
316
+ def scan(self, content: str, source_url: str = "") -> List[Dict[str, Any]]:
317
+ """Scans content and returns structured findings."""
318
+ findings = []
319
+ if not content:
320
+ return findings
321
+
322
+ lines = content.splitlines()
323
+
324
+ for rule in self.rules:
325
+ for match in rule.pattern.finditer(content):
326
+ matched_val = match.group(1) if match.groups() else match.group(0)
327
+ matched_val = matched_val.strip()
328
+
329
+ if rule.validator and not rule.validator(matched_val):
330
+ continue
331
+
332
+ start_pos = match.start()
333
+ line_idx = content.count("\n", 0, start_pos)
334
+ line_num = line_idx + 1
335
+
336
+ context_start = max(0, line_idx - 1)
337
+ context_end = min(len(lines), line_idx + 2)
338
+ context_lines = lines[context_start:context_end]
339
+ context_snippet = "\n".join(context_lines)
340
+
341
+ findings.append(
342
+ {
343
+ "rule": rule.name,
344
+ "severity": rule.severity,
345
+ "category": rule.category,
346
+ "description": rule.description,
347
+ "matched": matched_val,
348
+ "masked": self.mask_secret(matched_val),
349
+ "line_number": line_num,
350
+ "context_snippet": context_snippet[:250],
351
+ "source_url": source_url,
352
+ }
353
+ )
354
+
355
+ seen = set()
356
+ deduped = []
357
+ for f in findings:
358
+ key = (f["rule"], f["matched"])
359
+ if key not in seen:
360
+ seen.add(key)
361
+ deduped.append(f)
362
+
363
+ return deduped