osintkit 0.1.1 → 0.1.2

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.
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -1,86 +1,117 @@
1
1
  # osintkit
2
2
 
3
- OSINT CLI tool for personal digital footprint analysis.
3
+ OSINT CLI for personal digital footprint analysis. Input an email, phone, username, or name — get a risk-scored report saved locally as JSON, HTML, and Markdown.
4
+
5
+ MIT licensed. No server. Everything stays on your machine.
4
6
 
5
7
  ## Installation
6
8
 
7
9
  ```bash
8
- # From source
9
- cd osintkit-oss
10
- npm install -g .
11
-
12
- # Or use directly without installation
13
- PYTHONPATH=. python3 -m osintkit.cli <command>
10
+ npm install -g osintkit
14
11
  ```
15
12
 
16
- ## Quick Start
17
-
18
- ```bash
19
- # First-time setup (configure API keys)
20
- osintkit setup
21
-
22
- # Create a new profile
23
- osintkit new
24
-
25
- # List all profiles
26
- osintkit list
13
+ This automatically installs all Python dependencies (core + optional OSINT tools) via the postinstall script. Just run `osintkit new` when it's done.
27
14
 
28
- # Run a scan
29
- osintkit refresh <profile_id>
15
+ **Requirements:** Python 3.10+, Node.js 16+
30
16
 
31
- # View profile details
32
- osintkit open <profile_id>
17
+ ## Quick Start
33
18
 
34
- # Export data
35
- osintkit export <profile_id>
19
+ ```bash
20
+ osintkit setup # Configure API keys (optional, all have free tiers)
21
+ osintkit new # Create a profile and run a scan
22
+ osintkit list # View all profiles
23
+ osintkit refresh # Re-run scan on a profile
24
+ osintkit open # View profile details + open latest report
36
25
  ```
37
26
 
38
27
  ## Commands
39
28
 
40
29
  | Command | Description |
41
30
  |---------|-------------|
42
- | `osintkit new` | Create new profile |
43
- | `osintkit list` | List all profiles |
44
- | `osintkit refresh <id>` | Run scan for profile |
45
- | `osintkit open <id>` | Show profile details |
46
- | `osintkit export <id>` | Export as JSON/Markdown |
31
+ | `osintkit new` | Create a new profile and optionally run a scan |
32
+ | `osintkit list` | List all profiles with last risk score |
33
+ | `osintkit refresh [id]` | Re-run scan for a profile |
34
+ | `osintkit open [id]` | Show profile details and open latest report |
35
+ | `osintkit export [id]` | Export as JSON or Markdown |
47
36
  | `osintkit setup` | Configure API keys |
48
- | `osintkit delete <id>` | Delete profile |
37
+ | `osintkit delete [id]` | Delete a profile |
49
38
  | `osintkit version` | Show version |
50
39
 
51
- ## Features
40
+ ## What It Checks
41
+
42
+ ### Stage 1 — No API keys needed, works out of the box
52
43
 
53
- - **Profile Management** - Store and manage multiple target profiles
54
- - **Duplicate Detection** - Warns when creating profiles with existing info
55
- - **OSINT Modules** - 10 integrated OSINT data sources
56
- - **Risk Scoring** - 0-100 risk score based on findings
57
- - **Export** - JSON and Markdown report formats
44
+ | Module | Input | What it does |
45
+ |--------|-------|-------------|
46
+ | Maigret | username | 3000+ sites |
47
+ | Sherlock | username | 400+ sites |
48
+ | Holehe | email | 120+ platform registrations |
49
+ | HIBP k-anonymity | email | Password breach check (no key) |
50
+ | Gravatar | email | Profile existence + avatar |
51
+ | theHarvester | email/domain | Web presence, subdomains |
52
+ | crt.sh | email/domain | Certificate transparency |
53
+ | Wayback CDX | email | Historical web appearances |
54
+ | libphonenumber | phone | Carrier, region, line type (offline) |
55
+ | Paste search | email | Paste site appearances |
56
+ | Data brokers | name/email | Google CSE broker scan |
57
+ | Dark web | email | Ahmia / public index |
58
+ | Breach lookup | email | BreachDirectory |
58
59
 
59
- ## API Keys (Optional)
60
+ ### Stage 2 — Optional free API keys, runs first when configured
60
61
 
61
- Free API keys available for enhanced functionality:
62
+ | Service | Input | Free Tier |
63
+ |---------|-------|-----------|
64
+ | HaveIBeenPwned | email | Free w/ key |
65
+ | LeakCheck | email/phone/user | Free tier |
66
+ | NumVerify | phone | 100/month |
67
+ | Hunter.io | name + domain | 50/month |
68
+ | GitHub API | username | 5000/hr w/ key |
69
+ | SecurityTrails | domain | Free tier |
62
70
 
63
- | Service | Free Limit | Purpose |
64
- |---------|------------|---------|
65
- | Have I Been Pwned | 10/min | Breach database |
66
- | NumVerify | 100/month | Phone validation |
67
- | Intelbase | 100/month | Dark web + paste |
68
- | BreachDirectory | 50/day | Breach lookups |
69
- | Google CSE | 100/day | Data broker detection |
71
+ Stage 2 always runs first when a key is configured. If rate-limited or key missing, falls back to Stage 1 automatically.
70
72
 
71
- ## Requirements
73
+ ## Output
72
74
 
73
- - Python 3.11+
74
- - Node.js 12+
75
- - pip
75
+ Each scan creates a folder at `~/osint-results/<target>_<date>/` containing:
76
+ - `report.html` — rendered report with risk score
77
+ - `findings.json` — full structured data
78
+ - `findings.md` — markdown summary
76
79
 
77
- ## External Tools (Optional)
80
+ Risk score is 0–100 based on breach exposure, social footprint, data broker listings, and dark web/paste appearances.
81
+
82
+ ## API Keys (All Optional)
83
+
84
+ Run `osintkit setup` to configure. All keys are optional — the tool works without any of them.
85
+
86
+ ```
87
+ ~/.osintkit/config.yaml
88
+ ```
89
+
90
+ | Service | Get key at |
91
+ |---------|-----------|
92
+ | HaveIBeenPwned | haveibeenpwned.com/API/Key |
93
+ | LeakCheck | leakcheck.io |
94
+ | NumVerify | numverify.com |
95
+ | Hunter.io | hunter.io |
96
+ | GitHub | github.com/settings/tokens |
97
+ | Intelbase | intelbase.is |
98
+ | BreachDirectory | rapidapi.com |
99
+ | Google CSE | developers.google.com/custom-search |
100
+ | SecurityTrails | securitytrails.com |
101
+
102
+ ## Run from Source
78
103
 
79
- For full functionality, install:
80
104
  ```bash
