sharkit 0.2.0.dev1__tar.gz → 0.2.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.
Files changed (35) hide show
  1. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/PKG-INFO +2 -2
  2. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/README.md +1 -1
  3. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/pyproject.toml +1 -1
  4. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/commands/builtins.py +33 -16
  5. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/console/prompt.py +8 -3
  6. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/console/repl.py +10 -3
  7. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/output/renderer.py +23 -6
  8. {sharkit-0.2.0.dev1/src/sharkit/tools → sharkit-0.2.0.dev2/src/sharkit/tools/osint/recon/hybrid}/blackbird.py +14 -4
  9. sharkit-0.2.0.dev2/src/sharkit/tools/osint/recon/username/maigret.py +251 -0
  10. sharkit-0.2.0.dev2/src/sharkit/tools/osint/recon/username/sherlock.py +251 -0
  11. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/tools/registry.py +14 -0
  12. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/.gitignore +0 -0
  13. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/LICENSE +0 -0
  14. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/__init__.py +0 -0
  15. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/__main__.py +0 -0
  16. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/app.py +0 -0
  17. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/commands/__init__.py +0 -0
  18. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/commands/base.py +0 -0
  19. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/commands/registry.py +0 -0
  20. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/config/__init__.py +0 -0
  21. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/config/manager.py +0 -0
  22. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/config/paths.py +0 -0
  23. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/console/__init__.py +0 -0
  24. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/console/completion.py +0 -0
  25. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/exceptions.py +0 -0
  26. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/history/__init__.py +0 -0
  27. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/history/manager.py +0 -0
  28. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/network/__init__.py +0 -0
  29. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/network/client.py +0 -0
  30. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/output/__init__.py +0 -0
  31. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/output/theme.py +0 -0
  32. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/tools/__init__.py +0 -0
  33. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/tools/base.py +0 -0
  34. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/tools/loader.py +0 -0
  35. {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/tools/manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sharkit
3
- Version: 0.2.0.dev1
3
+ Version: 0.2.0.dev2
4
4
  Summary: OSINT / HUMINT / GEOINT research framework
5
5
  Project-URL: Homepage, https://github.com/sshsrv/sharkit
6
6
  Project-URL: Repository, https://github.com/sshsrv/sharkit
@@ -27,7 +27,7 @@ Requires-Dist: prompt-toolkit<4.0.0,>=3.0.0
27
27
  Description-Content-Type: text/markdown
28
28
 
29
29
  <div align="center"><img width="640" height="347" alt="banner" src="https://github.com/user-attachments/assets/c027fb38-fe0f-4baa-b35f-597f9f8cf39b" /></div>
30
-
30
+ <br>
31
31
  <div align="center">
32
32
 
33
33
  ![PyPI](https://img.shields.io/pypi/v/sharkit?labelColor=000000&color=ff5faf&style=flat)
@@ -1,5 +1,5 @@
1
1
  <div align="center"><img width="640" height="347" alt="banner" src="https://github.com/user-attachments/assets/c027fb38-fe0f-4baa-b35f-597f9f8cf39b" /></div>
2
-
2
+ <br>
3
3
  <div align="center">
4
4
 
5
5
  ![PyPI](https://img.shields.io/pypi/v/sharkit?labelColor=000000&color=ff5faf&style=flat)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sharkit"
3
- version = "0.2.0.dev1"
3
+ version = "0.2.0.dev2"
4
4
  description = "OSINT / HUMINT / GEOINT research framework"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -187,6 +187,7 @@ class UseCommand(Command):
187
187
  if tool_cls is not None and tool_cls.metadata.install is not None:
188
188
  spec = tool_cls.metadata.install
189
189
  name = tool_cls.metadata.name
190
+ display = tool_registry.format_display(path) if tool_registry else name
190
191
  if tool_manager is not None and not tool_manager.is_installed(name):
191
192
  if not self._prompt_install(name):
192
193
  self._log(context, f'Tool "{name}" is not installed. Run: install {name}')
@@ -195,7 +196,7 @@ class UseCommand(Command):
195
196
 
196
197
  def on_progress(msg: str) -> None:
197
198
  if renderer is not None:
198
- renderer.log_line(f"install ({name})", msg, color=GREEN)
199
+ renderer.log_line(f"install {display}", msg, color=GREEN)
199
200
 
200
201
  ok, message = tool_manager.install(name, spec, on_progress=on_progress)
201
202
  if not ok:
@@ -203,6 +204,15 @@ class UseCommand(Command):
203
204
  return None
204
205
  context.current_tool = path
205
206
  self._log(context, f'Tool "{path}" selected.')
207
+ if tool_cls is not None and tool_cls.metadata.install is not None:
208
+ renderer = context.session.get("renderer")
209
+ if renderer is not None:
210
+ install_spec = tool_cls.metadata.install
211
+ renderer.support_creator(
212
+ tool_cls.metadata.author,
213
+ tool_cls.metadata.name,
214
+ install_spec.git_url,
215
+ )
206
216
  return None
207
217
 
208
218
  def _prompt_install(self, name: str) -> bool:
@@ -552,6 +562,9 @@ class HistoryCommand(Command):
552
562
  if clear:
553
563
  manager.clear()
554
564
  context.session["history"] = []
565
+ sharkit_history = context.session.get("sharkit_history")
566
+ if sharkit_history is not None:
567
+ sharkit_history.clear()
555
568
  return "[+] Command history and history file cleared"
556
569
 
557
570
  all_entries = manager.get_history(10_000_000)
@@ -614,23 +627,24 @@ class InstallCommand(Command):
614
627
  tool_cls = tool_registry.get_tool(name)
615
628
  if tool_cls is None or tool_cls.metadata.install is None:
616
629
  return f'Tool "{name}" is not an external tool or was not found.'
630
+ display = tool_registry.format_display(tool_registry.get_tool_path(name))
617
631
  if tool_manager.is_installed(name):
618
632
  renderer = context.session.get("renderer")
619
633
  if renderer is not None:
620
- renderer.log_line(f"install ({name})", "already installed", color=GREEN)
634
+ renderer.log_line(f"install {display}", "already installed", color=GREEN)
621
635
  return None
622
636
  renderer = context.session.get("renderer")
623
637
 
624
638
  def on_progress(msg: str) -> None:
625
639
  if renderer is not None:
626
- renderer.log_line(f"install ({name})", msg, color=GREEN)
640
+ renderer.log_line(f"install {display}", msg, color=GREEN)
627
641
 
628
642
  ok, message = tool_manager.install(name, tool_cls.metadata.install, on_progress=on_progress)
629
643
  if renderer is not None:
630
644
  if ok:
631
- renderer.log_line(f"install ({name})", "installed", color=GREEN)
645
+ renderer.log_line(f"install {display}", "installed", color=GREEN)
632
646
  else:
633
- renderer.log_line(f"install ({name})", f"failed: {message}", color=GREEN)
647
+ renderer.log_line(f"install {display}", f"failed: {message}", color=GREEN)
634
648
  return None
635
649
  return str(message)
636
650
 
@@ -657,23 +671,24 @@ class UpgradeCommand(Command):
657
671
  tool_cls = tool_registry.get_tool(name)
658
672
  if tool_cls is None or tool_cls.metadata.install is None:
659
673
  return f'Tool "{name}" is not an external tool or was not found.'
674
+ display = tool_registry.format_display(tool_registry.get_tool_path(name))
660
675
  if not tool_manager.is_installed(name):
661
676
  renderer = context.session.get("renderer")
662
677
  if renderer is not None:
663
- renderer.log_line(f"upgrade ({name})", "not installed (skipped)", color=BLUE)
678
+ renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
664
679
  return None
665
680
  renderer = context.session.get("renderer")
666
681
 
667
682
  def on_progress(msg: str) -> None:
668
683
  if renderer is not None:
669
- renderer.log_line(f"upgrade ({name})", msg, color=BLUE)
684
+ renderer.log_line(f"upgrade {display}", msg, color=BLUE)
670
685
 
671
686
  ok, message = tool_manager.update(name, tool_cls.metadata.install, on_progress=on_progress)
672
687
  if renderer is not None:
673
688
  if ok:
674
- renderer.log_line(f"upgrade ({name})", "upgraded", color=BLUE)
689
+ renderer.log_line(f"upgrade {display}", "upgraded", color=BLUE)
675
690
  else:
676
- renderer.log_line(f"upgrade ({name})", f"failed: {message}", color=BLUE)
691
+ renderer.log_line(f"upgrade {display}", f"failed: {message}", color=BLUE)
677
692
  return None
678
693
  return str(message)
679
694
 
@@ -699,23 +714,24 @@ class UpgradeCommand(Command):
699
714
  if spec is None:
700
715
  continue
701
716
  name = meta.name
717
+ display = tool_registry.format_display(tool_registry.get_tool_path(name))
702
718
  if not tool_manager.is_installed(name):
703
719
  renderer = context.session.get("renderer")
704
720
  if renderer is not None:
705
- renderer.log_line(f"upgrade ({name})", "not installed (skipped)", color=BLUE)
721
+ renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
706
722
  continue
707
723
  renderer = context.session.get("renderer")
708
724
 
709
- def on_progress(msg: str, name: str = name, renderer: Any = renderer) -> None:
725
+ def on_progress(msg: str, display: str = display, renderer: Any = renderer) -> None:
710
726
  if renderer is not None:
711
- renderer.log_line(f"upgrade ({name})", msg, color=BLUE)
727
+ renderer.log_line(f"upgrade {display}", msg, color=BLUE)
712
728
 
713
729
  ok, message = tool_manager.update(name, spec, on_progress=on_progress)
714
730
  if renderer is not None:
715
731
  if ok:
716
- renderer.log_line(f"upgrade ({name})", "upgraded", color=BLUE)
732
+ renderer.log_line(f"upgrade {display}", "upgraded", color=BLUE)
717
733
  else:
718
- renderer.log_line(f"upgrade ({name})", f"failed: {message}", color=BLUE)
734
+ renderer.log_line(f"upgrade {display}", f"failed: {message}", color=BLUE)
719
735
  return None
720
736
 
721
737
 
@@ -737,13 +753,14 @@ class RemoveCommand(Command):
737
753
  tool_cls = tool_registry.get_tool(name)
738
754
  if tool_cls is None or tool_cls.metadata.install is None:
739
755
  return f'Tool "{name}" is not an external tool or was not found.'
756
+ display = tool_registry.format_display(tool_registry.get_tool_path(name))
740
757
  renderer = context.session.get("renderer")
741
758
  ok, message = tool_manager.uninstall(name)
742
759
  if renderer is not None:
743
760
  if ok:
744
- renderer.log_line(f"remove ({name})", "removed", color=RED)
761
+ renderer.log_line(f"remove {display}", "removed", color=RED)
745
762
  else:
746
- renderer.log_line(f"remove ({name})", f"failed: {message}", color=RED)
763
+ renderer.log_line(f"remove {display}", f"failed: {message}", color=RED)
747
764
  return None
748
765
  return str(message)
749
766
 
@@ -9,16 +9,21 @@ class PromptRenderer:
9
9
  return "shark > "
10
10
 
11
11
  def tool_prompt(self, tool_name: str) -> str:
12
- return f"shark ({tool_name}) > "
12
+ if "/" in tool_name:
13
+ parts = tool_name.split("/", 1)
14
+ formatted = f"{parts[0]}({parts[1]})"
15
+ else:
16
+ formatted = tool_name
17
+ return f"shark {formatted} > "
13
18
 
14
19
  def render(self, prompt: str) -> list[tuple[str, str]]:
15
20
  if "(" in prompt and ")" in prompt:
16
21
  before, rest = prompt.split("(", 1)
17
- tool_name, after = rest.split(")", 1)
22
+ tool_path, after = rest.split(")", 1)
18
23
  return [
19
24
  (PROMPT_STYLE, before),
20
25
  ("", "("),
21
- (MODULE_STYLE, tool_name),
26
+ (MODULE_STYLE, tool_path),
22
27
  ("", ")"),
23
28
  ("", after),
24
29
  ]
@@ -36,6 +36,11 @@ class _SharkitHistory(History):
36
36
  def store_string(self, string: str) -> None:
37
37
  self._manager.add_command(string)
38
38
 
39
+ def clear(self) -> None:
40
+ """Reset the in-memory buffer so next load() re-reads from file."""
41
+ self._loaded = False
42
+ self._loaded_strings = []
43
+
39
44
 
40
45
  class _ConsoleCompleter(Completer):
41
46
  def __init__(
@@ -100,9 +105,9 @@ class Console:
100
105
 
101
106
  def _get_prompt(self) -> Any:
102
107
  if self._current_tool is not None:
103
- prompt = f"shark ({self._current_tool}) > "
108
+ prompt = self._prompt_renderer.tool_prompt(self._current_tool)
104
109
  else:
105
- prompt = "shark > "
110
+ prompt = self._prompt_renderer.default_prompt()
106
111
  return self._prompt_renderer.render(prompt)
107
112
 
108
113
  def _init_session(self) -> None:
@@ -113,12 +118,14 @@ class Console:
113
118
  tool_registry=self._tool_registry,
114
119
  get_current_tool=lambda: self._current_tool,
115
120
  )
121
+ self._sharkit_history = _SharkitHistory(self._history_manager)
116
122
  self._prompt_session = PromptSession[str](
117
- history=_SharkitHistory(self._history_manager),
123
+ history=self._sharkit_history,
118
124
  completer=self._completer,
119
125
  style=_PROMPT_STYLE,
120
126
  complete_while_typing=True,
121
127
  )
128
+ self._session_data["sharkit_history"] = self._sharkit_history
122
129
 
123
130
  def run(self) -> None:
124
131
  self._init_session()
@@ -1,6 +1,7 @@
1
1
  import re
2
2
  import shutil
3
3
  import textwrap
4
+ import unicodedata
4
5
 
5
6
  from sharkit import __version__
6
7
  from sharkit.output.theme import (
@@ -32,7 +33,8 @@ x88: `)8b. 888E~?888L .@88 "8888" 4888>'88" 888E`"88*" ''888E` 8888
32
33
 
33
34
 
34
35
  def _visible_len(text: str) -> int:
35
- return len(ANSI_RE.sub("", text))
36
+ clean = ANSI_RE.sub("", text)
37
+ return sum(2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1 for ch in clean)
36
38
 
37
39
 
38
40
  def _pad(text: str, width: int) -> str:
@@ -55,7 +57,7 @@ def _term_width() -> int:
55
57
  class Renderer:
56
58
  def panel(
57
59
  self,
58
- title: str,
60
+ title: str | None,
59
61
  content: str,
60
62
  min_width: int = 0,
61
63
  centered: bool = False,
@@ -73,11 +75,12 @@ class Renderer:
73
75
  else:
74
76
  lines = content.splitlines()
75
77
  content_width = max((_visible_len(line) for line in lines), default=0)
76
- title_width = _visible_len(title)
78
+ title_width = _visible_len(title) if title else 0
77
79
  inner = max(content_width, title_width + 3, min_width)
78
- box_width = inner + 4
79
80
 
80
- if centered:
81
+ if title is None:
82
+ top = f"{PINK}╭{'─' * (inner + 2)}╮{RESET}"
83
+ elif centered:
81
84
  gap = inner - title_width
82
85
  left = gap // 2
83
86
  right = gap - left
@@ -88,6 +91,7 @@ class Renderer:
88
91
  else:
89
92
  dashes = max(0, inner - title_width - 1)
90
93
  top = f"{PINK}╭─ {BOLD}{WHITE}{title}{RESET}{PINK} {'─' * dashes}╮{RESET}"
94
+
91
95
  if centered:
92
96
  body = [f"{PINK}│{RESET} {_center(line, inner)} {PINK}│{RESET}" for line in lines]
93
97
  else:
@@ -96,7 +100,7 @@ class Renderer:
96
100
 
97
101
  all_lines = [top, *body, bottom]
98
102
  if centered:
99
- pad = max(0, (_term_width() - box_width) // 2)
103
+ pad = max(0, (_term_width() - (inner + 4)) // 2)
100
104
  all_lines = [" " * pad + ln for ln in all_lines]
101
105
  for ln in all_lines:
102
106
  print(ln)
@@ -257,3 +261,16 @@ class Renderer:
257
261
  else:
258
262
  prefix = f"{RESET}{' ' * (len(name) + 1)}{color}{BOLD}│{RESET} "
259
263
  print(prefix + line)
264
+
265
+ def support_creator(self, author: str, tool_name: str, git_url: str) -> None:
266
+ lines = [
267
+ f"Thanks to {author} for building {tool_name}!",
268
+ "If you find this tool useful, please consider:",
269
+ "",
270
+ f"⭐ Star the repo: {git_url}",
271
+ f"🐛 Report issues: {git_url}/issues",
272
+ f"💰 Sponsor the author: {git_url}/sponsors",
273
+ "",
274
+ f"{DIM}Without creators like them, sharkit wouldn't exist! :3{RESET}",
275
+ ]
276
+ self.panel(None, "\n".join(lines))
@@ -144,6 +144,7 @@ class BlackbirdTool(Tool):
144
144
  first = True
145
145
  current = ""
146
146
  started = False
147
+ pending_blank = False
147
148
  try:
148
149
  while True:
149
150
  try:
@@ -165,11 +166,20 @@ class BlackbirdTool(Tool):
165
166
  started = True
166
167
  for ch in chunk.decode("utf-8", errors="replace"):
167
168
  if ch == "\n":
168
- if renderer is not None:
169
- renderer.gutter(name, _clean_ansi(current), tool_color, first)
169
+ if current:
170
+ if pending_blank:
171
+ if renderer is not None:
172
+ renderer.gutter(name, "", tool_color, False)
173
+ else:
174
+ print()
175
+ pending_blank = False
176
+ if renderer is not None:
177
+ renderer.gutter(name, _clean_ansi(current), tool_color, first)
178
+ else:
179
+ print(_clean_ansi(current))
180
+ first = False
170
181
  else:
171
- print(_clean_ansi(current))
172
- first = False
182
+ pending_blank = True
173
183
  current = ""
174
184
  elif ch == "\r":
175
185
  current = ""
@@ -0,0 +1,251 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import fcntl
5
+ import os
6
+ import pty
7
+ import re
8
+ import select
9
+ import struct
10
+ import subprocess
11
+ import termios
12
+ from dataclasses import replace
13
+
14
+ from sharkit.output.theme import PINK, hex_to_ansi
15
+ from sharkit.tools.base import (
16
+ ExecutionContext,
17
+ OptionDefinition,
18
+ Result,
19
+ Tool,
20
+ ToolInstallSpec,
21
+ ToolMetadata,
22
+ parse_bool,
23
+ )
24
+
25
+ _CSI_RE = re.compile(r"\x1b\[[0-9;?]*[a-zA-Z]")
26
+ _OSC_RE = re.compile(r"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)", re.DOTALL)
27
+
28
+
29
+ def _clean_ansi(text: str) -> str:
30
+ text = _OSC_RE.sub("", text)
31
+ return _CSI_RE.sub(lambda m: m.group(0) if m.group(0).endswith("m") else "", text)
32
+
33
+
34
+ class MaigretTool(Tool):
35
+ metadata = ToolMetadata(
36
+ name="maigret",
37
+ description="Maigret — username OSINT across 3000+ sites with recursive checks",
38
+ category="recon",
39
+ author="soxoj",
40
+ version="0.1.0",
41
+ safety="safe",
42
+ color="#FF6B35",
43
+ install=ToolInstallSpec(
44
+ git_url="https://github.com/soxoj/maigret.git",
45
+ pip_args=["maigret"],
46
+ ),
47
+ )
48
+
49
+ def __init__(self) -> None:
50
+ self._options = {
51
+ "username": OptionDefinition(
52
+ name="username",
53
+ description="Target username to hunt",
54
+ required=True,
55
+ ),
56
+ "timeout": OptionDefinition(
57
+ name="timeout",
58
+ description="Timeout per site in seconds",
59
+ required=False,
60
+ default=None,
61
+ ),
62
+ "output": OptionDefinition(
63
+ name="output",
64
+ description="Write results to file",
65
+ required=False,
66
+ default=None,
67
+ ),
68
+ "json": OptionDefinition(
69
+ name="json",
70
+ description="Export results as JSON",
71
+ required=False,
72
+ default=None,
73
+ type="bool",
74
+ ),
75
+ "csv": OptionDefinition(
76
+ name="csv",
77
+ description="Export results as CSV",
78
+ required=False,
79
+ default=None,
80
+ type="bool",
81
+ ),
82
+ "site": OptionDefinition(
83
+ name="site",
84
+ description="Limit to specific site(s) (comma-separated)",
85
+ required=False,
86
+ default=None,
87
+ ),
88
+ "top_sites": OptionDefinition(
89
+ name="top_sites",
90
+ description="Only check top N most popular sites",
91
+ required=False,
92
+ default=None,
93
+ ),
94
+ }
95
+
96
+ def get_metadata(self) -> ToolMetadata:
97
+ return self.metadata
98
+
99
+ def get_options(self) -> dict[str, OptionDefinition]:
100
+ return self._options
101
+
102
+ def set_option(self, key: str, value: str) -> None:
103
+ if key not in self._options:
104
+ raise ValueError(f'Option "{key}" not found.')
105
+ self._options[key] = replace(self._options[key], value=value)
106
+
107
+ def execute(self, context: ExecutionContext) -> Result:
108
+ from sharkit.tools.manager import ToolManager
109
+
110
+ name = self.metadata.name
111
+ tool_color = hex_to_ansi(self.metadata.color) if self.metadata.color else PINK
112
+ manager = ToolManager()
113
+ if not manager.is_installed(name):
114
+ return Result(
115
+ success=False,
116
+ error=f'Tool "{name}" is not installed. Run: install {name}',
117
+ )
118
+ spec = self.metadata.install
119
+ if spec is None:
120
+ return Result(success=False, error="Tool has no install spec.")
121
+
122
+ install_dir = manager.install_path(name)
123
+ maigret_bin = install_dir / "venv" / "bin" / "maigret"
124
+ if not maigret_bin.exists():
125
+ return Result(
126
+ success=False,
127
+ error=f"Maigret binary not found: {maigret_bin}",
128
+ )
129
+
130
+ username = context.options.get("username") or ""
131
+ if not username:
132
+ return Result(success=False, error="Option 'username' is required.")
133
+
134
+ args = [username, "--no-progressbar"]
135
+ timeout = context.options.get("timeout")
136
+ if timeout:
137
+ args.extend(["--timeout", str(timeout)])
138
+ output = context.options.get("output")
139
+ if output:
140
+ args.extend(["--output", str(output)])
141
+ if parse_bool(context.options.get("json")):
142
+ args.append("--json")
143
+ if parse_bool(context.options.get("csv")):
144
+ args.append("--csv")
145
+ site = context.options.get("site")
146
+ if site:
147
+ for s in str(site).split(","):
148
+ args.extend(["--site", s.strip()])
149
+ top_sites = context.options.get("top_sites")
150
+ if top_sites:
151
+ args.extend(["--top-sites", str(top_sites)])
152
+
153
+ renderer = context.renderer
154
+ master, slave = pty.openpty()
155
+ try:
156
+ winsize = struct.pack("HHHH", 1000, 10000, 0, 0)
157
+ fcntl.ioctl(master, termios.TIOCSWINSZ, winsize)
158
+ attrs = termios.tcgetattr(master)
159
+ attrs[1] &= ~termios.OPOST
160
+ termios.tcsetattr(master, termios.TCSANOW, attrs)
161
+ except OSError:
162
+ pass
163
+
164
+ env = os.environ.copy()
165
+ env["TERM"] = "xterm-256color"
166
+ env["FORCE_COLOR"] = "1"
167
+ env["CLICOLOR"] = "1"
168
+ env["CLICOLOR_FORCE"] = "1"
169
+ env["COLUMNS"] = "10000"
170
+
171
+ try:
172
+ proc = subprocess.Popen(
173
+ [str(maigret_bin), *args],
174
+ stdout=slave,
175
+ stderr=slave,
176
+ stdin=subprocess.DEVNULL,
177
+ cwd=str(install_dir),
178
+ env=env,
179
+ start_new_session=True,
180
+ )
181
+ except Exception as e:
182
+ return Result(success=False, error=f"Failed to run {name}: {e}")
183
+ finally:
184
+ with contextlib.suppress(OSError):
185
+ os.close(slave)
186
+
187
+ first = True
188
+ current = ""
189
+ started = False
190
+ pending_blank = False
191
+ try:
192
+ while True:
193
+ try:
194
+ r, _, _ = select.select([master], [], [], 0.5)
195
+ except (OSError, ValueError):
196
+ break
197
+ if not r:
198
+ if proc.poll() is not None:
199
+ break
200
+ continue
201
+ try:
202
+ chunk = os.read(master, 65536)
203
+ except OSError:
204
+ break
205
+ if not chunk:
206
+ break
207
+ if not started:
208
+ chunk = chunk.lstrip(b"\n")
209
+ started = True
210
+ for ch in chunk.decode("utf-8", errors="replace"):
211
+ if ch == "\n":
212
+ if current:
213
+ if pending_blank:
214
+ if renderer is not None:
215
+ renderer.gutter(name, "", tool_color, False)
216
+ else:
217
+ print()
218
+ pending_blank = False
219
+ if renderer is not None:
220
+ renderer.gutter(name, _clean_ansi(current), tool_color, first)
221
+ else:
222
+ print(_clean_ansi(current))
223
+ first = False
224
+ else:
225
+ pending_blank = True
226
+ current = ""
227
+ elif ch == "\r":
228
+ current = ""
229
+ else:
230
+ current += ch
231
+ except KeyboardInterrupt:
232
+ with contextlib.suppress(OSError):
233
+ proc.kill()
234
+ current = ""
235
+ if renderer is not None:
236
+ renderer.gutter(name, "Run aborted (Ctrl+C).", tool_color, first)
237
+ else:
238
+ print("Run aborted (Ctrl+C).")
239
+ return Result(success=True, data={})
240
+ finally:
241
+ if current:
242
+ if renderer is not None:
243
+ renderer.gutter(name, _clean_ansi(current), tool_color, first)
244
+ else:
245
+ print(_clean_ansi(current))
246
+ with contextlib.suppress(OSError):
247
+ os.close(master)
248
+ with contextlib.suppress(OSError):
249
+ proc.wait()
250
+
251
+ return Result(success=True, data={})
@@ -0,0 +1,251 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import fcntl
5
+ import os
6
+ import pty
7
+ import re
8
+ import select
9
+ import struct
10
+ import subprocess
11
+ import termios
12
+ from dataclasses import replace
13
+
14
+ from sharkit.output.theme import PINK, hex_to_ansi
15
+ from sharkit.tools.base import (
16
+ ExecutionContext,
17
+ OptionDefinition,
18
+ Result,
19
+ Tool,
20
+ ToolInstallSpec,
21
+ ToolMetadata,
22
+ parse_bool,
23
+ )
24
+
25
+ _CSI_RE = re.compile(r"\x1b\[[0-9;?]*[a-zA-Z]")
26
+ _OSC_RE = re.compile(r"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)", re.DOTALL)
27
+
28
+
29
+ def _clean_ansi(text: str) -> str:
30
+ text = _OSC_RE.sub("", text)
31
+ return _CSI_RE.sub(lambda m: m.group(0) if m.group(0).endswith("m") else "", text)
32
+
33
+
34
+ class SherlockTool(Tool):
35
+ metadata = ToolMetadata(
36
+ name="sherlock",
37
+ description="Sherlock — find usernames across 400+ social networks",
38
+ category="recon",
39
+ author="sherlock-project",
40
+ version="0.1.0",
41
+ safety="safe",
42
+ color="#4A7AFF",
43
+ install=ToolInstallSpec(
44
+ git_url="https://github.com/sherlock-project/sherlock.git",
45
+ pip_args=["sherlock-project"],
46
+ ),
47
+ )
48
+
49
+ def __init__(self) -> None:
50
+ self._options = {
51
+ "username": OptionDefinition(
52
+ name="username",
53
+ description="Target username to hunt",
54
+ required=True,
55
+ ),
56
+ "timeout": OptionDefinition(
57
+ name="timeout",
58
+ description="Timeout per site in seconds",
59
+ required=False,
60
+ default=None,
61
+ ),
62
+ "output": OptionDefinition(
63
+ name="output",
64
+ description="Write results to file",
65
+ required=False,
66
+ default=None,
67
+ ),
68
+ "json": OptionDefinition(
69
+ name="json",
70
+ description="Export results as JSON",
71
+ required=False,
72
+ default=None,
73
+ type="bool",
74
+ ),
75
+ "csv": OptionDefinition(
76
+ name="csv",
77
+ description="Export results as CSV",
78
+ required=False,
79
+ default=None,
80
+ type="bool",
81
+ ),
82
+ "site": OptionDefinition(
83
+ name="site",
84
+ description="Limit to specific site(s) (comma-separated)",
85
+ required=False,
86
+ default=None,
87
+ ),
88
+ "print_found": OptionDefinition(
89
+ name="print_found",
90
+ description="Only print sites where the username was found",
91
+ required=False,
92
+ default=None,
93
+ type="bool",
94
+ ),
95
+ }
96
+
97
+ def get_metadata(self) -> ToolMetadata:
98
+ return self.metadata
99
+
100
+ def get_options(self) -> dict[str, OptionDefinition]:
101
+ return self._options
102
+
103
+ def set_option(self, key: str, value: str) -> None:
104
+ if key not in self._options:
105
+ raise ValueError(f'Option "{key}" not found.')
106
+ self._options[key] = replace(self._options[key], value=value)
107
+
108
+ def execute(self, context: ExecutionContext) -> Result:
109
+ from sharkit.tools.manager import ToolManager
110
+
111
+ name = self.metadata.name
112
+ tool_color = hex_to_ansi(self.metadata.color) if self.metadata.color else PINK
113
+ manager = ToolManager()
114
+ if not manager.is_installed(name):
115
+ return Result(
116
+ success=False,
117
+ error=f'Tool "{name}" is not installed. Run: install {name}',
118
+ )
119
+ spec = self.metadata.install
120
+ if spec is None:
121
+ return Result(success=False, error="Tool has no install spec.")
122
+
123
+ install_dir = manager.install_path(name)
124
+ sherlock_bin = install_dir / "venv" / "bin" / "sherlock"
125
+ if not sherlock_bin.exists():
126
+ return Result(
127
+ success=False,
128
+ error=f"Sherlock binary not found: {sherlock_bin}",
129
+ )
130
+
131
+ username = context.options.get("username") or ""
132
+ if not username:
133
+ return Result(success=False, error="Option 'username' is required.")
134
+
135
+ args = [username]
136
+ timeout = context.options.get("timeout")
137
+ if timeout:
138
+ args.extend(["--timeout", str(timeout)])
139
+ output = context.options.get("output")
140
+ if output:
141
+ args.extend(["--output", str(output)])
142
+ if parse_bool(context.options.get("json")):
143
+ args.append("--json")
144
+ if parse_bool(context.options.get("csv")):
145
+ args.append("--csv")
146
+ site = context.options.get("site")
147
+ if site:
148
+ for s in str(site).split(","):
149
+ args.extend(["--site", s.strip()])
150
+ if parse_bool(context.options.get("print_found")):
151
+ args.append("--print-found")
152
+
153
+ renderer = context.renderer
154
+ master, slave = pty.openpty()
155
+ try:
156
+ winsize = struct.pack("HHHH", 1000, 10000, 0, 0)
157
+ fcntl.ioctl(master, termios.TIOCSWINSZ, winsize)
158
+ attrs = termios.tcgetattr(master)
159
+ attrs[1] &= ~termios.OPOST
160
+ termios.tcsetattr(master, termios.TCSANOW, attrs)
161
+ except OSError:
162
+ pass
163
+
164
+ env = os.environ.copy()
165
+ env["TERM"] = "xterm-256color"
166
+ env["FORCE_COLOR"] = "1"
167
+ env["CLICOLOR"] = "1"
168
+ env["CLICOLOR_FORCE"] = "1"
169
+ env["COLUMNS"] = "10000"
170
+
171
+ try:
172
+ proc = subprocess.Popen(
173
+ [str(sherlock_bin), *args],
174
+ stdout=slave,
175
+ stderr=slave,
176
+ stdin=subprocess.DEVNULL,
177
+ cwd=str(install_dir),
178
+ env=env,
179
+ start_new_session=True,
180
+ )
181
+ except Exception as e:
182
+ return Result(success=False, error=f"Failed to run {name}: {e}")
183
+ finally:
184
+ with contextlib.suppress(OSError):
185
+ os.close(slave)
186
+
187
+ first = True
188
+ current = ""
189
+ started = False
190
+ pending_blank = False
191
+ try:
192
+ while True:
193
+ try:
194
+ r, _, _ = select.select([master], [], [], 0.5)
195
+ except (OSError, ValueError):
196
+ break
197
+ if not r:
198
+ if proc.poll() is not None:
199
+ break
200
+ continue
201
+ try:
202
+ chunk = os.read(master, 65536)
203
+ except OSError:
204
+ break
205
+ if not chunk:
206
+ break
207
+ if not started:
208
+ chunk = chunk.lstrip(b"\n")
209
+ started = True
210
+ for ch in chunk.decode("utf-8", errors="replace"):
211
+ if ch == "\n":
212
+ if current:
213
+ if pending_blank:
214
+ if renderer is not None:
215
+ renderer.gutter(name, "", tool_color, False)
216
+ else:
217
+ print()
218
+ pending_blank = False
219
+ if renderer is not None:
220
+ renderer.gutter(name, _clean_ansi(current), tool_color, first)
221
+ else:
222
+ print(_clean_ansi(current))
223
+ first = False
224
+ else:
225
+ pending_blank = True
226
+ current = ""
227
+ elif ch == "\r":
228
+ current = ""
229
+ else:
230
+ current += ch
231
+ except KeyboardInterrupt:
232
+ with contextlib.suppress(OSError):
233
+ proc.kill()
234
+ current = ""
235
+ if renderer is not None:
236
+ renderer.gutter(name, "Run aborted (Ctrl+C).", tool_color, first)
237
+ else:
238
+ print("Run aborted (Ctrl+C).")
239
+ return Result(success=True, data={})
240
+ finally:
241
+ if current:
242
+ if renderer is not None:
243
+ renderer.gutter(name, _clean_ansi(current), tool_color, first)
244
+ else:
245
+ print(_clean_ansi(current))
246
+ with contextlib.suppress(OSError):
247
+ os.close(master)
248
+ with contextlib.suppress(OSError):
249
+ proc.wait()
250
+
251
+ return Result(success=True, data={})
@@ -71,3 +71,17 @@ class ToolRegistry:
71
71
  for path, name in self._paths.items():
72
72
  result[path] = self._tools[name]
73
73
  return result
74
+
75
+ def get_tool_path(self, name: str) -> str:
76
+ """Return the full path for a tool name, or just the name if no path."""
77
+ for path, tool_name in self._paths.items():
78
+ if tool_name == name:
79
+ return path
80
+ return name
81
+
82
+ def format_display(self, path: str) -> str:
83
+ """Format tool path as 'category(subpath)' for display."""
84
+ if "/" in path:
85
+ parts = path.split("/", 1)
86
+ return f"{parts[0]}({parts[1]})"
87
+ return path
File without changes
File without changes