reconax 0.2.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.
- reconax/__init__.py +98 -0
- reconax/cli.py +213 -0
- reconax/context.py +163 -0
- reconax/core.py +120 -0
- reconax/formatters/__init__.py +27 -0
- reconax/formatters/json_out.py +69 -0
- reconax/formatters/rich_out.py +166 -0
- reconax/http_client.py +117 -0
- reconax/models.py +510 -0
- reconax/modules/__init__.py +43 -0
- reconax/modules/attack_surface.py +156 -0
- reconax/modules/base.py +40 -0
- reconax/modules/cookies.py +198 -0
- reconax/modules/cors.py +115 -0
- reconax/modules/csp.py +121 -0
- reconax/modules/dns.py +124 -0
- reconax/modules/endpoints.py +277 -0
- reconax/modules/headers.py +88 -0
- reconax/modules/html.py +305 -0
- reconax/modules/http.py +24 -0
- reconax/modules/metadata.py +199 -0
- reconax/modules/resources.py +174 -0
- reconax/modules/robots.py +131 -0
- reconax/modules/score.py +270 -0
- reconax/modules/security_txt.py +105 -0
- reconax/modules/sitemap.py +123 -0
- reconax/modules/sri.py +213 -0
- reconax/modules/tech.py +315 -0
- reconax/modules/tls.py +243 -0
- reconax/parsers/__init__.py +6 -0
- reconax/parsers/cookies.py +153 -0
- reconax/parsers/dns_lookup.py +46 -0
- reconax/parsers/headers.py +46 -0
- reconax/parsers/html_parser.py +118 -0
- reconax/parsers/robots.py +82 -0
- reconax/shortcuts.py +311 -0
- reconax-0.2.0.dist-info/METADATA +324 -0
- reconax-0.2.0.dist-info/RECORD +41 -0
- reconax-0.2.0.dist-info/WHEEL +4 -0
- reconax-0.2.0.dist-info/entry_points.txt +2 -0
- reconax-0.2.0.dist-info/licenses/LICENSE +21 -0
reconax/__init__.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""ReconAx - lightweight, developer-friendly website analyzer."""
|
|
2
|
+
|
|
3
|
+
from .core import ReconAx
|
|
4
|
+
from .models import (
|
|
5
|
+
ReconReport,
|
|
6
|
+
HTTPResponse,
|
|
7
|
+
HeaderAnalysis,
|
|
8
|
+
CookieInfo,
|
|
9
|
+
CookieAnalysis,
|
|
10
|
+
HTMLAnalysis,
|
|
11
|
+
RobotsAnalysis,
|
|
12
|
+
DNSAnalysis,
|
|
13
|
+
TLSAnalysis,
|
|
14
|
+
Technology,
|
|
15
|
+
TechAnalysis,
|
|
16
|
+
SitemapAnalysis,
|
|
17
|
+
CORSAnalysis,
|
|
18
|
+
CSPAnalysis,
|
|
19
|
+
SRIAnalysis,
|
|
20
|
+
SecurityTxtAnalysis,
|
|
21
|
+
MetadataAnalysis,
|
|
22
|
+
ResourceInfo,
|
|
23
|
+
ResourceAnalysis,
|
|
24
|
+
EndpointInfo,
|
|
25
|
+
EndpointAnalysis,
|
|
26
|
+
AttackSurfaceAnalysis,
|
|
27
|
+
ScoreItem,
|
|
28
|
+
ScoreReport,
|
|
29
|
+
)
|
|
30
|
+
from .shortcuts import (
|
|
31
|
+
analyze,
|
|
32
|
+
headers,
|
|
33
|
+
cookies,
|
|
34
|
+
html,
|
|
35
|
+
robots,
|
|
36
|
+
dns,
|
|
37
|
+
tls,
|
|
38
|
+
tech,
|
|
39
|
+
sitemap,
|
|
40
|
+
cors,
|
|
41
|
+
csp,
|
|
42
|
+
sri,
|
|
43
|
+
security_txt,
|
|
44
|
+
metadata,
|
|
45
|
+
resources,
|
|
46
|
+
endpoints,
|
|
47
|
+
attack_surface,
|
|
48
|
+
score,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__version__ = "0.2.0"
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"ReconAx",
|
|
55
|
+
"ReconReport",
|
|
56
|
+
"HTTPResponse",
|
|
57
|
+
"HeaderAnalysis",
|
|
58
|
+
"CookieInfo",
|
|
59
|
+
"CookieAnalysis",
|
|
60
|
+
"HTMLAnalysis",
|
|
61
|
+
"RobotsAnalysis",
|
|
62
|
+
"DNSAnalysis",
|
|
63
|
+
"TLSAnalysis",
|
|
64
|
+
"Technology",
|
|
65
|
+
"TechAnalysis",
|
|
66
|
+
"SitemapAnalysis",
|
|
67
|
+
"CORSAnalysis",
|
|
68
|
+
"CSPAnalysis",
|
|
69
|
+
"SRIAnalysis",
|
|
70
|
+
"SecurityTxtAnalysis",
|
|
71
|
+
"MetadataAnalysis",
|
|
72
|
+
"ResourceInfo",
|
|
73
|
+
"ResourceAnalysis",
|
|
74
|
+
"EndpointInfo",
|
|
75
|
+
"EndpointAnalysis",
|
|
76
|
+
"AttackSurfaceAnalysis",
|
|
77
|
+
"ScoreItem",
|
|
78
|
+
"ScoreReport",
|
|
79
|
+
"analyze",
|
|
80
|
+
"headers",
|
|
81
|
+
"cookies",
|
|
82
|
+
"html",
|
|
83
|
+
"robots",
|
|
84
|
+
"dns",
|
|
85
|
+
"tls",
|
|
86
|
+
"tech",
|
|
87
|
+
"sitemap",
|
|
88
|
+
"cors",
|
|
89
|
+
"csp",
|
|
90
|
+
"sri",
|
|
91
|
+
"security_txt",
|
|
92
|
+
"metadata",
|
|
93
|
+
"resources",
|
|
94
|
+
"endpoints",
|
|
95
|
+
"attack_surface",
|
|
96
|
+
"score",
|
|
97
|
+
"__version__",
|
|
98
|
+
]
|
reconax/cli.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Command-line interface for ReconAx."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
# Ensure UTF-8 encoding on Windows to prevent charmap errors with Rich Unicode
|
|
11
|
+
if sys.stdout.encoding != "UTF-8":
|
|
12
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
13
|
+
if sys.stderr.encoding != "UTF-8":
|
|
14
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
|
|
18
|
+
from .core import ReconAx
|
|
19
|
+
from .formatters.json_out import report_to_json, result_to_json
|
|
20
|
+
from .formatters.rich_out import render_report, render_result
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
name="reconax",
|
|
24
|
+
help="⚡ ReconAx - lightweight, developer-friendly website analyzer.",
|
|
25
|
+
no_args_is_help=True,
|
|
26
|
+
add_completion=True,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _write_output(content: str, output_file: str | None) -> None:
|
|
31
|
+
if output_file:
|
|
32
|
+
Path(output_file).write_text(content, encoding="utf-8")
|
|
33
|
+
typer.echo(f"Report written to {output_file}")
|
|
34
|
+
else:
|
|
35
|
+
typer.echo(content)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _run_module(
|
|
39
|
+
url: str,
|
|
40
|
+
method: str,
|
|
41
|
+
*,
|
|
42
|
+
as_json: bool = False,
|
|
43
|
+
explain: bool = False,
|
|
44
|
+
output_file: str | None = None,
|
|
45
|
+
timeout: float = 10.0,
|
|
46
|
+
) -> None:
|
|
47
|
+
recon = ReconAx(url, timeout=timeout)
|
|
48
|
+
try:
|
|
49
|
+
result = getattr(recon, method)()
|
|
50
|
+
if as_json:
|
|
51
|
+
_write_output(result_to_json(result), output_file)
|
|
52
|
+
elif output_file:
|
|
53
|
+
raise typer.BadParameter("Use --json with -o/--output.")
|
|
54
|
+
else:
|
|
55
|
+
render_result(result, explain=explain)
|
|
56
|
+
except ValueError as exc:
|
|
57
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
58
|
+
raise typer.Exit(code=1)
|
|
59
|
+
except typer.Exit:
|
|
60
|
+
raise
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
typer.echo(f"ReconAx error: {exc}", err=True)
|
|
63
|
+
raise typer.Exit(code=1)
|
|
64
|
+
finally:
|
|
65
|
+
recon.close()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _module_command(
|
|
69
|
+
url: str,
|
|
70
|
+
method: str,
|
|
71
|
+
explain: bool,
|
|
72
|
+
json_output: bool,
|
|
73
|
+
timeout: float,
|
|
74
|
+
) -> None:
|
|
75
|
+
_run_module(
|
|
76
|
+
url,
|
|
77
|
+
method,
|
|
78
|
+
explain=explain,
|
|
79
|
+
as_json=json_output,
|
|
80
|
+
timeout=timeout,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command("analyze")
|
|
85
|
+
def analyze_command(
|
|
86
|
+
url: str = typer.Argument(..., help="Target HTTP/HTTPS URL."),
|
|
87
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
88
|
+
explain: bool = typer.Option(False, "--explain", help="Show explanations."),
|
|
89
|
+
no_dns: bool = typer.Option(False, "--no-dns", help="Skip DNS analysis."),
|
|
90
|
+
timeout: float = typer.Option(10.0, "--timeout", min=0.1),
|
|
91
|
+
output: str | None = typer.Option(None, "-o", "--output", help="Write JSON to a file."),
|
|
92
|
+
) -> None:
|
|
93
|
+
recon = ReconAx(url, timeout=timeout)
|
|
94
|
+
try:
|
|
95
|
+
report = recon.analyze(include_dns=not no_dns)
|
|
96
|
+
if json_output:
|
|
97
|
+
_write_output(report_to_json(report), output)
|
|
98
|
+
elif output:
|
|
99
|
+
raise typer.BadParameter("Use --json with -o/--output.")
|
|
100
|
+
else:
|
|
101
|
+
render_report(report, explain=explain)
|
|
102
|
+
except ValueError as exc:
|
|
103
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
104
|
+
raise typer.Exit(code=1)
|
|
105
|
+
except typer.Exit:
|
|
106
|
+
raise
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
typer.echo(f"ReconAx error: {exc}", err=True)
|
|
109
|
+
raise typer.Exit(code=1)
|
|
110
|
+
finally:
|
|
111
|
+
recon.close()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _standard_options():
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command("headers")
|
|
119
|
+
def headers_command(
|
|
120
|
+
url: str = typer.Argument(...),
|
|
121
|
+
explain: bool = typer.Option(False, "--explain"),
|
|
122
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
123
|
+
timeout: float = typer.Option(10.0, "--timeout", min=0.1),
|
|
124
|
+
) -> None:
|
|
125
|
+
_module_command(url, "headers", explain, json_output, timeout)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@app.command("cookies")
|
|
129
|
+
def cookies_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
130
|
+
_module_command(url, "cookies", explain, json_output, timeout)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@app.command("html")
|
|
134
|
+
def html_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
135
|
+
_module_command(url, "html", explain, json_output, timeout)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@app.command("robots")
|
|
139
|
+
def robots_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
140
|
+
_module_command(url, "robots", explain, json_output, timeout)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@app.command("dns")
|
|
144
|
+
def dns_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
145
|
+
_module_command(url, "dns", explain, json_output, timeout)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.command("tls")
|
|
149
|
+
def tls_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
150
|
+
_module_command(url, "tls", explain, json_output, timeout)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@app.command("sitemap")
|
|
154
|
+
def sitemap_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
155
|
+
_module_command(url, "sitemap", explain, json_output, timeout)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@app.command("tech")
|
|
159
|
+
def tech_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
160
|
+
_module_command(url, "tech", explain, json_output, timeout)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@app.command("cors")
|
|
164
|
+
def cors_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
165
|
+
_module_command(url, "cors", explain, json_output, timeout)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@app.command("csp")
|
|
169
|
+
def csp_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
170
|
+
_module_command(url, "csp", explain, json_output, timeout)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@app.command("sri")
|
|
174
|
+
def sri_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
175
|
+
_module_command(url, "sri", explain, json_output, timeout)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@app.command("security-txt")
|
|
179
|
+
def security_txt_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
180
|
+
_module_command(url, "security_txt", explain, json_output, timeout)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@app.command("metadata")
|
|
184
|
+
def metadata_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
185
|
+
_module_command(url, "metadata", explain, json_output, timeout)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@app.command("resources")
|
|
189
|
+
def resources_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
190
|
+
_module_command(url, "resources", explain, json_output, timeout)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@app.command("endpoints")
|
|
194
|
+
def endpoints_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
195
|
+
_module_command(url, "endpoints", explain, json_output, timeout)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@app.command("attack-surface")
|
|
199
|
+
def attack_surface_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
200
|
+
_module_command(url, "attack_surface", explain, json_output, timeout)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@app.command("score")
|
|
204
|
+
def score_command(url: str = typer.Argument(...), explain: bool = typer.Option(False, "--explain"), json_output: bool = typer.Option(False, "--json"), timeout: float = typer.Option(10.0, "--timeout", min=0.1)) -> None:
|
|
205
|
+
_module_command(url, "score", explain, json_output, timeout)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main() -> None:
|
|
209
|
+
app()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
main()
|
reconax/context.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Shared analysis context for ReconAx."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
from bs4 import BeautifulSoup
|
|
10
|
+
|
|
11
|
+
from .http_client import HTTPClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class AnalysisContext:
|
|
16
|
+
"""Shared state and lazy caches for one ReconAx target."""
|
|
17
|
+
|
|
18
|
+
url: str
|
|
19
|
+
timeout: float = 10.0
|
|
20
|
+
verify_ssl: bool = True
|
|
21
|
+
|
|
22
|
+
def __post_init__(self) -> None:
|
|
23
|
+
self._client = HTTPClient(
|
|
24
|
+
timeout=self.timeout,
|
|
25
|
+
verify_ssl=self.verify_ssl,
|
|
26
|
+
)
|
|
27
|
+
self._response: Any = None
|
|
28
|
+
self._soup: BeautifulSoup | None = None
|
|
29
|
+
self._results: dict[str, Any] = {}
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def normalized_url(self) -> str:
|
|
33
|
+
return self._client.normalize_url(self.url)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def origin(self) -> str:
|
|
37
|
+
parsed = urlparse(self.url)
|
|
38
|
+
return f"{parsed.scheme}://{parsed.netloc}"
|
|
39
|
+
|
|
40
|
+
def response(self, refresh: bool = False):
|
|
41
|
+
"""Return the cached primary HTTP response."""
|
|
42
|
+
if self._response is None or refresh:
|
|
43
|
+
self._response = self._client.fetch(self.normalized_url)
|
|
44
|
+
self._soup = None
|
|
45
|
+
self._results.clear()
|
|
46
|
+
return self._response
|
|
47
|
+
|
|
48
|
+
def html(self) -> BeautifulSoup:
|
|
49
|
+
"""Return the lazily parsed primary HTML document."""
|
|
50
|
+
if self._soup is None:
|
|
51
|
+
self._soup = BeautifulSoup(self.response().content or "", "lxml")
|
|
52
|
+
return self._soup
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def body(self) -> str:
|
|
56
|
+
return self.response().content or ""
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def response_headers(self) -> dict[str, str]:
|
|
60
|
+
return self.response().headers
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def final_url(self) -> str:
|
|
64
|
+
return self.response().final_url
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def status_code(self) -> int:
|
|
68
|
+
return self.response().status_code
|
|
69
|
+
|
|
70
|
+
def get(self, url: str, **kwargs: Any):
|
|
71
|
+
"""Perform a raw GET for secondary public resources."""
|
|
72
|
+
return self._client.raw_get(url, **kwargs)
|
|
73
|
+
|
|
74
|
+
def _cached_module(self, key: str, module_class: type):
|
|
75
|
+
if key not in self._results:
|
|
76
|
+
self._results[key] = module_class(self).analyze()
|
|
77
|
+
return self._results[key]
|
|
78
|
+
|
|
79
|
+
def http_result(self):
|
|
80
|
+
from .modules.http import HTTPModule
|
|
81
|
+
return self._cached_module("http", HTTPModule)
|
|
82
|
+
|
|
83
|
+
def headers_result(self):
|
|
84
|
+
from .modules.headers import HeadersModule
|
|
85
|
+
return self._cached_module("headers", HeadersModule)
|
|
86
|
+
|
|
87
|
+
def cookies_result(self):
|
|
88
|
+
from .modules.cookies import CookiesModule
|
|
89
|
+
return self._cached_module("cookies", CookiesModule)
|
|
90
|
+
|
|
91
|
+
def html_result(self):
|
|
92
|
+
from .modules.html import HTMLModule
|
|
93
|
+
return self._cached_module("html", HTMLModule)
|
|
94
|
+
|
|
95
|
+
def robots_result(self):
|
|
96
|
+
from .modules.robots import RobotsModule
|
|
97
|
+
return self._cached_module("robots", RobotsModule)
|
|
98
|
+
|
|
99
|
+
def dns_result(self):
|
|
100
|
+
from .modules.dns import DNSModule
|
|
101
|
+
return self._cached_module("dns", DNSModule)
|
|
102
|
+
|
|
103
|
+
def tls_result(self):
|
|
104
|
+
from .modules.tls import TLSModule
|
|
105
|
+
return self._cached_module("tls", TLSModule)
|
|
106
|
+
|
|
107
|
+
def tech_result(self):
|
|
108
|
+
from .modules.tech import TechModule
|
|
109
|
+
return self._cached_module("tech", TechModule)
|
|
110
|
+
|
|
111
|
+
def sitemap_result(self):
|
|
112
|
+
from .modules.sitemap import SitemapModule
|
|
113
|
+
return self._cached_module("sitemap", SitemapModule)
|
|
114
|
+
|
|
115
|
+
def cors_result(self):
|
|
116
|
+
from .modules.cors import CORSModule
|
|
117
|
+
return self._cached_module("cors", CORSModule)
|
|
118
|
+
|
|
119
|
+
def csp_result(self):
|
|
120
|
+
from .modules.csp import CSPModule
|
|
121
|
+
return self._cached_module("csp", CSPModule)
|
|
122
|
+
|
|
123
|
+
def sri_result(self):
|
|
124
|
+
from .modules.sri import SRIModule
|
|
125
|
+
return self._cached_module("sri", SRIModule)
|
|
126
|
+
|
|
127
|
+
def security_txt_result(self):
|
|
128
|
+
from .modules.security_txt import SecurityTxtModule
|
|
129
|
+
return self._cached_module("security_txt", SecurityTxtModule)
|
|
130
|
+
|
|
131
|
+
def metadata_result(self):
|
|
132
|
+
from .modules.metadata import MetadataModule
|
|
133
|
+
return self._cached_module("metadata", MetadataModule)
|
|
134
|
+
|
|
135
|
+
def resources_result(self):
|
|
136
|
+
from .modules.resources import ResourcesModule
|
|
137
|
+
return self._cached_module("resources", ResourcesModule)
|
|
138
|
+
|
|
139
|
+
def endpoints_result(self):
|
|
140
|
+
from .modules.endpoints import EndpointsModule
|
|
141
|
+
return self._cached_module("endpoints", EndpointsModule)
|
|
142
|
+
|
|
143
|
+
def attack_surface_result(self):
|
|
144
|
+
from .modules.attack_surface import AttackSurfaceModule
|
|
145
|
+
return self._cached_module("attack_surface", AttackSurfaceModule)
|
|
146
|
+
|
|
147
|
+
def score_result(self):
|
|
148
|
+
from .modules.score import ScoreModule
|
|
149
|
+
return self._cached_module("score", ScoreModule)
|
|
150
|
+
|
|
151
|
+
def clear_cache(self) -> None:
|
|
152
|
+
self._response = None
|
|
153
|
+
self._soup = None
|
|
154
|
+
self._results.clear()
|
|
155
|
+
|
|
156
|
+
def close(self) -> None:
|
|
157
|
+
self._client.close()
|
|
158
|
+
|
|
159
|
+
def __enter__(self) -> "AnalysisContext":
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
163
|
+
self.close()
|
reconax/core.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""High-level ReconAx API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .context import AnalysisContext
|
|
6
|
+
from .models import ReconReport
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ReconAx:
|
|
10
|
+
"""Main public interface for ReconAx analysis."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
url: str,
|
|
15
|
+
*,
|
|
16
|
+
timeout: float = 10.0,
|
|
17
|
+
verify_ssl: bool = True,
|
|
18
|
+
) -> None:
|
|
19
|
+
self.context = AnalysisContext(
|
|
20
|
+
url=url,
|
|
21
|
+
timeout=timeout,
|
|
22
|
+
verify_ssl=verify_ssl,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def url(self) -> str:
|
|
27
|
+
return self.context.url
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def normalized_url(self) -> str:
|
|
31
|
+
return self.context.normalized_url
|
|
32
|
+
|
|
33
|
+
def http(self):
|
|
34
|
+
return self.context.http_result()
|
|
35
|
+
|
|
36
|
+
def headers(self):
|
|
37
|
+
return self.context.headers_result()
|
|
38
|
+
|
|
39
|
+
def cookies(self):
|
|
40
|
+
return self.context.cookies_result()
|
|
41
|
+
|
|
42
|
+
def html(self):
|
|
43
|
+
return self.context.html_result()
|
|
44
|
+
|
|
45
|
+
def robots(self):
|
|
46
|
+
return self.context.robots_result()
|
|
47
|
+
|
|
48
|
+
def dns(self):
|
|
49
|
+
return self.context.dns_result()
|
|
50
|
+
|
|
51
|
+
def tls(self):
|
|
52
|
+
return self.context.tls_result()
|
|
53
|
+
|
|
54
|
+
def tech(self):
|
|
55
|
+
return self.context.tech_result()
|
|
56
|
+
|
|
57
|
+
def sitemap(self):
|
|
58
|
+
return self.context.sitemap_result()
|
|
59
|
+
|
|
60
|
+
def cors(self):
|
|
61
|
+
return self.context.cors_result()
|
|
62
|
+
|
|
63
|
+
def csp(self):
|
|
64
|
+
return self.context.csp_result()
|
|
65
|
+
|
|
66
|
+
def sri(self):
|
|
67
|
+
return self.context.sri_result()
|
|
68
|
+
|
|
69
|
+
def security_txt(self):
|
|
70
|
+
return self.context.security_txt_result()
|
|
71
|
+
|
|
72
|
+
def metadata(self):
|
|
73
|
+
return self.context.metadata_result()
|
|
74
|
+
|
|
75
|
+
def resources(self):
|
|
76
|
+
return self.context.resources_result()
|
|
77
|
+
|
|
78
|
+
def endpoints(self):
|
|
79
|
+
return self.context.endpoints_result()
|
|
80
|
+
|
|
81
|
+
def attack_surface(self):
|
|
82
|
+
return self.context.attack_surface_result()
|
|
83
|
+
|
|
84
|
+
def score(self):
|
|
85
|
+
return self.context.score_result()
|
|
86
|
+
|
|
87
|
+
def analyze(self, *, include_dns: bool = True) -> ReconReport:
|
|
88
|
+
"""Run the complete passive analysis using shared module caches."""
|
|
89
|
+
report = ReconReport(
|
|
90
|
+
target=self.url,
|
|
91
|
+
normalized_url=self.normalized_url,
|
|
92
|
+
http=self.http(),
|
|
93
|
+
headers=self.headers(),
|
|
94
|
+
cookies=self.cookies(),
|
|
95
|
+
html=self.html(),
|
|
96
|
+
robots=self.robots(),
|
|
97
|
+
dns=self.dns() if include_dns else None,
|
|
98
|
+
tls=self.tls(),
|
|
99
|
+
tech=self.tech(),
|
|
100
|
+
sitemap=self.sitemap(),
|
|
101
|
+
cors=self.cors(),
|
|
102
|
+
csp=self.csp(),
|
|
103
|
+
sri=self.sri(),
|
|
104
|
+
security_txt=self.security_txt(),
|
|
105
|
+
metadata=self.metadata(),
|
|
106
|
+
resources=self.resources(),
|
|
107
|
+
endpoints=self.endpoints(),
|
|
108
|
+
attack_surface=self.attack_surface(),
|
|
109
|
+
score=self.score(),
|
|
110
|
+
)
|
|
111
|
+
return report
|
|
112
|
+
|
|
113
|
+
def close(self) -> None:
|
|
114
|
+
self.context.close()
|
|
115
|
+
|
|
116
|
+
def __enter__(self) -> "ReconAx":
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
120
|
+
self.close()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Output formatters for ReconAx.
|
|
3
|
+
|
|
4
|
+
Formatters convert structured ReconAx results into human-friendly
|
|
5
|
+
terminal output or machine-readable JSON.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .json_out import (
|
|
9
|
+
report_to_dict,
|
|
10
|
+
report_to_json,
|
|
11
|
+
result_to_dict,
|
|
12
|
+
result_to_json,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .rich_out import (
|
|
16
|
+
render_report,
|
|
17
|
+
render_result,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"report_to_dict",
|
|
22
|
+
"report_to_json",
|
|
23
|
+
"result_to_dict",
|
|
24
|
+
"result_to_json",
|
|
25
|
+
"render_report",
|
|
26
|
+
"render_result",
|
|
27
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""JSON output formatter for ReconAx."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import asdict, is_dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _serialize(value: Any) -> Any:
|
|
11
|
+
"""Recursively convert ReconAx objects into JSON-compatible values."""
|
|
12
|
+
if hasattr(value, "to_dict") and callable(value.to_dict):
|
|
13
|
+
return _serialize(value.to_dict())
|
|
14
|
+
|
|
15
|
+
if is_dataclass(value):
|
|
16
|
+
return {
|
|
17
|
+
key: _serialize(item)
|
|
18
|
+
for key, item in asdict(value).items()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if isinstance(value, dict):
|
|
22
|
+
return {str(key): _serialize(item) for key, item in value.items()}
|
|
23
|
+
|
|
24
|
+
if isinstance(value, (list, tuple, set)):
|
|
25
|
+
return [_serialize(item) for item in value]
|
|
26
|
+
|
|
27
|
+
if hasattr(value, "value"):
|
|
28
|
+
try:
|
|
29
|
+
return value.value
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def result_to_dict(result: Any) -> dict[str, Any]:
|
|
37
|
+
serialized = _serialize(result)
|
|
38
|
+
return serialized if isinstance(serialized, dict) else {"result": serialized}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def result_to_json(result: Any, *, indent: int = 2) -> str:
|
|
42
|
+
return json.dumps(
|
|
43
|
+
result_to_dict(result),
|
|
44
|
+
indent=indent,
|
|
45
|
+
ensure_ascii=False,
|
|
46
|
+
sort_keys=False,
|
|
47
|
+
default=str,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def report_to_dict(report: Any) -> dict[str, Any]:
|
|
52
|
+
return result_to_dict(report)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def report_to_json(report: Any, *, indent: int = 2) -> str:
|
|
56
|
+
return json.dumps(
|
|
57
|
+
report_to_dict(report),
|
|
58
|
+
indent=indent,
|
|
59
|
+
ensure_ascii=False,
|
|
60
|
+
sort_keys=False,
|
|
61
|
+
default=str,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def write_json(data: Any, filename: str, *, indent: int = 2) -> None:
|
|
66
|
+
payload = _serialize(data)
|
|
67
|
+
with open(filename, "w", encoding="utf-8") as file:
|
|
68
|
+
json.dump(payload, file, indent=indent, ensure_ascii=False, default=str)
|
|
69
|
+
file.write("\n")
|