osintkit 0.1.3 → 0.1.5

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 (57) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +28 -24
  3. package/bin/osintkit.js +32 -20
  4. package/osintkit/__init__.py +1 -1
  5. package/osintkit/__pycache__/__init__.cpython-311.pyc +0 -0
  6. package/osintkit/__pycache__/cli.cpython-311.pyc +0 -0
  7. package/osintkit/__pycache__/config.cpython-311.pyc +0 -0
  8. package/osintkit/__pycache__/profiles.cpython-311.pyc +0 -0
  9. package/osintkit/__pycache__/risk.cpython-311.pyc +0 -0
  10. package/osintkit/__pycache__/scanner.cpython-311.pyc +0 -0
  11. package/osintkit/__pycache__/setup.cpython-311.pyc +0 -0
  12. package/osintkit/cli.py +59 -38
  13. package/osintkit/config.py +13 -0
  14. package/osintkit/modules/__init__.py +10 -0
  15. package/osintkit/modules/__pycache__/__init__.cpython-311.pyc +0 -0
  16. package/osintkit/modules/__pycache__/breach.cpython-311.pyc +0 -0
  17. package/osintkit/modules/__pycache__/brokers.cpython-311.pyc +0 -0
  18. package/osintkit/modules/__pycache__/certs.cpython-311.pyc +0 -0
  19. package/osintkit/modules/__pycache__/dark_web.cpython-311.pyc +0 -0
  20. package/osintkit/modules/__pycache__/gravatar.cpython-311.pyc +0 -0
  21. package/osintkit/modules/__pycache__/harvester.cpython-311.pyc +0 -0
  22. package/osintkit/modules/__pycache__/hibp.cpython-311.pyc +0 -0
  23. package/osintkit/modules/__pycache__/hibp_kanon.cpython-311.pyc +0 -0
  24. package/osintkit/modules/__pycache__/holehe.cpython-311.pyc +0 -0
  25. package/osintkit/modules/__pycache__/libphonenumber_info.cpython-311.pyc +0 -0
  26. package/osintkit/modules/__pycache__/paste.cpython-311.pyc +0 -0
  27. package/osintkit/modules/__pycache__/phone.cpython-311.pyc +0 -0
  28. package/osintkit/modules/__pycache__/sherlock.cpython-311.pyc +0 -0
  29. package/osintkit/modules/__pycache__/social.cpython-311.pyc +0 -0
  30. package/osintkit/modules/__pycache__/wayback.cpython-311.pyc +0 -0
  31. package/osintkit/modules/gravatar.py +0 -0
  32. package/osintkit/modules/libphonenumber_info.py +0 -0
  33. package/osintkit/modules/sherlock.py +0 -0
  34. package/osintkit/modules/stage2/__init__.py +0 -0
  35. package/osintkit/modules/stage2/__pycache__/__init__.cpython-311.pyc +0 -0
  36. package/osintkit/modules/stage2/__pycache__/github_api.cpython-311.pyc +0 -0
  37. package/osintkit/modules/stage2/__pycache__/hunter.cpython-311.pyc +0 -0
  38. package/osintkit/modules/stage2/__pycache__/leakcheck.cpython-311.pyc +0 -0
  39. package/osintkit/modules/stage2/__pycache__/numverify.cpython-311.pyc +0 -0
  40. package/osintkit/modules/stage2/__pycache__/securitytrails.cpython-311.pyc +0 -0
  41. package/osintkit/modules/stage2/github_api.py +12 -9
  42. package/osintkit/modules/stage2/hunter.py +7 -5
  43. package/osintkit/modules/stage2/leakcheck.py +7 -5
  44. package/osintkit/modules/stage2/numverify.py +7 -5
  45. package/osintkit/modules/stage2/securitytrails.py +7 -5
  46. package/osintkit/output/__pycache__/__init__.cpython-311.pyc +0 -0
  47. package/osintkit/output/__pycache__/html_writer.cpython-311.pyc +0 -0
  48. package/osintkit/output/__pycache__/json_writer.cpython-311.pyc +0 -0
  49. package/osintkit/output/__pycache__/md_writer.cpython-311.pyc +0 -0
  50. package/osintkit/output/md_writer.py +0 -0
  51. package/osintkit/output/templates/report.html +313 -50
  52. package/osintkit/scanner.py +25 -2
  53. package/osintkit/setup.py +21 -10
  54. package/package.json +10 -2
  55. package/postinstall.js +69 -50
  56. package/pyproject.toml +1 -1
  57. package/osintkit/__pycache__/__main__.cpython-311.pyc +0 -0
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -33,7 +33,9 @@ osintkit open # View profile details + open latest report
33
33
  | `osintkit refresh [id]` | Re-run scan for a profile |
