kmcp 0.1.5.dev0__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 (84) hide show
  1. kali_mcp/__init__.py +3 -0
  2. kali_mcp/__main__.py +28 -0
  3. kali_mcp/_version.py +24 -0
  4. kali_mcp/cli.py +169 -0
  5. kali_mcp/config.py +95 -0
  6. kali_mcp/db/__init__.py +1 -0
  7. kali_mcp/db/manager.py +248 -0
  8. kali_mcp/execution.py +439 -0
  9. kali_mcp/logging_utils.py +74 -0
  10. kali_mcp/models.py +51 -0
  11. kali_mcp/monitor/__init__.py +105 -0
  12. kali_mcp/monitor/sessions.py +70 -0
  13. kali_mcp/monitor/sse_client.py +87 -0
  14. kali_mcp/monitor/tui.py +364 -0
  15. kali_mcp/notifications.py +130 -0
  16. kali_mcp/prompts.py +184 -0
  17. kali_mcp/registry.py +33 -0
  18. kali_mcp/resources.py +263 -0
  19. kali_mcp/responses.py +39 -0
  20. kali_mcp/security.py +15 -0
  21. kali_mcp/server.py +535 -0
  22. kali_mcp/tools/__init__.py +125 -0
  23. kali_mcp/tools/amass.py +83 -0
  24. kali_mcp/tools/arjun.py +86 -0
  25. kali_mcp/tools/base.py +100 -0
  26. kali_mcp/tools/bettercap.py +99 -0
  27. kali_mcp/tools/bloodhound.py +68 -0
  28. kali_mcp/tools/commix.py +73 -0
  29. kali_mcp/tools/crackmapexec.py +100 -0
  30. kali_mcp/tools/dirb.py +75 -0
  31. kali_mcp/tools/dursgo.py +156 -0
  32. kali_mcp/tools/enum4linux.py +59 -0
  33. kali_mcp/tools/farsight.py +127 -0
  34. kali_mcp/tools/ffuf.py +85 -0
  35. kali_mcp/tools/file_download.py +119 -0
  36. kali_mcp/tools/file_read.py +144 -0
  37. kali_mcp/tools/file_write.py +141 -0
  38. kali_mcp/tools/flowlyt.py +88 -0
  39. kali_mcp/tools/generic_command.py +95 -0
  40. kali_mcp/tools/gobuster.py +84 -0
  41. kali_mcp/tools/hashcat.py +101 -0
  42. kali_mcp/tools/httpx.py +85 -0
  43. kali_mcp/tools/hydra.py +99 -0
  44. kali_mcp/tools/john.py +100 -0
  45. kali_mcp/tools/katana.py +81 -0
  46. kali_mcp/tools/masscan.py +85 -0
  47. kali_mcp/tools/medusa.py +102 -0
  48. kali_mcp/tools/msfconsole.py +59 -0
  49. kali_mcp/tools/naabu.py +100 -0
  50. kali_mcp/tools/netexec.py +86 -0
  51. kali_mcp/tools/nmap.py +94 -0
  52. kali_mcp/tools/normalization.py +18 -0
  53. kali_mcp/tools/nuclei.py +87 -0
  54. kali_mcp/tools/parallel_execute.py +246 -0
  55. kali_mcp/tools/persist.py +436 -0
  56. kali_mcp/tools/playbook_ad.py +99 -0
  57. kali_mcp/tools/playbook_engine.py +142 -0
  58. kali_mcp/tools/playbook_engine_tool.py +72 -0
  59. kali_mcp/tools/playbook_recon.py +115 -0
  60. kali_mcp/tools/playbook_tools.py +125 -0
  61. kali_mcp/tools/playbook_webapp.py +105 -0
  62. kali_mcp/tools/python_command.py +100 -0
  63. kali_mcp/tools/responder.py +85 -0
  64. kali_mcp/tools/searchsploit.py +123 -0
  65. kali_mcp/tools/spiderfoot.py +75 -0
  66. kali_mcp/tools/sqlmap.py +73 -0
  67. kali_mcp/tools/subfinder.py +69 -0
  68. kali_mcp/tools/theharvester.py +80 -0
  69. kali_mcp/tools/vault_tool.py +489 -0
  70. kali_mcp/tools/wfuzz.py +87 -0
  71. kali_mcp/tools/whatweb.py +82 -0
  72. kali_mcp/tools/wpscan.py +76 -0
  73. kali_mcp/tools/zap.py +79 -0
  74. kali_mcp/tools/zighound.py +139 -0
  75. kali_mcp/tools/zizmor.py +98 -0
  76. kali_mcp/utils/__init__.py +0 -0
  77. kali_mcp/utils/process.py +36 -0
  78. kali_mcp/utils/vault.py +471 -0
  79. kali_mcp/validation.py +82 -0
  80. kmcp-0.1.5.dev0.dist-info/METADATA +251 -0
  81. kmcp-0.1.5.dev0.dist-info/RECORD +84 -0
  82. kmcp-0.1.5.dev0.dist-info/WHEEL +5 -0
  83. kmcp-0.1.5.dev0.dist-info/entry_points.txt +4 -0
  84. kmcp-0.1.5.dev0.dist-info/top_level.txt +1 -0
