sufa 0.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.
- sufa/__init__.py +3 -0
- sufa/__main__.py +5 -0
- sufa/burp/__init__.py +3 -0
- sufa/burp/bridge.py +33 -0
- sufa/burp/extension.py +138 -0
- sufa/cli/__init__.py +0 -0
- sufa/cli/app.py +49 -0
- sufa/cli/commands/__init__.py +0 -0
- sufa/cli/commands/config_cmd.py +65 -0
- sufa/cli/commands/findings.py +115 -0
- sufa/cli/commands/import_cmd.py +145 -0
- sufa/cli/commands/project.py +66 -0
- sufa/cli/commands/provider.py +63 -0
- sufa/cli/commands/proxy.py +44 -0
- sufa/cli/commands/replay.py +79 -0
- sufa/cli/commands/report.py +70 -0
- sufa/cli/commands/scan.py +119 -0
- sufa/cli/commands/server.py +37 -0
- sufa/cli/display.py +146 -0
- sufa/core/__init__.py +0 -0
- sufa/core/ai/__init__.py +4 -0
- sufa/core/ai/base.py +55 -0
- sufa/core/ai/claude.py +98 -0
- sufa/core/ai/cost.py +66 -0
- sufa/core/ai/gemini.py +109 -0
- sufa/core/ai/ollama.py +93 -0
- sufa/core/ai/openai.py +92 -0
- sufa/core/ai/router.py +136 -0
- sufa/core/core_types.py +15 -0
- sufa/core/db/__init__.py +4 -0
- sufa/core/db/engine.py +53 -0
- sufa/core/db/models.py +136 -0
- sufa/core/db/repository.py +350 -0
- sufa/core/dedup/__init__.py +4 -0
- sufa/core/dedup/deduplicator.py +51 -0
- sufa/core/dedup/fingerprint.py +54 -0
- sufa/core/events/__init__.py +7 -0
- sufa/core/events/bus.py +82 -0
- sufa/core/events/events.py +90 -0
- sufa/core/graph/__init__.py +12 -0
- sufa/core/graph/attack_graph.py +125 -0
- sufa/core/graph/builder.py +89 -0
- sufa/core/graph/chain_analyzer.py +128 -0
- sufa/core/http/__init__.py +4 -0
- sufa/core/http/normalizer.py +109 -0
- sufa/core/http/parser.py +84 -0
- sufa/core/http/redactor.py +98 -0
- sufa/core/models/__init__.py +35 -0
- sufa/core/models/finding.py +76 -0
- sufa/core/models/project.py +18 -0
- sufa/core/models/request.py +62 -0
- sufa/core/models/scan.py +61 -0
- sufa/core/plugins/__init__.py +5 -0
- sufa/core/plugins/interface.py +53 -0
- sufa/core/plugins/loader.py +95 -0
- sufa/core/plugins/registry.py +45 -0
- sufa/core/reports/__init__.py +5 -0
- sufa/core/reports/html.py +136 -0
- sufa/core/reports/json_report.py +62 -0
- sufa/core/reports/sarif.py +93 -0
- sufa/core/reports/templates/report.html +102 -0
- sufa/core/rules/__init__.py +3 -0
- sufa/core/rules/builtin/__init__.py +0 -0
- sufa/core/rules/builtin/owasp_top10.yaml +128 -0
- sufa/core/rules/custom/__init__.py +0 -0
- sufa/core/rules/engine.py +152 -0
- sufa/core/scanner/__init__.py +4 -0
- sufa/core/scanner/active/__init__.py +3 -0
- sufa/core/scanner/active/engine.py +108 -0
- sufa/core/scanner/active/fuzz.py +94 -0
- sufa/core/scanner/active/verifier.py +89 -0
- sufa/core/scanner/passive.py +315 -0
- sufa/core/scanner/profiles.py +67 -0
- sufa/core/scanner/scope.py +114 -0
- sufa/core/session/__init__.py +3 -0
- sufa/core/session/manager.py +116 -0
- sufa/core/traffic/__init__.py +3 -0
- sufa/core/traffic/models.py +22 -0
- sufa/core/traffic/replay.py +48 -0
- sufa/core/traffic/repository.py +19 -0
- sufa/core/traffic/store.py +90 -0
- sufa/core/utils/__init__.py +4 -0
- sufa/core/utils/config.py +144 -0
- sufa/core/utils/logging.py +41 -0
- sufa/crawler/__init__.py +3 -0
- sufa/crawler/api_discovery.py +132 -0
- sufa/crawler/js_parser.py +45 -0
- sufa/crawler/spider.py +101 -0
- sufa/payloads/idor.txt +10 -0
- sufa/payloads/lfi.txt +13 -0
- sufa/payloads/sqli.txt +16 -0
- sufa/payloads/ssrf.txt +13 -0
- sufa/payloads/xss.txt +15 -0
- sufa/proxy/__init__.py +3 -0
- sufa/proxy/handler.py +22 -0
- sufa/proxy/interceptor.py +108 -0
- sufa/server/__init__.py +3 -0
- sufa/server/app.py +52 -0
- sufa/server/auth/__init__.py +3 -0
- sufa/server/auth/rbac.py +73 -0
- sufa/server/middleware/__init__.py +0 -0
- sufa/server/middleware/audit.py +33 -0
- sufa/server/routes/__init__.py +0 -0
- sufa/server/routes/findings.py +61 -0
- sufa/server/routes/projects.py +63 -0
- sufa/server/routes/scans.py +59 -0
- sufa/server/routes/traffic.py +63 -0
- sufa-0.1.0.dist-info/METADATA +139 -0
- sufa-0.1.0.dist-info/RECORD +112 -0
- sufa-0.1.0.dist-info/WHEEL +4 -0
- sufa-0.1.0.dist-info/entry_points.txt +2 -0
- sufa-0.1.0.dist-info/licenses/LICENSE +21 -0
sufa/__init__.py
ADDED
sufa/__main__.py
ADDED
sufa/burp/__init__.py
ADDED
sufa/burp/bridge.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Bridge communication protocol between Burp extension and sufa server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from sufa.core.http.parser import build_request, build_response
|
|
6
|
+
from sufa.core.models import TrafficEntry
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_burp_traffic(data: dict) -> TrafficEntry:
|
|
10
|
+
"""Parse traffic data sent from the Burp bridge into a TrafficEntry."""
|
|
11
|
+
req_data = data.get("request", {})
|
|
12
|
+
resp_data = data.get("response", {})
|
|
13
|
+
|
|
14
|
+
request = build_request(
|
|
15
|
+
method=req_data.get("method", "GET"),
|
|
16
|
+
url=req_data.get("url", ""),
|
|
17
|
+
headers=req_data.get("headers", {}),
|
|
18
|
+
body=req_data.get("body", ""),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
response = None
|
|
22
|
+
if resp_data.get("status_code"):
|
|
23
|
+
response = build_response(
|
|
24
|
+
status_code=resp_data.get("status_code", 0),
|
|
25
|
+
headers=resp_data.get("headers", {}),
|
|
26
|
+
body=resp_data.get("body", ""),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return TrafficEntry(
|
|
30
|
+
request=request,
|
|
31
|
+
response=response,
|
|
32
|
+
source=data.get("source", "burp"),
|
|
33
|
+
)
|
sufa/burp/extension.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Thin Jython bridge for Burp Suite integration.
|
|
2
|
+
|
|
3
|
+
This file is designed to be loaded as a Burp extension. It forwards traffic
|
|
4
|
+
to a running sufa instance over HTTP, keeping all intelligence in the Python 3 codebase.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
1. Start sufa server: sufa server start --port 9000
|
|
8
|
+
2. In Burp: Extender -> Extensions -> Add -> extension.py
|
|
9
|
+
3. Traffic captured in Burp is forwarded to sufa for analysis
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# NOTE: This module uses Python 2/Jython compatible syntax for Burp compatibility.
|
|
13
|
+
# It communicates with the main sufa process (Python 3) via HTTP.
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import threading
|
|
17
|
+
|
|
18
|
+
import urllib2
|
|
19
|
+
|
|
20
|
+
SUFA_URL = "http://localhost:9000"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BurpExtender:
|
|
24
|
+
"""Burp Suite extension that bridges to sufa."""
|
|
25
|
+
|
|
26
|
+
def registerExtenderCallbacks(self, callbacks):
|
|
27
|
+
self._callbacks = callbacks
|
|
28
|
+
self._helpers = callbacks.getHelpers()
|
|
29
|
+
callbacks.setExtensionName("sufa Bridge")
|
|
30
|
+
callbacks.registerHttpListener(self)
|
|
31
|
+
callbacks.registerContextMenuFactory(self)
|
|
32
|
+
|
|
33
|
+
self._sufa_url = SUFA_URL
|
|
34
|
+
self._enabled = True
|
|
35
|
+
|
|
36
|
+
self.stdout = callbacks.getStdout()
|
|
37
|
+
self._log("[sufa] Bridge extension loaded")
|
|
38
|
+
self._log("[sufa] Forwarding traffic to: " + self._sufa_url)
|
|
39
|
+
|
|
40
|
+
def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo):
|
|
41
|
+
if messageIsRequest:
|
|
42
|
+
return
|
|
43
|
+
if not self._enabled:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
thread = threading.Thread(
|
|
47
|
+
target=self._forward_traffic,
|
|
48
|
+
args=(messageInfo,),
|
|
49
|
+
)
|
|
50
|
+
thread.daemon = True
|
|
51
|
+
thread.start()
|
|
52
|
+
|
|
53
|
+
def _forward_traffic(self, messageInfo):
|
|
54
|
+
try:
|
|
55
|
+
request_info = self._helpers.analyzeRequest(messageInfo)
|
|
56
|
+
url = str(request_info.getUrl())
|
|
57
|
+
method = str(request_info.getMethod())
|
|
58
|
+
|
|
59
|
+
request_bytes = messageInfo.getRequest()
|
|
60
|
+
response_bytes = messageInfo.getResponse()
|
|
61
|
+
|
|
62
|
+
req_headers = {}
|
|
63
|
+
for header in request_info.getHeaders():
|
|
64
|
+
header = str(header)
|
|
65
|
+
if ": " in header:
|
|
66
|
+
k, v = header.split(": ", 1)
|
|
67
|
+
req_headers[k] = v
|
|
68
|
+
|
|
69
|
+
req_body = ""
|
|
70
|
+
body_offset = request_info.getBodyOffset()
|
|
71
|
+
if body_offset < len(request_bytes):
|
|
72
|
+
req_body = self._helpers.bytesToString(request_bytes[body_offset:])
|
|
73
|
+
|
|
74
|
+
resp_info = self._helpers.analyzeResponse(response_bytes) if response_bytes else None
|
|
75
|
+
status_code = resp_info.getStatusCode() if resp_info else 0
|
|
76
|
+
|
|
77
|
+
resp_headers = {}
|
|
78
|
+
resp_body = ""
|
|
79
|
+
if resp_info:
|
|
80
|
+
for header in resp_info.getHeaders():
|
|
81
|
+
header = str(header)
|
|
82
|
+
if ": " in header:
|
|
83
|
+
k, v = header.split(": ", 1)
|
|
84
|
+
resp_headers[k] = v
|
|
85
|
+
resp_body_offset = resp_info.getBodyOffset()
|
|
86
|
+
if resp_body_offset < len(response_bytes):
|
|
87
|
+
raw_bytes = response_bytes[resp_body_offset:]
|
|
88
|
+
resp_body = self._helpers.bytesToString(raw_bytes)[:3000]
|
|
89
|
+
|
|
90
|
+
payload = json.dumps(
|
|
91
|
+
{
|
|
92
|
+
"request": {
|
|
93
|
+
"method": method,
|
|
94
|
+
"url": url,
|
|
95
|
+
"headers": req_headers,
|
|
96
|
+
"body": req_body[:2000],
|
|
97
|
+
},
|
|
98
|
+
"response": {
|
|
99
|
+
"status_code": status_code,
|
|
100
|
+
"headers": resp_headers,
|
|
101
|
+
"body": resp_body,
|
|
102
|
+
},
|
|
103
|
+
"source": "burp",
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
req = urllib2.Request(
|
|
108
|
+
self._sufa_url + "/api/v1/traffic",
|
|
109
|
+
data=payload,
|
|
110
|
+
headers={"Content-Type": "application/json"},
|
|
111
|
+
)
|
|
112
|
+
urllib2.urlopen(req, timeout=5)
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
self._log("[sufa] Error forwarding: " + str(e))
|
|
116
|
+
|
|
117
|
+
def createMenuItems(self, invocation):
|
|
118
|
+
from javax.swing import JMenuItem
|
|
119
|
+
|
|
120
|
+
items = []
|
|
121
|
+
item = JMenuItem("Analyze with sufa")
|
|
122
|
+
item.addActionListener(lambda e: self._analyze_selected(invocation))
|
|
123
|
+
items.append(item)
|
|
124
|
+
return items
|
|
125
|
+
|
|
126
|
+
def _analyze_selected(self, invocation):
|
|
127
|
+
messages = invocation.getSelectedMessages()
|
|
128
|
+
if not messages:
|
|
129
|
+
return
|
|
130
|
+
for msg in messages:
|
|
131
|
+
self._forward_traffic(msg)
|
|
132
|
+
self._log(f"[sufa] Sent {len(messages)} requests for analysis")
|
|
133
|
+
|
|
134
|
+
def _log(self, msg):
|
|
135
|
+
try:
|
|
136
|
+
self.stdout.write((msg + "\n").encode())
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
sufa/cli/__init__.py
ADDED
|
File without changes
|
sufa/cli/app.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Main CLI application for sufa."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from sufa.cli.commands import (
|
|
8
|
+
config_cmd,
|
|
9
|
+
findings,
|
|
10
|
+
import_cmd,
|
|
11
|
+
project,
|
|
12
|
+
provider,
|
|
13
|
+
proxy,
|
|
14
|
+
replay,
|
|
15
|
+
report,
|
|
16
|
+
scan,
|
|
17
|
+
server,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
name="sufa",
|
|
22
|
+
help="AI-powered web vulnerability analysis platform.",
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
add_completion=False,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
app.add_typer(scan.app, name="scan", help="Scan targets for vulnerabilities")
|
|
28
|
+
app.add_typer(findings.app, name="findings", help="Manage vulnerability findings")
|
|
29
|
+
app.add_typer(project.app, name="project", help="Manage projects")
|
|
30
|
+
app.add_typer(config_cmd.app, name="config", help="View and modify configuration")
|
|
31
|
+
app.add_typer(provider.app, name="provider", help="Manage AI providers")
|
|
32
|
+
app.add_typer(report.app, name="report", help="Generate reports")
|
|
33
|
+
app.add_typer(import_cmd.app, name="import", help="Import traffic from files")
|
|
34
|
+
app.add_typer(proxy.app, name="proxy", help="Intercept proxy for capturing traffic")
|
|
35
|
+
app.add_typer(replay.app, name="replay", help="Replay stored requests")
|
|
36
|
+
app.add_typer(server.app, name="server", help="Start the API server (Enterprise)")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.callback(invoke_without_command=True)
|
|
40
|
+
def main_callback(ctx: typer.Context) -> None:
|
|
41
|
+
if ctx.invoked_subcommand is None:
|
|
42
|
+
from sufa.cli.display import print_banner
|
|
43
|
+
|
|
44
|
+
print_banner()
|
|
45
|
+
raise typer.Exit()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main() -> None:
|
|
49
|
+
app()
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""CLI commands for configuration management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from sufa.cli.display import console, print_config_table, print_error, print_success
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(no_args_is_help=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_config():
|
|
13
|
+
from sufa.core.utils.config import SufaConfig
|
|
14
|
+
|
|
15
|
+
return SufaConfig()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("set")
|
|
19
|
+
def config_set(
|
|
20
|
+
key: str = typer.Argument(..., help="Configuration key (e.g., ai.provider)"),
|
|
21
|
+
value: str = typer.Argument(..., help="Configuration value"),
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Set a configuration value."""
|
|
24
|
+
config = _get_config()
|
|
25
|
+
config.set(key, value)
|
|
26
|
+
print_success(f"{key} = {value}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command("get")
|
|
30
|
+
def config_get(
|
|
31
|
+
key: str = typer.Argument(..., help="Configuration key"),
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Get a configuration value."""
|
|
34
|
+
config = _get_config()
|
|
35
|
+
val = config.get(key)
|
|
36
|
+
if val is None:
|
|
37
|
+
print_error(f"Unknown key: {key}")
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
display_val = str(val)
|
|
40
|
+
if "key" in key.lower() and display_val:
|
|
41
|
+
display_val = display_val[:4] + "****"
|
|
42
|
+
console.print(f"{key} = {display_val}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.command("list")
|
|
46
|
+
def config_list() -> None:
|
|
47
|
+
"""Show all configuration values."""
|
|
48
|
+
config = _get_config()
|
|
49
|
+
print_config_table(config.get_all())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command("reset")
|
|
53
|
+
def config_reset(
|
|
54
|
+
confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Reset configuration to defaults."""
|
|
57
|
+
if not confirm:
|
|
58
|
+
confirm = typer.confirm("Reset all configuration to defaults?")
|
|
59
|
+
if confirm:
|
|
60
|
+
from sufa.core.utils.config import _DEFAULT_CONFIG, SufaConfig
|
|
61
|
+
|
|
62
|
+
config = SufaConfig()
|
|
63
|
+
for key, val in _DEFAULT_CONFIG.items():
|
|
64
|
+
config.set(key, val)
|
|
65
|
+
print_success("Configuration reset to defaults")
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""CLI commands for managing findings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from sufa.cli.display import console, print_error, print_findings_table, print_info, print_success
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(no_args_is_help=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_repo():
|
|
13
|
+
from sufa.core.db import Repository, init_db
|
|
14
|
+
from sufa.core.utils.config import SufaConfig
|
|
15
|
+
|
|
16
|
+
config = SufaConfig()
|
|
17
|
+
init_db(config.get_db_path())
|
|
18
|
+
return Repository()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command("list")
|
|
22
|
+
def list_findings(
|
|
23
|
+
scan_id: str = typer.Option("", "--scan", help="Filter by scan ID"),
|
|
24
|
+
severity: str = typer.Option("", "--severity", "-s", help="Filter by severity"),
|
|
25
|
+
status: str = typer.Option("", "--status", help="Filter by status"),
|
|
26
|
+
limit: int = typer.Option(50, "--limit", "-n", help="Max results"),
|
|
27
|
+
) -> None:
|
|
28
|
+
"""List vulnerability findings."""
|
|
29
|
+
repo = _get_repo()
|
|
30
|
+
findings = repo.list_findings(scan_id=scan_id, severity=severity, status=status, limit=limit)
|
|
31
|
+
|
|
32
|
+
if not findings:
|
|
33
|
+
print_info("No findings found.")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
print_findings_table([f.model_dump() for f in findings])
|
|
37
|
+
print_info(f"{len(findings)} findings shown")
|
|
38
|
+
repo.close()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("show")
|
|
42
|
+
def show_finding(
|
|
43
|
+
finding_id: str = typer.Argument(..., help="Finding ID"),
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Show detailed information about a finding."""
|
|
46
|
+
repo = _get_repo()
|
|
47
|
+
finding = repo.get_finding(finding_id)
|
|
48
|
+
|
|
49
|
+
if not finding:
|
|
50
|
+
partial = repo.list_findings(limit=500)
|
|
51
|
+
matches = [f for f in partial if f.id.startswith(finding_id)]
|
|
52
|
+
if len(matches) == 1:
|
|
53
|
+
finding = matches[0]
|
|
54
|
+
else:
|
|
55
|
+
print_error(f"Finding not found: {finding_id}")
|
|
56
|
+
raise typer.Exit(1)
|
|
57
|
+
|
|
58
|
+
from rich.panel import Panel
|
|
59
|
+
from rich.text import Text
|
|
60
|
+
|
|
61
|
+
from sufa.cli.display import SEVERITY_COLORS
|
|
62
|
+
|
|
63
|
+
sev_style = SEVERITY_COLORS.get(finding.severity.value, "")
|
|
64
|
+
header = Text(f"{finding.title}", style="bold")
|
|
65
|
+
|
|
66
|
+
console.print(
|
|
67
|
+
Panel(
|
|
68
|
+
header,
|
|
69
|
+
title=f"Finding {finding.id[:8]}",
|
|
70
|
+
border_style=sev_style or "cyan",
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
console.print(f" [bold]Severity:[/bold] {finding.severity.value.upper()}")
|
|
74
|
+
console.print(
|
|
75
|
+
f" [bold]Confidence:[/bold] {finding.confidence.value} ({finding.confidence_score}%)"
|
|
76
|
+
)
|
|
77
|
+
console.print(f" [bold]CWE:[/bold] {finding.cwe_id} {finding.cwe_url}")
|
|
78
|
+
console.print(f" [bold]OWASP:[/bold] {finding.owasp_category}")
|
|
79
|
+
console.print(f" [bold]URL:[/bold] {finding.url}")
|
|
80
|
+
console.print(f" [bold]Parameter:[/bold] {finding.parameter}")
|
|
81
|
+
console.print(f" [bold]Status:[/bold] {finding.status.value}")
|
|
82
|
+
console.print()
|
|
83
|
+
if finding.description:
|
|
84
|
+
console.print(Panel(finding.description, title="Description"))
|
|
85
|
+
if finding.evidence:
|
|
86
|
+
console.print(Panel(finding.evidence, title="Evidence"))
|
|
87
|
+
if finding.remediation:
|
|
88
|
+
console.print(Panel(finding.remediation, title="Remediation"))
|
|
89
|
+
if finding.exploit_steps:
|
|
90
|
+
console.print("[bold]Exploit Steps:[/bold]")
|
|
91
|
+
for step in finding.exploit_steps:
|
|
92
|
+
console.print(f" {step.step_number}. {step.description}")
|
|
93
|
+
|
|
94
|
+
repo.close()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@app.command("triage")
|
|
98
|
+
def triage_finding(
|
|
99
|
+
finding_id: str = typer.Argument(..., help="Finding ID"),
|
|
100
|
+
status: str = typer.Option(
|
|
101
|
+
..., "--status", "-s", help="New status: confirmed, false_positive, accepted_risk, fixed"
|
|
102
|
+
),
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Update the triage status of a finding."""
|
|
105
|
+
valid = {"confirmed", "false_positive", "accepted_risk", "fixed", "open"}
|
|
106
|
+
if status not in valid:
|
|
107
|
+
print_error(f"Invalid status. Must be one of: {', '.join(valid)}")
|
|
108
|
+
raise typer.Exit(1)
|
|
109
|
+
|
|
110
|
+
repo = _get_repo()
|
|
111
|
+
if repo.update_finding_status(finding_id, status):
|
|
112
|
+
print_success(f"Finding {finding_id[:8]} updated to: {status}")
|
|
113
|
+
else:
|
|
114
|
+
print_error(f"Finding not found: {finding_id}")
|
|
115
|
+
repo.close()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""CLI commands for importing traffic from files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from sufa.cli.display import print_error, print_success
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(no_args_is_help=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.callback(invoke_without_command=True)
|
|
16
|
+
def import_file(
|
|
17
|
+
file_path: str = typer.Argument(..., help="Path to HAR or Burp XML file"),
|
|
18
|
+
fmt: str = typer.Option("auto", "--format", "-f", help="File format: auto, har, burp"),
|
|
19
|
+
scan_id: str = typer.Option("", "--scan", help="Associate with scan ID"),
|
|
20
|
+
project_id: str = typer.Option("", "--project", help="Associate with project ID"),
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Import HTTP traffic from a file."""
|
|
23
|
+
path = Path(file_path)
|
|
24
|
+
if not path.exists():
|
|
25
|
+
print_error(f"File not found: {file_path}")
|
|
26
|
+
raise typer.Exit(1)
|
|
27
|
+
|
|
28
|
+
if fmt == "auto":
|
|
29
|
+
if path.suffix.lower() == ".har":
|
|
30
|
+
fmt = "har"
|
|
31
|
+
elif path.suffix.lower() in (".xml", ".burp"):
|
|
32
|
+
fmt = "burp"
|
|
33
|
+
else:
|
|
34
|
+
print_error(
|
|
35
|
+
f"Cannot detect format for {path.suffix}. Use --format har or --format burp"
|
|
36
|
+
)
|
|
37
|
+
raise typer.Exit(1)
|
|
38
|
+
|
|
39
|
+
from sufa.core.db import Repository, init_db
|
|
40
|
+
from sufa.core.events import EventBus
|
|
41
|
+
from sufa.core.traffic import TrafficStore
|
|
42
|
+
from sufa.core.utils.config import SufaConfig
|
|
43
|
+
|
|
44
|
+
config = SufaConfig()
|
|
45
|
+
init_db(config.get_db_path())
|
|
46
|
+
repo = Repository()
|
|
47
|
+
store = TrafficStore(repo, EventBus())
|
|
48
|
+
|
|
49
|
+
count = 0
|
|
50
|
+
if fmt == "har":
|
|
51
|
+
count = _import_har(path, store, scan_id, project_id)
|
|
52
|
+
elif fmt == "burp":
|
|
53
|
+
count = _import_burp_xml(path, store, scan_id, project_id)
|
|
54
|
+
|
|
55
|
+
print_success(f"Imported {count} traffic entries from {path.name}")
|
|
56
|
+
repo.close()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _import_har(path: Path, store, scan_id: str, project_id: str) -> int:
|
|
60
|
+
from sufa.core.http.parser import build_request, build_response
|
|
61
|
+
from sufa.core.models import TrafficEntry
|
|
62
|
+
|
|
63
|
+
with open(path) as f:
|
|
64
|
+
data = json.load(f)
|
|
65
|
+
|
|
66
|
+
entries = data.get("log", {}).get("entries", [])
|
|
67
|
+
count = 0
|
|
68
|
+
|
|
69
|
+
for entry in entries:
|
|
70
|
+
try:
|
|
71
|
+
req_data = entry.get("request", {})
|
|
72
|
+
resp_data = entry.get("response", {})
|
|
73
|
+
|
|
74
|
+
headers = {h["name"]: h["value"] for h in req_data.get("headers", [])}
|
|
75
|
+
body = ""
|
|
76
|
+
if req_data.get("postData"):
|
|
77
|
+
body = req_data["postData"].get("text", "")
|
|
78
|
+
|
|
79
|
+
req = build_request(
|
|
80
|
+
method=req_data.get("method", "GET"),
|
|
81
|
+
url=req_data.get("url", ""),
|
|
82
|
+
headers=headers,
|
|
83
|
+
body=body,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
resp_headers = {h["name"]: h["value"] for h in resp_data.get("headers", [])}
|
|
87
|
+
resp_body = ""
|
|
88
|
+
if resp_data.get("content"):
|
|
89
|
+
resp_body = resp_data["content"].get("text", "")
|
|
90
|
+
|
|
91
|
+
resp = build_response(
|
|
92
|
+
status_code=resp_data.get("status", 0),
|
|
93
|
+
headers=resp_headers,
|
|
94
|
+
body=resp_body,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
traffic = TrafficEntry(
|
|
98
|
+
request=req,
|
|
99
|
+
response=resp,
|
|
100
|
+
scan_id=scan_id,
|
|
101
|
+
project_id=project_id,
|
|
102
|
+
source="har_import",
|
|
103
|
+
)
|
|
104
|
+
store.store(traffic)
|
|
105
|
+
count += 1
|
|
106
|
+
except Exception:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
return count
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _import_burp_xml(path: Path, store, scan_id: str, project_id: str) -> int:
|
|
113
|
+
import xml.etree.ElementTree as ET
|
|
114
|
+
|
|
115
|
+
from sufa.core.http.parser import build_request, build_response
|
|
116
|
+
from sufa.core.models import TrafficEntry
|
|
117
|
+
|
|
118
|
+
tree = ET.parse(path)
|
|
119
|
+
root = tree.getroot()
|
|
120
|
+
count = 0
|
|
121
|
+
|
|
122
|
+
for item in root.iter("item"):
|
|
123
|
+
try:
|
|
124
|
+
url = item.findtext("url", "")
|
|
125
|
+
method = item.findtext("method", "GET")
|
|
126
|
+
status = int(item.findtext("status", "0"))
|
|
127
|
+
req_body = item.findtext("request", "")
|
|
128
|
+
resp_body = item.findtext("response", "")
|
|
129
|
+
|
|
130
|
+
req = build_request(method=method, url=url, body=req_body or "")
|
|
131
|
+
resp = build_response(status_code=status, body=resp_body or "")
|
|
132
|
+
|
|
133
|
+
traffic = TrafficEntry(
|
|
134
|
+
request=req,
|
|
135
|
+
response=resp,
|
|
136
|
+
scan_id=scan_id,
|
|
137
|
+
project_id=project_id,
|
|
138
|
+
source="burp_import",
|
|
139
|
+
)
|
|
140
|
+
store.store(traffic)
|
|
141
|
+
count += 1
|
|
142
|
+
except Exception:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
return count
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""CLI commands for project management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from sufa.cli.display import print_error, print_info, print_projects_table, print_success
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(no_args_is_help=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_repo():
|
|
13
|
+
from sufa.core.db import Repository, init_db
|
|
14
|
+
from sufa.core.utils.config import SufaConfig
|
|
15
|
+
|
|
16
|
+
config = SufaConfig()
|
|
17
|
+
init_db(config.get_db_path())
|
|
18
|
+
return Repository()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command("create")
|
|
22
|
+
def create_project(
|
|
23
|
+
name: str = typer.Argument(..., help="Project name"),
|
|
24
|
+
description: str = typer.Option("", "--desc", "-d", help="Project description"),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Create a new project."""
|
|
27
|
+
from sufa.core.models import Project
|
|
28
|
+
|
|
29
|
+
repo = _get_repo()
|
|
30
|
+
project = Project(name=name, description=description)
|
|
31
|
+
repo.save_project(project)
|
|
32
|
+
print_success(f"Project created: {project.id[:8]} - {name}")
|
|
33
|
+
repo.close()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command("list")
|
|
37
|
+
def list_projects() -> None:
|
|
38
|
+
"""List all projects."""
|
|
39
|
+
repo = _get_repo()
|
|
40
|
+
projects = repo.list_projects()
|
|
41
|
+
|
|
42
|
+
if not projects:
|
|
43
|
+
print_info("No projects found. Create one with: sufa project create <name>")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
print_projects_table([p.model_dump() for p in projects])
|
|
47
|
+
repo.close()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.command("delete")
|
|
51
|
+
def delete_project(
|
|
52
|
+
project_id: str = typer.Argument(..., help="Project ID"),
|
|
53
|
+
confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Delete a project."""
|
|
56
|
+
if not confirm:
|
|
57
|
+
confirm = typer.confirm(f"Delete project {project_id[:8]}?")
|
|
58
|
+
if not confirm:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
repo = _get_repo()
|
|
62
|
+
if repo.delete_project(project_id):
|
|
63
|
+
print_success(f"Project {project_id[:8]} deleted")
|
|
64
|
+
else:
|
|
65
|
+
print_error(f"Project not found: {project_id}")
|
|
66
|
+
repo.close()
|