reconforge 1.0.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.
- reconforge/__init__.py +29 -0
- reconforge/analyzer.py +309 -0
- reconforge/api.py +196 -0
- reconforge/batch.py +192 -0
- reconforge/cache.py +124 -0
- reconforge/cli.py +415 -0
- reconforge/compare.py +185 -0
- reconforge/config.py +81 -0
- reconforge/dns_enum.py +217 -0
- reconforge/export.py +409 -0
- reconforge/logger.py +73 -0
- reconforge/portscan.py +106 -0
- reconforge/report.py +136 -0
- reconforge/scopecheck.py +105 -0
- reconforge/subdomains.py +79 -0
- reconforge/techdetect.py +79 -0
- reconforge/utils.py +124 -0
- reconforge/webserver.py +167 -0
- reconforge-1.0.0.dist-info/METADATA +364 -0
- reconforge-1.0.0.dist-info/RECORD +24 -0
- reconforge-1.0.0.dist-info/WHEEL +5 -0
- reconforge-1.0.0.dist-info/entry_points.txt +2 -0
- reconforge-1.0.0.dist-info/licenses/LICENSE +21 -0
- reconforge-1.0.0.dist-info/top_level.txt +1 -0
reconforge/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""ReconForge - AI-assisted recon toolkit for bug bounty hunters."""
|
|
2
|
+
|
|
3
|
+
__version__ = "1.0.0"
|
|
4
|
+
__author__ = "Feras"
|
|
5
|
+
__license__ = "MIT"
|
|
6
|
+
|
|
7
|
+
from .cache import ReconCache, get_cache
|
|
8
|
+
from .cli import cli
|
|
9
|
+
from .config import Config, get_config
|
|
10
|
+
from .logger import ReconForgeLogger, get_logger
|
|
11
|
+
from .portscan import scan_port, scan_ports
|
|
12
|
+
from .scopecheck import check_targets
|
|
13
|
+
from .subdomains import fetch_subdomains
|
|
14
|
+
from .techdetect import detect_technologies
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"cli",
|
|
18
|
+
"ReconCache",
|
|
19
|
+
"get_cache",
|
|
20
|
+
"Config",
|
|
21
|
+
"get_config",
|
|
22
|
+
"ReconForgeLogger",
|
|
23
|
+
"get_logger",
|
|
24
|
+
"scan_port",
|
|
25
|
+
"scan_ports",
|
|
26
|
+
"check_targets",
|
|
27
|
+
"fetch_subdomains",
|
|
28
|
+
"detect_technologies",
|
|
29
|
+
]
|
reconforge/analyzer.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Advanced analysis and filtering utilities for ReconForge findings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FindingsAnalyzer:
|
|
9
|
+
"""Analyze and filter recon findings for insights."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, findings: Dict[str, Any]):
|
|
12
|
+
"""Initialize analyzer with findings."""
|
|
13
|
+
self.findings = findings
|
|
14
|
+
self.subdomains = findings.get("subdomains", [])
|
|
15
|
+
self.ports = findings.get("ports", {})
|
|
16
|
+
self.technologies = findings.get("technologies", {})
|
|
17
|
+
|
|
18
|
+
def get_open_ports_summary(self) -> Dict[str, Any]:
|
|
19
|
+
"""Get summary of open ports across all hosts."""
|
|
20
|
+
open_ports = {}
|
|
21
|
+
port_count = {}
|
|
22
|
+
|
|
23
|
+
for host, results in self.ports.items():
|
|
24
|
+
for result in results:
|
|
25
|
+
if result.get("status") == "open":
|
|
26
|
+
port = result.get("port")
|
|
27
|
+
if port not in open_ports:
|
|
28
|
+
open_ports[port] = []
|
|
29
|
+
port_count[port] = 0
|
|
30
|
+
open_ports[port].append(host)
|
|
31
|
+
port_count[port] += 1
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
"open_ports": open_ports,
|
|
35
|
+
"port_count": port_count,
|
|
36
|
+
"total_open": sum(port_count.values()),
|
|
37
|
+
"unique_ports": len(open_ports),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
def get_technology_summary(self) -> Dict[str, Any]:
|
|
41
|
+
"""Get summary of detected technologies."""
|
|
42
|
+
tech_count = {}
|
|
43
|
+
tech_hosts = {}
|
|
44
|
+
|
|
45
|
+
for url, result in self.technologies.items():
|
|
46
|
+
for tech in result.get("technologies", []):
|
|
47
|
+
if tech not in tech_count:
|
|
48
|
+
tech_count[tech] = 0
|
|
49
|
+
tech_hosts[tech] = []
|
|
50
|
+
tech_count[tech] += 1
|
|
51
|
+
tech_hosts[tech].append(url)
|
|
52
|
+
|
|
53
|
+
# Sort by frequency
|
|
54
|
+
sorted_techs = sorted(tech_count.items(), key=lambda x: x[1], reverse=True)
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
"technology_count": dict(sorted_techs),
|
|
58
|
+
"technology_hosts": tech_hosts,
|
|
59
|
+
"unique_technologies": len(tech_count),
|
|
60
|
+
"most_common": sorted_techs[0][0] if sorted_techs else None,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def find_interesting_ports(self) -> List[Dict[str, Any]]:
|
|
64
|
+
"""Find potentially interesting open ports."""
|
|
65
|
+
interesting = {
|
|
66
|
+
80: "HTTP",
|
|
67
|
+
443: "HTTPS",
|
|
68
|
+
8080: "HTTP Alt",
|
|
69
|
+
8443: "HTTPS Alt",
|
|
70
|
+
3000: "Node.js",
|
|
71
|
+
5000: "Flask/Dev",
|
|
72
|
+
8000: "Django/Dev",
|
|
73
|
+
9000: "SonarQube",
|
|
74
|
+
27017: "MongoDB",
|
|
75
|
+
6379: "Redis",
|
|
76
|
+
5432: "PostgreSQL",
|
|
77
|
+
3306: "MySQL",
|
|
78
|
+
1433: "MSSQL",
|
|
79
|
+
22: "SSH",
|
|
80
|
+
21: "FTP",
|
|
81
|
+
25: "SMTP",
|
|
82
|
+
110: "POP3",
|
|
83
|
+
143: "IMAP",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
found = []
|
|
87
|
+
for host, results in self.ports.items():
|
|
88
|
+
for result in results:
|
|
89
|
+
if result.get("status") == "open":
|
|
90
|
+
port = result.get("port")
|
|
91
|
+
if port in interesting:
|
|
92
|
+
found.append({
|
|
93
|
+
"host": host,
|
|
94
|
+
"port": port,
|
|
95
|
+
"service": interesting[port],
|
|
96
|
+
"banner": result.get("banner", ""),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return sorted(found, key=lambda x: x["port"])
|
|
100
|
+
|
|
101
|
+
def find_security_headers(self) -> Dict[str, Any]:
|
|
102
|
+
"""Analyze security headers across all URLs."""
|
|
103
|
+
security_headers = {
|
|
104
|
+
"Strict-Transport-Security": "HSTS",
|
|
105
|
+
"Content-Security-Policy": "CSP",
|
|
106
|
+
"X-Content-Type-Options": "X-Content-Type-Options",
|
|
107
|
+
"X-Frame-Options": "Clickjacking Protection",
|
|
108
|
+
"X-XSS-Protection": "XSS Protection",
|
|
109
|
+
"Referrer-Policy": "Referrer Policy",
|
|
110
|
+
"Permissions-Policy": "Permissions Policy",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
found = {}
|
|
114
|
+
missing = {}
|
|
115
|
+
|
|
116
|
+
for url, result in self.technologies.items():
|
|
117
|
+
headers = result.get("headers", {})
|
|
118
|
+
for header, name in security_headers.items():
|
|
119
|
+
if header in headers:
|
|
120
|
+
if name not in found:
|
|
121
|
+
found[name] = []
|
|
122
|
+
found[name].append(url)
|
|
123
|
+
else:
|
|
124
|
+
if name not in missing:
|
|
125
|
+
missing[name] = []
|
|
126
|
+
missing[name].append(url)
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
"found": found,
|
|
130
|
+
"missing": missing,
|
|
131
|
+
"coverage": len(found) / len(security_headers) if security_headers else 0,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
def find_potential_vulnerabilities(self) -> List[Dict[str, Any]]:
|
|
135
|
+
"""Find potential vulnerability indicators."""
|
|
136
|
+
vulnerabilities = []
|
|
137
|
+
|
|
138
|
+
# Check for outdated technologies
|
|
139
|
+
outdated_patterns = {
|
|
140
|
+
"Apache/2.2": "Outdated Apache",
|
|
141
|
+
"nginx/1.0": "Outdated nginx",
|
|
142
|
+
"PHP/5": "Outdated PHP",
|
|
143
|
+
"jQuery/1": "Outdated jQuery",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for url, result in self.technologies.items():
|
|
147
|
+
headers = result.get("headers", {})
|
|
148
|
+
server = headers.get("Server", "")
|
|
149
|
+
|
|
150
|
+
for pattern, vuln in outdated_patterns.items():
|
|
151
|
+
if pattern.lower() in server.lower():
|
|
152
|
+
vulnerabilities.append({
|
|
153
|
+
"url": url,
|
|
154
|
+
"type": "Outdated Software",
|
|
155
|
+
"finding": vuln,
|
|
156
|
+
"severity": "Medium",
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
# Check for missing security headers
|
|
160
|
+
security_check = self.find_security_headers()
|
|
161
|
+
for header, urls in security_check["missing"].items():
|
|
162
|
+
if urls:
|
|
163
|
+
vulnerabilities.append({
|
|
164
|
+
"url": urls[0] if urls else "Unknown",
|
|
165
|
+
"type": "Missing Security Header",
|
|
166
|
+
"finding": f"Missing {header}",
|
|
167
|
+
"severity": "Low",
|
|
168
|
+
"affected_urls": len(urls),
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
return vulnerabilities
|
|
172
|
+
|
|
173
|
+
def get_subdomain_statistics(self) -> Dict[str, Any]:
|
|
174
|
+
"""Get statistics about discovered subdomains."""
|
|
175
|
+
subdomain_patterns = {}
|
|
176
|
+
for subdomain in self.subdomains:
|
|
177
|
+
parts = subdomain.split(".")
|
|
178
|
+
if len(parts) > 2:
|
|
179
|
+
prefix = parts[0]
|
|
180
|
+
if prefix not in subdomain_patterns:
|
|
181
|
+
subdomain_patterns[prefix] = 0
|
|
182
|
+
subdomain_patterns[prefix] += 1
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
"total_subdomains": len(self.subdomains),
|
|
186
|
+
"unique_prefixes": len(subdomain_patterns),
|
|
187
|
+
"common_prefixes": sorted(
|
|
188
|
+
subdomain_patterns.items(), key=lambda x: x[1], reverse=True
|
|
189
|
+
)[:10],
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
def get_risk_assessment(self) -> Dict[str, Any]:
|
|
193
|
+
"""Get overall risk assessment."""
|
|
194
|
+
score = 0
|
|
195
|
+
findings = []
|
|
196
|
+
|
|
197
|
+
# Check for open ports
|
|
198
|
+
open_summary = self.get_open_ports_summary()
|
|
199
|
+
if open_summary["total_open"] > 5:
|
|
200
|
+
score += 20
|
|
201
|
+
findings.append(f"Many open ports detected ({open_summary['total_open']})")
|
|
202
|
+
|
|
203
|
+
# Check for common vulnerable ports
|
|
204
|
+
vulnerable_ports = [3306, 27017, 6379, 5432, 1433]
|
|
205
|
+
for port in vulnerable_ports:
|
|
206
|
+
if port in open_summary["open_ports"]:
|
|
207
|
+
score += 15
|
|
208
|
+
findings.append(f"Potentially exposed database port: {port}")
|
|
209
|
+
|
|
210
|
+
# Check for missing security headers
|
|
211
|
+
security = self.find_security_headers()
|
|
212
|
+
if security["coverage"] < 0.5:
|
|
213
|
+
score += 25
|
|
214
|
+
findings.append("Missing multiple security headers")
|
|
215
|
+
|
|
216
|
+
# Check for outdated software
|
|
217
|
+
vulns = self.find_potential_vulnerabilities()
|
|
218
|
+
if vulns:
|
|
219
|
+
score += 10 * len(vulns)
|
|
220
|
+
findings.append(f"Found {len(vulns)} potential vulnerability indicators")
|
|
221
|
+
|
|
222
|
+
# Normalize score to 0-100
|
|
223
|
+
score = min(score, 100)
|
|
224
|
+
|
|
225
|
+
risk_level = "Low" if score < 30 else "Medium" if score < 70 else "High"
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
"risk_score": score,
|
|
229
|
+
"risk_level": risk_level,
|
|
230
|
+
"findings": findings,
|
|
231
|
+
"recommendations": self._get_recommendations(risk_level),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
def _get_recommendations(self, risk_level: str) -> List[str]:
|
|
235
|
+
"""Get recommendations based on risk level."""
|
|
236
|
+
recommendations = []
|
|
237
|
+
|
|
238
|
+
if risk_level == "High":
|
|
239
|
+
recommendations.extend([
|
|
240
|
+
"Conduct thorough security assessment",
|
|
241
|
+
"Review exposed services and ports",
|
|
242
|
+
"Implement security headers",
|
|
243
|
+
"Update outdated software",
|
|
244
|
+
"Consider WAF deployment",
|
|
245
|
+
])
|
|
246
|
+
elif risk_level == "Medium":
|
|
247
|
+
recommendations.extend([
|
|
248
|
+
"Review security configuration",
|
|
249
|
+
"Add missing security headers",
|
|
250
|
+
"Update software to latest versions",
|
|
251
|
+
"Implement additional monitoring",
|
|
252
|
+
])
|
|
253
|
+
else:
|
|
254
|
+
recommendations.extend([
|
|
255
|
+
"Continue security monitoring",
|
|
256
|
+
"Keep software updated",
|
|
257
|
+
"Maintain security headers",
|
|
258
|
+
])
|
|
259
|
+
|
|
260
|
+
return recommendations
|
|
261
|
+
|
|
262
|
+
def generate_summary_report(self) -> str:
|
|
263
|
+
"""Generate a text summary of findings."""
|
|
264
|
+
report = []
|
|
265
|
+
report.append("=" * 60)
|
|
266
|
+
report.append("RECONFORGE ANALYSIS SUMMARY")
|
|
267
|
+
report.append("=" * 60)
|
|
268
|
+
report.append("")
|
|
269
|
+
|
|
270
|
+
# Subdomain stats
|
|
271
|
+
subdomain_stats = self.get_subdomain_statistics()
|
|
272
|
+
report.append(f"Subdomains: {subdomain_stats['total_subdomains']}")
|
|
273
|
+
report.append(f"Unique Prefixes: {subdomain_stats['unique_prefixes']}")
|
|
274
|
+
report.append("")
|
|
275
|
+
|
|
276
|
+
# Open ports
|
|
277
|
+
port_summary = self.get_open_ports_summary()
|
|
278
|
+
report.append(f"Open Ports: {port_summary['total_open']}")
|
|
279
|
+
report.append(f"Unique Ports: {port_summary['unique_ports']}")
|
|
280
|
+
report.append("")
|
|
281
|
+
|
|
282
|
+
# Technologies
|
|
283
|
+
tech_summary = self.get_technology_summary()
|
|
284
|
+
report.append(f"Technologies Detected: {tech_summary['unique_technologies']}")
|
|
285
|
+
if tech_summary["most_common"]:
|
|
286
|
+
report.append(f"Most Common: {tech_summary['most_common']}")
|
|
287
|
+
report.append("")
|
|
288
|
+
|
|
289
|
+
# Risk assessment
|
|
290
|
+
risk = self.get_risk_assessment()
|
|
291
|
+
report.append(f"Risk Score: {risk['risk_score']}/100")
|
|
292
|
+
report.append(f"Risk Level: {risk['risk_level']}")
|
|
293
|
+
report.append("")
|
|
294
|
+
|
|
295
|
+
if risk["findings"]:
|
|
296
|
+
report.append("Key Findings:")
|
|
297
|
+
for finding in risk["findings"]:
|
|
298
|
+
report.append(f" - {finding}")
|
|
299
|
+
report.append("")
|
|
300
|
+
|
|
301
|
+
if risk["recommendations"]:
|
|
302
|
+
report.append("Recommendations:")
|
|
303
|
+
for rec in risk["recommendations"]:
|
|
304
|
+
report.append(f" - {rec}")
|
|
305
|
+
|
|
306
|
+
report.append("")
|
|
307
|
+
report.append("=" * 60)
|
|
308
|
+
|
|
309
|
+
return "\n".join(report)
|
reconforge/api.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Programmatic API for ReconForge - use ReconForge in your Python code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from .cache import get_cache
|
|
8
|
+
from .config import get_config
|
|
9
|
+
from .logger import get_logger
|
|
10
|
+
from .portscan import scan_ports
|
|
11
|
+
from .report import generate_report as _generate_report
|
|
12
|
+
from .scopecheck import check_targets
|
|
13
|
+
from .subdomains import fetch_subdomains
|
|
14
|
+
from .techdetect import detect_technologies
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ReconForgeAPI:
|
|
18
|
+
"""Main API class for ReconForge."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, use_cache: bool = True, log_file: Optional[str] = None):
|
|
21
|
+
"""Initialize ReconForge API.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
use_cache: Enable result caching
|
|
25
|
+
log_file: Optional log file path
|
|
26
|
+
"""
|
|
27
|
+
self.cache = get_cache() if use_cache else None
|
|
28
|
+
self.config = get_config()
|
|
29
|
+
self.logger = get_logger(log_file=log_file)
|
|
30
|
+
|
|
31
|
+
def discover_subdomains(self, domain: str, timeout: Optional[float] = None) -> Dict[str, Any]:
|
|
32
|
+
"""Discover subdomains for a domain.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
domain: Target domain
|
|
36
|
+
timeout: Request timeout (uses config default if None)
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Dictionary with subdomains and errors
|
|
40
|
+
"""
|
|
41
|
+
timeout = timeout or self.config.get("timeout", 10.0)
|
|
42
|
+
|
|
43
|
+
# Check cache
|
|
44
|
+
if self.cache:
|
|
45
|
+
cache_key = f"subdomains:{domain}"
|
|
46
|
+
cached = self.cache.get(cache_key)
|
|
47
|
+
if cached:
|
|
48
|
+
self.logger.info(f"Returning cached subdomains for {domain}")
|
|
49
|
+
return cached
|
|
50
|
+
|
|
51
|
+
self.logger.info(f"Discovering subdomains for {domain}")
|
|
52
|
+
subdomains, errors = fetch_subdomains(domain, timeout=timeout)
|
|
53
|
+
|
|
54
|
+
result = {
|
|
55
|
+
"domain": domain,
|
|
56
|
+
"subdomains": subdomains,
|
|
57
|
+
"errors": errors,
|
|
58
|
+
"count": len(subdomains),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Cache result
|
|
62
|
+
if self.cache:
|
|
63
|
+
self.cache.set(cache_key, result)
|
|
64
|
+
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
def scan_ports(
|
|
68
|
+
self,
|
|
69
|
+
host: str,
|
|
70
|
+
ports: Optional[List[int]] = None,
|
|
71
|
+
timeout: Optional[float] = None,
|
|
72
|
+
concurrent: Optional[bool] = None,
|
|
73
|
+
) -> Dict[str, Any]:
|
|
74
|
+
"""Scan ports on a host.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
host: Target host
|
|
78
|
+
ports: List of ports to scan (uses defaults if None)
|
|
79
|
+
timeout: Socket timeout (uses config default if None)
|
|
80
|
+
concurrent: Use concurrent scanning (uses config default if None)
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Dictionary with scan results
|
|
84
|
+
"""
|
|
85
|
+
timeout = timeout or self.config.get("port_scan_timeout", 2.0)
|
|
86
|
+
concurrent = concurrent if concurrent is not None else self.config.get("concurrent_scanning", True)
|
|
87
|
+
|
|
88
|
+
self.logger.info(f"Scanning ports on {host}")
|
|
89
|
+
result = scan_ports(host, ports=ports, timeout=timeout, concurrent=concurrent)
|
|
90
|
+
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
def detect_technologies(self, url: str, timeout: Optional[float] = None) -> Dict[str, Any]:
|
|
94
|
+
"""Detect technologies on a URL.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
url: Target URL
|
|
98
|
+
timeout: Request timeout (uses config default if None)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Dictionary with detected technologies
|
|
102
|
+
"""
|
|
103
|
+
timeout = timeout or self.config.get("timeout", 10.0)
|
|
104
|
+
|
|
105
|
+
self.logger.info(f"Detecting technologies on {url}")
|
|
106
|
+
result = detect_technologies(url, timeout=timeout)
|
|
107
|
+
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
def check_scope(self, targets_file: str, scope_file: str) -> Dict[str, Any]:
|
|
111
|
+
"""Check if targets are in scope.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
targets_file: Path to targets file
|
|
115
|
+
scope_file: Path to scope file
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Dictionary with in-scope and out-of-scope targets
|
|
119
|
+
"""
|
|
120
|
+
self.logger.info(f"Checking scope for targets in {targets_file}")
|
|
121
|
+
result = check_targets(targets_file, scope_file)
|
|
122
|
+
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
def generate_report(
|
|
126
|
+
self,
|
|
127
|
+
domain: str,
|
|
128
|
+
output: str,
|
|
129
|
+
timeout: Optional[float] = None,
|
|
130
|
+
max_hosts: Optional[int] = None,
|
|
131
|
+
concurrent: Optional[bool] = None,
|
|
132
|
+
) -> Dict[str, Any]:
|
|
133
|
+
"""Generate a comprehensive recon report.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
domain: Target domain
|
|
137
|
+
output: Output file path
|
|
138
|
+
timeout: Request timeout (uses config default if None)
|
|
139
|
+
max_hosts: Maximum hosts to scan
|
|
140
|
+
concurrent: Use concurrent scanning (uses config default if None)
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Dictionary with report generation results
|
|
144
|
+
"""
|
|
145
|
+
timeout = timeout or self.config.get("timeout", 10.0)
|
|
146
|
+
concurrent = concurrent if concurrent is not None else self.config.get("concurrent_scanning", True)
|
|
147
|
+
|
|
148
|
+
self.logger.info(f"Generating report for {domain}")
|
|
149
|
+
result = _generate_report(domain, output, timeout=timeout, max_hosts=max_hosts, concurrent=concurrent)
|
|
150
|
+
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
def get_cache_stats(self) -> Dict[str, Any]:
|
|
154
|
+
"""Get cache statistics."""
|
|
155
|
+
if not self.cache:
|
|
156
|
+
return {"cache_enabled": False}
|
|
157
|
+
|
|
158
|
+
return self.cache.get_stats()
|
|
159
|
+
|
|
160
|
+
def clear_cache(self) -> None:
|
|
161
|
+
"""Clear all cached results."""
|
|
162
|
+
if self.cache:
|
|
163
|
+
self.cache.clear()
|
|
164
|
+
self.logger.info("Cache cleared")
|
|
165
|
+
|
|
166
|
+
def cleanup_expired_cache(self) -> int:
|
|
167
|
+
"""Remove expired cache entries.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Number of deleted entries
|
|
171
|
+
"""
|
|
172
|
+
if not self.cache:
|
|
173
|
+
return 0
|
|
174
|
+
|
|
175
|
+
count = self.cache.cleanup_expired()
|
|
176
|
+
self.logger.info(f"Cleaned up {count} expired cache entries")
|
|
177
|
+
return count
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# Convenience functions for quick access
|
|
181
|
+
def discover_subdomains(domain: str) -> Dict[str, Any]:
|
|
182
|
+
"""Quick function to discover subdomains."""
|
|
183
|
+
api = ReconForgeAPI()
|
|
184
|
+
return api.discover_subdomains(domain)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def scan_ports_quick(host: str) -> Dict[str, Any]:
|
|
188
|
+
"""Quick function to scan ports."""
|
|
189
|
+
api = ReconForgeAPI()
|
|
190
|
+
return api.scan_ports(host)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def detect_technologies_quick(url: str) -> Dict[str, Any]:
|
|
194
|
+
"""Quick function to detect technologies."""
|
|
195
|
+
api = ReconForgeAPI()
|
|
196
|
+
return api.detect_technologies(url)
|