34
34
  | `osintkit open [id]` | Show profile details and open latest report |
35
35
  | `osintkit export [id]` | Export as JSON or Markdown |
36
- | `osintkit setup` | Configure API keys |
36
+ | `osintkit setup` | Configure API keys interactively (preserves existing keys) |
37
+ | `osintkit config set-key <key> <value>` | Update a single API key without touching others |
38
+ | `osintkit config show` | Show which API keys are set (values hidden) |
37
39
  | `osintkit delete [id]` | Delete a profile |
38
40
  | `osintkit version` | Show version |
39
41
 
@@ -56,19 +58,19 @@ osintkit open # View profile details + open latest report
56
58
  | Data brokers | name/email | Google CSE broker scan |
57
59
  | Dark web | email | Ahmia / public index |
58
60
  | Breach lookup | email | BreachDirectory |
61
+ | GitHub | username | Public profile (always runs, no key needed) |
59
62
 
60
- ### Stage 2 — Optional free API keys, runs first when configured
63
+ ### Stage 2 — Optional API keys, unlocks extra data sources
61
64
 
62
- | Service | Input | Free Tier |
63
- |---------|-------|-----------|
64
- | HaveIBeenPwned | email | Free w/ key |
65
+ | Service | Input | Tier |
66
+ |---------|-------|------|
67
+ | HaveIBeenPwned | email | Paid ($3.50/month) |
65
68
  | 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 |
69
+ | NumVerify | phone | 100/month free |
70
+ | Hunter.io | email | 25/month free |
71
+ | SecurityTrails | domain | Paid |
70
72
 
71
- Stage 2 always runs first when a key is configured. If rate-limited or key missing, falls back to Stage 1 automatically.
73
+ Stage 2 modules only run when a key is configured. If rate-limited, the scan continues gracefully with Stage 1 results — rate-limited modules are shown as yellow, not red.
72
74
 
73
75
  ## Output
74
76
 
@@ -81,23 +83,25 @@ Risk score is 0–100 based on breach exposure, social footprint, data broker li
81
83
 
82
84
  ## API Keys (All Optional)
83
85
 
84
- Run `osintkit setup` to configure. All keys are optional — the tool works without any of them.
86
+ Keys are stored in `~/.osintkit/config.yaml` (permissions: 600).
85
87
 
