stackscan 2.1.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.
Files changed (45) hide show
  1. stackscan/__init__.py +5 -0
  2. stackscan/__main__.py +4 -0
  3. stackscan/analyzers/__init__.py +29 -0
  4. stackscan/analyzers/creds.py +218 -0
  5. stackscan/analyzers/cve.py +458 -0
  6. stackscan/analyzers/exposure.py +73 -0
  7. stackscan/analyzers/infra.py +114 -0
  8. stackscan/analyzers/security.py +26 -0
  9. stackscan/analyzers/services.py +285 -0
  10. stackscan/analyzers/tech.py +178 -0
  11. stackscan/cli.py +750 -0
  12. stackscan/config/__init__.py +19 -0
  13. stackscan/config/sigdb_loader.py +74 -0
  14. stackscan/config/sources.py +238 -0
  15. stackscan/core/__init__.py +3 -0
  16. stackscan/core/core.py +60 -0
  17. stackscan/data/builtin.sigdb +0 -0
  18. stackscan/data/cve.json.gz +0 -0
  19. stackscan/data/reekeer-logo.png +0 -0
  20. stackscan/data/subdomains.txt +522 -0
  21. stackscan/export.py +574 -0
  22. stackscan/net/__init__.py +22 -0
  23. stackscan/net/dns.py +168 -0
  24. stackscan/net/fingerprint.py +41 -0
  25. stackscan/net/geo.py +70 -0
  26. stackscan/net/ipinfo.py +94 -0
  27. stackscan/net/ports.py +219 -0
  28. stackscan/net/resolver.py +54 -0
  29. stackscan/net/subdomains.py +457 -0
  30. stackscan/net/tld.py +126 -0
  31. stackscan/net/tls.py +78 -0
  32. stackscan/render.py +541 -0
  33. stackscan/scan.py +545 -0
  34. stackscan/theme.py +23 -0
  35. stackscan/types/__init__.py +43 -0
  36. stackscan/types/models.py +18 -0
  37. stackscan/types/output.py +367 -0
  38. stackscan/utils/__init__.py +4 -0
  39. stackscan/utils/paths.py +10 -0
  40. stackscan/utils/urls.py +39 -0
  41. stackscan-2.1.0.dist-info/METADATA +341 -0
  42. stackscan-2.1.0.dist-info/RECORD +45 -0
  43. stackscan-2.1.0.dist-info/WHEEL +4 -0
  44. stackscan-2.1.0.dist-info/entry_points.txt +2 -0
  45. stackscan-2.1.0.dist-info/licenses/LICENSE +18 -0
