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.
- stackscan/__init__.py +5 -0
- stackscan/__main__.py +4 -0
- stackscan/analyzers/__init__.py +29 -0
- stackscan/analyzers/creds.py +218 -0
- stackscan/analyzers/cve.py +458 -0
- stackscan/analyzers/exposure.py +73 -0
- stackscan/analyzers/infra.py +114 -0
- stackscan/analyzers/security.py +26 -0
- stackscan/analyzers/services.py +285 -0
- stackscan/analyzers/tech.py +178 -0
- stackscan/cli.py +750 -0
- stackscan/config/__init__.py +19 -0
- stackscan/config/sigdb_loader.py +74 -0
- stackscan/config/sources.py +238 -0
- stackscan/core/__init__.py +3 -0
- stackscan/core/core.py +60 -0
- stackscan/data/builtin.sigdb +0 -0
- stackscan/data/cve.json.gz +0 -0
- stackscan/data/reekeer-logo.png +0 -0
- stackscan/data/subdomains.txt +522 -0
- stackscan/export.py +574 -0
- stackscan/net/__init__.py +22 -0
- stackscan/net/dns.py +168 -0
- stackscan/net/fingerprint.py +41 -0
- stackscan/net/geo.py +70 -0
- stackscan/net/ipinfo.py +94 -0
- stackscan/net/ports.py +219 -0
- stackscan/net/resolver.py +54 -0
- stackscan/net/subdomains.py +457 -0
- stackscan/net/tld.py +126 -0
- stackscan/net/tls.py +78 -0
- stackscan/render.py +541 -0
- stackscan/scan.py +545 -0
- stackscan/theme.py +23 -0
- stackscan/types/__init__.py +43 -0
- stackscan/types/models.py +18 -0
- stackscan/types/output.py +367 -0
- stackscan/utils/__init__.py +4 -0
- stackscan/utils/paths.py +10 -0
- stackscan/utils/urls.py +39 -0
- stackscan-2.1.0.dist-info/METADATA +341 -0
- stackscan-2.1.0.dist-info/RECORD +45 -0
- stackscan-2.1.0.dist-info/WHEEL +4 -0
- stackscan-2.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
}
|
stackscan/utils/paths.py
ADDED
stackscan/utils/urls.py
ADDED
|
@@ -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"
|