osintcase 0.2.0__py3-none-any.whl
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.
- osintcase/__init__.py +3 -0
- osintcase/__main__.py +3 -0
- osintcase/_window.py +22 -0
- osintcase/assets/icon-256.png +0 -0
- osintcase/assets/icon.icns +0 -0
- osintcase/assets/icon.ico +0 -0
- osintcase/assets/icon.png +0 -0
- osintcase/cli.py +93 -0
- osintcase/server.py +91 -0
- osintcase/shortcut.py +192 -0
- osintcase/static/app.config.example.json +8 -0
- osintcase/static/assets/amusement-park-black-CRcS8pR6.png +0 -0
- osintcase/static/assets/amusement-park-white-B2Xo73Vo.png +0 -0
- osintcase/static/assets/book-library-black-lHui0cWf.png +0 -0
- osintcase/static/assets/book-library-white-DVy0OQ8w.png +0 -0
- osintcase/static/assets/chakra-petch-latin-500-normal-BR1ody1F.woff2 +0 -0
- osintcase/static/assets/chakra-petch-latin-500-normal-CnUQnZ4D.woff +0 -0
- osintcase/static/assets/chakra-petch-latin-600-normal-DQKfcdKo.woff +0 -0
- osintcase/static/assets/chakra-petch-latin-600-normal-DVQm9bgb.woff2 +0 -0
- osintcase/static/assets/chakra-petch-latin-700-normal-CnDBPjkL.woff2 +0 -0
- osintcase/static/assets/chakra-petch-latin-700-normal-D1s_c2du.woff +0 -0
- osintcase/static/assets/chakra-petch-latin-ext-500-normal-BCHeNDEx.woff +0 -0
- osintcase/static/assets/chakra-petch-latin-ext-500-normal-gA6791b0.woff2 +0 -0
- osintcase/static/assets/chakra-petch-latin-ext-600-normal-CdGvbdDU.woff2 +0 -0
- osintcase/static/assets/chakra-petch-latin-ext-600-normal-nL80L4xU.woff +0 -0
- osintcase/static/assets/chakra-petch-latin-ext-700-normal-BeviJPUl.woff +0 -0
- osintcase/static/assets/chakra-petch-latin-ext-700-normal-DAkvJhej.woff2 +0 -0
- osintcase/static/assets/chakra-petch-thai-500-normal-CJG_V2_m.woff +0 -0
- osintcase/static/assets/chakra-petch-thai-500-normal-DeexCGiz.woff2 +0 -0
- osintcase/static/assets/chakra-petch-thai-600-normal-BiM5MXH8.woff +0 -0
- osintcase/static/assets/chakra-petch-thai-600-normal-C620THcd.woff2 +0 -0
- osintcase/static/assets/chakra-petch-thai-700-normal-B7WL5pBr.woff2 +0 -0
- osintcase/static/assets/chakra-petch-thai-700-normal-vZLZ_5L8.woff +0 -0
- osintcase/static/assets/chakra-petch-vietnamese-500-normal-BVzUBLGs.woff +0 -0
- osintcase/static/assets/chakra-petch-vietnamese-600-normal-Pvj4qcw_.woff +0 -0
- osintcase/static/assets/chakra-petch-vietnamese-700-normal-gQuUA8Wu.woff +0 -0
- osintcase/static/assets/coffee-cup-black-CCyzsteI.png +0 -0
- osintcase/static/assets/coffee-cup-white-BzDvAJ1-.png +0 -0
- osintcase/static/assets/discord-CbcovB1n.png +0 -0
- osintcase/static/assets/eat-food-black-nbzGZrVR.png +0 -0
- osintcase/static/assets/eat-food-white-pKAxg8e0.png +0 -0
- osintcase/static/assets/email-black-B5Z-68i9.png +0 -0
- osintcase/static/assets/facebook-ejnf6rkB.png +0 -0
- osintcase/static/assets/family-black-CVTYwxmB.png +0 -0
- osintcase/static/assets/family-person-dark-B0grYou3.png +0 -0
- osintcase/static/assets/google-DuXP27S6.png +0 -0
- osintcase/static/assets/gym-black-ByooSTWg.png +0 -0
- osintcase/static/assets/gym-white-DvYbRXTC.png +0 -0
- osintcase/static/assets/home-black-Dzx5QTpF.png +0 -0
- osintcase/static/assets/home-white-Dn7nRoVD.png +0 -0
- osintcase/static/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- osintcase/static/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- osintcase/static/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- osintcase/static/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- osintcase/static/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- osintcase/static/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- osintcase/static/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- osintcase/static/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- osintcase/static/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- osintcase/static/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- osintcase/static/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- osintcase/static/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- osintcase/static/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- osintcase/static/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- osintcase/static/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- osintcase/static/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- osintcase/static/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- osintcase/static/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- osintcase/static/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- osintcase/static/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
- osintcase/static/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
- osintcase/static/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
- osintcase/static/assets/index-BFpkUFXp.js +147 -0
- osintcase/static/assets/index-D-g9PJ6O.css +1 -0
- osintcase/static/assets/instagram-CVFJBcOL.png +0 -0
- osintcase/static/assets/linkedin-6JOBZbxI.png +0 -0
- osintcase/static/assets/location-black-DmAm6tO4.png +0 -0
- osintcase/static/assets/location-white-DBUC44Ad.png +0 -0
- osintcase/static/assets/logo-160-DdNRKo0i.png +0 -0
- osintcase/static/assets/logo-512-CMltuiTp.webp +0 -0
- osintcase/static/assets/logo-ink-160-C8oLaqJ8.png +0 -0
- osintcase/static/assets/logo-ink-512-078j9DW1.webp +0 -0
- osintcase/static/assets/movie-theater-black-DK10hghK.png +0 -0
- osintcase/static/assets/movie-theater-white-CcxlHP0N.png +0 -0
- osintcase/static/assets/park-black-CKo7JSMw.png +0 -0
- osintcase/static/assets/park-white-BjwssZFv.png +0 -0
- osintcase/static/assets/phone-dark-C6fnnsXb.png +0 -0
- osintcase/static/assets/school-black-yjs2QgDc.png +0 -0
- osintcase/static/assets/school-white-OQ9j1LIK.png +0 -0
- osintcase/static/assets/shopping-mall-black-DGYiSAdv.png +0 -0
- osintcase/static/assets/shopping-mall-white-sLRo_FSv.png +0 -0
- osintcase/static/assets/snapchat-DKOOa2bz.png +0 -0
- osintcase/static/assets/social-BCxDlPO_.png +0 -0
- osintcase/static/assets/spotify-Bzxdv3z1.png +0 -0
- osintcase/static/assets/telegram-BwLyKQnC.png +0 -0
- osintcase/static/assets/tik-tok-C3xTsDPx.png +0 -0
- osintcase/static/assets/twitter-black-DTBoECSh.png +0 -0
- osintcase/static/assets/twitter-white-rOyJNF09.png +0 -0
- osintcase/static/assets/vehicle-black-CqU6kX7s.png +0 -0
- osintcase/static/assets/vin-black-vXFhis48.png +0 -0
- osintcase/static/assets/youtube-SsrGC1K8.png +0 -0
- osintcase/static/favicon.png +0 -0
- osintcase/static/index.html +15 -0
- osintcase-0.2.0.dist-info/METADATA +45 -0
- osintcase-0.2.0.dist-info/RECORD +143 -0
- osintcase-0.2.0.dist-info/WHEEL +5 -0
- osintcase-0.2.0.dist-info/entry_points.txt +2 -0
- osintcase-0.2.0.dist-info/licenses/LICENSE +674 -0
- osintcase-0.2.0.dist-info/top_level.txt +1 -0
osintcase/__init__.py
ADDED
osintcase/__main__.py
ADDED
osintcase/_window.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Runs the native window. Started as a child process by cli.py so a missing system
|
|
2
|
+
library (for example libxcb-cursor0 for Qt) cannot take the whole program down."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run(url: str, storage: str, icon: str) -> int:
|
|
10
|
+
import webview # pywebview
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
webview.settings["ALLOW_DOWNLOADS"] = True
|
|
14
|
+
except Exception:
|
|
15
|
+
pass
|
|
16
|
+
webview.create_window("OSINT Case", url, width=1440, height=900, min_size=(1000, 640))
|
|
17
|
+
webview.start(private_mode=False, storage_path=storage, icon=icon)
|
|
18
|
+
return 0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
raise SystemExit(run(sys.argv[1], sys.argv[2], sys.argv[3]))
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
osintcase/cli.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
import webbrowser
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import __version__, server, shortcut
|
|
11
|
+
|
|
12
|
+
ICON = Path(__file__).resolve().parent / "assets" / "icon.png"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parser() -> argparse.ArgumentParser:
|
|
16
|
+
p = argparse.ArgumentParser(
|
|
17
|
+
prog="osintcase",
|
|
18
|
+
description="OSINT Case — investigation case workspace. Opens in its own window by default.",
|
|
19
|
+
)
|
|
20
|
+
p.add_argument("--web", action="store_true", help="open in your web browser instead of a window")
|
|
21
|
+
p.add_argument("--port", type=int, default=None, help=f"local port (default {server.DEFAULT_PORT})")
|
|
22
|
+
p.add_argument("--no-open", action="store_true", help="with --web: start the server but do not open a browser")
|
|
23
|
+
p.add_argument("--install-shortcut", action="store_true", help="add OSINT Case (with logo) to the application menu")
|
|
24
|
+
p.add_argument("--uninstall-shortcut", action="store_true", help="remove the application menu entry")
|
|
25
|
+
p.add_argument("--version", action="version", version=f"osintcase {__version__}")
|
|
26
|
+
return p
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _serve_forever(url: str, open_browser: bool) -> int:
|
|
30
|
+
print(f"OSINT Case {__version__} running at {url} (Ctrl+C to stop)")
|
|
31
|
+
if open_browser:
|
|
32
|
+
webbrowser.open(url)
|
|
33
|
+
try:
|
|
34
|
+
while True:
|
|
35
|
+
time.sleep(3600)
|
|
36
|
+
except KeyboardInterrupt:
|
|
37
|
+
print()
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _gui(url: str) -> bool:
|
|
42
|
+
"""Open the native window in a child process. False means no window could be shown."""
|
|
43
|
+
storage = shortcut.DATA_DIR / "webview"
|
|
44
|
+
storage.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
started = time.monotonic()
|
|
46
|
+
try:
|
|
47
|
+
rc = subprocess.run(
|
|
48
|
+
[sys.executable, "-m", "osintcase._window", url, str(storage), str(ICON)]
|
|
49
|
+
).returncode
|
|
50
|
+
except OSError as exc:
|
|
51
|
+
print(f"Could not start the window ({exc}).", file=sys.stderr)
|
|
52
|
+
return False
|
|
53
|
+
if rc == 0:
|
|
54
|
+
return True
|
|
55
|
+
if time.monotonic() - started > 20: # it ran for a while and then died: do not reopen in a browser
|
|
56
|
+
return True
|
|
57
|
+
if sys.platform.startswith("linux"):
|
|
58
|
+
print(
|
|
59
|
+
"The window could not start. On Debian/Ubuntu this is usually fixed with:\n"
|
|
60
|
+
" sudo apt install libxcb-cursor0 libxkbcommon-x11-0 libxcb-icccm4 libxcb-keysyms1 libxcb-shape0",
|
|
61
|
+
file=sys.stderr,
|
|
62
|
+
)
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main(argv: list[str] | None = None) -> int:
|
|
67
|
+
args = _parser().parse_args(argv)
|
|
68
|
+
|
|
69
|
+
if args.install_shortcut:
|
|
70
|
+
for item in shortcut.install():
|
|
71
|
+
print("created:", item)
|
|
72
|
+
return 0
|
|
73
|
+
if args.uninstall_shortcut:
|
|
74
|
+
for item in shortcut.remove():
|
|
75
|
+
print("removed:", item)
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
_httpd, url, changed = server.start(args.port)
|
|
80
|
+
except RuntimeError as exc:
|
|
81
|
+
print(exc, file=sys.stderr)
|
|
82
|
+
return 1
|
|
83
|
+
if changed:
|
|
84
|
+
print("Note: default port is busy; using a different one. Saved browser data is per port.", file=sys.stderr)
|
|
85
|
+
|
|
86
|
+
if args.web:
|
|
87
|
+
return _serve_forever(url, not args.no_open)
|
|
88
|
+
|
|
89
|
+
shortcut.ensure_once()
|
|
90
|
+
if _gui(url):
|
|
91
|
+
return 0
|
|
92
|
+
print("Falling back to the browser (install a GUI toolkit or use --web explicitly).", file=sys.stderr)
|
|
93
|
+
return _serve_forever(url, True)
|
osintcase/server.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Tiny local static server for the bundled OSINT Case web app (127.0.0.1 only)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import mimetypes
|
|
6
|
+
import posixpath
|
|
7
|
+
import socket
|
|
8
|
+
import threading
|
|
9
|
+
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from urllib.parse import unquote, urlsplit
|
|
12
|
+
|
|
13
|
+
STATIC_DIR = Path(__file__).resolve().parent / "static"
|
|
14
|
+
DEFAULT_PORT = 47321 # fixed so the browser keeps the same origin (and saved data) between runs
|
|
15
|
+
|
|
16
|
+
mimetypes.add_type("application/javascript", ".js")
|
|
17
|
+
mimetypes.add_type("application/javascript", ".mjs")
|
|
18
|
+
mimetypes.add_type("image/webp", ".webp")
|
|
19
|
+
mimetypes.add_type("font/woff2", ".woff2")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _Handler(SimpleHTTPRequestHandler):
|
|
23
|
+
server_version = "OsintCase"
|
|
24
|
+
|
|
25
|
+
def __init__(self, *args, **kwargs):
|
|
26
|
+
super().__init__(*args, directory=str(STATIC_DIR), **kwargs)
|
|
27
|
+
|
|
28
|
+
# Block DNS-rebinding style requests: only accept local Host headers.
|
|
29
|
+
def _host_ok(self) -> bool:
|
|
30
|
+
host = (self.headers.get("Host") or "").rsplit(":", 1)[0].strip("[]").lower()
|
|
31
|
+
return host in {"127.0.0.1", "localhost", "::1"}
|
|
32
|
+
|
|
33
|
+
def do_GET(self): # noqa: N802
|
|
34
|
+
if not self._host_ok():
|
|
35
|
+
self.send_error(403, "Forbidden")
|
|
36
|
+
return
|
|
37
|
+
super().do_GET()
|
|
38
|
+
|
|
39
|
+
def do_HEAD(self): # noqa: N802
|
|
40
|
+
if not self._host_ok():
|
|
41
|
+
self.send_error(403, "Forbidden")
|
|
42
|
+
return
|
|
43
|
+
super().do_HEAD()
|
|
44
|
+
|
|
45
|
+
def translate_path(self, path: str) -> str:
|
|
46
|
+
path = posixpath.normpath(unquote(urlsplit(path).path))
|
|
47
|
+
if path in ("", ".", "/"):
|
|
48
|
+
path = "/index.html"
|
|
49
|
+
target = (STATIC_DIR / path.lstrip("/")).resolve()
|
|
50
|
+
if STATIC_DIR not in target.parents and target != STATIC_DIR:
|
|
51
|
+
return str(STATIC_DIR / "index.html")
|
|
52
|
+
return str(target)
|
|
53
|
+
|
|
54
|
+
def end_headers(self):
|
|
55
|
+
self.send_header("Cache-Control", "no-cache")
|
|
56
|
+
self.send_header("X-Content-Type-Options", "nosniff")
|
|
57
|
+
self.send_header("Referrer-Policy", "no-referrer")
|
|
58
|
+
super().end_headers()
|
|
59
|
+
|
|
60
|
+
def log_message(self, fmt, *args): # quiet by default
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _port_free(host: str, port: int) -> bool:
|
|
65
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
66
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
67
|
+
try:
|
|
68
|
+
s.bind((host, port))
|
|
69
|
+
except OSError:
|
|
70
|
+
return False
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def start(port: int | None = None, host: str = "127.0.0.1") -> tuple[ThreadingHTTPServer, str, bool]:
|
|
75
|
+
"""Start the server in a daemon thread.
|
|
76
|
+
|
|
77
|
+
Returns (server, url, port_changed). If the preferred port is busy a random free
|
|
78
|
+
port is used (saved browser data is per-origin, so the caller may want to warn).
|
|
79
|
+
"""
|
|
80
|
+
if not (STATIC_DIR / "index.html").is_file():
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
"Bundled web app not found. Reinstall the package, or run tools/build_pypi.sh in the source tree."
|
|
83
|
+
)
|
|
84
|
+
wanted = port if port else DEFAULT_PORT
|
|
85
|
+
changed = False
|
|
86
|
+
if not _port_free(host, wanted):
|
|
87
|
+
wanted, changed = 0, True
|
|
88
|
+
httpd = ThreadingHTTPServer((host, wanted), _Handler)
|
|
89
|
+
httpd.daemon_threads = True
|
|
90
|
+
threading.Thread(target=httpd.serve_forever, name="osintcase-http", daemon=True).start()
|
|
91
|
+
return httpd, f"http://{host}:{httpd.server_address[1]}/", changed
|
osintcase/shortcut.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Create / remove the application-menu entry (with the logo icon).
|
|
2
|
+
|
|
3
|
+
Linux: ~/.local/share/applications/osintcase.desktop + hicolor icons
|
|
4
|
+
Windows: Start Menu shortcut (OSINT Case.lnk) using icon.ico
|
|
5
|
+
macOS: ~/Applications/OSINT Case.app (small launcher bundle)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
ASSETS = Path(__file__).resolve().parent / "assets"
|
|
17
|
+
NAME = "OSINT Case"
|
|
18
|
+
DATA_DIR = Path(os.environ.get("OSINTCASE_HOME", Path.home() / ".osintcase"))
|
|
19
|
+
MARKER = DATA_DIR / ".shortcut-v1"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _command() -> list[str]:
|
|
23
|
+
"""Absolute command that launches the GUI (works for pip, pipx and venvs)."""
|
|
24
|
+
exe = shutil.which("osintcase")
|
|
25
|
+
if exe and Path(exe).resolve().parent in {Path(sys.executable).resolve().parent, Path(sys.prefix).resolve() / "bin"}:
|
|
26
|
+
return [exe]
|
|
27
|
+
return [sys.executable, "-m", "osintcase"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _quote(parts: list[str]) -> str:
|
|
31
|
+
return " ".join('"%s"' % p if (" " in p or "\\" in p) else p for p in parts)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------- Linux
|
|
35
|
+
def _linux_install() -> list[str]:
|
|
36
|
+
base = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
|
|
37
|
+
made = []
|
|
38
|
+
for size, src in ((256, "icon-256.png"), (512, "icon.png")):
|
|
39
|
+
dst = base / "icons" / "hicolor" / f"{size}x{size}" / "apps" / "osintcase.png"
|
|
40
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
shutil.copyfile(ASSETS / src, dst)
|
|
42
|
+
made.append(str(dst))
|
|
43
|
+
desktop = base / "applications" / "osintcase.desktop"
|
|
44
|
+
desktop.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
desktop.write_text(
|
|
46
|
+
"[Desktop Entry]\n"
|
|
47
|
+
"Type=Application\n"
|
|
48
|
+
f"Name={NAME}\n"
|
|
49
|
+
"GenericName=Investigation case workspace\n"
|
|
50
|
+
"Comment=Profiling, notes, link analysis and reports for OSINT investigations\n"
|
|
51
|
+
f"Exec={_quote(_command())}\n"
|
|
52
|
+
"Icon=osintcase\n"
|
|
53
|
+
"Terminal=false\n"
|
|
54
|
+
"Categories=Utility;Security;Network;\n"
|
|
55
|
+
"StartupWMClass=osintcase\n"
|
|
56
|
+
"Keywords=OSINT;investigation;case;intelligence;\n",
|
|
57
|
+
encoding="utf-8",
|
|
58
|
+
)
|
|
59
|
+
desktop.chmod(0o755)
|
|
60
|
+
made.append(str(desktop))
|
|
61
|
+
for tool, args in (
|
|
62
|
+
("update-desktop-database", [str(desktop.parent)]),
|
|
63
|
+
("gtk-update-icon-cache", ["-f", "-t", str(base / "icons" / "hicolor")]),
|
|
64
|
+
):
|
|
65
|
+
if shutil.which(tool):
|
|
66
|
+
subprocess.run([tool, *args], check=False, capture_output=True)
|
|
67
|
+
return made
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _linux_remove() -> list[str]:
|
|
71
|
+
base = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
|
|
72
|
+
gone = []
|
|
73
|
+
for p in (
|
|
74
|
+
base / "applications" / "osintcase.desktop",
|
|
75
|
+
base / "icons" / "hicolor" / "256x256" / "apps" / "osintcase.png",
|
|
76
|
+
base / "icons" / "hicolor" / "512x512" / "apps" / "osintcase.png",
|
|
77
|
+
):
|
|
78
|
+
if p.exists():
|
|
79
|
+
p.unlink()
|
|
80
|
+
gone.append(str(p))
|
|
81
|
+
return gone
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# -------------------------------------------------------------- Windows
|
|
85
|
+
def _win_lnk() -> Path:
|
|
86
|
+
appdata = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
|
|
87
|
+
return appdata / "Microsoft" / "Windows" / "Start Menu" / "Programs" / f"{NAME}.lnk"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _win_install() -> list[str]:
|
|
91
|
+
ico = DATA_DIR / "osintcase.ico"
|
|
92
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
shutil.copyfile(ASSETS / "icon.ico", ico)
|
|
94
|
+
py = Path(sys.executable)
|
|
95
|
+
pyw = py.with_name("pythonw.exe")
|
|
96
|
+
target = str(pyw if pyw.exists() else py)
|
|
97
|
+
lnk = _win_lnk()
|
|
98
|
+
lnk.parent.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
ps = (
|
|
100
|
+
"$s=(New-Object -ComObject WScript.Shell).CreateShortcut('%s');"
|
|
101
|
+
"$s.TargetPath='%s';$s.Arguments='-m osintcase';$s.IconLocation='%s';"
|
|
102
|
+
"$s.Description='%s';$s.WorkingDirectory='%s';$s.Save()"
|
|
103
|
+
) % (lnk, target, ico, NAME, Path.home())
|
|
104
|
+
subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=True, capture_output=True)
|
|
105
|
+
return [str(lnk)]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _win_remove() -> list[str]:
|
|
109
|
+
lnk = _win_lnk()
|
|
110
|
+
if lnk.exists():
|
|
111
|
+
lnk.unlink()
|
|
112
|
+
return [str(lnk)]
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------- macOS
|
|
117
|
+
def _mac_app() -> Path:
|
|
118
|
+
return Path.home() / "Applications" / f"{NAME}.app"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _mac_install() -> list[str]:
|
|
122
|
+
app = _mac_app()
|
|
123
|
+
(app / "Contents" / "MacOS").mkdir(parents=True, exist_ok=True)
|
|
124
|
+
(app / "Contents" / "Resources").mkdir(parents=True, exist_ok=True)
|
|
125
|
+
shutil.copyfile(ASSETS / "icon.icns", app / "Contents" / "Resources" / "icon.icns")
|
|
126
|
+
(app / "Contents" / "Info.plist").write_text(
|
|
127
|
+
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
128
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n'
|
|
129
|
+
'<plist version="1.0"><dict>'
|
|
130
|
+
f"<key>CFBundleName</key><string>{NAME}</string>"
|
|
131
|
+
f"<key>CFBundleDisplayName</key><string>{NAME}</string>"
|
|
132
|
+
"<key>CFBundleIdentifier</key><string>com.osintcase.app</string>"
|
|
133
|
+
"<key>CFBundleExecutable</key><string>osintcase</string>"
|
|
134
|
+
"<key>CFBundleIconFile</key><string>icon</string>"
|
|
135
|
+
"<key>CFBundlePackageType</key><string>APPL</string>"
|
|
136
|
+
"<key>LSMinimumSystemVersion</key><string>10.13</string>"
|
|
137
|
+
"</dict></plist>\n",
|
|
138
|
+
encoding="utf-8",
|
|
139
|
+
)
|
|
140
|
+
launcher = app / "Contents" / "MacOS" / "osintcase"
|
|
141
|
+
launcher.write_text("#!/bin/sh\nexec %s\n" % _quote(_command()), encoding="utf-8")
|
|
142
|
+
launcher.chmod(0o755)
|
|
143
|
+
return [str(app)]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _mac_remove() -> list[str]:
|
|
147
|
+
app = _mac_app()
|
|
148
|
+
if app.exists():
|
|
149
|
+
shutil.rmtree(app)
|
|
150
|
+
return [str(app)]
|
|
151
|
+
return []
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# --------------------------------------------------------------- public
|
|
155
|
+
def install() -> list[str]:
|
|
156
|
+
if sys.platform.startswith("linux"):
|
|
157
|
+
made = _linux_install()
|
|
158
|
+
elif sys.platform == "win32":
|
|
159
|
+
made = _win_install()
|
|
160
|
+
elif sys.platform == "darwin":
|
|
161
|
+
made = _mac_install()
|
|
162
|
+
else:
|
|
163
|
+
raise RuntimeError(f"Shortcuts are not supported on {sys.platform}")
|
|
164
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
MARKER.write_text("1", encoding="utf-8")
|
|
166
|
+
return made
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def remove() -> list[str]:
|
|
170
|
+
if sys.platform.startswith("linux"):
|
|
171
|
+
gone = _linux_remove()
|
|
172
|
+
elif sys.platform == "win32":
|
|
173
|
+
gone = _win_remove()
|
|
174
|
+
elif sys.platform == "darwin":
|
|
175
|
+
gone = _mac_remove()
|
|
176
|
+
else:
|
|
177
|
+
gone = []
|
|
178
|
+
MARKER.unlink(missing_ok=True)
|
|
179
|
+
# remember that the user removed it: do not recreate automatically
|
|
180
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
(DATA_DIR / ".shortcut-declined").write_text("1", encoding="utf-8")
|
|
182
|
+
return gone
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def ensure_once() -> None:
|
|
186
|
+
"""First GUI start: create the menu entry silently (never raises)."""
|
|
187
|
+
if os.environ.get("OSINTCASE_NO_SHORTCUT") or MARKER.exists() or (DATA_DIR / ".shortcut-declined").exists():
|
|
188
|
+
return
|
|
189
|
+
try:
|
|
190
|
+
install()
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Copy this file to app.config.json (without the .example) and fill in your keys. app.config.json is gitignored so your keys never get committed.",
|
|
3
|
+
"googleMaps": {
|
|
4
|
+
"apiKey": "",
|
|
5
|
+
"_mapIdHint": "Optional: paste a Map ID from Google Cloud Console → Map Management → Map Styles. If left blank, the app uses a default that works but logs an InvalidKeyMapError in the console.",
|
|
6
|
+
"mapId": ""
|
|
7
|
+
}
|
|
8
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|