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.
- kali_mcp/__init__.py +3 -0
- kali_mcp/__main__.py +28 -0
- kali_mcp/_version.py +24 -0
- kali_mcp/cli.py +169 -0
- kali_mcp/config.py +95 -0
- kali_mcp/db/__init__.py +1 -0
- kali_mcp/db/manager.py +248 -0
- kali_mcp/execution.py +439 -0
- kali_mcp/logging_utils.py +74 -0
- kali_mcp/models.py +51 -0
- kali_mcp/monitor/__init__.py +105 -0
- kali_mcp/monitor/sessions.py +70 -0
- kali_mcp/monitor/sse_client.py +87 -0
- kali_mcp/monitor/tui.py +364 -0
- kali_mcp/notifications.py +130 -0
- kali_mcp/prompts.py +184 -0
- kali_mcp/registry.py +33 -0
- kali_mcp/resources.py +263 -0
- kali_mcp/responses.py +39 -0
- kali_mcp/security.py +15 -0
- kali_mcp/server.py +535 -0
- kali_mcp/tools/__init__.py +125 -0
- kali_mcp/tools/amass.py +83 -0
- kali_mcp/tools/arjun.py +86 -0
- kali_mcp/tools/base.py +100 -0
- kali_mcp/tools/bettercap.py +99 -0
- kali_mcp/tools/bloodhound.py +68 -0
- kali_mcp/tools/commix.py +73 -0
- kali_mcp/tools/crackmapexec.py +100 -0
- kali_mcp/tools/dirb.py +75 -0
- kali_mcp/tools/dursgo.py +156 -0
- kali_mcp/tools/enum4linux.py +59 -0
- kali_mcp/tools/farsight.py +127 -0
- kali_mcp/tools/ffuf.py +85 -0
- kali_mcp/tools/file_download.py +119 -0
- kali_mcp/tools/file_read.py +144 -0
- kali_mcp/tools/file_write.py +141 -0
- kali_mcp/tools/flowlyt.py +88 -0
- kali_mcp/tools/generic_command.py +95 -0
- kali_mcp/tools/gobuster.py +84 -0
- kali_mcp/tools/hashcat.py +101 -0
- kali_mcp/tools/httpx.py +85 -0
- kali_mcp/tools/hydra.py +99 -0
- kali_mcp/tools/john.py +100 -0
- kali_mcp/tools/katana.py +81 -0
- kali_mcp/tools/masscan.py +85 -0
- kali_mcp/tools/medusa.py +102 -0
- kali_mcp/tools/msfconsole.py +59 -0
- kali_mcp/tools/naabu.py +100 -0
- kali_mcp/tools/netexec.py +86 -0
- kali_mcp/tools/nmap.py +94 -0
- kali_mcp/tools/normalization.py +18 -0
- kali_mcp/tools/nuclei.py +87 -0
- kali_mcp/tools/parallel_execute.py +246 -0
- kali_mcp/tools/persist.py +436 -0
- kali_mcp/tools/playbook_ad.py +99 -0
- kali_mcp/tools/playbook_engine.py +142 -0
- kali_mcp/tools/playbook_engine_tool.py +72 -0
- kali_mcp/tools/playbook_recon.py +115 -0
- kali_mcp/tools/playbook_tools.py +125 -0
- kali_mcp/tools/playbook_webapp.py +105 -0
- kali_mcp/tools/python_command.py +100 -0
- kali_mcp/tools/responder.py +85 -0
- kali_mcp/tools/searchsploit.py +123 -0
- kali_mcp/tools/spiderfoot.py +75 -0
- kali_mcp/tools/sqlmap.py +73 -0
- kali_mcp/tools/subfinder.py +69 -0
- kali_mcp/tools/theharvester.py +80 -0
- kali_mcp/tools/vault_tool.py +489 -0
- kali_mcp/tools/wfuzz.py +87 -0
- kali_mcp/tools/whatweb.py +82 -0
- kali_mcp/tools/wpscan.py +76 -0
- kali_mcp/tools/zap.py +79 -0
- kali_mcp/tools/zighound.py +139 -0
- kali_mcp/tools/zizmor.py +98 -0
- kali_mcp/utils/__init__.py +0 -0
- kali_mcp/utils/process.py +36 -0
- kali_mcp/utils/vault.py +471 -0
- kali_mcp/validation.py +82 -0
- kmcp-0.1.5.dev0.dist-info/METADATA +251 -0
- kmcp-0.1.5.dev0.dist-info/RECORD +84 -0
- kmcp-0.1.5.dev0.dist-info/WHEEL +5 -0
- kmcp-0.1.5.dev0.dist-info/entry_points.txt +4 -0
- kmcp-0.1.5.dev0.dist-info/top_level.txt +1 -0
kali_mcp/__init__.py
ADDED
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()
|
kali_mcp/db/__init__.py
ADDED
|
@@ -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"
|