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.
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/PKG-INFO +2 -2
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/README.md +1 -1
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/pyproject.toml +1 -1
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/commands/builtins.py +33 -16
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/console/prompt.py +8 -3
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/console/repl.py +10 -3
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/output/renderer.py +23 -6
- {sharkit-0.2.0.dev1/src/sharkit/tools → sharkit-0.2.0.dev2/src/sharkit/tools/osint/recon/hybrid}/blackbird.py +14 -4
- sharkit-0.2.0.dev2/src/sharkit/tools/osint/recon/username/maigret.py +251 -0
- sharkit-0.2.0.dev2/src/sharkit/tools/osint/recon/username/sherlock.py +251 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/tools/registry.py +14 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/.gitignore +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/LICENSE +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/__main__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/app.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/commands/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/commands/base.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/commands/registry.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/config/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/config/manager.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/config/paths.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/console/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/console/completion.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/exceptions.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/history/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/history/manager.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/network/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/network/client.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/output/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/output/theme.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/tools/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/tools/base.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.0.dev2}/src/sharkit/tools/loader.py +0 -0
- {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.
|
|
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
|

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

|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
645
|
+
renderer.log_line(f"install {display}", "installed", color=GREEN)
|
|
632
646
|
else:
|
|
633
|
-
renderer.log_line(f"install
|
|
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
|
|
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
|
|
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
|
|
689
|
+
renderer.log_line(f"upgrade {display}", "upgraded", color=BLUE)
|
|
675
690
|
else:
|
|
676
|
-
renderer.log_line(f"upgrade
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
732
|
+
renderer.log_line(f"upgrade {display}", "upgraded", color=BLUE)
|
|
717
733
|
else:
|
|
718
|
-
renderer.log_line(f"upgrade
|
|
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
|
|
761
|
+
renderer.log_line(f"remove {display}", "removed", color=RED)
|
|
745
762
|
else:
|
|
746
|
-
renderer.log_line(f"remove
|
|
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
|
-
|
|
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)
|
|
@@ -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
|
|
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 = ""
|
|
@@ -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
|
|
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
|