kali_mcp/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Kali MCP Server — exposes Kali Linux penetration-testing tools via MCP."""
2
+
3
+ __version__ = "1.0.0"
kali_mcp/__main__.py ADDED
@@ -0,0 +1,28 @@
1
+ """Entry point for ``python -m kali_mcp``.
2
+
3
+ Usage:
4
+
5
+ python -m kali_mcp # start the MCP server (default)
6
+ python -m kali_mcp start [--host HOST] [--port PORT] # start the MCP server (alias: s)
7
+ python -m kali_mcp monitor [<url>] # launch the interactive monitor TUI (alias: m)
8
+ python -m kali_mcp purge <scope> [--confirm] # purge data: db | vault | all (alias: p)
9
+
10
+ The unified ``kmcp`` console script maps to :func:`kali_mcp.cli.main`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import sys
16
+
17
+ from kali_mcp.cli import main as cli_main
18
+
19
+
20
+ def main() -> None:
21
+ argv = sys.argv[1:]
22
+ if not argv:
23
+ argv = ["start"]
24
+ sys.exit(cli_main(argv))
25
+
26
+
27
+ if __name__ == "__main__":
28
+ main()
kali_mcp/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.5.dev0'
22
+ __version_tuple__ = version_tuple = (0, 1, 5, 'dev0')
23
+
24
+ __commit_id__ = commit_id = None
kali_mcp/cli.py ADDED
@@ -0,0 +1,169 @@
1
+ """Kali MCP CLI — unified entry point for server, monitor, and data purge.
2
+
3
+ Usage:
4
+
5
+ kmcp start [--host HOST] [--port PORT] # start the MCP server (alias: s)
6
+ kmcp monitor [URL] # launch the monitor TUI (alias: m)
7
+ kmcp purge SCOPE [--confirm] # purge data: db | vault | all (alias: p)
8
+
9
+ Environment variables (MCP_HOST, MCP_PORT, etc.) are read from .env via python-dotenv,
10
+ just like ``just start``.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import os
17
+ import shutil
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ try:
22
+ from dotenv import load_dotenv
23
+ except Exception: # pragma: no cover - optional dependency missing # noqa: BLE001
24
+
25
+ def load_dotenv(*args, **kwargs):
26
+ return None
27
+
28
+
29
+ REPO_ROOT = Path(__file__).resolve().parent.parent.parent
30
+
31
+
32
+ def _load_env() -> None:
33
+ """Load .env from the repo root (idempotent)."""
34
+ load_dotenv(str(REPO_ROOT / ".env"), override=False)
35
+
36
+
37
+ def cmd_start(args: argparse.Namespace) -> int:
38
+ """Start the MCP server."""
39
+ _load_env()
40
+ os.environ.setdefault("MCP_HOST", args.host or os.getenv("MCP_HOST", "0.0.0.0"))
41
+ os.environ.setdefault("MCP_PORT", args.port or os.getenv("MCP_PORT", "8399"))
42
+
43
+ from kali_mcp.server import main
44
+
45
+ main()
46
+ return 0
47
+
48
+
49
+ def cmd_monitor(args: argparse.Namespace) -> int:
50
+ """Launch the interactive monitor TUI."""
51
+ _load_env()
52
+ url = args.url or os.getenv("MCP_MONITOR_URL", "http://127.0.0.1:8399")
53
+
54
+ from kali_mcp.monitor.tui import main as monitor_main
55
+
56
+ monitor_main(url)
57
+ return 0
58
+
59
+
60
+ def _confirm(prompt: str) -> bool:
61
+ """Ask the user for confirmation. Returns True on 'y'/'yes'."""
62
+ try:
63
+ response = input(f"{prompt} [y/N] ").strip().lower()
64
+ except (EOFError, KeyboardInterrupt):
65
+ print("\naborted")
66
+ return False
67
+ return response in ("y", "yes")
68
+
69
+
70
+ def cmd_purge(args: argparse.Namespace) -> int:
71
+ """Purge stored data (SQLite DB and/or vault)."""
72
+ _load_env()
73
+
74
+ scope = args.scope.lower()
75
+
76
+ valid = {"db", "vault", "all"}
77
+ if scope not in valid:
78
+ print(f"unknown scope '{scope}' — use: db | vault | all", file=sys.stderr)
79
+ return 1
80
+
81
+ if not args.confirm and not _confirm(f"Purge '{scope}' data — this cannot be undone"):
82
+ print("aborted")
83
+ return 1
84
+
85
+ removed: list[str] = []
86
+
87
+ def rm(path: Path) -> None:
88
+ if path.exists():
89
+ if path.is_dir():
90
+ shutil.rmtree(path, ignore_errors=True)
91
+ else:
92
+ try:
93
+ path.unlink()
94
+ except FileNotFoundError:
95
+ pass
96
+ removed.append(str(path))
97
+
98
+ if scope in ("db", "all"):
99
+ for name in ("kali-mcp.sqlite", "kali-mcp.sqlite-wal", "kali-mcp.sqlite-shm"):
100
+ rm(REPO_ROOT / "data" / name)
101
+
102
+ if scope in ("vault", "all"):
103
+ rm(REPO_ROOT / "artifacts" / "vault")
104
+
105
+ print(f"purged: {scope}")
106
+ if removed:
107
+ print("removed:")
108
+ for path in removed:
109
+ print(f" {path}")
110
+ else:
111
+ print("nothing to remove")
112
+ return 0
113
+
114
+
115
+ def build_parser() -> argparse.ArgumentParser:
116
+ parser = argparse.ArgumentParser(
117
+ prog="kmcp",
118
+ description="Kali MCP Server CLI — server, monitor, and data purge utilities.",
119
+ )
120
+ sub = parser.add_subparsers(dest="command", required=True)
121
+
122
+ # start / s
123
+ p_start = sub.add_parser("start", aliases=["s"], help="Start the MCP server")
124
+ p_start.add_argument(
125
+ "--host", default=None, help="Listen address (default: MCP_HOST or 0.0.0.0)"
126
+ )
127
+ p_start.add_argument("--port", default=None, help="Listen port (default: MCP_PORT or 8399)")
128
+ p_start.set_defaults(func=cmd_start)
129
+
130
+ # monitor / m
131
+ p_monitor = sub.add_parser("monitor", aliases=["m"], help="Launch the monitor TUI")
132
+ p_monitor.add_argument(
133
+ "url",
134
+ nargs="?",
135
+ default=None,
136
+ help="Base URL of the MCP server (default: http://127.0.0.1:8399)",
137
+ )
138
+ p_monitor.set_defaults(func=cmd_monitor)
139
+
140
+ # purge / p
141
+ p_purge = sub.add_parser("purge", aliases=["p"], help="Purge stored data")
142
+ p_purge.add_argument(
143
+ "scope",
144
+ choices=["db", "vault", "all"],
145
+ help="What to purge: db (SQLite), vault (artifacts/vault), or all",
146
+ )
147
+ p_purge.add_argument(
148
+ "--confirm",
149
+ "-y",
150
+ action="store_true",
151
+ help="Skip confirmation prompt",
152
+ )
153
+ p_purge.set_defaults(func=cmd_purge)
154
+
155
+ return parser
156
+
157
+
158
+ def main(argv: list[str] | None = None) -> int:
159
+ if argv is None:
160
+ argv = sys.argv[1:]
161
+ if not argv:
162
+ argv = ["start"]
163
+ parser = build_parser()
164
+ args = parser.parse_args(argv)
165
+ return args.func(args)
166
+
167
+
168
+ if __name__ == "__main__":
169
+ sys.exit(main())
kali_mcp/config.py ADDED
@@ -0,0 +1,95 @@
1
+ """Server configuration loaded from environment variables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+
9
+ # Optional: dotenv for local development. If not installed, fall back gracefully.
10
+ try:
11
+ from dotenv import load_dotenv
12
+ except Exception: # pragma: no cover - optional dependency missing in test env # noqa: BLE001
13
+
14
+ def load_dotenv(*args, **kwargs):
15
+ return None
16
+
17
+
18
+ load_dotenv(verbose=True)
19
+
20
+
21
+ def _int_env(key: str, default: int) -> int:
22
+ """Parse int from env var with safe fallback."""
23
+ val = os.getenv(key, "")
24
+ if not val:
25
+ return default
26
+ try:
27
+ return int(val)
28
+ except ValueError:
29
+ return default
30
+
31
+
32
+ def _bool_env(key: str, default: bool) -> bool:
33
+ """Parse bool from env var with safe fallback."""
34
+ val = os.getenv(key, "")
35
+ if not val:
36
+ return default
37
+ return val.lower() in ("1", "true", "yes")
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class Config:
42
+ # Server
43
+ host: str = field(default_factory=lambda: os.getenv("MCP_HOST", "0.0.0.0"))
44
+ port: int = field(default_factory=lambda: _int_env("MCP_PORT", 8399))
45
+ server_name: str = field(default_factory=lambda: os.getenv("MCP_SERVER_NAME", "kali-mcp"))
46
+ server_version: str = field(default_factory=lambda: os.getenv("MCP_SERVER_VERSION", "1.0.0"))
47
+
48
+ # Execution
49
+ default_timeout: int = field(default_factory=lambda: _int_env("MCP_DEFAULT_TIMEOUT", 300))
50
+ max_timeout: int = field(default_factory=lambda: _int_env("MCP_MAX_TIMEOUT", 3600))
51
+ max_concurrent: int = field(default_factory=lambda: _int_env("MCP_MAX_CONCURRENT", 10))
52
+
53
+ # Paths
54
+ log_dir: Path = field(default_factory=lambda: Path(os.getenv("MCP_LOG_DIR", "logs")))
55
+ artifact_dir: Path = field(
56
+ default_factory=lambda: Path(os.getenv("MCP_ARTIFACT_DIR", "artifacts"))
57
+ )
58
+
59
+ # Encoding
60
+ default_encoding: str = field(default_factory=lambda: os.getenv("MCP_ENCODING", "utf-8"))
61
+
62
+ # Debug
63
+ debug: bool = field(default_factory=lambda: _bool_env("MCP_DEBUG", False))
64
+
65
+ # Transport: "sse" (default) or "streamable-http"
66
+ transport: str = field(default_factory=lambda: os.getenv("MCP_TRANSPORT", "sse"))
67
+
68
+ # Auth
69
+ api_token: str = field(default_factory=lambda: os.getenv("MCP_API_TOKEN", ""))
70
+
71
+ # CORS
72
+ allowed_origins: str = field(default_factory=lambda: os.getenv("MCP_ALLOWED_ORIGINS", "*"))
73
+
74
+ # Rate limiting (0 = unlimited)
75
+ rate_limit_per_minute: int = field(default_factory=lambda: _int_env("MCP_RATE_LIMIT", 0))
76
+
77
+ # Vault
78
+ vault_max_row_bytes: int = field(
79
+ default_factory=lambda: _int_env("MCP_VAULT_MAX_ROW_BYTES", 26_214_400)
80
+ ) # 25MB ceiling per vault row (0 = unlimited)
81
+
82
+ # Database
83
+ db_path: Path = field(
84
+ default_factory=lambda: Path(os.getenv("MCP_DB_PATH", "data/kali-mcp.sqlite"))
85
+ )
86
+ db_enabled: bool = field(default_factory=lambda: _bool_env("MCP_DB_ENABLED", True))
87
+
88
+ def __post_init__(self) -> None:
89
+ self.log_dir.mkdir(parents=True, exist_ok=True)
90
+ self.artifact_dir.mkdir(parents=True, exist_ok=True)
91
+ if self.db_enabled:
92
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
93
+
94
+
95
+ config = Config()
@@ -0,0 +1 @@
1
+ """Database package for Kali MCP — SQLite storage backend."""
kali_mcp/db/manager.py ADDED
@@ -0,0 +1,248 @@
1
+ """SQLite data manager for Kali MCP.
2
+
3
+ Provides a lightweight, thread-safe interface to store targets, scans,
4
+ findings, and credentials discovered by AI agents.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sqlite3
10
+ from pathlib import Path
11
+
12
+ from kali_mcp.config import config
13
+
14
+
15
+ class DBManager:
16
+ def __init__(self, path: Path) -> None:
17
+ self.path = Path(path)
18
+ self.conn = sqlite3.connect(str(self.path), check_same_thread=False)
19
+ self.conn.execute("PRAGMA foreign_keys = ON;")
20
+ self.conn.execute("PRAGMA journal_mode=WAL;")
21
+ self.conn.row_factory = sqlite3.Row
22
+
23
+ def ensure_schema(self) -> None:
24
+ cur = self.conn.cursor()
25
+ cur.executescript(
26
+ """
27
+ CREATE TABLE IF NOT EXISTS targets (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ target TEXT NOT NULL UNIQUE,
30
+ target_type TEXT NOT NULL,
31
+ scope TEXT NOT NULL DEFAULT 'in-scope',
32
+ tags TEXT,
33
+ notes TEXT,
34
+ created_at TEXT NOT NULL,
35
+ updated_at TEXT NOT NULL
36
+ );
37
+ CREATE INDEX IF NOT EXISTS idx_targets_scope ON targets(scope);
38
+ CREATE INDEX IF NOT EXISTS idx_targets_type ON targets(target_type);
39
+
40
+ CREATE TABLE IF NOT EXISTS scans (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ target_id INTEGER NOT NULL,
43
+ tool TEXT NOT NULL,
44
+ command TEXT NOT NULL,
45
+ started_at TEXT NOT NULL,
46
+ completed_at TEXT,
47
+ duration REAL,
48
+ success INTEGER DEFAULT 0,
49
+ findings_count INTEGER DEFAULT 0,
50
+ artifact_uri TEXT,
51
+ FOREIGN KEY (target_id) REFERENCES targets(id)
52
+ );
53
+ CREATE INDEX IF NOT EXISTS idx_scans_target ON scans(target_id);
54
+ CREATE INDEX IF NOT EXISTS idx_scans_tool ON scans(tool);
55
+
56
+ CREATE TABLE IF NOT EXISTS findings (
57
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
58
+ target_id INTEGER NOT NULL,
59
+ scan_id INTEGER,
60
+ tool TEXT NOT NULL,
61
+ finding_type TEXT NOT NULL,
62
+ severity TEXT,
63
+ title TEXT NOT NULL,
64
+ description TEXT,
65
+ evidence TEXT,
66
+ port INTEGER,
67
+ service TEXT,
68
+ url TEXT,
69
+ status TEXT DEFAULT 'open',
70
+ created_at TEXT NOT NULL,
71
+ updated_at TEXT NOT NULL,
72
+ FOREIGN KEY (target_id) REFERENCES targets(id),
73
+ FOREIGN KEY (scan_id) REFERENCES scans(id)
74
+ );
75
+ CREATE INDEX IF NOT EXISTS idx_findings_target ON findings(target_id);
76
+ CREATE INDEX IF NOT EXISTS idx_findings_severity ON findings(severity);
77
+ CREATE INDEX IF NOT EXISTS idx_findings_status ON findings(status);
78
+
79
+ CREATE TABLE IF NOT EXISTS credentials (
80
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
81
+ target_id INTEGER NOT NULL,
82
+ service TEXT NOT NULL,
83
+ username TEXT NOT NULL,
84
+ password TEXT,
85
+ hash TEXT,
86
+ source_tool TEXT NOT NULL,
87
+ captured_at TEXT NOT NULL,
88
+ notes TEXT,
89
+ FOREIGN KEY (target_id) REFERENCES targets(id)
90
+ );
91
+ CREATE INDEX IF NOT EXISTS idx_credentials_target ON credentials(target_id);
92
+ CREATE INDEX IF NOT EXISTS idx_credentials_service ON credentials(service);
93
+ """
94
+ )
95
+ self.conn.commit()
96
+
97
+ def close(self) -> None:
98
+ try:
99
+ self.conn.close()
100
+ except Exception: # noqa: BLE001, S110
101
+ pass
102
+
103
+ # Convenience helpers for common operations
104
+ def upsert_target(
105
+ self,
106
+ target: str,
107
+ target_type: str = "host",
108
+ scope: str = "in-scope",
109
+ tags: str = "",
110
+ notes: str = "",
111
+ ) -> int:
112
+ cur = self.conn.cursor()
113
+ cur.execute("SELECT id FROM targets WHERE target = ?", (target,))
114
+ row = cur.fetchone()
115
+ now = __import__("datetime").datetime.utcnow().isoformat() + "Z"
116
+ if row:
117
+ tid = int(row[0])
118
+ cur.execute(
119
+ "UPDATE targets SET target_type=?, scope=?, tags=?, notes=?, updated_at=? WHERE id=?",
120
+ (target_type, scope, tags, notes, now, tid),
121
+ )
122
+ else:
123
+ cur.execute(
124
+ "INSERT INTO targets (target, target_type, scope, tags, notes, created_at, updated_at) VALUES (?,?,?,?,?,?,?)",
125
+ (target, target_type, scope, tags, notes, now, now),
126
+ )
127
+ tid = cur.lastrowid
128
+ self.conn.commit()
129
+ return int(tid)
130
+
131
+ def add_scan(
132
+ self,
133
+ target_id: int,
134
+ tool: str,
135
+ command: str,
136
+ started_at: str,
137
+ completed_at: str,
138
+ duration: float,
139
+ success: int,
140
+ findings_count: int,
141
+ artifact_uri: str | None,
142
+ ) -> int:
143
+ cur = self.conn.cursor()
144
+ cur.execute(
145
+ "INSERT INTO scans (target_id, tool, command, started_at, completed_at, duration, success, findings_count, artifact_uri) VALUES (?,?,?,?,?,?,?,?,?)",
146
+ (
147
+ target_id,
148
+ tool,
149
+ command,
150
+ started_at,
151
+ completed_at,
152
+ duration,
153
+ int(bool(success)),
154
+ int(findings_count),
155
+ artifact_uri,
156
+ ),
157
+ )
158
+ self.conn.commit()
159
+ return cur.lastrowid
160
+
161
+ def add_finding(
162
+ self,
163
+ target_id: int,
164
+ scan_id: int | None,
165
+ tool: str,
166
+ finding_type: str,
167
+ severity: str,
168
+ title: str,
169
+ description: str,
170
+ evidence: str,
171
+ port: int | None = None,
172
+ service: str | None = None,
173
+ url: str | None = None,
174
+ status: str = "open",
175
+ ) -> int:
176
+ cur = self.conn.cursor()
177
+ now = __import__("datetime").datetime.utcnow().isoformat() + "Z"
178
+ cur.execute(
179
+ "INSERT INTO findings (target_id, scan_id, tool, finding_type, severity, title, description, evidence, port, service, url, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
180
+ (
181
+ target_id,
182
+ scan_id,
183
+ tool,
184
+ finding_type,
185
+ severity,
186
+ title,
187
+ description,
188
+ evidence,
189
+ port,
190
+ service,
191
+ url,
192
+ status,
193
+ now,
194
+ now,
195
+ ),
196
+ )
197
+ self.conn.commit()
198
+ return cur.lastrowid
199
+
200
+ def add_credential(
201
+ self,
202
+ target_id: int,
203
+ service: str,
204
+ username: str,
205
+ password: str | None = None,
206
+ hash_value: str | None = None,
207
+ source_tool: str = "",
208
+ notes: str = "",
209
+ ) -> int:
210
+ cur = self.conn.cursor()
211
+ now = __import__("datetime").datetime.utcnow().isoformat() + "Z"
212
+ cur.execute(
213
+ "INSERT INTO credentials (target_id, service, username, password, hash, source_tool, captured_at, notes) VALUES (?,?,?,?,?,?,?,?)",
214
+ (
215
+ target_id,
216
+ service,
217
+ username,
218
+ password,
219
+ hash_value,
220
+ source_tool,
221
+ now,
222
+ notes,
223
+ ),
224
+ )
225
+ self.conn.commit()
226
+ return cur.lastrowid
227
+
228
+
229
+ _db_singleton: DBManager | None = None
230
+
231
+
232
+ def get_db() -> DBManager | None:
233
+ global _db_singleton
234
+ if _db_singleton is None:
235
+ try:
236
+ if getattr(config, "db_enabled", True):
237
+ _db_singleton = DBManager(config.db_path)
238
+ _db_singleton.ensure_schema()
239
+ except Exception: # noqa: BLE001
240
+ _db_singleton = None
241
+ return _db_singleton
242
+
243
+
244
+ def _now():
245
+ import datetime
246
+ from datetime import timezone
247
+
248
+ return datetime.datetime.now(timezone.utc).isoformat() + "Z"