sharkit 0.2.0.dev1__tar.gz → 0.2.1.dev1__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.1.dev1}/PKG-INFO +2 -2
  2. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/README.md +1 -1
  3. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/pyproject.toml +1 -1
  4. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/app.py +2 -2
  5. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/commands/builtins.py +40 -17
  6. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/console/prompt.py +8 -3
  7. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/console/repl.py +10 -3
  8. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/output/renderer.py +23 -6
  9. {sharkit-0.2.0.dev1/src/sharkit/tools → sharkit-0.2.1.dev1/src/sharkit/tools/osint/recon/hybrid}/blackbird.py +16 -4
  10. sharkit-0.2.1.dev1/src/sharkit/tools/osint/recon/username/maigret.py +252 -0
  11. sharkit-0.2.1.dev1/src/sharkit/tools/osint/recon/username/sherlock.py +252 -0
  12. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/tools/registry.py +14 -0
  13. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/.gitignore +0 -0
  14. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/LICENSE +0 -0
  15. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/__init__.py +0 -0
  16. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/__main__.py +0 -0
  17. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/commands/__init__.py +0 -0
  18. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/commands/base.py +0 -0
  19. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/commands/registry.py +0 -0
  20. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/config/__init__.py +0 -0
  21. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/config/manager.py +0 -0
  22. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/config/paths.py +0 -0
  23. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/console/__init__.py +0 -0
  24. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/console/completion.py +0 -0
  25. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/exceptions.py +0 -0
  26. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/history/__init__.py +0 -0
  27. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/history/manager.py +0 -0
  28. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/network/__init__.py +0 -0
  29. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/network/client.py +0 -0
  30. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/output/__init__.py +0 -0
  31. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/output/theme.py +0 -0
  32. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/tools/__init__.py +0 -0
  33. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/tools/base.py +0 -0
  34. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/tools/loader.py +0 -0
  35. {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/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.1.dev1
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.1.dev1"
4
4
  description = "OSINT / HUMINT / GEOINT research framework"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -20,7 +20,7 @@ NON_LINUX_MESSAGE = (
20
20
  "it was built for open water,\n"
21
21
  "where it can actually swim.\n"
22
22
  "\n"
23
- "windows/mac is more of a glass aquarium.\n"
23
+ "windows is more of a glass aquarium.\n"
24
24
  "tidy, bright, and a little too enclosed.\n"
25
25
  "\n"
26
26
  "the shark doesn't dislike the aquarium.\n"
@@ -37,7 +37,7 @@ def main() -> int:
37
37
  sys.stdout.flush()
38
38
  renderer = Renderer()
39
39
 
40
- if sys.platform != "linux":
40
+ if sys.platform == "win32":
41
41
  renderer.banner(tool_count=0, message=NON_LINUX_MESSAGE)
42
42
  return 1
43
43
 
@@ -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:
@@ -372,7 +382,13 @@ class SetCommand(Command):
372
382
  tool_instance.set_option(key, value)
373
383
  renderer = context.session.get("renderer")
374
384
  if renderer is not None:
375
- renderer.log_line(f"set ({context.current_tool})", f"{key} = {value}")
385
+ tool_registry = context.session.get("tool_registry")
386
+ display = context.current_tool
387
+ if tool_registry is not None and context.current_tool is not None:
388
+ display = tool_registry.format_display(
389
+ tool_registry.get_tool_path(context.current_tool),
390
+ )
391
+ renderer.log_line(f"set {display}", f"{key} = {value}")
376
392
  return None
377
393
  return f"Set {key} = {value}"
378
394
  except Exception as e:
@@ -552,6 +568,9 @@ class HistoryCommand(Command):
552
568
  if clear:
553
569
  manager.clear()
554
570
  context.session["history"] = []
571
+ sharkit_history = context.session.get("sharkit_history")
572
+ if sharkit_history is not None:
573
+ sharkit_history.clear()
555
574
  return "[+] Command history and history file cleared"
556
575
 
557
576
  all_entries = manager.get_history(10_000_000)
@@ -614,23 +633,24 @@ class InstallCommand(Command):
614
633
  tool_cls = tool_registry.get_tool(name)
615
634
  if tool_cls is None or tool_cls.metadata.install is None:
616
635
  return f'Tool "{name}" is not an external tool or was not found.'
636
+ display = tool_registry.format_display(tool_registry.get_tool_path(name))
617
637
  if tool_manager.is_installed(name):
618
638
  renderer = context.session.get("renderer")
619
639
  if renderer is not None:
620
- renderer.log_line(f"install ({name})", "already installed", color=GREEN)
640
+ renderer.log_line(f"install {display}", "already installed", color=GREEN)
621
641
  return None
622
642
  renderer = context.session.get("renderer")
623
643
 
624
644
  def on_progress(msg: str) -> None:
625
645
  if renderer is not None:
626
- renderer.log_line(f"install ({name})", msg, color=GREEN)
646
+ renderer.log_line(f"install {display}", msg, color=GREEN)
627
647
 
628
648
  ok, message = tool_manager.install(name, tool_cls.metadata.install, on_progress=on_progress)
629
649
  if renderer is not None:
630
650
  if ok:
631
- renderer.log_line(f"install ({name})", "installed", color=GREEN)
651
+ renderer.log_line(f"install {display}", "installed", color=GREEN)
632
652
  else:
633
- renderer.log_line(f"install ({name})", f"failed: {message}", color=GREEN)
653
+ renderer.log_line(f"install {display}", f"failed: {message}", color=GREEN)
634
654
  return None
635
655
  return str(message)
636
656
 
@@ -657,23 +677,24 @@ class UpgradeCommand(Command):
657
677
  tool_cls = tool_registry.get_tool(name)
658
678
  if tool_cls is None or tool_cls.metadata.install is None:
659
679
  return f'Tool "{name}" is not an external tool or was not found.'
680
+ display = tool_registry.format_display(tool_registry.get_tool_path(name))
660
681
  if not tool_manager.is_installed(name):
661
682
  renderer = context.session.get("renderer")
662
683
  if renderer is not None:
663
- renderer.log_line(f"upgrade ({name})", "not installed (skipped)", color=BLUE)
684
+ renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
664
685
  return None
665
686
  renderer = context.session.get("renderer")
666
687
 
667
688
  def on_progress(msg: str) -> None:
668
689
  if renderer is not None:
669
- renderer.log_line(f"upgrade ({name})", msg, color=BLUE)
690
+ renderer.log_line(f"upgrade {display}", msg, color=BLUE)
670
691
 
671
692
  ok, message = tool_manager.update(name, tool_cls.metadata.install, on_progress=on_progress)
672
693
  if renderer is not None:
673
694
  if ok:
674
- renderer.log_line(f"upgrade ({name})", "upgraded", color=BLUE)
695
+ renderer.log_line(f"upgrade {display}", "upgraded", color=BLUE)
675
696
  else:
676
- renderer.log_line(f"upgrade ({name})", f"failed: {message}", color=BLUE)
697
+ renderer.log_line(f"upgrade {display}", f"failed: {message}", color=BLUE)
677
698
  return None
678
699
  return str(message)
679
700
 
@@ -699,23 +720,24 @@ class UpgradeCommand(Command):
699
720
  if spec is None:
700
721
  continue
701
722
  name = meta.name
723
+ display = tool_registry.format_display(tool_registry.get_tool_path(name))
702
724
  if not tool_manager.is_installed(name):
703
725
  renderer = context.session.get("renderer")
704
726
  if renderer is not None:
705
- renderer.log_line(f"upgrade ({name})", "not installed (skipped)", color=BLUE)
727
+ renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
706
728
  continue
707
729
  renderer = context.session.get("renderer")
708
730
 
709
- def on_progress(msg: str, name: str = name, renderer: Any = renderer) -> None:
731
+ def on_progress(msg: str, display: str = display, renderer: Any = renderer) -> None:
710
732
  if renderer is not None:
711
- renderer.log_line(f"upgrade ({name})", msg, color=BLUE)
733
+ renderer.log_line(f"upgrade {display}", msg, color=BLUE)
712
734
 
713
735
  ok, message = tool_manager.update(name, spec, on_progress=on_progress)
714
736
  if renderer is not None:
715
737
  if ok:
716
- renderer.log_line(f"upgrade ({name})", "upgraded", color=BLUE)
738
+ renderer.log_line(f"upgrade {display}", "upgraded", color=BLUE)
717
739
  else:
718
- renderer.log_line(f"upgrade ({name})", f"failed: {message}", color=BLUE)
740
+ renderer.log_line(f"upgrade {display}", f"failed: {message}", color=BLUE)
719
741
  return None
720
742
 
721
743
 
@@ -737,13 +759,14 @@ class RemoveCommand(Command):
737
759
  tool_cls = tool_registry.get_tool(name)
738
760
  if tool_cls is None or tool_cls.metadata.install is None:
739
761
  return f'Tool "{name}" is not an external tool or was not found.'
762
+ display = tool_registry.format_display(tool_registry.get_tool_path(name))
740
763
  renderer = context.session.get("renderer")
741
764
  ok, message = tool_manager.uninstall(name)
742
765
  if renderer is not None:
743
766
  if ok:
744
- renderer.log_line(f"remove ({name})", "removed", color=RED)
767
+ renderer.log_line(f"remove {display}", "removed", color=RED)
745
768
  else:
746
- renderer.log_line(f"remove ({name})", f"failed: {message}", color=RED)
769
+ renderer.log_line(f"remove {display}", f"failed: {message}", color=RED)
747
770
  return None
748
771
  return str(message)
749
772
 
@@ -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 = ""
@@ -181,6 +191,8 @@ class BlackbirdTool(Tool):
181
191
  proc.kill()
182
192
  current = ""
183
193
  if renderer is not None:
194
+ # \r\033[K clears the ^C echo from the terminal before printing.
195
+ print("\r\033[K", end="")
184
196
  renderer.gutter(name, "Run aborted (Ctrl+C).", tool_color, first)
185
197
  else:
186
198
  print("Run aborted (Ctrl+C).")
@@ -0,0 +1,252 @@
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
+ print("\r\033[K", end="")
237
+ renderer.gutter(name, "Run aborted (Ctrl+C).", tool_color, first)
238
+ else:
239
+ print("Run aborted (Ctrl+C).")
240
+ return Result(success=True, data={})
241
+ finally:
242
+ if current:
243
+ if renderer is not None:
244
+ renderer.gutter(name, _clean_ansi(current), tool_color, first)
245
+ else:
246
+ print(_clean_ansi(current))
247
+ with contextlib.suppress(OSError):
248
+ os.close(master)
249
+ with contextlib.suppress(OSError):
250
+ proc.wait()
251
+
252
+ return Result(success=True, data={})
@@ -0,0 +1,252 @@
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
+ print("\r\033[K", end="")
237
+ renderer.gutter(name, "Run aborted (Ctrl+C).", tool_color, first)
238
+ else:
239
+ print("Run aborted (Ctrl+C).")
240
+ return Result(success=True, data={})
241
+ finally:
242
+ if current:
243
+ if renderer is not None:
244
+ renderer.gutter(name, _clean_ansi(current), tool_color, first)
245
+ else:
246
+ print(_clean_ansi(current))
247
+ with contextlib.suppress(OSError):
248
+ os.close(master)
249
+ with contextlib.suppress(OSError):
250
+ proc.wait()
251
+
252
+ 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