sharkit 0.2.0.dev1__tar.gz → 0.2.1__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.0.dev1 → sharkit-0.2.1}/PKG-INFO +2 -2
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/README.md +1 -1
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/pyproject.toml +1 -1
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/app.py +2 -2
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/commands/builtins.py +114 -24
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/console/prompt.py +8 -3
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/console/repl.py +10 -3
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/output/renderer.py +27 -7
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/tools/manager.py +15 -1
- {sharkit-0.2.0.dev1/src/sharkit/tools → sharkit-0.2.1/src/sharkit/tools/osint/recon/hybrid}/blackbird.py +17 -5
- sharkit-0.2.1/src/sharkit/tools/osint/recon/username/maigret.py +252 -0
- sharkit-0.2.1/src/sharkit/tools/osint/recon/username/sherlock.py +252 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/tools/registry.py +14 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/.gitignore +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/LICENSE +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/__main__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/commands/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/commands/base.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/commands/registry.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/config/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/config/manager.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/config/paths.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/console/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/console/completion.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/exceptions.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/history/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/history/manager.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/network/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/network/client.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/output/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/output/theme.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/tools/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/tools/base.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/tools/loader.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sharkit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|

|
|
@@ -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
|

|
|
@@ -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
|
|
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
|
|
40
|
+
if sys.platform == "win32":
|
|
41
41
|
renderer.banner(tool_count=0, message=NON_LINUX_MESSAGE)
|
|
42
42
|
return 1
|
|
43
43
|
|
|
@@ -187,15 +187,22 @@ 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}')
|
|
193
194
|
return None
|
|
194
195
|
renderer = context.session.get("renderer")
|
|
196
|
+
first_progress = [True]
|
|
195
197
|
|
|
196
198
|
def on_progress(msg: str) -> None:
|
|
197
199
|
if renderer is not None:
|
|
198
|
-
|
|
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)
|
|
199
206
|
|
|
200
207
|
ok, message = tool_manager.install(name, spec, on_progress=on_progress)
|
|
201
208
|
if not ok:
|
|
@@ -203,6 +210,15 @@ class UseCommand(Command):
|
|
|
203
210
|
return None
|
|
204
211
|
context.current_tool = path
|
|
205
212
|
self._log(context, f'Tool "{path}" selected.')
|
|
213
|
+
if tool_cls is not None and tool_cls.metadata.install is not None:
|
|
214
|
+
renderer = context.session.get("renderer")
|
|
215
|
+
if renderer is not None:
|
|
216
|
+
install_spec = tool_cls.metadata.install
|
|
217
|
+
renderer.support_creator(
|
|
218
|
+
tool_cls.metadata.author,
|
|
219
|
+
tool_cls.metadata.name,
|
|
220
|
+
install_spec.git_url,
|
|
221
|
+
)
|
|
206
222
|
return None
|
|
207
223
|
|
|
208
224
|
def _prompt_install(self, name: str) -> bool:
|
|
@@ -372,7 +388,13 @@ class SetCommand(Command):
|
|
|
372
388
|
tool_instance.set_option(key, value)
|
|
373
389
|
renderer = context.session.get("renderer")
|
|
374
390
|
if renderer is not None:
|
|
375
|
-
|
|
391
|
+
tool_registry = context.session.get("tool_registry")
|
|
392
|
+
display = context.current_tool
|
|
393
|
+
if tool_registry is not None and context.current_tool is not None:
|
|
394
|
+
display = tool_registry.format_display(
|
|
395
|
+
tool_registry.get_tool_path(context.current_tool),
|
|
396
|
+
)
|
|
397
|
+
renderer.log_line(f"set {display}", f"{key} = {value}")
|
|
376
398
|
return None
|
|
377
399
|
return f"Set {key} = {value}"
|
|
378
400
|
except Exception as e:
|
|
@@ -552,6 +574,9 @@ class HistoryCommand(Command):
|
|
|
552
574
|
if clear:
|
|
553
575
|
manager.clear()
|
|
554
576
|
context.session["history"] = []
|
|
577
|
+
sharkit_history = context.session.get("sharkit_history")
|
|
578
|
+
if sharkit_history is not None:
|
|
579
|
+
sharkit_history.clear()
|
|
555
580
|
return "[+] Command history and history file cleared"
|
|
556
581
|
|
|
557
582
|
all_entries = manager.get_history(10_000_000)
|
|
@@ -614,23 +639,44 @@ class InstallCommand(Command):
|
|
|
614
639
|
tool_cls = tool_registry.get_tool(name)
|
|
615
640
|
if tool_cls is None or tool_cls.metadata.install is None:
|
|
616
641
|
return f'Tool "{name}" is not an external tool or was not found.'
|
|
617
|
-
|
|
642
|
+
tool_name = tool_cls.metadata.name
|
|
643
|
+
display = tool_registry.format_display(tool_registry.get_tool_path(tool_name))
|
|
644
|
+
if tool_manager.is_installed(tool_name):
|
|
618
645
|
renderer = context.session.get("renderer")
|
|
619
646
|
if renderer is not None:
|
|
620
|
-
renderer.log_line(f"install
|
|
647
|
+
renderer.log_line(f"install {display}", "already installed", color=GREEN)
|
|
621
648
|
return None
|
|
622
649
|
renderer = context.session.get("renderer")
|
|
650
|
+
first_progress = [True]
|
|
623
651
|
|
|
624
652
|
def on_progress(msg: str) -> None:
|
|
625
653
|
if renderer is not None:
|
|
626
|
-
|
|
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)
|
|
627
660
|
|
|
628
|
-
|
|
661
|
+
install_spec = tool_cls.metadata.install
|
|
662
|
+
ok, message = tool_manager.install(
|
|
663
|
+
tool_name, install_spec, on_progress=on_progress
|
|
664
|
+
)
|
|
629
665
|
if renderer is not None:
|
|
666
|
+
tag = f"install {display}"
|
|
630
667
|
if ok:
|
|
631
|
-
|
|
668
|
+
msg = "installed"
|
|
669
|
+
elif message == "Aborted":
|
|
670
|
+
msg = "aborted"
|
|
671
|
+
print("\r\033[K", end="")
|
|
672
|
+
else:
|
|
673
|
+
msg = f"failed: {message}"
|
|
674
|
+
if first_progress[0]:
|
|
675
|
+
renderer.log_line(tag, msg, color=GREEN)
|
|
632
676
|
else:
|
|
633
|
-
|
|
677
|
+
lines = msg.splitlines() or [""]
|
|
678
|
+
for line in lines:
|
|
679
|
+
renderer.gutter(tag, line, GREEN, is_first=False)
|
|
634
680
|
return None
|
|
635
681
|
return str(message)
|
|
636
682
|
|
|
@@ -657,23 +703,44 @@ class UpgradeCommand(Command):
|
|
|
657
703
|
tool_cls = tool_registry.get_tool(name)
|
|
658
704
|
if tool_cls is None or tool_cls.metadata.install is None:
|
|
659
705
|
return f'Tool "{name}" is not an external tool or was not found.'
|
|
660
|
-
|
|
706
|
+
tool_name = tool_cls.metadata.name
|
|
707
|
+
display = tool_registry.format_display(tool_registry.get_tool_path(tool_name))
|
|
708
|
+
if not tool_manager.is_installed(tool_name):
|
|
661
709
|
renderer = context.session.get("renderer")
|
|
662
710
|
if renderer is not None:
|
|
663
|
-
renderer.log_line(f"upgrade
|
|
711
|
+
renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
|
|
664
712
|
return None
|
|
665
713
|
renderer = context.session.get("renderer")
|
|
714
|
+
first_progress = [True]
|
|
666
715
|
|
|
667
716
|
def on_progress(msg: str) -> None:
|
|
668
717
|
if renderer is not None:
|
|
669
|
-
|
|
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)
|
|
670
724
|
|
|
671
|
-
|
|
725
|
+
upgrade_spec = tool_cls.metadata.install
|
|
726
|
+
ok, message = tool_manager.update(
|
|
727
|
+
tool_name, upgrade_spec, on_progress=on_progress
|
|
728
|
+
)
|
|
672
729
|
if renderer is not None:
|
|
730
|
+
tag = f"upgrade {display}"
|
|
673
731
|
if ok:
|
|
674
|
-
|
|
732
|
+
msg = "upgraded"
|
|
733
|
+
elif message == "Aborted":
|
|
734
|
+
msg = "aborted"
|
|
735
|
+
print("\r\033[K", end="")
|
|
736
|
+
else:
|
|
737
|
+
msg = f"failed: {message}"
|
|
738
|
+
if first_progress[0]:
|
|
739
|
+
renderer.log_line(tag, msg, color=BLUE)
|
|
675
740
|
else:
|
|
676
|
-
|
|
741
|
+
lines = msg.splitlines() or [""]
|
|
742
|
+
for line in lines:
|
|
743
|
+
renderer.gutter(tag, line, BLUE, is_first=False)
|
|
677
744
|
return None
|
|
678
745
|
return str(message)
|
|
679
746
|
|
|
@@ -699,23 +766,44 @@ class UpgradeCommand(Command):
|
|
|
699
766
|
if spec is None:
|
|
700
767
|
continue
|
|
701
768
|
name = meta.name
|
|
769
|
+
display = tool_registry.format_display(tool_registry.get_tool_path(name))
|
|
702
770
|
if not tool_manager.is_installed(name):
|
|
703
771
|
renderer = context.session.get("renderer")
|
|
704
772
|
if renderer is not None:
|
|
705
|
-
renderer.log_line(f"upgrade
|
|
773
|
+
renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
|
|
706
774
|
continue
|
|
707
775
|
renderer = context.session.get("renderer")
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
776
|
+
first_progress = [True]
|
|
777
|
+
|
|
778
|
+
def on_progress(
|
|
779
|
+
msg: str,
|
|
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)
|
|
712
790
|
|
|
713
791
|
ok, message = tool_manager.update(name, spec, on_progress=on_progress)
|
|
714
792
|
if renderer is not None:
|
|
793
|
+
tag = f"upgrade {display}"
|
|
715
794
|
if ok:
|
|
716
|
-
|
|
795
|
+
msg = "upgraded"
|
|
796
|
+
elif message == "Aborted":
|
|
797
|
+
msg = "aborted"
|
|
798
|
+
print("\r\033[K", end="")
|
|
799
|
+
else:
|
|
800
|
+
msg = f"failed: {message}"
|
|
801
|
+
if first_progress[0]:
|
|
802
|
+
renderer.log_line(tag, msg, color=BLUE)
|
|
717
803
|
else:
|
|
718
|
-
|
|
804
|
+
lines = msg.splitlines() or [""]
|
|
805
|
+
for line in lines:
|
|
806
|
+
renderer.gutter(tag, line, BLUE, is_first=False)
|
|
719
807
|
return None
|
|
720
808
|
|
|
721
809
|
|
|
@@ -737,13 +825,15 @@ class RemoveCommand(Command):
|
|
|
737
825
|
tool_cls = tool_registry.get_tool(name)
|
|
738
826
|
if tool_cls is None or tool_cls.metadata.install is None:
|
|
739
827
|
return f'Tool "{name}" is not an external tool or was not found.'
|
|
828
|
+
tool_name = tool_cls.metadata.name
|
|
829
|
+
display = tool_registry.format_display(tool_registry.get_tool_path(tool_name))
|
|
740
830
|
renderer = context.session.get("renderer")
|
|
741
|
-
ok, message = tool_manager.uninstall(
|
|
831
|
+
ok, message = tool_manager.uninstall(tool_name)
|
|
742
832
|
if renderer is not None:
|
|
743
833
|
if ok:
|
|
744
|
-
renderer.log_line(f"remove
|
|
834
|
+
renderer.log_line(f"remove {display}", "removed", color=RED)
|
|
745
835
|
else:
|
|
746
|
-
renderer.log_line(f"remove
|
|
836
|
+
renderer.log_line(f"remove {display}", f"failed: {message}", color=RED)
|
|
747
837
|
return None
|
|
748
838
|
return str(message)
|
|
749
839
|
|
|
@@ -9,16 +9,21 @@ class PromptRenderer:
|
|
|
9
9
|
return "shark > "
|
|
10
10
|
|
|
11
11
|
def tool_prompt(self, tool_name: str) -> str:
|
|
12
|
-
|
|
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
|
-
|
|
22
|
+
tool_path, after = rest.split(")", 1)
|
|
18
23
|
return [
|
|
19
24
|
(PROMPT_STYLE, before),
|
|
20
25
|
("", "("),
|
|
21
|
-
(MODULE_STYLE,
|
|
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 =
|
|
108
|
+
prompt = self._prompt_renderer.tool_prompt(self._current_tool)
|
|
104
109
|
else:
|
|
105
|
-
prompt =
|
|
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=
|
|
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
|
-
|
|
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
|
|
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() -
|
|
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)
|
|
@@ -249,7 +253,10 @@ class Renderer:
|
|
|
249
253
|
print(text, end="")
|
|
250
254
|
|
|
251
255
|
def log_line(self, tag: str, message: str, color: str = PINK) -> None:
|
|
252
|
-
|
|
256
|
+
lines = message.splitlines() or [""]
|
|
257
|
+
self.gutter(tag, lines[0], color, is_first=True)
|
|
258
|
+
for line in lines[1:]:
|
|
259
|
+
print(f"{color}{BOLD}│{RESET} {line}")
|
|
253
260
|
|
|
254
261
|
def gutter(self, name: str, line: str, color: str, is_first: bool) -> None:
|
|
255
262
|
if is_first:
|
|
@@ -257,3 +264,16 @@ class Renderer:
|
|
|
257
264
|
else:
|
|
258
265
|
prefix = f"{RESET}{' ' * (len(name) + 1)}{color}{BOLD}│{RESET} "
|
|
259
266
|
print(prefix + line)
|
|
267
|
+
|
|
268
|
+
def support_creator(self, author: str, tool_name: str, git_url: str) -> None:
|
|
269
|
+
lines = [
|
|
270
|
+
f"Thanks to {author} for building {tool_name}!",
|
|
271
|
+
"If you find this tool useful, please consider:",
|
|
272
|
+
"",
|
|
273
|
+
f"⭐ Star the repo: {git_url}",
|
|
274
|
+
f"🐛 Report issues: {git_url}/issues",
|
|
275
|
+
f"💰 Sponsor the author: {git_url}/sponsors",
|
|
276
|
+
"",
|
|
277
|
+
f"{DIM}Without creators like them, sharkit wouldn't exist! :3{RESET}",
|
|
278
|
+
]
|
|
279
|
+
self.panel(None, "\n".join(lines))
|
|
@@ -54,7 +54,11 @@ class ToolManager:
|
|
|
54
54
|
self._run([pip, "install", *spec.pip_args])
|
|
55
55
|
dest.joinpath(".installed").write_text(name)
|
|
56
56
|
return True, f'Tool "{name}" installed.'
|
|
57
|
+
except KeyboardInterrupt:
|
|
58
|
+
shutil.rmtree(dest, ignore_errors=True)
|
|
59
|
+
return False, "Aborted"
|
|
57
60
|
except Exception as e:
|
|
61
|
+
shutil.rmtree(dest, ignore_errors=True)
|
|
58
62
|
return False, f'Failed to install "{name}": {e}'
|
|
59
63
|
|
|
60
64
|
def update(
|
|
@@ -82,6 +86,8 @@ class ToolManager:
|
|
|
82
86
|
on_progress("upgrading packages...")
|
|
83
87
|
self._run([pip, "install", "--upgrade", *spec.pip_args])
|
|
84
88
|
return True, f'Tool "{name}" updated.'
|
|
89
|
+
except KeyboardInterrupt:
|
|
90
|
+
return False, "Aborted"
|
|
85
91
|
except Exception as e:
|
|
86
92
|
return False, f'Failed to update "{name}": {e}'
|
|
87
93
|
|
|
@@ -96,4 +102,12 @@ class ToolManager:
|
|
|
96
102
|
return False, f'Failed to uninstall "{name}": {e}'
|
|
97
103
|
|
|
98
104
|
def _run(self, cmd: list[str]) -> None:
|
|
99
|
-
subprocess.run(cmd,
|
|
105
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
106
|
+
if result.returncode != 0:
|
|
107
|
+
stderr = result.stderr.strip() if result.stderr else ""
|
|
108
|
+
stdout = result.stdout.strip() if result.stdout else ""
|
|
109
|
+
raw = stderr or stdout or "No output"
|
|
110
|
+
# Truncate long build output — keep last 20 lines where the actual error lives
|
|
111
|
+
lines = raw.splitlines()
|
|
112
|
+
detail = "\n".join(lines[-20:]) if len(lines) > 20 else raw
|
|
113
|
+
raise RuntimeError(detail)
|
|
@@ -107,7 +107,7 @@ class BlackbirdTool(Tool):
|
|
|
107
107
|
success=False,
|
|
108
108
|
error="Both 'type' and 'query' options are required.",
|
|
109
109
|
)
|
|
110
|
-
args = [f"--{stype}", query
|
|
110
|
+
args = [f"--{stype}", query]
|
|
111
111
|
if parse_bool(context.options.get("pdf")):
|
|
112
112
|
args.append("--pdf")
|
|
113
113
|
renderer = context.renderer
|
|
@@ -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
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|