sharkit 0.2.1.dev5__tar.gz → 1.0.0.dev2__tar.gz
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.
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/PKG-INFO +1 -1
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/pyproject.toml +1 -1
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/__init__.py +1 -1
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/__main__.py +2 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/app.py +4 -2
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/commands/base.py +3 -3
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/commands/builtins.py +92 -69
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/config/manager.py +7 -9
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/config/paths.py +8 -1
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/console/completion.py +45 -8
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/console/prompt.py +14 -2
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/console/repl.py +2 -4
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/exceptions.py +3 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/history/manager.py +20 -5
- sharkit-1.0.0.dev2/src/sharkit/network/client.py +95 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/output/renderer.py +33 -12
- sharkit-1.0.0.dev2/src/sharkit/output/theme.py +38 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/tools/base.py +16 -4
- sharkit-1.0.0.dev2/src/sharkit/tools/flatten_tools.sh +107 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/geoint/recon/wifi/mylnikov.py +79 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/humint/__init__.py +0 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/humint/factcheck/truthcheck.py +195 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/humint/factcheck/verifact.py +188 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/tools/loader.py +5 -1
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/tools/manager.py +22 -8
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/analytics/crypto/mempool.py +85 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/analytics/hashes/hash_decrypt.py +90 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/analytics/hashes/hash_lookup.py +71 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/analytics/images/exif_reader.py +157 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/analytics/text/base64_tool.py +79 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/analytics/text/text_entropy.py +81 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/gather/scraperx.py +187 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/gather/scweet.py +194 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/darkweb/ahmia_search.py +74 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/darkweb/pyahmia.py +187 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/domains/cve_search.py +155 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/domains/dns_lookup.py +180 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/domains/otx_query.py +126 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/domains/urlhaus_check.py +127 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/domains/whois_lookup.py +141 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/emails/email_reputation.py +87 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/emails/email_security.py +131 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/emails/holehe.py +210 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/emails/mailaccess.py +199 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/emails/theharvester.py +216 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/tools/osint/recon/hybrid/blackbird.py +12 -9
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/leaks/circl_hashlookup.py +77 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/leaks/hibp_passwords.py +83 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/leaks/leak_check.py +85 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/leaks/psbdmp.py +75 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/leaks/xposed_or_not.py +80 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/bgpv6.py +94 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/http_headers.py +108 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/ip_geolocate.py +86 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/ip_info.py +76 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/port_scanner.py +140 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/robtex.py +89 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/shodan_internetdb.py +80 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/ssl_check.py +116 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/phones/phoneinfoga.py +210 -0
- {sharkit-0.2.1.dev5/src/sharkit/tools/osint/recon/username → sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/usernames}/maigret.py +12 -9
- {sharkit-0.2.1.dev5/src/sharkit/tools/osint/recon/username → sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/usernames}/sherlock.py +12 -9
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/usernames/social_analyzer.py +210 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/usernames/socialscan.py +198 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/usernames/socid_extractor.py +210 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/web/robots_check.py +138 -0
- sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/web/wayback_check.py +122 -0
- sharkit-0.2.1.dev5/src/sharkit/network/client.py +0 -81
- sharkit-0.2.1.dev5/src/sharkit/output/theme.py +0 -18
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/.gitignore +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/LICENSE +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/README.md +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/commands/__init__.py +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/commands/registry.py +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/config/__init__.py +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/console/__init__.py +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/history/__init__.py +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/network/__init__.py +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/output/__init__.py +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/tools/__init__.py +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/tools/registry.py +0 -0
|
@@ -33,8 +33,9 @@ NON_LINUX_MESSAGE = (
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def main() -> int:
|
|
36
|
-
sys.stdout.
|
|
37
|
-
|
|
36
|
+
if sys.stdout.isatty():
|
|
37
|
+
sys.stdout.write("\033[2J\033[3J\033[H")
|
|
38
|
+
sys.stdout.flush()
|
|
38
39
|
renderer = Renderer()
|
|
39
40
|
|
|
40
41
|
if sys.platform == "win32":
|
|
@@ -69,6 +70,7 @@ def main() -> int:
|
|
|
69
70
|
history_manager=history_manager,
|
|
70
71
|
renderer=renderer,
|
|
71
72
|
)
|
|
73
|
+
console._session_data["http_client"] = http_client
|
|
72
74
|
|
|
73
75
|
try:
|
|
74
76
|
console.run()
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Any, ClassVar
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@dataclass
|
|
@@ -15,10 +15,10 @@ class CommandContext:
|
|
|
15
15
|
|
|
16
16
|
class Command(ABC):
|
|
17
17
|
name: str
|
|
18
|
-
aliases: list[str] = []
|
|
18
|
+
aliases: ClassVar[list[str]] = []
|
|
19
19
|
description: str = ""
|
|
20
20
|
usage: str = ""
|
|
21
|
-
options: list[tuple[str, str]] = []
|
|
21
|
+
options: ClassVar[list[tuple[str, str]]] = []
|
|
22
22
|
notes: str = ""
|
|
23
23
|
|
|
24
24
|
@abstractmethod
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import sys
|
|
5
|
+
from collections.abc import Callable
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from typing import Any
|
|
6
8
|
|
|
7
9
|
from sharkit import __version__
|
|
8
10
|
from sharkit.commands.base import Command, CommandContext
|
|
11
|
+
from sharkit.exceptions import CommandNotFoundError
|
|
9
12
|
from sharkit.output.theme import BLUE, BOLD, GREEN, PINK, RED, RESET
|
|
10
13
|
from sharkit.tools.base import Tool
|
|
11
14
|
|
|
@@ -22,6 +25,19 @@ def _normalize_bool(value: str) -> str:
|
|
|
22
25
|
return value
|
|
23
26
|
|
|
24
27
|
|
|
28
|
+
def _make_progress_callback(
|
|
29
|
+
renderer: Any, tag: str, color: str, first_progress: list[bool]
|
|
30
|
+
) -> Callable[[str], None]:
|
|
31
|
+
def on_progress(msg: str) -> None:
|
|
32
|
+
if renderer is not None:
|
|
33
|
+
if first_progress[0]:
|
|
34
|
+
renderer.gutter(tag, msg, color, is_first=True)
|
|
35
|
+
first_progress[0] = False
|
|
36
|
+
else:
|
|
37
|
+
renderer.gutter(tag, msg, color, is_first=False)
|
|
38
|
+
return on_progress
|
|
39
|
+
|
|
40
|
+
|
|
25
41
|
def _get_tool_instance(context: CommandContext) -> Tool | None:
|
|
26
42
|
tool_name = context.current_tool
|
|
27
43
|
if tool_name is None:
|
|
@@ -70,7 +86,7 @@ class HelpCommand(Command):
|
|
|
70
86
|
return "No command registry available."
|
|
71
87
|
try:
|
|
72
88
|
cmd_class = registry.get_command(name)
|
|
73
|
-
except
|
|
89
|
+
except (CommandNotFoundError, KeyError):
|
|
74
90
|
return f'Command "{name}" not found.'
|
|
75
91
|
if cmd_class is None:
|
|
76
92
|
return f'Command "{name}" not found.'
|
|
@@ -173,7 +189,7 @@ class UseCommand(Command):
|
|
|
173
189
|
def _log(self, context: CommandContext, message: str) -> None:
|
|
174
190
|
renderer = context.session.get("renderer")
|
|
175
191
|
if renderer is not None:
|
|
176
|
-
renderer.
|
|
192
|
+
renderer.dash_line("use", message)
|
|
177
193
|
|
|
178
194
|
def validate_args(self, args: list[str]) -> tuple[bool, str | None]:
|
|
179
195
|
if not args:
|
|
@@ -194,15 +210,9 @@ class UseCommand(Command):
|
|
|
194
210
|
return None
|
|
195
211
|
renderer = context.session.get("renderer")
|
|
196
212
|
first_progress = [True]
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
tag = f"install {display}"
|
|
201
|
-
if first_progress[0]:
|
|
202
|
-
renderer.gutter(tag, msg, GREEN, is_first=True)
|
|
203
|
-
first_progress[0] = False
|
|
204
|
-
else:
|
|
205
|
-
renderer.gutter(tag, msg, GREEN, is_first=False)
|
|
213
|
+
on_progress = _make_progress_callback(
|
|
214
|
+
renderer, f"install {display}", GREEN, first_progress
|
|
215
|
+
)
|
|
206
216
|
|
|
207
217
|
ok, message = tool_manager.install(name, spec, on_progress=on_progress)
|
|
208
218
|
if not ok:
|
|
@@ -268,7 +278,7 @@ class InfoCommand(Command):
|
|
|
268
278
|
f"safety {metadata.safety}",
|
|
269
279
|
]
|
|
270
280
|
return "\n".join(lines)
|
|
271
|
-
except
|
|
281
|
+
except (CommandNotFoundError, AttributeError):
|
|
272
282
|
return f"Tool: {context.current_tool}"
|
|
273
283
|
|
|
274
284
|
|
|
@@ -276,39 +286,68 @@ class ShowCommand(Command):
|
|
|
276
286
|
name = "show"
|
|
277
287
|
aliases = []
|
|
278
288
|
description = "Show tools or tool options"
|
|
279
|
-
usage = "show <
|
|
289
|
+
usage = "show <all|osint|humint|geoint|options> [tool]"
|
|
280
290
|
|
|
281
291
|
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
282
292
|
if not args:
|
|
283
293
|
if context.current_tool is not None:
|
|
284
294
|
return self._show_options(context, None)
|
|
285
|
-
return self._show_tools(context)
|
|
295
|
+
return self._show_tools(context, None)
|
|
286
296
|
if args[0] == "options":
|
|
287
297
|
return self._show_options(context, args[1] if len(args) > 1 else None)
|
|
288
|
-
if args[0] == "
|
|
289
|
-
return self._show_tools(context)
|
|
298
|
+
if args[0] == "all":
|
|
299
|
+
return self._show_tools(context, None)
|
|
300
|
+
if args[0] in ("osint", "humint", "geoint"):
|
|
301
|
+
return self._show_tools(context, args[0])
|
|
290
302
|
return f"Unknown show subcommand: {args[0]}\nUsage: {self.usage}"
|
|
291
303
|
|
|
292
|
-
def _show_tools(self, context: CommandContext) -> str | None:
|
|
304
|
+
def _show_tools(self, context: CommandContext, category: str | None) -> str | None:
|
|
293
305
|
tool_registry = context.session.get("tool_registry")
|
|
294
306
|
tool_manager = context.session.get("tool_manager")
|
|
295
307
|
renderer = context.session.get("renderer")
|
|
296
308
|
if tool_registry is None or renderer is None:
|
|
297
309
|
return "Tool registry not available."
|
|
310
|
+
|
|
298
311
|
all_tools = tool_registry.get_all_tools()
|
|
299
312
|
if not all_tools:
|
|
300
313
|
return "No tools available."
|
|
314
|
+
|
|
315
|
+
def matches_category(tool_path: str, tool_cls: type) -> bool:
|
|
316
|
+
if category is None:
|
|
317
|
+
return True
|
|
318
|
+
|
|
319
|
+
normalized_category = category.lower()
|
|
320
|
+
tool_category = (tool_cls.metadata.category or "").lower()
|
|
321
|
+
first_segment = tool_category.split(".")[0]
|
|
322
|
+
return (
|
|
323
|
+
normalized_category == tool_category
|
|
324
|
+
or normalized_category == first_segment
|
|
325
|
+
or normalized_category in tool_category
|
|
326
|
+
or tool_path.lower().startswith(f"{normalized_category}/")
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
filtered = [
|
|
330
|
+
(tool_path, tool_cls)
|
|
331
|
+
for tool_path, tool_cls in all_tools.items()
|
|
332
|
+
if matches_category(tool_path, tool_cls)
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
if not filtered:
|
|
336
|
+
return f'No tools found for category "{category}".'
|
|
337
|
+
|
|
338
|
+
title = f"show > {category or 'all'}"
|
|
301
339
|
rows = []
|
|
302
|
-
for tool_path, tool_cls in
|
|
303
|
-
|
|
304
|
-
if
|
|
305
|
-
tool_manager is not None and tool_manager.is_installed(
|
|
340
|
+
for tool_path, tool_cls in sorted(filtered, key=lambda item: item[0]):
|
|
341
|
+
meta = tool_cls.metadata
|
|
342
|
+
if meta.install is None or (
|
|
343
|
+
tool_manager is not None and tool_manager.is_installed(meta.name)
|
|
306
344
|
):
|
|
307
345
|
installed = f"{BOLD}{PINK}Yes{RESET}"
|
|
308
346
|
else:
|
|
309
347
|
installed = ""
|
|
310
|
-
rows.append([tool_path, installed,
|
|
311
|
-
|
|
348
|
+
rows.append([tool_path, installed, meta.description])
|
|
349
|
+
|
|
350
|
+
renderer.table(title, ["Tool", "Installed", "Description"], rows)
|
|
312
351
|
return None
|
|
313
352
|
|
|
314
353
|
def _show_options(self, context: CommandContext, tool_arg: str | None) -> str | None:
|
|
@@ -319,12 +358,12 @@ class ShowCommand(Command):
|
|
|
319
358
|
if tool_arg is not None:
|
|
320
359
|
tool_cls = tool_registry.get_tool(tool_arg)
|
|
321
360
|
if tool_cls is None:
|
|
322
|
-
return f'Tool "{tool_arg}" not found.\nTry: show
|
|
361
|
+
return f'Tool "{tool_arg}" not found.\nTry: show all'
|
|
323
362
|
instance = tool_cls()
|
|
324
363
|
title_tool = tool_cls.metadata.name
|
|
325
364
|
else:
|
|
326
365
|
if context.current_tool is None:
|
|
327
|
-
return "No tool selected. Use 'show
|
|
366
|
+
return "No tool selected. Use 'show all' or 'use <tool>' first."
|
|
328
367
|
tool_cls = tool_registry.get_tool(context.current_tool)
|
|
329
368
|
if tool_cls is None:
|
|
330
369
|
return "Current tool not found."
|
|
@@ -338,7 +377,11 @@ class ShowCommand(Command):
|
|
|
338
377
|
return "This tool has no options."
|
|
339
378
|
rows = []
|
|
340
379
|
for opt in options.values():
|
|
341
|
-
value =
|
|
380
|
+
value = (
|
|
381
|
+
opt.value
|
|
382
|
+
if opt.value is not None
|
|
383
|
+
else (opt.default if opt.default is not None else "not set")
|
|
384
|
+
)
|
|
342
385
|
required = " (required)" if opt.required else ""
|
|
343
386
|
rows.append([opt.name, value, f"{opt.description}{required}"])
|
|
344
387
|
renderer.table(
|
|
@@ -347,7 +390,7 @@ class ShowCommand(Command):
|
|
|
347
390
|
rows,
|
|
348
391
|
)
|
|
349
392
|
return None
|
|
350
|
-
except
|
|
393
|
+
except (CommandNotFoundError, KeyError, AttributeError):
|
|
351
394
|
return "Could not retrieve options for tool."
|
|
352
395
|
|
|
353
396
|
|
|
@@ -374,6 +417,8 @@ class SetCommand(Command):
|
|
|
374
417
|
return f'Option "{key}" not found.\nAvailable options: {available}'
|
|
375
418
|
option = options[key]
|
|
376
419
|
raw = " ".join(args[1:])
|
|
420
|
+
if not raw and option.required:
|
|
421
|
+
return f'Option "{key}" requires a value.'
|
|
377
422
|
if option.type == "bool":
|
|
378
423
|
value = _normalize_bool(raw)
|
|
379
424
|
elif option.choices:
|
|
@@ -394,7 +439,7 @@ class SetCommand(Command):
|
|
|
394
439
|
display = tool_registry.format_display(
|
|
395
440
|
tool_registry.get_tool_path(context.current_tool),
|
|
396
441
|
)
|
|
397
|
-
renderer.
|
|
442
|
+
renderer.dash_line(f"set {display}", f"{key} = {value}")
|
|
398
443
|
return None
|
|
399
444
|
return f"Set {key} = {value}"
|
|
400
445
|
except Exception as e:
|
|
@@ -452,7 +497,10 @@ class RunCommand(Command):
|
|
|
452
497
|
return "Tool registry not available."
|
|
453
498
|
try:
|
|
454
499
|
options = tool_instance.get_options()
|
|
455
|
-
option_values = {
|
|
500
|
+
option_values = {
|
|
501
|
+
k: v.value if v.value is not None else (v.default if v.default is not None else "")
|
|
502
|
+
for k, v in options.items()
|
|
503
|
+
}
|
|
456
504
|
config_dir = context.session.get("config_dir") or Path("/tmp")
|
|
457
505
|
from sharkit.tools.base import ExecutionContext
|
|
458
506
|
|
|
@@ -475,7 +523,9 @@ class RunCommand(Command):
|
|
|
475
523
|
if result.success:
|
|
476
524
|
if result.data:
|
|
477
525
|
if renderer is not None:
|
|
478
|
-
|
|
526
|
+
tool_name = tool_instance.metadata.name
|
|
527
|
+
tool_color = tool_instance.metadata.color or "#888888"
|
|
528
|
+
renderer.tool_output(result.data, tool_name, tool_color)
|
|
479
529
|
return None
|
|
480
530
|
lines = [f"{key}: {value}" for key, value in result.data.items()]
|
|
481
531
|
return "\n".join(lines) if lines else "Tool executed successfully."
|
|
@@ -486,7 +536,8 @@ class RunCommand(Command):
|
|
|
486
536
|
renderer.error(f"Tool execution failed: {result.error}")
|
|
487
537
|
return None
|
|
488
538
|
return f"Tool execution failed: {result.error}"
|
|
489
|
-
except
|
|
539
|
+
except (RuntimeError, ValueError, OSError) as e:
|
|
540
|
+
logging.getLogger(__name__).exception("Tool execution error")
|
|
490
541
|
return f"Tool execution error: {e}"
|
|
491
542
|
|
|
492
543
|
|
|
@@ -500,7 +551,7 @@ class SearchCommand(Command):
|
|
|
500
551
|
valid, error = self.validate_args(args)
|
|
501
552
|
if not valid:
|
|
502
553
|
return error
|
|
503
|
-
query = args
|
|
554
|
+
query = " ".join(args).lower()
|
|
504
555
|
tool_registry = context.session.get("tool_registry")
|
|
505
556
|
if tool_registry is None:
|
|
506
557
|
return "Tool registry not available."
|
|
@@ -648,15 +699,9 @@ class InstallCommand(Command):
|
|
|
648
699
|
return None
|
|
649
700
|
renderer = context.session.get("renderer")
|
|
650
701
|
first_progress = [True]
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
tag = f"install {display}"
|
|
655
|
-
if first_progress[0]:
|
|
656
|
-
renderer.gutter(tag, msg, GREEN, is_first=True)
|
|
657
|
-
first_progress[0] = False
|
|
658
|
-
else:
|
|
659
|
-
renderer.gutter(tag, msg, GREEN, is_first=False)
|
|
702
|
+
on_progress = _make_progress_callback(
|
|
703
|
+
renderer, f"install {display}", GREEN, first_progress
|
|
704
|
+
)
|
|
660
705
|
|
|
661
706
|
install_spec = tool_cls.metadata.install
|
|
662
707
|
ok, message = tool_manager.install(
|
|
@@ -706,21 +751,12 @@ class UpgradeCommand(Command):
|
|
|
706
751
|
tool_name = tool_cls.metadata.name
|
|
707
752
|
display = tool_registry.format_display(tool_registry.get_tool_path(tool_name))
|
|
708
753
|
if not tool_manager.is_installed(tool_name):
|
|
709
|
-
renderer = context.session.get("renderer")
|
|
710
|
-
if renderer is not None:
|
|
711
|
-
renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
|
|
712
754
|
return None
|
|
713
755
|
renderer = context.session.get("renderer")
|
|
714
756
|
first_progress = [True]
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
tag = f"upgrade {display}"
|
|
719
|
-
if first_progress[0]:
|
|
720
|
-
renderer.gutter(tag, msg, BLUE, is_first=True)
|
|
721
|
-
first_progress[0] = False
|
|
722
|
-
else:
|
|
723
|
-
renderer.gutter(tag, msg, BLUE, is_first=False)
|
|
757
|
+
on_progress = _make_progress_callback(
|
|
758
|
+
renderer, f"upgrade {display}", BLUE, first_progress
|
|
759
|
+
)
|
|
724
760
|
|
|
725
761
|
upgrade_spec = tool_cls.metadata.install
|
|
726
762
|
ok, message = tool_manager.update(
|
|
@@ -768,25 +804,12 @@ class UpgradeCommand(Command):
|
|
|
768
804
|
name = meta.name
|
|
769
805
|
display = tool_registry.format_display(tool_registry.get_tool_path(name))
|
|
770
806
|
if not tool_manager.is_installed(name):
|
|
771
|
-
renderer = context.session.get("renderer")
|
|
772
|
-
if renderer is not None:
|
|
773
|
-
renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
|
|
774
807
|
continue
|
|
775
808
|
renderer = context.session.get("renderer")
|
|
776
809
|
first_progress = [True]
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
_first: list[bool] = first_progress,
|
|
781
|
-
_tag: str = f"upgrade {display}",
|
|
782
|
-
_renderer: Any = renderer,
|
|
783
|
-
) -> None:
|
|
784
|
-
if _renderer is not None:
|
|
785
|
-
if _first[0]:
|
|
786
|
-
_renderer.gutter(_tag, msg, BLUE, is_first=True)
|
|
787
|
-
_first[0] = False
|
|
788
|
-
else:
|
|
789
|
-
_renderer.gutter(_tag, msg, BLUE, is_first=False)
|
|
810
|
+
on_progress = _make_progress_callback(
|
|
811
|
+
renderer, f"upgrade {display}", BLUE, first_progress
|
|
812
|
+
)
|
|
790
813
|
|
|
791
814
|
ok, message = tool_manager.update(name, spec, on_progress=on_progress)
|
|
792
815
|
if renderer is not None:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
from sharkit.config.paths import (
|
|
@@ -34,7 +35,7 @@ class ConfigManager:
|
|
|
34
35
|
def _load(self) -> None:
|
|
35
36
|
try:
|
|
36
37
|
content = self._config_file.read_text(encoding="utf-8")
|
|
37
|
-
except OSError as exc:
|
|
38
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
38
39
|
raise ConfigurationError(f"Failed to read config file: {exc}") from exc
|
|
39
40
|
|
|
40
41
|
for line_number, raw_line in enumerate(content.splitlines(), start=1):
|
|
@@ -49,16 +50,13 @@ class ConfigManager:
|
|
|
49
50
|
self._config[key.strip()] = value.strip()
|
|
50
51
|
|
|
51
52
|
def _save(self) -> None:
|
|
52
|
-
lines = []
|
|
53
|
-
for key, value in sorted(self._config.items()):
|
|
54
|
-
lines.append(f"{key}={value}")
|
|
55
|
-
content = "\n".join(lines)
|
|
56
|
-
if content:
|
|
57
|
-
content += "\n"
|
|
58
53
|
try:
|
|
59
|
-
self._config_file.
|
|
54
|
+
tmp = self._config_file.with_suffix(".tmp")
|
|
55
|
+
lines = [f"{k}={v}" for k, v in sorted(self._config.items())]
|
|
56
|
+
tmp.write_text("\n".join(lines) + "\n" if lines else "", encoding="utf-8")
|
|
57
|
+
os.replace(tmp, self._config_file)
|
|
60
58
|
except OSError as exc:
|
|
61
|
-
raise ConfigurationError(f"Failed to
|
|
59
|
+
raise ConfigurationError(f"Failed to save config: {exc}") from exc
|
|
62
60
|
|
|
63
61
|
def get(self, key: str, default: str | None = None) -> str | None:
|
|
64
62
|
return self._config.get(key, default)
|
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
+
from sharkit.exceptions import ConfigurationError
|
|
6
|
+
|
|
5
7
|
_BASE_DIR_NAME = "sharkit"
|
|
6
8
|
|
|
7
9
|
|
|
@@ -11,7 +13,12 @@ def _get_xdg_config_home() -> Path:
|
|
|
11
13
|
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
12
14
|
if xdg:
|
|
13
15
|
return Path(xdg)
|
|
14
|
-
|
|
16
|
+
try:
|
|
17
|
+
return Path.home() / ".config"
|
|
18
|
+
except RuntimeError:
|
|
19
|
+
raise ConfigurationError(
|
|
20
|
+
"Cannot determine home directory. Set XDG_CONFIG_HOME manually."
|
|
21
|
+
) from None
|
|
15
22
|
|
|
16
23
|
|
|
17
24
|
def get_config_dir() -> Path:
|
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from sharkit.commands.registry import CommandRegistry
|
|
4
|
+
from sharkit.tools.base import OptionDefinition
|
|
4
5
|
from sharkit.tools.registry import ToolRegistry
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class CompletionEngine:
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self._cached_tool: str | None = None
|
|
11
|
+
self._cached_options: dict[str, OptionDefinition] | None = None
|
|
12
|
+
|
|
13
|
+
def _load_options(
|
|
14
|
+
self, tool_registry: ToolRegistry, current_tool: str
|
|
15
|
+
) -> dict[str, OptionDefinition]:
|
|
16
|
+
if current_tool == self._cached_tool and self._cached_options is not None:
|
|
17
|
+
return self._cached_options
|
|
18
|
+
tool_cls = tool_registry.get_tool(current_tool)
|
|
19
|
+
if tool_cls is None:
|
|
20
|
+
self._cached_tool = None
|
|
21
|
+
self._cached_options = None
|
|
22
|
+
return {}
|
|
23
|
+
instance = tool_cls()
|
|
24
|
+
self._cached_options = instance.get_options()
|
|
25
|
+
self._cached_tool = current_tool
|
|
26
|
+
return self._cached_options
|
|
27
|
+
|
|
8
28
|
def get_completions(
|
|
9
29
|
self,
|
|
10
30
|
text: str,
|
|
@@ -22,22 +42,39 @@ class CompletionEngine:
|
|
|
22
42
|
|
|
23
43
|
if text_lower.startswith("show "):
|
|
24
44
|
prefix = text_lower[5:]
|
|
25
|
-
return sorted(
|
|
45
|
+
return sorted(
|
|
46
|
+
s for s in ("all", "osint", "humint", "geoint", "options")
|
|
47
|
+
if s.startswith(prefix)
|
|
48
|
+
)
|
|
26
49
|
|
|
27
50
|
if text_lower.startswith("set ") or text_lower.startswith("unset "):
|
|
28
51
|
if current_tool is None:
|
|
29
52
|
return []
|
|
53
|
+
is_set = text_lower.startswith("set ")
|
|
54
|
+
body = text[4:] if is_set else text[6:]
|
|
55
|
+
parts = body.split(None, 1)
|
|
56
|
+
|
|
30
57
|
try:
|
|
31
|
-
|
|
32
|
-
if tool_cls is None:
|
|
33
|
-
return []
|
|
34
|
-
instance = tool_cls()
|
|
35
|
-
options = instance.get_options()
|
|
36
|
-
prefix = text_lower[4:] if text_lower.startswith("set ") else text_lower[6:]
|
|
37
|
-
return sorted(k for k in options if k.lower().startswith(prefix))
|
|
58
|
+
options = self._load_options(tool_registry, current_tool)
|
|
38
59
|
except Exception:
|
|
39
60
|
return []
|
|
40
61
|
|
|
62
|
+
if len(parts) < 2:
|
|
63
|
+
prefix = parts[0] if parts else ""
|
|
64
|
+
exact = options.get(prefix)
|
|
65
|
+
if exact and exact.choices:
|
|
66
|
+
return list(exact.choices)
|
|
67
|
+
elif exact and exact.type == "bool":
|
|
68
|
+
return ["true", "false"]
|
|
69
|
+
return sorted(k for k in options if k.lower().startswith(prefix))
|
|
70
|
+
|
|
71
|
+
option_name = parts[0]
|
|
72
|
+
value_prefix = parts[1]
|
|
73
|
+
opt = options.get(option_name)
|
|
74
|
+
if opt is None or not opt.choices:
|
|
75
|
+
return []
|
|
76
|
+
return sorted(c for c in opt.choices if c.lower().startswith(value_prefix.lower()))
|
|
77
|
+
|
|
41
78
|
if text_lower.startswith("install "):
|
|
42
79
|
prefix = text_lower[8:]
|
|
43
80
|
return sorted(
|
|
@@ -16,7 +16,19 @@ class PromptRenderer:
|
|
|
16
16
|
formatted = tool_name
|
|
17
17
|
return f"shark {formatted} > "
|
|
18
18
|
|
|
19
|
-
def render(self, prompt: str) -> list[tuple[str, str]]:
|
|
19
|
+
def render(self, prompt: str, tool_name: str | None = None) -> list[tuple[str, str]]:
|
|
20
|
+
if tool_name is not None and "/" in tool_name:
|
|
21
|
+
cat, rest = tool_name.split("/", 1)
|
|
22
|
+
before = "shark "
|
|
23
|
+
after = " > "
|
|
24
|
+
return [
|
|
25
|
+
(PROMPT_STYLE, before),
|
|
26
|
+
(MODULE_STYLE, cat),
|
|
27
|
+
("", "("),
|
|
28
|
+
(MODULE_STYLE, rest),
|
|
29
|
+
("", ")"),
|
|
30
|
+
("", after),
|
|
31
|
+
]
|
|
20
32
|
if "(" in prompt and ")" in prompt:
|
|
21
33
|
before, rest = prompt.split("(", 1)
|
|
22
34
|
tool_path, after = rest.split(")", 1)
|
|
@@ -25,6 +37,6 @@ class PromptRenderer:
|
|
|
25
37
|
("", "("),
|
|
26
38
|
(MODULE_STYLE, tool_path),
|
|
27
39
|
("", ")"),
|
|
28
|
-
(
|
|
40
|
+
(PROMPT_STYLE, after),
|
|
29
41
|
]
|
|
30
42
|
return [(PROMPT_STYLE, prompt)]
|
|
@@ -31,7 +31,7 @@ class _SharkitHistory(History):
|
|
|
31
31
|
super().__init__()
|
|
32
32
|
|
|
33
33
|
def load_history_strings(self) -> list[str]:
|
|
34
|
-
return self._manager.get_history()
|
|
34
|
+
return list(reversed(self._manager.get_history()))
|
|
35
35
|
|
|
36
36
|
def store_string(self, string: str) -> None:
|
|
37
37
|
self._manager.add_command(string)
|
|
@@ -108,7 +108,7 @@ class Console:
|
|
|
108
108
|
prompt = self._prompt_renderer.tool_prompt(self._current_tool)
|
|
109
109
|
else:
|
|
110
110
|
prompt = self._prompt_renderer.default_prompt()
|
|
111
|
-
return self._prompt_renderer.render(prompt)
|
|
111
|
+
return self._prompt_renderer.render(prompt, tool_name=self._current_tool)
|
|
112
112
|
|
|
113
113
|
def _init_session(self) -> None:
|
|
114
114
|
self._prompt_renderer = PromptRenderer()
|
|
@@ -144,8 +144,6 @@ class Console:
|
|
|
144
144
|
if not stripped:
|
|
145
145
|
continue
|
|
146
146
|
|
|
147
|
-
if not line[:1].isspace():
|
|
148
|
-
self._history_manager.add_command(stripped)
|
|
149
147
|
self._session_data["history"] = self._history_manager.get_history()
|
|
150
148
|
|
|
151
149
|
if not self.process_command(stripped):
|
|
@@ -6,11 +6,13 @@ from pathlib import Path
|
|
|
6
6
|
from sharkit.config.paths import get_history_file
|
|
7
7
|
from sharkit.exceptions import ConfigurationError
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
r"(password|passwd|token|api[_-]?key|secret|
|
|
9
|
+
_SENSITIVE_RE = re.compile(
|
|
10
|
+
r"\b(password|passwd|token|api[_-]?key|secret|auth_token|credential)\b",
|
|
11
11
|
re.IGNORECASE,
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
+
MAX_HISTORY = 10_000
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
class HistoryManager:
|
|
16
18
|
_history_file: Path
|
|
@@ -46,7 +48,7 @@ class HistoryManager:
|
|
|
46
48
|
raise ConfigurationError(f"Failed to write history file: {exc}") from exc
|
|
47
49
|
|
|
48
50
|
def _is_sensitive(self, command: str) -> bool:
|
|
49
|
-
return bool(
|
|
51
|
+
return bool(_SENSITIVE_RE.search(command))
|
|
50
52
|
|
|
51
53
|
def add_command(self, command: str) -> None:
|
|
52
54
|
if command[:1].isspace():
|
|
@@ -60,6 +62,8 @@ class HistoryManager:
|
|
|
60
62
|
if lines and lines[-1] == stripped:
|
|
61
63
|
return
|
|
62
64
|
lines.append(stripped)
|
|
65
|
+
if len(lines) > MAX_HISTORY:
|
|
66
|
+
lines = lines[-MAX_HISTORY:]
|
|
63
67
|
self._write_lines(lines)
|
|
64
68
|
|
|
65
69
|
def get_history(self, limit: int = 100) -> list[str]:
|
|
@@ -74,10 +78,21 @@ class HistoryManager:
|
|
|
74
78
|
commands.append(line)
|
|
75
79
|
return commands[-limit:]
|
|
76
80
|
|
|
77
|
-
def search(self, query: str) -> list[str]:
|
|
81
|
+
def search(self, query: str, limit: int = 50) -> list[str]:
|
|
78
82
|
lines = self._read_lines()
|
|
79
83
|
query_lower = query.lower()
|
|
80
|
-
|
|
84
|
+
results = []
|
|
85
|
+
for line in reversed(lines):
|
|
86
|
+
if not line:
|
|
87
|
+
continue
|
|
88
|
+
if line.startswith("#"):
|
|
89
|
+
continue
|
|
90
|
+
display = line[1:] if line.startswith("+") else line
|
|
91
|
+
if query_lower in display.lower():
|
|
92
|
+
results.append(display)
|
|
93
|
+
if len(results) >= limit:
|
|
94
|
+
break
|
|
95
|
+
return results
|
|
81
96
|
|
|
82
97
|
def clear(self) -> None:
|
|
83
98
|
self._write_lines([])
|