86
- ```
87
- ~/.osintkit/config.yaml
88
+ ```bash
89
+ osintkit config set-key hunter YOUR_KEY # add or update one key
90
+ osintkit config show # see which keys are set
91
+ osintkit setup # interactive wizard (preserves existing keys)
88
92
  ```
89
93
 
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 |
94
+ | Service | Where to get it | Free? |
95
+ |---------|----------------|-------|
96
+ | HaveIBeenPwned | haveibeenpwned.com/API/Key | Paid ($3.50/mo) |
97
+ | LeakCheck | leakcheck.io | Free tier |
98
+ | NumVerify | numverify.com | 100 req/month free |
99
+ | Hunter.io | hunter.io | 25 req/month free |
100
+ | GitHub | github.com/settings/tokens | Free (raises rate limit) |
101
+ | Intelbase | intelbase.is | 100 req/month free |
102
+ | BreachDirectory | rapidapi.com (search "BreachDirectory") | 50 req/day free |
103
+ | Google CSE | developers.google.com/custom-search | 100 req/day free |
104
+ | SecurityTrails | securitytrails.com | Paid |
101
105
 
102
106
  ## Run from Source
103
107
 
package/bin/osintkit.js CHANGED
@@ -1,42 +1,54 @@
1
1
  #!/usr/bin/env node
2
- const { spawn } = require('child_process');
2
+ const { spawn, spawnSync } = require('child_process');
3
3
  const path = require('path');
4
4
  const fs = require('fs');
5
5
 
6
- // The npm package root is one level up from bin/
7
6
  const packageDir = path.dirname(path.dirname(__filename));
7
+ const isWin = process.platform === 'win32';
8
+
9
+ const venvPython = isWin
10
+ ? path.join(packageDir, '.venv', 'Scripts', 'python.exe')
11
+ : path.join(packageDir, '.venv', 'bin', 'python3');
12
+
13
+ // If venv doesn't exist yet, run postinstall first (self-healing first run)
14
+ if (!fs.existsSync(venvPython)) {
15
+ const postinstall = path.join(packageDir, 'postinstall.js');
16
+ if (fs.existsSync(postinstall)) {
17
+ console.log('osintkit: first run — setting up Python environment...');
18
+ const r = spawnSync(process.execPath, [postinstall], {
19
+ stdio: 'inherit',
20
+ cwd: packageDir,
21
+ });
22
+ if (!fs.existsSync(venvPython)) {
23
+ console.error('\nosintkit: setup failed. Run manually:');
24
+ console.error(` node ${postinstall}`);
25
+ process.exit(1);
26
+ }
27
+ }
28
+ }
8
29
 
9
- // Prefer venv Python (installed by postinstall), fall back to system Python.
10
- // Checks both Unix (.venv/bin/) and Windows (.venv/Scripts/) paths.
11
30
  function findPython() {
31
+ // Prefer venv Python (Unix + Windows)
12
32
  const venvCandidates = [
13
- path.join(packageDir, '.venv', 'bin', 'python3'), // Unix
14
- path.join(packageDir, '.venv', 'bin', 'python'), // Unix fallback
15
- path.join(packageDir, '.venv', 'Scripts', 'python.exe'), // Windows
16
- path.join(packageDir, 'venv', 'bin', 'python3'), // Unix alt name
17
- path.join(packageDir, 'venv', 'bin', 'python'), // Unix alt fallback
18
- path.join(packageDir, 'venv', 'Scripts', 'python.exe'), // Windows alt name
33
+ path.join(packageDir, '.venv', 'bin', 'python3'),
34
+ path.join(packageDir, '.venv', 'bin', 'python'),
35
+ path.join(packageDir, '.venv', 'Scripts', 'python.exe'),
36
+ path.join(packageDir, 'venv', 'bin', 'python3'),
37
+ path.join(packageDir, 'venv', 'Scripts', 'python.exe'),
19
38
  ];
20
39
  for (const p of venvCandidates) {
21
40
  if (fs.existsSync(p)) return p;
22
41
  }
23
-
24
42
  // Fall back to system Python
25
- const systemCandidates = process.platform === 'win32'
26
- ? ['python', 'python3']
27
- : ['python3.11', 'python3', 'python'];
43
+ const systemCandidates = isWin ? ['python', 'python3'] : ['python3.11', 'python3', 'python'];
28
44
  for (const bin of systemCandidates) {
29
- try {
30
- require('child_process').execSync(`${bin} --version`, { stdio: 'ignore' });
31
- return bin;
32
- } catch (_) {}
45
+ const r = spawnSync(bin, ['--version'], { stdio: 'pipe' });
46
+ if (r.status === 0) return bin;
33
47
  }
34
48
  return 'python3';
35
49
  }
36
50
 
37
51
  const pythonBin = findPython();
38
-
39
- // Always set PYTHONPATH so the package is importable regardless of install method
40
52
  const env = {
41
53
  ...process.env,
42
54
  PYTHONPATH: process.env.PYTHONPATH
@@ -1,3 +1,3 @@
1
1
  """osintkit - OSINT CLI tool for personal digital footprint analysis."""
2
2
 
3
- __version__ = "0.1.3"
3
+ __version__ = "0.1.5"
package/osintkit/cli.py CHANGED
@@ -17,8 +17,9 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
17
17
 
18
18
  from osintkit import __version__
19
19
  from osintkit.scanner import Scanner
20
- from osintkit.config import load_config, Config, APIKeys
20
+ from osintkit.config import load_config, save_config, Config, APIKeys
21
21
  from osintkit.profiles import Profile, ProfileStore, ScanHistory
22
+ from osintkit.setup import update_api_key
22
23
 
23
24
  app = typer.Typer(help="OSINT CLI for personal digital footprint analysis", invoke_without_command=True)
24
25
  console = Console()
@@ -291,20 +292,15 @@ def run_scan_for_profile(profile: Profile) -> dict:
291
292
 
292
293
  @app.command()
293
294
  def setup():
294
- """Configure API keys."""
295
+ """Configure API keys. Existing keys are preserved unless a new value is entered."""
295
296
  config_path = Path.home() / ".osintkit" / "config.yaml"
296
-
297
- if config_path.exists():
298
- console.print(f"\n[yellow]Config already exists: {config_path}[/yellow]")
299
- if not Confirm.ask("Overwrite?"):
300
- return
301
-
297
+ existing = load_config(config_path)
298
+
302
299
  console.print("\n[bold cyan]═══ API Key Setup ═══[/bold cyan]\n")
303
- console.print("Enter your API keys (press Enter to skip).\n")
304
-
305
- keys = {}
300
+ console.print("Press [bold]Enter[/bold] to keep an existing key. Type a new value to update it.\n")
301
+
306
302
  api_key_list = [
307
- ("hibp", "HaveIBeenPwned (hibp)", "https://haveibeenpwned.com/API/Key"),
303
+ ("hibp", "HaveIBeenPwned", "https://haveibeenpwned.com/API/Key"),
308
304
  ("breachdirectory", "BreachDirectory", "https://rapidapi.com/"),
309
305
  ("leakcheck", "LeakCheck", "https://leakcheck.io/"),
310
306
  ("intelbase", "Intelbase", "https://intelbase.is/"),
@@ -316,37 +312,62 @@ def setup():
316
312
  ("github", "GitHub Personal Access Token", "https://github.com/settings/tokens"),
317
313
  ("securitytrails", "SecurityTrails", "https://securitytrails.com"),
318
314
  ]
319
-
315
+
316
+ keys_dict = existing.api_keys.model_dump()
317
+
320
318
  for key_name, label, url in api_key_list:
319
+ current = keys_dict.get(key_name, "")
320
+ status = "[green][set][/green]" if current else "[dim][not set][/dim]"
321
321
  hint = f" ({url})" if url else ""
322
- value = Prompt.ask(f"{label}{hint}", default="")
323
- keys[key_name] = value.strip()
324
-
325
- config_path.parent.mkdir(parents=True, exist_ok=True)
326
- config_content = f"""# osintkit Configuration
327
- output_dir: ~/osint-results
328
- timeout_seconds: 120
329
-
330
- api_keys:
331
- hibp: "{keys.get('hibp', '')}"
332
- breachdirectory: "{keys.get('breachdirectory', '')}"
333
- leakcheck: "{keys.get('leakcheck', '')}"
334
- intelbase: "{keys.get('intelbase', '')}"
335
- google_cse_key: "{keys.get('google_cse_key', '')}"
336
- google_cse_cx: "{keys.get('google_cse_cx', '')}"
337
- numverify: "{keys.get('numverify', '')}"
338
- emailrep: "{keys.get('emailrep', '')}"
339
- resend: ""
340
- hunter: "{keys.get('hunter', '')}"
341
- github: "{keys.get('github', '')}"
342
- securitytrails: "{keys.get('securitytrails', '')}"
343
- epieos: ""
344
- """
345
- config_path.write_text(config_content)
346
- config_path.chmod(0o600) # API keys must not be world-readable
322
+ value = Prompt.ask(f" {status} {label}{hint}", default="")
323
+ if value.strip():
324
+ keys_dict[key_name] = value.strip()
325
+
326
+ updated = Config(
327
+ output_dir=existing.output_dir,
328
+ timeout_seconds=existing.timeout_seconds,
329
+ api_keys=APIKeys(**keys_dict),
330
+ )
331
+ save_config(updated, config_path)
347
332
  console.print(f"\n[green]✓[/green] Config saved: {config_path}")
348
333
 
349
334
 
335
+ config_app = typer.Typer(help="Manage osintkit configuration.")
336
+ app.add_typer(config_app, name="config")
337
+
338
+
339
+ @config_app.command("set-key")
340
+ def config_set_key(
341
+ key: str = typer.Argument(..., help="API key name (e.g. github, hunter)"),
342
+ value: str = typer.Argument(..., help="The API key value"),
343
+ ):
344
+ """Update a single API key without touching others."""
345
+ valid_keys = set(APIKeys.model_fields.keys())
346
+ if key not in valid_keys:
347
+ console.print(f"[red]Unknown key '{key}'.[/red]")
348
+ console.print(f"Valid keys: {', '.join(sorted(valid_keys))}")
349
+ raise typer.Exit(1)
350
+ update_api_key(key, value)
351
+
352
+
353
+ @config_app.command("show")
354
+ def config_show():
355
+ """Show which API keys are set (values are not shown)."""
356
+ config_path = Path.home() / ".osintkit" / "config.yaml"
357
+ cfg = load_config(config_path)
358
+ keys_dict = cfg.api_keys.model_dump()
359
+
360
+ table = Table(title="API Keys", show_header=True)
361
+ table.add_column("Key", style="cyan")
362
+ table.add_column("Status")
363
+
364
+ for key_name in sorted(keys_dict.keys()):
365
+ status = "[green]set[/green]" if keys_dict[key_name] else "[dim]not set[/dim]"
366
+ table.add_row(key_name, status)
367
+
368
+ console.print(table)
369
+
370
+
350
371
  @app.command()
351
372
  def new():
352
373
  """Create a new person profile."""
@@ -32,6 +32,19 @@ class Config(BaseModel):
32
32
  api_keys: APIKeys = Field(default_factory=APIKeys)
33
33
 
34
34
 
35
+ def save_config(config: Config, config_path: Path) -> None:
36
+ """Save configuration to YAML file."""
37
+ config_path.parent.mkdir(parents=True, exist_ok=True)
38
+ data = {
39
+ "output_dir": config.output_dir,
40
+ "timeout_seconds": config.timeout_seconds,
41
+ "api_keys": config.api_keys.model_dump(),
42
+ }
43
+ with open(config_path, "w") as f:
44
+ yaml.dump(data, f, default_flow_style=False)
45
+ config_path.chmod(0o600)
46
+
47
+
35
48
  def load_config(config_path: Path) -> Config:
36
49
  """Load configuration from YAML file.
37
50
 
@@ -3,4 +3,14 @@
3
3
 
4
4
  class ModuleError(Exception):
5
5
  """Base exception for module failures."""
6
+ pass
7
+
8
+
9
+ class RateLimitError(ModuleError):
10
+ """Raised when an API rate limit (429) is hit."""
11
+ pass
12
+
13
+
14
+ class InvalidKeyError(ModuleError):
15
+ """Raised when an API key is invalid or unauthorized (401/403)."""
6
16
  pass
File without changes
File without changes
File without changes
File without changes
@@ -4,6 +4,8 @@ from typing import Dict, List
4
4
 
5
5
  import httpx
6
6
 
7
+ from osintkit.modules import RateLimitError, InvalidKeyError
8
+
7
9
 
8
10
  async def run(inputs: dict, api_key: str) -> List[Dict]:
9
11
  """Look up a GitHub user profile via the GitHub REST API.
@@ -23,19 +25,20 @@ async def run(inputs: dict, api_key: str) -> List[Dict]:
23
25
  return []
24
26
 
25
27
  try:
28
+ headers = {"Accept": "application/vnd.github.v3+json"}
29
+ if api_key:
30
+ headers["Authorization"] = f"token {api_key}"
31
+
26
32
  async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, connect=5.0)) as client:
27
33
  response = await client.get(
28
34
  f"https://api.github.com/users/{username}",
29
- headers={
30
- "Authorization": f"token {api_key}",
31
- "Accept": "application/vnd.github.v3+json",
32
- },
35
+ headers=headers,
33
36
  )
34
37
 
35
38
  if response.status_code == 429:
36
- raise Exception("429 rate limited")
39
+ raise RateLimitError("GitHub API rate limit reached")
37
40
  if response.status_code in (401, 403):
38
- raise Exception("401 invalid key")
41
+ raise InvalidKeyError("GitHub token invalid or unauthorized")
39
42
  if response.status_code == 404:
40
43
  return []
41
44
 
@@ -59,7 +62,7 @@ async def run(inputs: dict, api_key: str) -> List[Dict]:
59
62
  "url": data.get("html_url"),
60
63
  }]
61
64
 
62
- except Exception as e:
63
- if "429" in str(e) or "401" in str(e):
64
- raise
65
+ except (RateLimitError, InvalidKeyError):
66
+ raise
67
+ except Exception:
65
68
  return []
@@ -4,6 +4,8 @@ from typing import Dict, List
4
4
 
5
5
  import httpx
6
6
 
7
+ from osintkit.modules import RateLimitError, InvalidKeyError
8
+
7
9
 
8
10
  async def run(inputs: dict, api_key: str) -> List[Dict]:
9
11
  """Verify an email address via Hunter.io email-verifier endpoint.
@@ -30,9 +32,9 @@ async def run(inputs: dict, api_key: str) -> List[Dict]:
30
32
  )
31
33
 
32
34
  if response.status_code == 429:
33
- raise Exception("429 rate limited")
35
+ raise RateLimitError("Hunter.io rate limit reached")
34
36
  if response.status_code in (401, 403):
35
- raise Exception("401 invalid key")
37
+ raise InvalidKeyError("Hunter.io API key invalid or unauthorized")
36
38
 
37
39
  data = response.json()
38
40
  result = data.get("data", {})
@@ -58,7 +60,7 @@ async def run(inputs: dict, api_key: str) -> List[Dict]:
58
60
  "url": None,
59
61
  }]
60
62
 
61
- except Exception as e:
62
- if "429" in str(e) or "401" in str(e):
63
- raise
63
+ except (RateLimitError, InvalidKeyError):
64
+ raise
65
+ except Exception:
64
66
  return []
@@ -4,6 +4,8 @@ from typing import Dict, List
4
4
 
5
5
  import httpx
6
6
 
7
+ from osintkit.modules import RateLimitError, InvalidKeyError
8
+
7
9
 
8
10
  async def run(inputs: dict, api_key: str) -> List[Dict]:
9
11
  """Query LeakCheck.io for email breach records.
@@ -30,9 +32,9 @@ async def run(inputs: dict, api_key: str) -> List[Dict]:
30
32
  )
31
33
 
32
34
  if response.status_code == 429:
33
- raise Exception("429 rate limited")
35
+ raise RateLimitError("LeakCheck rate limit reached")
34
36
  if response.status_code in (401, 403):
35
- raise Exception("401 invalid key")
37
+ raise InvalidKeyError("LeakCheck API key invalid or unauthorized")
36
38
 
37
39
  data = response.json()
38
40
  if not data.get("success") or not data.get("result"):
@@ -52,7 +54,7 @@ async def run(inputs: dict, api_key: str) -> List[Dict]:
52
54
  })
53
55
  return findings
54
56
 
55
- except Exception as e:
56
- if "429" in str(e) or "401" in str(e):
57
- raise
57
+ except (RateLimitError, InvalidKeyError):
58
+ raise
59
+ except Exception:
58
60
  return []
@@ -4,6 +4,8 @@ from typing import Dict, List
4
4
 
5
5
  import httpx
6
6
 
7
+ from osintkit.modules import RateLimitError, InvalidKeyError
8
+
7
9
 
8
10
  async def run(inputs: dict, api_key: str) -> List[Dict]:
9
11
  """Validate a phone number via the NumVerify/apilayer API.
@@ -30,9 +32,9 @@ async def run(inputs: dict, api_key: str) -> List[Dict]:
30
32
  )
31
33
 
32
34
  if response.status_code == 429:
33
- raise Exception("429 rate limited")
35
+ raise RateLimitError("NumVerify rate limit reached")
34
36
  if response.status_code in (401, 403):
35
- raise Exception("401 invalid key")
37
+ raise InvalidKeyError("NumVerify API key invalid or unauthorized")
36
38
 
37
39
  data = response.json()
38
40
  if not data.get("valid") and data.get("error"):
@@ -56,7 +58,7 @@ async def run(inputs: dict, api_key: str) -> List[Dict]:
56
58
  "url": None,
57
59
  }]
58
60
 
59
- except Exception as e:
60
- if "429" in str(e) or "401" in str(e):
61
- raise
61
+ except (RateLimitError, InvalidKeyError):
62
+ raise
63
+ except Exception:
62
64
  return []
@@ -4,6 +4,8 @@ from typing import Dict, List
4
4
 
5
5
  import httpx
6
6
 
7
+ from osintkit.modules import RateLimitError, InvalidKeyError
8
+
7
9
 
8
10
  async def run(inputs: dict, api_key: str) -> List[Dict]:
9
11
  """Enumerate subdomains for the target's email domain via SecurityTrails API.
@@ -35,9 +37,9 @@ async def run(inputs: dict, api_key: str) -> List[Dict]:
35
37
  )
36
38
 
37
39
  if response.status_code == 429:
38
- raise Exception("429 rate limited")
40
+ raise RateLimitError("SecurityTrails rate limit reached")
39
41
  if response.status_code in (401, 403):
40
- raise Exception("401 invalid key")
42
+ raise InvalidKeyError("SecurityTrails API key invalid or unauthorized")
41
43
 
42
44
  data = response.json()
43
45
  subdomains = data.get("subdomains", [])
@@ -59,7 +61,7 @@ async def run(inputs: dict, api_key: str) -> List[Dict]:
59
61
  })
60
62
  return findings
61
63
 
62
- except Exception as e:
63
- if "429" in str(e) or "401" in str(e):
64
- raise
64
+ except (RateLimitError, InvalidKeyError):
65
+ raise
66
+ except Exception:
65
67
  return []
File without changes