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.
Files changed (112) hide show
  1. sufa/__init__.py +3 -0
  2. sufa/__main__.py +5 -0
  3. sufa/burp/__init__.py +3 -0
  4. sufa/burp/bridge.py +33 -0
  5. sufa/burp/extension.py +138 -0
  6. sufa/cli/__init__.py +0 -0
  7. sufa/cli/app.py +49 -0
  8. sufa/cli/commands/__init__.py +0 -0
  9. sufa/cli/commands/config_cmd.py +65 -0
  10. sufa/cli/commands/findings.py +115 -0
  11. sufa/cli/commands/import_cmd.py +145 -0
  12. sufa/cli/commands/project.py +66 -0
  13. sufa/cli/commands/provider.py +63 -0
  14. sufa/cli/commands/proxy.py +44 -0
  15. sufa/cli/commands/replay.py +79 -0
  16. sufa/cli/commands/report.py +70 -0
  17. sufa/cli/commands/scan.py +119 -0
  18. sufa/cli/commands/server.py +37 -0
  19. sufa/cli/display.py +146 -0
  20. sufa/core/__init__.py +0 -0
  21. sufa/core/ai/__init__.py +4 -0
  22. sufa/core/ai/base.py +55 -0
  23. sufa/core/ai/claude.py +98 -0
  24. sufa/core/ai/cost.py +66 -0
  25. sufa/core/ai/gemini.py +109 -0
  26. sufa/core/ai/ollama.py +93 -0
  27. sufa/core/ai/openai.py +92 -0
  28. sufa/core/ai/router.py +136 -0
  29. sufa/core/core_types.py +15 -0
  30. sufa/core/db/__init__.py +4 -0
  31. sufa/core/db/engine.py +53 -0
  32. sufa/core/db/models.py +136 -0
  33. sufa/core/db/repository.py +350 -0
  34. sufa/core/dedup/__init__.py +4 -0
  35. sufa/core/dedup/deduplicator.py +51 -0
  36. sufa/core/dedup/fingerprint.py +54 -0
  37. sufa/core/events/__init__.py +7 -0
  38. sufa/core/events/bus.py +82 -0
  39. sufa/core/events/events.py +90 -0
  40. sufa/core/graph/__init__.py +12 -0
  41. sufa/core/graph/attack_graph.py +125 -0
  42. sufa/core/graph/builder.py +89 -0
  43. sufa/core/graph/chain_analyzer.py +128 -0
  44. sufa/core/http/__init__.py +4 -0
  45. sufa/core/http/normalizer.py +109 -0
  46. sufa/core/http/parser.py +84 -0
  47. sufa/core/http/redactor.py +98 -0
  48. sufa/core/models/__init__.py +35 -0
  49. sufa/core/models/finding.py +76 -0
  50. sufa/core/models/project.py +18 -0
  51. sufa/core/models/request.py +62 -0
  52. sufa/core/models/scan.py +61 -0
  53. sufa/core/plugins/__init__.py +5 -0
  54. sufa/core/plugins/interface.py +53 -0
  55. sufa/core/plugins/loader.py +95 -0
  56. sufa/core/plugins/registry.py +45 -0
  57. sufa/core/reports/__init__.py +5 -0
  58. sufa/core/reports/html.py +136 -0
  59. sufa/core/reports/json_report.py +62 -0
  60. sufa/core/reports/sarif.py +93 -0
  61. sufa/core/reports/templates/report.html +102 -0
  62. sufa/core/rules/__init__.py +3 -0
  63. sufa/core/rules/builtin/__init__.py +0 -0
  64. sufa/core/rules/builtin/owasp_top10.yaml +128 -0
  65. sufa/core/rules/custom/__init__.py +0 -0
  66. sufa/core/rules/engine.py +152 -0
  67. sufa/core/scanner/__init__.py +4 -0
  68. sufa/core/scanner/active/__init__.py +3 -0
  69. sufa/core/scanner/active/engine.py +108 -0
  70. sufa/core/scanner/active/fuzz.py +94 -0
  71. sufa/core/scanner/active/verifier.py +89 -0
  72. sufa/core/scanner/passive.py +315 -0
  73. sufa/core/scanner/profiles.py +67 -0
  74. sufa/core/scanner/scope.py +114 -0
  75. sufa/core/session/__init__.py +3 -0
  76. sufa/core/session/manager.py +116 -0
  77. sufa/core/traffic/__init__.py +3 -0
  78. sufa/core/traffic/models.py +22 -0
  79. sufa/core/traffic/replay.py +48 -0
  80. sufa/core/traffic/repository.py +19 -0
  81. sufa/core/traffic/store.py +90 -0
  82. sufa/core/utils/__init__.py +4 -0
  83. sufa/core/utils/config.py +144 -0
  84. sufa/core/utils/logging.py +41 -0
  85. sufa/crawler/__init__.py +3 -0
  86. sufa/crawler/api_discovery.py +132 -0
  87. sufa/crawler/js_parser.py +45 -0
  88. sufa/crawler/spider.py +101 -0
  89. sufa/payloads/idor.txt +10 -0
  90. sufa/payloads/lfi.txt +13 -0
  91. sufa/payloads/sqli.txt +16 -0
  92. sufa/payloads/ssrf.txt +13 -0
  93. sufa/payloads/xss.txt +15 -0
  94. sufa/proxy/__init__.py +3 -0
  95. sufa/proxy/handler.py +22 -0
  96. sufa/proxy/interceptor.py +108 -0
  97. sufa/server/__init__.py +3 -0
  98. sufa/server/app.py +52 -0
  99. sufa/server/auth/__init__.py +3 -0
  100. sufa/server/auth/rbac.py +73 -0
  101. sufa/server/middleware/__init__.py +0 -0
  102. sufa/server/middleware/audit.py +33 -0
  103. sufa/server/routes/__init__.py +0 -0
  104. sufa/server/routes/findings.py +61 -0
  105. sufa/server/routes/projects.py +63 -0
  106. sufa/server/routes/scans.py +59 -0
  107. sufa/server/routes/traffic.py +63 -0
  108. sufa-0.1.0.dist-info/METADATA +139 -0
  109. sufa-0.1.0.dist-info/RECORD +112 -0
  110. sufa-0.1.0.dist-info/WHEEL +4 -0
  111. sufa-0.1.0.dist-info/entry_points.txt +2 -0
  112. sufa-0.1.0.dist-info/licenses/LICENSE +21 -0
sufa/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """sufa -- AI-powered web vulnerability analysis platform."""
2
+
3
+ __version__ = "0.1.0"
sufa/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running sufa as `python -m sufa`."""
2
+
3
+ from sufa.cli.app import main
4
+
5
+ main()
sufa/burp/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from sufa.burp.bridge import parse_burp_traffic
2
+
3
+ __all__ = ["parse_burp_traffic"]
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()