81
- pip install maigret holehe theHarvester
105
+ git clone https://github.com/diesesschnitzel/osintkit.git
106
+ cd osintkit
107
+ pip install -r requirements.txt -r requirements-tools.txt
108
+ PYTHONPATH=. python3 -m osintkit.cli new
82
109
  ```
83
110
 
111
+ ## Ethics
112
+
113
+ Only use osintkit on targets you have explicit permission to investigate. GDPR applies to EU subjects. A disclaimer is shown before every scan.
114
+
84
115
  ## License
85
116
 
86
- MIT
117
+ MIT
package/bin/osintkit.js CHANGED
@@ -3,10 +3,24 @@ const { spawn } = require('child_process');
3
3
  const path = require('path');
4
4
  const fs = require('fs');
5
5
 
6
- // Prefer python3.11, fall back to python3, then python
6
+ // The npm package root is one level up from bin/
7
+ const packageDir = path.dirname(path.dirname(__filename));
8
+
9
+ // Prefer venv Python (installed by postinstall), fall back to system Python
7
10
  function findPython() {
8
- const candidates = ['python3.11', 'python3', 'python'];
9
- for (const bin of candidates) {
11
+ const venvCandidates = [
12
+ path.join(packageDir, '.venv', 'bin', 'python3'),
13
+ path.join(packageDir, '.venv', 'bin', 'python'),
14
+ path.join(packageDir, 'venv', 'bin', 'python3'),
15
+ path.join(packageDir, 'venv', 'bin', 'python'),
16
+ ];
17
+ for (const p of venvCandidates) {
18
+ if (fs.existsSync(p)) return p;
19
+ }
20
+
21
+ // Fall back to system Python
22
+ const systemCandidates = ['python3.11', 'python3', 'python'];
23
+ for (const bin of systemCandidates) {
10
24
  try {
11
25
  require('child_process').execSync(`${bin} --version`, { stdio: 'ignore' });
12
26
  return bin;
@@ -15,14 +29,25 @@ function findPython() {
15
29
  return 'python3';
16
30
  }
17
31
 
18
- const python = spawn(findPython(), ['-m', 'osintkit', ...process.argv.slice(2)], {
32
+ const pythonBin = findPython();
33
+
34
+ // Always set PYTHONPATH so the package is importable regardless of install method
35
+ const env = {
36
+ ...process.env,
37
+ PYTHONPATH: process.env.PYTHONPATH
38
+ ? `${packageDir}:${process.env.PYTHONPATH}`
39
+ : packageDir,
40
+ };
41
+
42
+ const child = spawn(pythonBin, ['-m', 'osintkit', ...process.argv.slice(2)], {
19
43
  stdio: 'inherit',
20
- env: process.env
44
+ cwd: packageDir,
45
+ env,
21
46
  });
22
47
 
23
- python.on('error', () => {
48
+ child.on('error', () => {
24
49
  console.error('Error: Python 3.10+ required. Install from https://python.org');
25
50
  process.exit(1);
26
51
  });
27
52
 
28
- python.on('close', (code) => process.exit(code || 0));
53
+ child.on('close', (code) => process.exit(code || 0));
@@ -1,3 +1,3 @@
1
1
  """osintkit - OSINT CLI tool for personal digital footprint analysis."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
File without changes
package/osintkit/cli.py CHANGED
@@ -3,6 +3,7 @@
3
3
  import sys
4
4
  import json
5
5
  import logging
6
+ import threading
6
7
  from pathlib import Path
7
8
  from typing import Optional
8
9
  from datetime import datetime
@@ -14,14 +15,67 @@ from rich.table import Table
14
15
  from rich.prompt import Prompt, Confirm
15
16
  from rich.progress import Progress, SpinnerColumn, TextColumn
16
17
 
18
+ from osintkit import __version__
17
19
  from osintkit.scanner import Scanner
18
20
  from osintkit.config import load_config, Config, APIKeys
19
21
  from osintkit.profiles import Profile, ProfileStore, ScanHistory
20
22
 
21
- app = typer.Typer(help="OSINT CLI for personal digital footprint analysis")
23
+ app = typer.Typer(help="OSINT CLI for personal digital footprint analysis", invoke_without_command=True)
22
24
  console = Console()
23
25
  store = ProfileStore()
24
26
  logger = logging.getLogger(__name__)
27
+ _update_thread = None
28
+
29
+
30
+ @app.callback()
31
+ def _startup(ctx: typer.Context):
32
+ """Start background version check on every invocation."""
33
+ global _update_thread
34
+ _update_thread = _start_update_check()
35
+
36
+ # ---- Version update check ----
37
+
38
+ _update_available: Optional[str] = None # Set to newer version string if one exists
39
+
40
+
41
+ def _check_for_update_bg():
42
+ """Check npm registry for a newer version in a background thread (non-blocking)."""
43
+ global _update_available
44
+ try:
45
+ import httpx
46
+ resp = httpx.get(
47
+ "https://registry.npmjs.org/osintkit/latest",
48
+ timeout=3.0,
49
+ headers={"Accept": "application/json"},
50
+ )
51
+ if resp.status_code == 200:
52
+ latest = resp.json().get("version", "")
53
+ if latest and latest != __version__:
54
+ from packaging.version import Version
55
+ if Version(latest) > Version(__version__):
56
+ _update_available = latest
57
+ except Exception:
58
+ pass # Never crash the app over a version check
59
+
60
+
61
+ def _start_update_check():
62
+ """Start the background version check thread."""
63
+ t = threading.Thread(target=_check_for_update_bg, daemon=True)
64
+ t.start()
65
+ return t
66
+
67
+
68
+ def _print_update_notice():
69
+ """Print update notice if a newer version was found."""
70
+ if _update_available:
71
+ console.print(Panel(
72
+ f"[bold cyan]osintkit {_update_available}[/bold cyan] is available "
73
+ f"[dim](you have {__version__})[/dim]\n"
74
+ "[dim]Run:[/dim] [bold]npm install -g osintkit[/bold]",
75
+ title="[bold yellow]⬆ Update available[/bold yellow]",
76
+ border_style="yellow",
77
+ padding=(0, 2),
78
+ ))
25
79
 
26
80
 
27
81
  def _print_ethics_banner():
@@ -123,6 +177,7 @@ api_keys:
123
177
  epieos: ""
124
178
  """
125
179
  config_path.write_text(config_content)
180
+ config_path.chmod(0o600) # API keys must not be world-readable
126
181
  console.print(f"\n[green]✓[/green] Config saved to {config_path}")
127
182
  return True
128
183
 
@@ -288,6 +343,7 @@ api_keys:
288
343
  epieos: ""
289
344
  """
290
345
  config_path.write_text(config_content)
346
+ config_path.chmod(0o600) # API keys must not be world-readable
291
347
  console.print(f"\n[green]✓[/green] Config saved: {config_path}")
292
348
 
293
349
 
@@ -402,6 +458,9 @@ def list():
402
458
 
403
459
  console.print(table)
404
460
  console.print()
461
+ if _update_thread:
462
+ _update_thread.join(timeout=4)
463
+ _print_update_notice()
405
464
 
406
465
 
407
466
  @app.command()
@@ -440,6 +499,7 @@ def refresh(profile_ref: str = typer.Argument(None, help="Profile ID or name")):
440
499
  )
441
500
  store.add_scan_result(profile.id, scan_record)
442
501
  console.print(f"\n[green]✓[/green] Scan saved to profile history")
502
+ _print_update_notice()
443
503
 
444
504
 
445
505
  @app.command()
@@ -605,8 +665,10 @@ def delete(profile_ref: str = typer.Argument(None, help="Profile ID or name")):
605
665
  @app.command()
606
666
  def version():
607
667
  """Show version."""
608
- from osintkit import __version__
668
+ if _update_thread:
669
+ _update_thread.join(timeout=4)
609
670
  console.print(f"osintkit v{__version__}")
671
+ _print_update_notice()
610
672
 
611
673
 
612
674
  if __name__ == "__main__":
@@ -1,4 +1,17 @@
1
- """HIBP k-anonymity password exposure check using SHA1 prefix API."""
1
+ """HIBP k-anonymity password exposure check using SHA1 prefix API.
2
+
3
+ Mock format for tests
4
+ ---------------------
5
+ The API returns plain text, one entry per line: ``HASHSUFFIX:COUNT``
6
+ The suffix is uppercase hex (35 chars = SHA1 40 minus 5-char prefix).
7
+
8
+ Example mock response.text for email "test@example.com":
9
+ sha1 = hashlib.sha1(b"test@example.com").hexdigest().upper()
10
+ prefix, suffix = sha1[:5], sha1[5:]
11
+ mock_text = f"{suffix}:42\\nDEADBEEFCAFE00000000000000000000000:1\\n"
12
+
13
+ The module checks each line for a matching suffix and reads the count.
14
+ """
2
15
 
3
16
  import hashlib
4
17
  import logging
@@ -1,4 +1,22 @@
1
- """Wayback Machine CDX API lookup for email domain and username."""
1
+ """Wayback Machine CDX API lookup for email domain and username.
2
+
3
+ Mock format for tests
4
+ ---------------------
5
+ The CDX API returns JSON: a list of lists where the FIRST row is a header
6
+ and subsequent rows are data. Each row matches the ``fl`` fields requested.
7
+ This module requests ``fl=original,timestamp``, so:
8
+
9
+ [
10
+ ["original", "timestamp"], # header row
11
+ ["http://example.com/page", "20210315120000"], # data row
12
+ ["http://example.com/other", "20200101000000"],
13
+ ]
14
+
15
+ An empty result (or only the header row) is treated as no findings::
16
+
17
+ [] # API returned nothing
18
+ [["original", "timestamp"]] # header only — no data
19
+ """
2
20
 
3
21
  from typing import Any, Dict, List
4
22
 
@@ -54,6 +54,21 @@ class ProfileStore:
54
54
  with open(self.store_path, "w") as f:
55
55
  json.dump(profiles, f, indent=2, default=str)
56
56
 
57
+ def find_duplicate(self, profile: Profile) -> Optional[Profile]:
58
+ """Return first existing profile that shares email, username, or phone with the given profile.
59
+
60
+ Used before create() to warn the user about potential duplicates.
61
+ """
62
+ profiles = self._load()
63
+ for p in profiles.values():
64
+ if profile.email and p.get("email") and profile.email.lower() == p["email"].lower():
65
+ return Profile(**p)
66
+ if profile.username and p.get("username") and profile.username.lower() == p["username"].lower():
67
+ return Profile(**p)
68
+ if profile.phone and p.get("phone") and profile.phone == p["phone"]:
69
+ return Profile(**p)
70
+ return None
71
+
57
72
  def create(self, profile: Profile) -> Profile:
58
73
  """Create a new profile."""
59
74
  profiles = self._load()
package/osintkit/risk.py CHANGED
@@ -32,11 +32,20 @@ def calculate_risk_score(findings: Dict[str, List]) -> int:
32
32
  dark_count = len(findings.get("dark_web", [])) + len(findings.get("paste_sites", []))
33
33
  score += min(15, dark_count * 5)
34
34
 
35
- # Password exposure - typically 0 since we don't have passwords
36
- # If data contains count, scale it
37
- password_data = findings.get("password_exposure", [])
38
- if password_data and isinstance(password_data[0], dict) and password_data[0].get("data", {}).get("count"):
39
- pw_count = password_data[0]["data"]["count"]
40
- score += min(15, pw_count // 1000)
41
-
35
+ # Password / hash exposure (15 points max)
36
+ # Covers both: HIBP full API ("password_exposure") and k-anonymity check ("hibp_kanon")
37
+ pw_score = 0
38
+ for key in ("password_exposure", "hibp_kanon"):
39
+ pw_data = findings.get(key, [])
40
+ if pw_data and isinstance(pw_data[0], dict):
41
+ count = pw_data[0].get("data", {}).get("count", 0)
42
+ if count:
43
+ pw_score = max(pw_score, min(15, count // 1000))
44
+ # Even a single confirmed exposure without a count is worth some points
45
+ if pw_score == 0:
46
+ all_pw = findings.get("password_exposure", []) + findings.get("hibp_kanon", [])
47
+ if all_pw:
48
+ pw_score = 5
49
+ score += pw_score
50
+
42
51
  return min(100, score)
package/osintkit/setup.py CHANGED
@@ -121,11 +121,13 @@ def run_setup_wizard():
121
121
  config_path = config_dir / "config.yaml"
122
122
  with open(config_path, "w") as f:
123
123
  yaml.dump(config, f, default_flow_style=False)
124
-
124
+ config_path.chmod(0o600) # owner read/write only — API keys must not be world-readable
125
+
125
126
  # Create profiles file
126
127
  profiles_path = config_dir / "profiles.json"
127
128
  if not profiles_path.exists():
128
129
  profiles_path.write_text("{}")
130
+ profiles_path.chmod(0o600)
129
131
 
130
132
  console.print(f"\n[green]✓[/green] Config saved to: {config_path}")
131
133
  console.print("[green]✓[/green] Ready to use!")
package/package.json CHANGED
@@ -1,19 +1,21 @@
1
1
  {
2
2
  "name": "osintkit",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "OSINT CLI for personal digital footprint analysis",
5
5
  "bin": {
6
6
  "osintkit": "./bin/osintkit.js"
7
7
  },
8
8
  "scripts": {
9
- "postinstall": "pip3 install -e . --quiet || true"
9
+ "postinstall": "node postinstall.js"
10
10
  },
11
11
  "files": [
12
12
  "bin/",
13
13
  "osintkit/",
14
14
  "pyproject.toml",
15
15
  "requirements.txt",
16
- "README.md"
16
+ "requirements-tools.txt",
17
+ "README.md",
18
+ "postinstall.js"
17
19
  ],
18
20
  "keywords": ["osint", "security", "privacy", "cli", "recon"],
19
21
  "author": "",
package/postinstall.js ADDED
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * osintkit postinstall — installs all Python dependencies automatically.
4
+ *
5
+ * Runs after `npm install -g osintkit`.
6
+ * Installs:
7
+ * 1. Core Python deps (typer, rich, httpx, pydantic, etc.) from requirements.txt
8
+ * 2. Optional OSINT tools (maigret, holehe, sherlock) from requirements-tools.txt
9
+ *
10
+ * Uses --break-system-packages on Linux/macOS where needed (Python 3.11+).
11
+ * Falls back gracefully if pip isn't available.
12
+ */
13
+
14
+ const { execSync } = require('child_process');
15
+ const path = require('path');
16
+ const fs = require('fs');
17
+
18
+ const packageDir = __dirname;
19
+ const req = path.join(packageDir, 'requirements.txt');
20
+ const reqTools = path.join(packageDir, 'requirements-tools.txt');
21
+
22
+ function findPip() {
23
+ for (const bin of ['pip3', 'pip']) {
24
+ try {
25
+ execSync(`${bin} --version`, { stdio: 'ignore' });
26
+ return bin;
27
+ } catch (_) {}
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function pipInstall(pip, requirementsFile, label) {
33
+ if (!fs.existsSync(requirementsFile)) {
34
+ console.log(` ⚠️ ${label}: requirements file not found, skipping`);
35
+ return;
36
+ }
37
+
38
+ const flags = ['-r', requirementsFile, '-q', '--disable-pip-version-check'];
39
+
40
+ // Try with --break-system-packages first (needed on modern Linux/macOS)
41
+ try {
42
+ execSync(`${pip} install ${flags.join(' ')} --break-system-packages`, {
43
+ stdio: 'pipe',
44
+ cwd: packageDir,
45
+ });
46
+ console.log(` ✅ ${label} installed`);
47
+ return;
48
+ } catch (_) {}
49
+
50
+ // Fall back without the flag (works on older systems / virtual envs)
51
+ try {
52
+ execSync(`${pip} install ${flags.join(' ')}`, {
53
+ stdio: 'pipe',
54
+ cwd: packageDir,
55
+ });
56
+ console.log(` ✅ ${label} installed`);
57
+ return;
58
+ } catch (err) {
59
+ console.log(` ⚠️ ${label}: could not install (${err.message.split('\n')[0]})`);
60
+ }
61
+ }
62
+
63
+ console.log('\nosintkit: installing Python dependencies...\n');
64
+
65
+ const pip = findPip();
66
+ if (!pip) {
67
+ console.log(' ⚠️ pip not found — skipping Python dependency install.');
68
+ console.log(' Run manually: pip3 install -r requirements.txt -r requirements-tools.txt');
69
+ process.exit(0);
70
+ }
71
+
72
+ pipInstall(pip, req, 'Core dependencies');
73
+ pipInstall(pip, reqTools, 'OSINT tools (maigret, holehe, sherlock)');
74
+
75
+ console.log('\nosintkit ready. Run: osintkit new\n');
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "osintkit"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "OSINT CLI tool for personal digital footprint analysis"
5
5
  authors = ["Your Name <you@example.com>"]
6
6
  license = "MIT"
@@ -17,6 +17,7 @@ pyyaml = "^6.0.1"
17
17
  pydantic = "^2.5.0"
18
18
  aiofiles = "^23.2.1"
19
19
  phonenumbers = ">=8.13"
20
+ packaging = ">=23.0"
20
21
  questionary = ">=2.0"
21
22
 
22
23
  [tool.poetry.group.dev.dependencies]
@@ -0,0 +1,12 @@
1
+ # Optional subprocess-based OSINT tools used by osintkit modules.
2
+ # Install these to enable social profile enumeration, email account checks,
3
+ # and username search:
4
+ #
5
+ # pip install -r requirements-tools.txt
6
+ #
7
+ # They are not listed as poetry dependencies because they are standalone CLI
8
+ # tools invoked via subprocess rather than imported as Python libraries.
9
+
10
+ maigret>=0.4.4
11
+ holehe>=1.1.9
12
+ sherlock-project>=0.14.3
package/requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
1
  typer[all]>=0.9
2
+ packaging>=23.0
2
3
  rich>=13.7
3
4
  httpx>=0.27
4
5
  pyyaml>=6.0