@@ -0,0 +1,367 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, TypeAlias
5
+
6
+ DetectedTech: TypeAlias = dict[str, list[str]]
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class Technology:
11
+ name: str
12
+ categories: tuple[str, ...] = ()
13
+ evidence: tuple[str, ...] = ()
14
+ location: str = ""
15
+
16
+ def to_dict(self) -> dict[str, Any]:
17
+ return {
18
+ "name": self.name,
19
+ "categories": list(self.categories),
20
+ "evidence": list(self.evidence),
21
+ "location": self.location,
22
+ }
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class NetworkInfo:
27
+ host: str
28
+ ipv4: tuple[str, ...] = ()
29
+ ipv6: tuple[str, ...] = ()
30
+ cname: tuple[str, ...] = ()
31
+ mx: tuple[str, ...] = ()
32
+ ns: tuple[str, ...] = ()
33
+ txt: tuple[str, ...] = ()
34
+ soa: tuple[str, ...] = ()
35
+ caa: tuple[str, ...] = ()
36
+ reverse_dns: dict[str, str] = field(default_factory=dict[str, str])
37
+ geo: dict[str, dict[str, str]] = field(default_factory=dict[str, dict[str, str]])
38
+ domains: tuple[str, ...] = ()
39
+
40
+ def to_dict(self) -> dict[str, Any]:
41
+ return {
42
+ "host": self.host,
43
+ "ipv4": list(self.ipv4),
44
+ "ipv6": list(self.ipv6),
45
+ "cname": list(self.cname),
46
+ "mx": list(self.mx),
47
+ "ns": list(self.ns),
48
+ "txt": list(self.txt),
49
+ "soa": list(self.soa),
50
+ "caa": list(self.caa),
51
+ "reverse_dns": self.reverse_dns,
52
+ "geo": self.geo,
53
+ "domains": list(self.domains),
54
+ }
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class TlsInfo:
59
+ subject: str | None = None
60
+ issuer: str | None = None
61
+ subject_alt_names: tuple[str, ...] = ()
62
+ not_before: str | None = None
63
+ not_after: str | None = None
64
+ protocol: str | None = None
65
+ cipher: str | None = None
66
+ alpn: str | None = None
67
+
68
+ def to_dict(self) -> dict[str, Any]:
69
+ return {
70
+ "subject": self.subject,
71
+ "issuer": self.issuer,
72
+ "subject_alt_names": list(self.subject_alt_names),
73
+ "not_before": self.not_before,
74
+ "not_after": self.not_after,
75
+ "protocol": self.protocol,
76
+ "cipher": self.cipher,
77
+ "alpn": self.alpn,
78
+ }
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class InfraInfo:
83
+ cdn: tuple[str, ...] = ()
84
+ waf: tuple[str, ...] = ()
85
+ proxy: tuple[str, ...] = ()
86
+ server: tuple[str, ...] = ()
87
+ notes: tuple[str, ...] = ()
88
+
89
+ def to_dict(self) -> dict[str, Any]:
90
+ return {
91
+ "cdn": list(self.cdn),
92
+ "waf": list(self.waf),
93
+ "proxy": list(self.proxy),
94
+ "server": list(self.server),
95
+ "notes": list(self.notes),
96
+ }
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class SecurityHeaders:
101
+ present: dict[str, str] = field(default_factory=dict[str, str])
102
+ missing: tuple[str, ...] = ()
103
+
104
+ def to_dict(self) -> dict[str, Any]:
105
+ return {"present": self.present, "missing": list(self.missing)}
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class ExposureInfo:
110
+ robots_txt: bool = False
111
+ sitemap: bool = False
112
+ security_txt: bool = False
113
+ git_exposed: bool = False
114
+ findings: tuple[str, ...] = ()
115
+ urls: dict[str, str] = field(default_factory=dict[str, str])
116
+
117
+ def to_dict(self) -> dict[str, Any]:
118
+ return {
119
+ "robots_txt": self.robots_txt,
120
+ "sitemap": self.sitemap,
121
+ "security_txt": self.security_txt,
122
+ "git_exposed": self.git_exposed,
123
+ "findings": list(self.findings),
124
+ "urls": self.urls,
125
+ }
126
+
127
+
128
+ @dataclass(frozen=True)
129
+ class Software:
130
+ name: str
131
+ version: str | None = None
132
+ source: str = ""
133
+ location: str = ""
134
+
135
+ def to_dict(self) -> dict[str, Any]:
136
+ return {
137
+ "name": self.name,
138
+ "version": self.version,
139
+ "source": self.source,
140
+ "location": self.location,
141
+ }
142
+
143
+
144
+ @dataclass(frozen=True)
145
+ class CveMatch:
146
+ id: str
147
+ product: str
148
+ version: str | None
149
+ severity: str
150
+ cvss: float | None
151
+ confidence: int
152
+ summary: str
153
+ url: str | None = None
154
+ locations: tuple[str, ...] = ()
155
+ sources: tuple[str, ...] = ()
156
+
157
+ def to_dict(self) -> dict[str, Any]:
158
+ return {
159
+ "id": self.id,
160
+ "product": self.product,
161
+ "version": self.version,
162
+ "severity": self.severity,
163
+ "cvss": self.cvss,
164
+ "confidence": self.confidence,
165
+ "summary": self.summary,
166
+ "url": self.url,
167
+ "locations": list(self.locations),
168
+ "sources": list(self.sources),
169
+ }
170
+
171
+
172
+ @dataclass(frozen=True)
173
+ class Port:
174
+ port: int
175
+ protocol: str = "tcp"
176
+ state: str = "open"
177
+ service: str | None = None
178
+ product: str | None = None
179
+ version: str | None = None
180
+ host: str | None = None
181
+
182
+ def to_dict(self) -> dict[str, Any]:
183
+ return {
184
+ "port": self.port,
185
+ "protocol": self.protocol,
186
+ "state": self.state,
187
+ "service": self.service,
188
+ "product": self.product,
189
+ "version": self.version,
190
+ "host": self.host,
191
+ }
192
+
193
+
194
+ @dataclass(frozen=True)
195
+ class PortScan:
196
+ scanner: str
197
+ ports: tuple[Port, ...] = ()
198
+ note: str | None = None
199
+
200
+ def to_dict(self) -> dict[str, Any]:
201
+ return {
202
+ "scanner": self.scanner,
203
+ "ports": [port.to_dict() for port in self.ports],
204
+ "note": self.note,
205
+ }
206
+
207
+
208
+ @dataclass(frozen=True)
209
+ class Subdomain:
210
+ name: str
211
+ addresses: tuple[str, ...] = ()
212
+ source: str = ""
213
+
214
+ def to_dict(self) -> dict[str, Any]:
215
+ return {"name": self.name, "addresses": list(self.addresses), "source": self.source}
216
+
217
+
218
+ @dataclass(frozen=True)
219
+ class IpInfo:
220
+ ip: str
221
+ country: str | None = None
222
+ city: str | None = None
223
+ org: str | None = None
224
+ isp: str | None = None
225
+ asn: str | None = None
226
+ is_cdn: bool = False
227
+ source: str = ""
228
+
229
+ def to_dict(self) -> dict[str, Any]:
230
+ return {
231
+ "ip": self.ip,
232
+ "country": self.country,
233
+ "city": self.city,
234
+ "org": self.org,
235
+ "isp": self.isp,
236
+ "asn": self.asn,
237
+ "is_cdn": self.is_cdn,
238
+ "source": self.source,
239
+ }
240
+
241
+
242
+ @dataclass(frozen=True)
243
+ class CredFinding:
244
+ target: str
245
+ service: str
246
+ kind: str
247
+ detail: str = ""
248
+ username: str | None = None
249
+ password: str | None = None
250
+
251
+ def to_dict(self) -> dict[str, Any]:
252
+ return {
253
+ "target": self.target,
254
+ "service": self.service,
255
+ "kind": self.kind,
256
+ "detail": self.detail,
257
+ "username": self.username,
258
+ "password": self.password,
259
+ }
260
+
261
+
262
+ @dataclass(frozen=True)
263
+ class SiteFinding:
264
+ url: str
265
+ final_url: str | None = None
266
+ status: int | None = None
267
+ error: str | None = None
268
+ technologies: list[Technology] = field(default_factory=list[Technology])
269
+ software: list[Software] = field(default_factory=list[Software])
270
+ infra: InfraInfo = field(default_factory=InfraInfo)
271
+ security: SecurityHeaders = field(default_factory=SecurityHeaders)
272
+ exposure: ExposureInfo | None = None
273
+ protocols: list[str] = field(default_factory=list[str])
274
+
275
+ def to_dict(self) -> dict[str, Any]:
276
+ return {
277
+ "url": self.url,
278
+ "final_url": self.final_url,
279
+ "status": self.status,
280
+ "error": self.error,
281
+ "technologies": [tech.to_dict() for tech in self.technologies],
282
+ "software": [sw.to_dict() for sw in self.software],
283
+ "infra": self.infra.to_dict(),
284
+ "security": self.security.to_dict(),
285
+ "exposure": self.exposure.to_dict() if self.exposure else None,
286
+ "protocols": list(self.protocols),
287
+ }
288
+
289
+
290
+ @dataclass(frozen=True)
291
+ class ServiceFinding:
292
+ name: str
293
+ kind: str
294
+ evidence: str
295
+ severity: str = "INFO"
296
+
297
+ def to_dict(self) -> dict[str, Any]:
298
+ return {
299
+ "name": self.name,
300
+ "kind": self.kind,
301
+ "evidence": self.evidence,
302
+ "severity": self.severity,
303
+ }
304
+
305
+
306
+ @dataclass
307
+ class ScanReport:
308
+ url: str
309
+ final_url: str | None = None
310
+ status: int | None = None
311
+ error: str | None = None
312
+ elapsed: float | None = None
313
+ technologies: list[Technology] = field(default_factory=list[Technology])
314
+ infra: InfraInfo = field(default_factory=InfraInfo)
315
+ security: SecurityHeaders = field(default_factory=SecurityHeaders)
316
+ network: NetworkInfo | None = None
317
+ tls: TlsInfo | None = None
318
+ exposure: ExposureInfo | None = None
319
+ software: list[Software] = field(default_factory=list[Software])
320
+ cves: list[CveMatch] = field(default_factory=list[CveMatch])
321
+ ports: PortScan | None = None
322
+ subdomains: list[Subdomain] = field(default_factory=list[Subdomain])
323
+ ip_info: list[IpInfo] = field(default_factory=list[IpInfo])
324
+ creds: list[CredFinding] = field(default_factory=list[CredFinding])
325
+ protocols: list[str] = field(default_factory=list[str])
326
+ site_findings: list[SiteFinding] = field(default_factory=list[SiteFinding])
327
+ services: list[ServiceFinding] = field(default_factory=list[ServiceFinding])
328
+
329
+ def by_category(self) -> DetectedTech:
330
+ grouped: dict[str, list[str]] = {}
331
+ for tech in self.technologies:
332
+ cats = tech.categories or ("uncategorized",)
333
+ for cat in cats:
334
+ grouped.setdefault(cat, [])
335
+ if tech.name not in grouped[cat]:
336
+ grouped[cat].append(tech.name)
337
+ return {cat: sorted(names) for cat, names in grouped.items()}
338
+
339
+ def all_technologies(self) -> list[Technology]:
340
+ techs: list[Technology] = list(self.technologies)
341
+ for site in self.site_findings:
342
+ techs.extend(site.technologies)
343
+ return techs
344
+
345
+ def to_dict(self) -> dict[str, Any]:
346
+ return {
347
+ "url": self.url,
348
+ "final_url": self.final_url,
349
+ "status": self.status,
350
+ "error": self.error,
351
+ "elapsed": self.elapsed,
352
+ "technologies": [tech.to_dict() for tech in self.technologies],
353
+ "infra": self.infra.to_dict(),
354
+ "security": self.security.to_dict(),
355
+ "network": self.network.to_dict() if self.network else None,
356
+ "tls": self.tls.to_dict() if self.tls else None,
357
+ "exposure": self.exposure.to_dict() if self.exposure else None,
358
+ "software": [item.to_dict() for item in self.software],
359
+ "cves": [cve.to_dict() for cve in self.cves],
360
+ "ports": self.ports.to_dict() if self.ports else None,
361
+ "subdomains": [sub.to_dict() for sub in self.subdomains],
362
+ "ip_info": [info.to_dict() for info in self.ip_info],
363
+ "creds": [finding.to_dict() for finding in self.creds],
364
+ "protocols": list(self.protocols),
365
+ "site_findings": [site.to_dict() for site in self.site_findings],
366
+ "services": [service.to_dict() for service in self.services],
367
+ }
@@ -0,0 +1,4 @@
1
+ from .paths import db_dir
2
+ from .urls import expand_cidr, host_of, is_cidr, is_https, normalize_url, port_of
3
+
4
+ __all__ = ["db_dir", "expand_cidr", "host_of", "is_cidr", "is_https", "normalize_url", "port_of"]
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ def db_dir() -> Path:
8
+ home = os.environ.get("STACKSCAN_HOME")
9
+ base = Path(home) if home else Path.home() / ".local" / "stackscan"
10
+ return base / "db"
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ import ipaddress
4
+ from urllib.parse import urlparse
5
+
6
+
7
+ def is_cidr(raw: str) -> bool:
8
+ try:
9
+ ipaddress.ip_network(raw, strict=False)
10
+ except ValueError:
11
+ return False
12
+ return "/" in raw
13
+
14
+
15
+ def expand_cidr(raw: str) -> list[str]:
16
+ network = ipaddress.ip_network(raw, strict=False)
17
+ return [str(host) for host in network]
18
+
19
+
20
+ def normalize_url(raw: str) -> str:
21
+ if raw.startswith(("http://", "https://")):
22
+ return raw
23
+ return "https://" + raw
24
+
25
+
26
+ def host_of(url: str) -> str:
27
+ parsed = urlparse(url if "://" in url else "https://" + url)
28
+ return (parsed.hostname or "").lower()
29
+
30
+
31
+ def port_of(url: str) -> int:
32
+ parsed = urlparse(url)
33
+ if parsed.port is not None:
34
+ return parsed.port
35
+ return 443 if is_https(url) else 80
36
+
37
+
38
+ def is_https(url: str) -> bool:
39
+ return urlparse(url).scheme == "https"