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.
Files changed (143) hide show
  1. osintcase/__init__.py +3 -0
  2. osintcase/__main__.py +3 -0
  3. osintcase/_window.py +22 -0
  4. osintcase/assets/icon-256.png +0 -0
  5. osintcase/assets/icon.icns +0 -0
  6. osintcase/assets/icon.ico +0 -0
  7. osintcase/assets/icon.png +0 -0
  8. osintcase/cli.py +93 -0
  9. osintcase/server.py +91 -0
  10. osintcase/shortcut.py +192 -0
  11. osintcase/static/app.config.example.json +8 -0
  12. osintcase/static/assets/amusement-park-black-CRcS8pR6.png +0 -0
  13. osintcase/static/assets/amusement-park-white-B2Xo73Vo.png +0 -0
  14. osintcase/static/assets/book-library-black-lHui0cWf.png +0 -0
  15. osintcase/static/assets/book-library-white-DVy0OQ8w.png +0 -0
  16. osintcase/static/assets/chakra-petch-latin-500-normal-BR1ody1F.woff2 +0 -0
  17. osintcase/static/assets/chakra-petch-latin-500-normal-CnUQnZ4D.woff +0 -0
  18. osintcase/static/assets/chakra-petch-latin-600-normal-DQKfcdKo.woff +0 -0
  19. osintcase/static/assets/chakra-petch-latin-600-normal-DVQm9bgb.woff2 +0 -0
  20. osintcase/static/assets/chakra-petch-latin-700-normal-CnDBPjkL.woff2 +0 -0
  21. osintcase/static/assets/chakra-petch-latin-700-normal-D1s_c2du.woff +0 -0
  22. osintcase/static/assets/chakra-petch-latin-ext-500-normal-BCHeNDEx.woff +0 -0
  23. osintcase/static/assets/chakra-petch-latin-ext-500-normal-gA6791b0.woff2 +0 -0
  24. osintcase/static/assets/chakra-petch-latin-ext-600-normal-CdGvbdDU.woff2 +0 -0
  25. osintcase/static/assets/chakra-petch-latin-ext-600-normal-nL80L4xU.woff +0 -0
  26. osintcase/static/assets/chakra-petch-latin-ext-700-normal-BeviJPUl.woff +0 -0
  27. osintcase/static/assets/chakra-petch-latin-ext-700-normal-DAkvJhej.woff2 +0 -0
  28. osintcase/static/assets/chakra-petch-thai-500-normal-CJG_V2_m.woff +0 -0
  29. osintcase/static/assets/chakra-petch-thai-500-normal-DeexCGiz.woff2 +0 -0
  30. osintcase/static/assets/chakra-petch-thai-600-normal-BiM5MXH8.woff +0 -0
  31. osintcase/static/assets/chakra-petch-thai-600-normal-C620THcd.woff2 +0 -0
  32. osintcase/static/assets/chakra-petch-thai-700-normal-B7WL5pBr.woff2 +0 -0
  33. osintcase/static/assets/chakra-petch-thai-700-normal-vZLZ_5L8.woff +0 -0
  34. osintcase/static/assets/chakra-petch-vietnamese-500-normal-BVzUBLGs.woff +0 -0
  35. osintcase/static/assets/chakra-petch-vietnamese-600-normal-Pvj4qcw_.woff +0 -0
  36. osintcase/static/assets/chakra-petch-vietnamese-700-normal-gQuUA8Wu.woff +0 -0
  37. osintcase/static/assets/coffee-cup-black-CCyzsteI.png +0 -0
  38. osintcase/static/assets/coffee-cup-white-BzDvAJ1-.png +0 -0
  39. osintcase/static/assets/discord-CbcovB1n.png +0 -0
  40. osintcase/static/assets/eat-food-black-nbzGZrVR.png +0 -0
  41. osintcase/static/assets/eat-food-white-pKAxg8e0.png +0 -0
  42. osintcase/static/assets/email-black-B5Z-68i9.png +0 -0
  43. osintcase/static/assets/facebook-ejnf6rkB.png +0 -0
  44. osintcase/static/assets/family-black-CVTYwxmB.png +0 -0
  45. osintcase/static/assets/family-person-dark-B0grYou3.png +0 -0
  46. osintcase/static/assets/google-DuXP27S6.png +0 -0
  47. osintcase/static/assets/gym-black-ByooSTWg.png +0 -0
  48. osintcase/static/assets/gym-white-DvYbRXTC.png +0 -0
  49. osintcase/static/assets/home-black-Dzx5QTpF.png +0 -0
  50. osintcase/static/assets/home-white-Dn7nRoVD.png +0 -0
  51. osintcase/static/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  52. osintcase/static/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  53. osintcase/static/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  54. osintcase/static/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  55. osintcase/static/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  56. osintcase/static/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  57. osintcase/static/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  58. osintcase/static/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  59. osintcase/static/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  60. osintcase/static/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  61. osintcase/static/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  62. osintcase/static/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  63. osintcase/static/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  64. osintcase/static/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  65. osintcase/static/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  66. osintcase/static/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  67. osintcase/static/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  68. osintcase/static/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  69. osintcase/static/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  70. osintcase/static/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  71. osintcase/static/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
  72. osintcase/static/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
  73. osintcase/static/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
  74. osintcase/static/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
  75. osintcase/static/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
  76. osintcase/static/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
  77. osintcase/static/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
  78. osintcase/static/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
  79. osintcase/static/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
  80. osintcase/static/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
  81. osintcase/static/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
  82. osintcase/static/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
  83. osintcase/static/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
  84. osintcase/static/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
  85. osintcase/static/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
  86. osintcase/static/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
  87. osintcase/static/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
  88. osintcase/static/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
  89. osintcase/static/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  90. osintcase/static/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  91. osintcase/static/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  92. osintcase/static/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  93. osintcase/static/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  94. osintcase/static/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  95. osintcase/static/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
  96. osintcase/static/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
  97. osintcase/static/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
  98. osintcase/static/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
  99. osintcase/static/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
  100. osintcase/static/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
  101. osintcase/static/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
  102. osintcase/static/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
  103. osintcase/static/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
  104. osintcase/static/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
  105. osintcase/static/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
  106. osintcase/static/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
  107. osintcase/static/assets/index-BFpkUFXp.js +147 -0
  108. osintcase/static/assets/index-D-g9PJ6O.css +1 -0
  109. osintcase/static/assets/instagram-CVFJBcOL.png +0 -0
  110. osintcase/static/assets/linkedin-6JOBZbxI.png +0 -0
  111. osintcase/static/assets/location-black-DmAm6tO4.png +0 -0
  112. osintcase/static/assets/location-white-DBUC44Ad.png +0 -0
  113. osintcase/static/assets/logo-160-DdNRKo0i.png +0 -0
  114. osintcase/static/assets/logo-512-CMltuiTp.webp +0 -0
  115. osintcase/static/assets/logo-ink-160-C8oLaqJ8.png +0 -0
  116. osintcase/static/assets/logo-ink-512-078j9DW1.webp +0 -0
  117. osintcase/static/assets/movie-theater-black-DK10hghK.png +0 -0
  118. osintcase/static/assets/movie-theater-white-CcxlHP0N.png +0 -0
  119. osintcase/static/assets/park-black-CKo7JSMw.png +0 -0
  120. osintcase/static/assets/park-white-BjwssZFv.png +0 -0
  121. osintcase/static/assets/phone-dark-C6fnnsXb.png +0 -0
  122. osintcase/static/assets/school-black-yjs2QgDc.png +0 -0
  123. osintcase/static/assets/school-white-OQ9j1LIK.png +0 -0
  124. osintcase/static/assets/shopping-mall-black-DGYiSAdv.png +0 -0
  125. osintcase/static/assets/shopping-mall-white-sLRo_FSv.png +0 -0
  126. osintcase/static/assets/snapchat-DKOOa2bz.png +0 -0
  127. osintcase/static/assets/social-BCxDlPO_.png +0 -0
  128. osintcase/static/assets/spotify-Bzxdv3z1.png +0 -0
  129. osintcase/static/assets/telegram-BwLyKQnC.png +0 -0
  130. osintcase/static/assets/tik-tok-C3xTsDPx.png +0 -0
  131. osintcase/static/assets/twitter-black-DTBoECSh.png +0 -0
  132. osintcase/static/assets/twitter-white-rOyJNF09.png +0 -0
  133. osintcase/static/assets/vehicle-black-CqU6kX7s.png +0 -0
  134. osintcase/static/assets/vin-black-vXFhis48.png +0 -0
  135. osintcase/static/assets/youtube-SsrGC1K8.png +0 -0
  136. osintcase/static/favicon.png +0 -0
  137. osintcase/static/index.html +15 -0
  138. osintcase-0.2.0.dist-info/METADATA +45 -0
  139. osintcase-0.2.0.dist-info/RECORD +143 -0
  140. osintcase-0.2.0.dist-info/WHEEL +5 -0
  141. osintcase-0.2.0.dist-info/entry_points.txt +2 -0
  142. osintcase-0.2.0.dist-info/licenses/LICENSE +674 -0
  143. osintcase-0.2.0.dist-info/top_level.txt +1 -0
osintcase/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """OSINT Case — investigation case workspace (profiling, notes, link analysis, reports)."""
2
+
3
+ __version__ = "0.2.0"
osintcase/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
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
+ }