osintkit 0.1.4 → 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.
- package/LICENSE +0 -0
- package/README.md +28 -24
- package/osintkit/__init__.py +1 -1
- package/osintkit/__pycache__/__init__.cpython-311.pyc +0 -0
- package/osintkit/__pycache__/cli.cpython-311.pyc +0 -0
- package/osintkit/__pycache__/config.cpython-311.pyc +0 -0
- package/osintkit/__pycache__/profiles.cpython-311.pyc +0 -0
- package/osintkit/__pycache__/risk.cpython-311.pyc +0 -0
- package/osintkit/__pycache__/scanner.cpython-311.pyc +0 -0
- package/osintkit/__pycache__/setup.cpython-311.pyc +0 -0
- package/osintkit/cli.py +59 -38
- package/osintkit/config.py +13 -0
- package/osintkit/modules/__init__.py +10 -0
- package/osintkit/modules/__pycache__/__init__.cpython-311.pyc +0 -0
- package/osintkit/modules/__pycache__/breach.cpython-311.pyc +0 -0
- package/osintkit/modules/__pycache__/brokers.cpython-311.pyc +0 -0
- package/osintkit/modules/__pycache__/certs.cpython-311.pyc +0 -0
- package/osintkit/modules/__pycache__/dark_web.cpython-311.pyc +0 -0
- package/osintkit/modules/__pycache__/gravatar.cpython-311.pyc +0 -0
- package/osintkit/modules/__pycache__/harvester.cpython-311.pyc +0 -0
- package/osintkit/modules/__pycache__/hibp.cpython-311.pyc +0 -0
- package/osintkit/modules/__pycache__/hibp_kanon.cpython-311.pyc +0 -0
- package/osintkit/modules/__pycache__/holehe.cpython-311.pyc +0 -0
- package/osintkit/modules/__pycache__/libphonenumber_info.cpython-311.pyc +0 -0
- package/osintkit/modules/__pycache__/paste.cpython-311.pyc +0 -0
- package/osintkit/modules/__pycache__/phone.cpython-311.pyc +0 -0
- package/osintkit/modules/__pycache__/sherlock.cpython-311.pyc +0 -0
- package/osintkit/modules/__pycache__/social.cpython-311.pyc +0 -0
- package/osintkit/modules/__pycache__/wayback.cpython-311.pyc +0 -0
- package/osintkit/modules/gravatar.py +0 -0
- package/osintkit/modules/libphonenumber_info.py +0 -0
- package/osintkit/modules/sherlock.py +0 -0
- package/osintkit/modules/stage2/__init__.py +0 -0
- package/osintkit/modules/stage2/__pycache__/__init__.cpython-311.pyc +0 -0
- package/osintkit/modules/stage2/__pycache__/github_api.cpython-311.pyc +0 -0
- package/osintkit/modules/stage2/__pycache__/hunter.cpython-311.pyc +0 -0
- package/osintkit/modules/stage2/__pycache__/leakcheck.cpython-311.pyc +0 -0
- package/osintkit/modules/stage2/__pycache__/numverify.cpython-311.pyc +0 -0
- package/osintkit/modules/stage2/__pycache__/securitytrails.cpython-311.pyc +0 -0
- package/osintkit/modules/stage2/github_api.py +12 -9
- package/osintkit/modules/stage2/hunter.py +7 -5
- package/osintkit/modules/stage2/leakcheck.py +7 -5
- package/osintkit/modules/stage2/numverify.py +7 -5
- package/osintkit/modules/stage2/securitytrails.py +7 -5
- package/osintkit/output/__pycache__/__init__.cpython-311.pyc +0 -0
- package/osintkit/output/__pycache__/html_writer.cpython-311.pyc +0 -0
- package/osintkit/output/__pycache__/json_writer.cpython-311.pyc +0 -0
- package/osintkit/output/__pycache__/md_writer.cpython-311.pyc +0 -0
- package/osintkit/output/md_writer.py +0 -0
- package/osintkit/output/templates/report.html +313 -50
- package/osintkit/scanner.py +25 -2
- package/osintkit/setup.py +21 -10
- package/package.json +10 -2
- package/pyproject.toml +1 -1
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
|
|
63
|
+
### Stage 2 — Optional API keys, unlocks extra data sources
|
|
61
64
|
|
|
62
|
-
| Service | Input |
|
|
63
|
-
|
|
64
|
-
| HaveIBeenPwned | email |
|
|
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 |
|
|
68
|
-
|
|
|
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
|
|
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
|
-
|
|
86
|
+
Keys are stored in `~/.osintkit/config.yaml` (permissions: 600).
|
|
85
87
|
|
|
86
|
-
```
|
|
87
|
-
|
|
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 |
|
|
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/osintkit/__init__.py
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
output_dir
|
|
328
|
-
timeout_seconds
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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."""
|
package/osintkit/config.py
CHANGED
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
39
|
+
raise RateLimitError("GitHub API rate limit reached")
|
|
37
40
|
if response.status_code in (401, 403):
|
|
38
|
-
raise
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
35
|
+
raise RateLimitError("Hunter.io rate limit reached")
|
|
34
36
|
if response.status_code in (401, 403):
|
|
35
|
-
raise
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
35
|
+
raise RateLimitError("LeakCheck rate limit reached")
|
|
34
36
|
if response.status_code in (401, 403):
|
|
35
|
-
raise
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
35
|
+
raise RateLimitError("NumVerify rate limit reached")
|
|
34
36
|
if response.status_code in (401, 403):
|
|
35
|
-
raise
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
40
|
+
raise RateLimitError("SecurityTrails rate limit reached")
|
|
39
41
|
if response.status_code in (401, 403):
|
|
40
|
-
raise
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
except (RateLimitError, InvalidKeyError):
|
|
65
|
+
raise
|
|
66
|
+
except Exception:
|
|
65
67
|
return []
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
@@ -3,72 +3,335 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>osintkit Report</title>
|
|
6
|
+
<title>osintkit Report — {{ inputs.name or inputs.email or inputs.username or 'Unknown' }}</title>
|
|
7
7
|
<style>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.
|
|
14
|
-
.
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
.
|
|
20
|
-
.
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body { font-family: system-ui, -apple-system, sans-serif; background: #f5f5f5; color: #222; }
|
|
10
|
+
.container { max-width: 900px; margin: 0 auto; padding: 30px 20px; }
|
|
11
|
+
|
|
12
|
+
/* Header */
|
|
13
|
+
.header { background: #1a1a2e; color: white; padding: 30px; border-radius: 10px; margin-bottom: 24px; }
|
|
14
|
+
.header h1 { font-size: 1.4rem; font-weight: 600; margin-bottom: 4px; opacity: 0.7; }
|
|
15
|
+
.header .target { font-size: 1.8rem; font-weight: 700; margin-bottom: 12px; }
|
|
16
|
+
.header .meta { font-size: 0.85rem; opacity: 0.6; }
|
|
17
|
+
|
|
18
|
+
/* Risk score */
|
|
19
|
+
.risk-block { border-radius: 10px; padding: 24px 30px; margin-bottom: 24px; display: flex; align-items: center; gap: 30px; }
|
|
20
|
+
.risk-high { background: #fff0f0; border: 2px solid #e53e3e; }
|
|
21
|
+
.risk-medium { background: #fffbeb; border: 2px solid #d69e2e; }
|
|
22
|
+
.risk-low { background: #f0fff4; border: 2px solid #38a169; }
|
|
23
|
+
.risk-score { font-size: 3.5rem; font-weight: 800; line-height: 1; }
|
|
24
|
+
.risk-high .risk-score { color: #c53030; }
|
|
25
|
+
.risk-medium .risk-score { color: #b7791f; }
|
|
26
|
+
.risk-low .risk-score { color: #276749; }
|
|
27
|
+
.risk-label { font-size: 1.1rem; font-weight: 600; margin-bottom: 4px; }
|
|
28
|
+
.risk-desc { font-size: 0.9rem; opacity: 0.8; max-width: 500px; }
|
|
29
|
+
|
|
30
|
+
/* Score bar */
|
|
31
|
+
.score-bar-wrap { flex: 1; }
|
|
32
|
+
.score-bar-bg { background: #e2e8f0; border-radius: 999px; height: 12px; overflow: hidden; margin-top: 8px; }
|
|
33
|
+
.score-bar-fill { height: 100%; border-radius: 999px; transition: width 0.5s; }
|
|
34
|
+
.risk-high .score-bar-fill { background: #e53e3e; }
|
|
35
|
+
.risk-medium .score-bar-fill { background: #d69e2e; }
|
|
36
|
+
.risk-low .score-bar-fill { background: #38a169; }
|
|
37
|
+
|
|
38
|
+
/* Risk breakdown */
|
|
39
|
+
.breakdown { background: white; border-radius: 10px; padding: 20px 24px; margin-bottom: 24px; }
|
|
40
|
+
.breakdown h2 { font-size: 1rem; font-weight: 600; margin-bottom: 14px; color: #555; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
41
|
+
.breakdown-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; }
|
|
42
|
+
.breakdown-item { background: #f7f7f7; border-radius: 8px; padding: 12px; text-align: center; }
|
|
43
|
+
.breakdown-item .bi-val { font-size: 1.6rem; font-weight: 700; color: #2d3748; }
|
|
44
|
+
.breakdown-item .bi-label { font-size: 0.78rem; color: #718096; margin-top: 2px; }
|
|
45
|
+
|
|
46
|
+
/* Section card */
|
|
47
|
+
.card { background: white; border-radius: 10px; padding: 24px; margin-bottom: 20px; }
|
|
48
|
+
.card h2 { font-size: 1.05rem; font-weight: 700; margin-bottom: 16px; padding-bottom: 10px; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 8px; }
|
|
49
|
+
.badge { font-size: 0.75rem; padding: 2px 8px; border-radius: 999px; font-weight: 600; }
|
|
50
|
+
.badge-found { background: #fed7d7; color: #c53030; }
|
|
51
|
+
.badge-none { background: #e2e8f0; color: #4a5568; }
|
|
52
|
+
.badge-failed { background: #feebc8; color: #c05621; }
|
|
53
|
+
|
|
54
|
+
/* Finding items */
|
|
55
|
+
.finding { border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px; margin-bottom: 12px; }
|
|
56
|
+
.finding-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
|
|
57
|
+
.finding-type { font-weight: 600; font-size: 0.95rem; }
|
|
58
|
+
.finding-source { font-size: 0.8rem; color: #718096; background: #edf2f7; padding: 2px 8px; border-radius: 4px; white-space: nowrap; }
|
|
59
|
+
.finding-explanation { font-size: 0.88rem; color: #4a5568; margin-bottom: 10px; line-height: 1.5; }
|
|
60
|
+
.finding-data { background: #f7fafc; border-radius: 6px; padding: 10px 14px; font-size: 0.85rem; }
|
|
61
|
+
.finding-data table { width: 100%; border-collapse: collapse; }
|
|
62
|
+
.finding-data td { padding: 3px 8px 3px 0; vertical-align: top; }
|
|
63
|
+
.finding-data td:first-child { color: #718096; white-space: nowrap; padding-right: 16px; width: 1%; font-size: 0.82rem; }
|
|
64
|
+
.finding-data td a { color: #3182ce; word-break: break-all; }
|
|
65
|
+
.finding-url { margin-top: 8px; font-size: 0.85rem; }
|
|
66
|
+
.finding-url a { color: #3182ce; }
|
|
67
|
+
|
|
68
|
+
/* Module grid */
|
|
69
|
+
.module-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; }
|
|
70
|
+
.module-card { border-radius: 8px; padding: 12px 14px; border: 1px solid transparent; }
|
|
71
|
+
.module-card .mc-name { font-weight: 600; font-size: 0.88rem; margin-bottom: 3px; }
|
|
72
|
+
.module-card .mc-detail { font-size: 0.8rem; opacity: 0.8; }
|
|
73
|
+
.mc-done { background: #f0fff4; border-color: #9ae6b4; }
|
|
74
|
+
.mc-failed { background: #fffaf0; border-color: #fbd38d; }
|
|
75
|
+
.mc-skipped { background: #f7fafc; border-color: #e2e8f0; color: #a0aec0; }
|
|
76
|
+
|
|
77
|
+
/* Recommendations */
|
|
78
|
+
.rec-item { display: flex; gap: 12px; padding: 14px 0; border-bottom: 1px solid #f0f0f0; }
|
|
79
|
+
.rec-item:last-child { border-bottom: none; }
|
|
80
|
+
.rec-icon { font-size: 1.3rem; flex-shrink: 0; margin-top: 1px; }
|
|
81
|
+
.rec-title { font-weight: 600; font-size: 0.92rem; margin-bottom: 3px; }
|
|
82
|
+
.rec-body { font-size: 0.87rem; color: #4a5568; line-height: 1.5; }
|
|
83
|
+
|
|
84
|
+
/* Inputs summary */
|
|
85
|
+
.inputs-row { display: flex; flex-wrap: wrap; gap: 20px; }
|
|
86
|
+
.input-item .input-label { font-size: 0.78rem; color: #718096; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
87
|
+
.input-item .input-val { font-size: 0.95rem; font-weight: 500; }
|
|
88
|
+
|
|
89
|
+
.empty-state { color: #a0aec0; font-size: 0.9rem; text-align: center; padding: 20px; }
|
|
21
90
|
</style>
|
|
22
91
|
</head>
|
|
23
92
|
<body>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
93
|
+
<div class="container">
|
|
94
|
+
|
|
95
|
+
<!-- Header -->
|
|
96
|
+
<div class="header">
|
|
97
|
+
<h1>osintkit · OSINT Report</h1>
|
|
98
|
+
<div class="target">{{ inputs.name or inputs.email or inputs.username or inputs.phone or 'Unknown Target' }}</div>
|
|
99
|
+
<div class="meta">Scanned: {{ scan_date[:19]|replace('T', ' ') if scan_date else '—' }}</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<!-- Target info -->
|
|
103
|
+
<div class="card">
|
|
104
|
+
<h2>Target Information</h2>
|
|
105
|
+
<div class="inputs-row">
|
|
106
|
+
{% if inputs.name %}
|
|
107
|
+
<div class="input-item"><div class="input-label">Name</div><div class="input-val">{{ inputs.name }}</div></div>
|
|
108
|
+
{% endif %}
|
|
109
|
+
{% if inputs.email %}
|
|
110
|
+
<div class="input-item"><div class="input-label">Email</div><div class="input-val">{{ inputs.email }}</div></div>
|
|
111
|
+
{% endif %}
|
|
112
|
+
{% if inputs.username %}
|
|
113
|
+
<div class="input-item"><div class="input-label">Username</div><div class="input-val">{{ inputs.username }}</div></div>
|
|
114
|
+
{% endif %}
|
|
115
|
+
{% if inputs.phone %}
|
|
116
|
+
<div class="input-item"><div class="input-label">Phone</div><div class="input-val">{{ inputs.phone }}</div></div>
|
|
117
|
+
{% endif %}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<!-- Risk score -->
|
|
28
122
|
{% set score = risk_score %}
|
|
29
123
|
{% if score >= 70 %}
|
|
30
|
-
<div class="risk-
|
|
124
|
+
<div class="risk-block risk-high">
|
|
125
|
+
{% set risk_label = "High Risk" %}
|
|
126
|
+
{% set risk_desc = "Significant personal data is publicly exposed. Multiple findings across breaches, social media, and data brokers. Review and act on recommendations below." %}
|
|
31
127
|
{% elif score >= 40 %}
|
|
32
|
-
<div class="risk-
|
|
128
|
+
<div class="risk-block risk-medium">
|
|
129
|
+
{% set risk_label = "Medium Risk" %}
|
|
130
|
+
{% set risk_desc = "Some personal data is publicly accessible. A moderate digital footprint was found. Consider reviewing the findings and taking action on the most sensitive ones." %}
|
|
33
131
|
{% else %}
|
|
34
|
-
<div class="risk-
|
|
132
|
+
<div class="risk-block risk-low">
|
|
133
|
+
{% set risk_label = "Low Risk" %}
|
|
134
|
+
{% set risk_desc = "Limited public exposure found. Your digital footprint appears small. Review any findings below to make sure nothing unexpected was discovered." %}
|
|
35
135
|
{% endif %}
|
|
36
|
-
<
|
|
136
|
+
<div>
|
|
137
|
+
<div class="risk-score">{{ score }}</div>
|
|
138
|
+
<div style="font-size:0.8rem;opacity:0.6;margin-top:2px;">out of 100</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div style="flex:1">
|
|
141
|
+
<div class="risk-label">{{ risk_label }}</div>
|
|
142
|
+
<div class="risk-desc">{{ risk_desc }}</div>
|
|
143
|
+
<div class="score-bar-wrap">
|
|
144
|
+
<div class="score-bar-bg"><div class="score-bar-fill" style="width:{{ score }}%"></div></div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
37
147
|
</div>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
148
|
+
|
|
149
|
+
<!-- Score breakdown -->
|
|
150
|
+
{% set breach_count = findings.get('breach_exposure', [])|length %}
|
|
151
|
+
{% set social_count = (findings.get('social_profiles', [])|length) + (findings.get('sherlock', [])|length) %}
|
|
152
|
+
{% set broker_count = findings.get('data_brokers', [])|length %}
|
|
153
|
+
{% set dark_count = (findings.get('dark_web', [])|length) + (findings.get('paste_sites', [])|length) %}
|
|
154
|
+
{% set pw_count = (findings.get('password_exposure', [])|length) + (findings.get('hibp_kanon', [])|length) %}
|
|
155
|
+
{% set total_findings = findings.values()|map('length')|sum %}
|
|
156
|
+
|
|
157
|
+
<div class="breakdown">
|
|
158
|
+
<h2>Score Breakdown</h2>
|
|
159
|
+
<div class="breakdown-grid">
|
|
160
|
+
<div class="breakdown-item">
|
|
161
|
+
<div class="bi-val">{{ total_findings }}</div>
|
|
162
|
+
<div class="bi-label">Total findings</div>
|
|
51
163
|
</div>
|
|
52
|
-
|
|
164
|
+
<div class="breakdown-item">
|
|
165
|
+
<div class="bi-val">{{ breach_count }}</div>
|
|
166
|
+
<div class="bi-label">Data breaches</div>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="breakdown-item">
|
|
169
|
+
<div class="bi-val">{{ social_count }}</div>
|
|
170
|
+
<div class="bi-label">Social profiles</div>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="breakdown-item">
|
|
173
|
+
<div class="bi-val">{{ broker_count }}</div>
|
|
174
|
+
<div class="bi-label">Data broker listings</div>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="breakdown-item">
|
|
177
|
+
<div class="bi-val">{{ dark_count }}</div>
|
|
178
|
+
<div class="bi-label">Dark web / paste sites</div>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="breakdown-item">
|
|
181
|
+
<div class="bi-val">{{ pw_count }}</div>
|
|
182
|
+
<div class="bi-label">Password exposures</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
53
185
|
</div>
|
|
54
|
-
|
|
55
|
-
|
|
186
|
+
|
|
187
|
+
<!-- Findings by module -->
|
|
188
|
+
{% set type_explanations = {
|
|
189
|
+
'social_profile': 'An account linked to this username was found on a public platform. This means the username is registered and potentially active there.',
|
|
190
|
+
'email_profile': 'This email address is linked to a public profile. Other people can potentially find your name, photo, or other info via this address.',
|
|
191
|
+
'email_account': 'This email was used to register on this platform. The registration is detectable without logging in.',
|
|
192
|
+
'password_exposure':'This email or password has appeared in a known data breach. If you reuse passwords, those accounts may be at risk.',
|
|
193
|
+
'breach': 'Your email or data was found in a breach database. The leaked data may include passwords, personal info, or other sensitive fields.',
|
|
194
|
+
'web_archive': 'A historical snapshot of content related to this target was found in web archives. Old pages may contain data you thought was deleted.',
|
|
195
|
+
'certificate': 'An SSL certificate was issued for a domain linked to this target. This can reveal subdomains, services, or infrastructure.',
|
|
196
|
+
'phone_info': 'Technical information about this phone number was retrieved. This includes carrier, region, and line type — without contacting the number.',
|
|
197
|
+
'paste': 'Content containing this target\'s data was found on a paste site. Paste sites are often used to dump leaked credentials or personal data.',
|
|
198
|
+
'dark_web': 'A reference to this target was found on a dark web index or public darknet search. This may indicate leaked data or illicit activity.',
|
|
199
|
+
'data_broker': 'This target appears in a data broker listing. Data brokers collect and sell personal information including name, address, and relatives.',
|
|
200
|
+
'web_presence': 'The email domain or username was found in web crawl data, subdomains, or public web content.',
|
|
201
|
+
} %}
|
|
202
|
+
|
|
56
203
|
{% for module_name, module_findings in findings.items() %}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
204
|
+
{% if module_findings %}
|
|
205
|
+
<div class="card">
|
|
206
|
+
<h2>
|
|
207
|
+
{{ module_name | replace('_', ' ') | title }}
|
|
208
|
+
<span class="badge badge-found">{{ module_findings|length }} found</span>
|
|
209
|
+
</h2>
|
|
210
|
+
{% for finding in module_findings %}
|
|
211
|
+
<div class="finding">
|
|
212
|
+
<div class="finding-header">
|
|
213
|
+
<div class="finding-type">{{ finding.type | replace('_', ' ') | title }}</div>
|
|
214
|
+
<div class="finding-source">via {{ finding.source }}</div>
|
|
215
|
+
</div>
|
|
216
|
+
{% if finding.type in type_explanations %}
|
|
217
|
+
<div class="finding-explanation">{{ type_explanations[finding.type] }}</div>
|
|
218
|
+
{% endif %}
|
|
219
|
+
{% if finding.data %}
|
|
220
|
+
<div class="finding-data">
|
|
221
|
+
<table>
|
|
222
|
+
{% for key, val in finding.data.items() %}
|
|
223
|
+
{% if val and val != '' and val != 'unknown' and key != 'note' %}
|
|
224
|
+
<tr>
|
|
225
|
+
<td>{{ key | replace('_', ' ') }}</td>
|
|
226
|
+
<td>
|
|
227
|
+
{% if val is string and val.startswith('http') %}
|
|
228
|
+
<a href="{{ val }}" target="_blank">{{ val }}</a>
|
|
229
|
+
{% else %}
|
|
230
|
+
{{ val }}
|
|
66
231
|
{% endif %}
|
|
67
|
-
|
|
68
|
-
|
|
232
|
+
</td>
|
|
233
|
+
</tr>
|
|
234
|
+
{% endif %}
|
|
69
235
|
{% endfor %}
|
|
236
|
+
{% if finding.data.get('note') %}
|
|
237
|
+
<tr><td colspan="2" style="color:#718096;font-style:italic;padding-top:6px;">{{ finding.data.note }}</td></tr>
|
|
238
|
+
{% endif %}
|
|
239
|
+
</table>
|
|
70
240
|
</div>
|
|
71
|
-
|
|
241
|
+
{% endif %}
|
|
242
|
+
{% if finding.url %}
|
|
243
|
+
<div class="finding-url">🔗 <a href="{{ finding.url }}" target="_blank">{{ finding.url }}</a></div>
|
|
244
|
+
{% endif %}
|
|
245
|
+
</div>
|
|
246
|
+
{% endfor %}
|
|
247
|
+
</div>
|
|
248
|
+
{% endif %}
|
|
72
249
|
{% endfor %}
|
|
250
|
+
|
|
251
|
+
<!-- Recommendations -->
|
|
252
|
+
{% if total_findings > 0 %}
|
|
253
|
+
<div class="card">
|
|
254
|
+
<h2>What You Should Do</h2>
|
|
255
|
+
{% if pw_count > 0 %}
|
|
256
|
+
<div class="rec-item">
|
|
257
|
+
<div class="rec-icon">🔑</div>
|
|
258
|
+
<div>
|
|
259
|
+
<div class="rec-title">Change passwords for affected accounts</div>
|
|
260
|
+
<div class="rec-body">Your email appeared in one or more breach databases. Change the password for any account using this email, especially if you reuse passwords. Use a password manager to keep them unique.</div>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
{% endif %}
|
|
264
|
+
{% if breach_count > 0 %}
|
|
265
|
+
<div class="rec-item">
|
|
266
|
+
<div class="rec-icon">🛡️</div>
|
|
267
|
+
<div>
|
|
268
|
+
<div class="rec-title">Enable two-factor authentication</div>
|
|
269
|
+
<div class="rec-body">Your data was found in breach records. Enable 2FA on your email, banking, and any other important accounts to prevent unauthorized access even if passwords are known.</div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
{% endif %}
|
|
273
|
+
{% if broker_count > 0 %}
|
|
274
|
+
<div class="rec-item">
|
|
275
|
+
<div class="rec-icon">🗑️</div>
|
|
276
|
+
<div>
|
|
277
|
+
<div class="rec-title">Request removal from data brokers</div>
|
|
278
|
+
<div class="rec-body">Your information appears in data broker listings. You can request removal directly from each broker. Services like DeleteMe or Incogni can automate this across hundreds of brokers.</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
{% endif %}
|
|
282
|
+
{% if social_count > 0 %}
|
|
283
|
+
<div class="rec-item">
|
|
284
|
+
<div class="rec-icon">👤</div>
|
|
285
|
+
<div>
|
|
286
|
+
<div class="rec-title">Review your social media privacy settings</div>
|
|
287
|
+
<div class="rec-body">{{ social_count }} social profile(s) were found. Check the privacy settings on each platform — restrict who can see your posts, contact info, and friend/follower lists.</div>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
{% endif %}
|
|
291
|
+
{% if dark_count > 0 %}
|
|
292
|
+
<div class="rec-item">
|
|
293
|
+
<div class="rec-icon">⚠️</div>
|
|
294
|
+
<div>
|
|
295
|
+
<div class="rec-title">Monitor for identity fraud</div>
|
|
296
|
+
<div class="rec-body">References to your data were found on dark web indexes or paste sites. Consider placing a credit freeze with the major credit bureaus and setting up identity monitoring alerts.</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
{% endif %}
|
|
300
|
+
<div class="rec-item">
|
|
301
|
+
<div class="rec-icon">🔄</div>
|
|
302
|
+
<div>
|
|
303
|
+
<div class="rec-title">Re-scan periodically</div>
|
|
304
|
+
<div class="rec-body">Your digital footprint changes over time as new breaches occur and data spreads. Run <code>osintkit refresh</code> every few months to stay up to date.</div>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
{% endif %}
|
|
309
|
+
|
|
310
|
+
<!-- Module status -->
|
|
311
|
+
<div class="card">
|
|
312
|
+
<h2>Modules Run</h2>
|
|
313
|
+
<div class="module-grid">
|
|
314
|
+
{% for module_name, module_data in modules.items() %}
|
|
315
|
+
<div class="module-card mc-{{ module_data.status }}">
|
|
316
|
+
<div class="mc-name">{{ module_name | replace('_', ' ') | title }}</div>
|
|
317
|
+
<div class="mc-detail">
|
|
318
|
+
{% if module_data.status == 'done' %}
|
|
319
|
+
✓ {{ module_data.count }} finding{{ 's' if module_data.count != 1 else '' }}
|
|
320
|
+
{% elif module_data.status == 'failed' %}
|
|
321
|
+
⚠ {{ module_data.error | truncate(40) }}
|
|
322
|
+
{% else %}
|
|
323
|
+
— skipped
|
|
324
|
+
{% endif %}
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
{% endfor %}
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<div style="text-align:center;color:#a0aec0;font-size:0.8rem;margin-top:20px;padding-bottom:20px;">
|
|
332
|
+
Generated by osintkit · Only use on targets you have permission to investigate · GDPR applies to EU subjects
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
</div>
|
|
73
336
|
</body>
|
|
74
337
|
</html>
|
package/osintkit/scanner.py
CHANGED
|
@@ -8,6 +8,7 @@ from rich.console import Console
|
|
|
8
8
|
from rich.progress import Progress
|
|
9
9
|
|
|
10
10
|
from osintkit.config import Config
|
|
11
|
+
from osintkit.modules import RateLimitError, InvalidKeyError
|
|
11
12
|
from osintkit.output.json_writer import write_json
|
|
12
13
|
from osintkit.output.html_writer import write_html
|
|
13
14
|
from osintkit.output.md_writer import write_md
|
|
@@ -42,6 +43,7 @@ class Scanner:
|
|
|
42
43
|
("wayback", self._run_wayback, "Wayback Machine"),
|
|
43
44
|
("phone_info", self._run_phone_info, "Phone analysis"),
|
|
44
45
|
("hibp_kanon", self._run_hibp_kanon, "Password k-anonymity check"),
|
|
46
|
+
("github_api", self._run_stage2_github, "GitHub profile"), # always runs; token optional
|
|
45
47
|
]
|
|
46
48
|
|
|
47
49
|
# Stage 2 modules — only included when corresponding API key is set
|
|
@@ -50,7 +52,6 @@ class Scanner:
|
|
|
50
52
|
("leakcheck", api_keys.leakcheck, self._run_stage2_leakcheck, "LeakCheck breach lookup"),
|
|
51
53
|
("hunter", api_keys.hunter, self._run_stage2_hunter, "Hunter email verify"),
|
|
52
54
|
("numverify", api_keys.numverify, self._run_stage2_numverify, "NumVerify phone"),
|
|
53
|
-
("github_api", api_keys.github, self._run_stage2_github, "GitHub profile"),
|
|
54
55
|
(
|
|
55
56
|
"securitytrails",
|
|
56
57
|
api_keys.securitytrails,
|
|
@@ -166,6 +167,12 @@ class Scanner:
|
|
|
166
167
|
result = await func(inputs)
|
|
167
168
|
findings["modules"][name] = {"status": "done", "count": len(result)}
|
|
168
169
|
findings["findings"][name] = result
|
|
170
|
+
except RateLimitError as e:
|
|
171
|
+
findings["modules"][name] = {"status": "rate_limited", "error": str(e)}
|
|
172
|
+
findings["findings"][name] = []
|
|
173
|
+
except InvalidKeyError as e:
|
|
174
|
+
findings["modules"][name] = {"status": "invalid_key", "error": str(e)}
|
|
175
|
+
findings["findings"][name] = []
|
|
169
176
|
except Exception as e:
|
|
170
177
|
findings["modules"][name] = {"status": "failed", "error": str(e)}
|
|
171
178
|
findings["findings"][name] = []
|
|
@@ -203,13 +210,29 @@ class Scanner:
|
|
|
203
210
|
completed=True,
|
|
204
211
|
description=f"[green]done {task_info['desc']} ({len(result)})[/green]",
|
|
205
212
|
)
|
|
213
|
+
except RateLimitError as e:
|
|
214
|
+
findings["modules"][name] = {"status": "rate_limited", "error": str(e)}
|
|
215
|
+
findings["findings"][name] = []
|
|
216
|
+
progress.update(
|
|
217
|
+
task_info["task_id"],
|
|
218
|
+
completed=True,
|
|
219
|
+
description=f"[yellow]rate limited {task_info['desc']}[/yellow]",
|
|
220
|
+
)
|
|
221
|
+
except InvalidKeyError as e:
|
|
222
|
+
findings["modules"][name] = {"status": "invalid_key", "error": str(e)}
|
|
223
|
+
findings["findings"][name] = []
|
|
224
|
+
progress.update(
|
|
225
|
+
task_info["task_id"],
|
|
226
|
+
completed=True,
|
|
227
|
+
description=f"[yellow]invalid key {task_info['desc']}[/yellow]",
|
|
228
|
+
)
|
|
206
229
|
except Exception as e:
|
|
207
230
|
findings["modules"][name] = {"status": "failed", "error": str(e)}
|
|
208
231
|
findings["findings"][name] = []
|
|
209
232
|
progress.update(
|
|
210
233
|
task_info["task_id"],
|
|
211
234
|
completed=True,
|
|
212
|
-
description=f"[
|
|
235
|
+
description=f"[red]failed {task_info['desc']}[/red]",
|
|
213
236
|
)
|
|
214
237
|
|
|
215
238
|
async def main():
|
package/osintkit/setup.py
CHANGED
|
@@ -108,19 +108,29 @@ def run_setup_wizard():
|
|
|
108
108
|
"google_cse_cx": "",
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
# Save config
|
|
111
|
+
# Save config — merge with existing keys so we never wipe anything
|
|
112
112
|
config_dir = Path.home() / ".osintkit"
|
|
113
113
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
114
|
-
|
|
115
|
-
config = {
|
|
116
|
-
"output_dir": "~/osint-results",
|
|
117
|
-
"timeout_seconds": 120,
|
|
118
|
-
"api_keys": api_keys,
|
|
119
|
-
}
|
|
120
|
-
|
|
121
114
|
config_path = config_dir / "config.yaml"
|
|
115
|
+
|
|
116
|
+
if config_path.exists():
|
|
117
|
+
with open(config_path) as f:
|
|
118
|
+
existing = yaml.safe_load(f) or {}
|
|
119
|
+
existing_keys = existing.get("api_keys", {})
|
|
120
|
+
else:
|
|
121
|
+
existing = {"output_dir": "~/osint-results", "timeout_seconds": 120}
|
|
122
|
+
existing_keys = {}
|
|
123
|
+
|
|
124
|
+
# Only overwrite keys the user actually provided (non-empty)
|
|
125
|
+
for k, v in api_keys.items():
|
|
126
|
+
if v:
|
|
127
|
+
existing_keys[k] = v
|
|
128
|
+
elif k not in existing_keys:
|
|
129
|
+
existing_keys[k] = ""
|
|
130
|
+
|
|
131
|
+
existing["api_keys"] = existing_keys
|
|
122
132
|
with open(config_path, "w") as f:
|
|
123
|
-
yaml.dump(
|
|
133
|
+
yaml.dump(existing, f, default_flow_style=False)
|
|
124
134
|
config_path.chmod(0o600) # owner read/write only — API keys must not be world-readable
|
|
125
135
|
|
|
126
136
|
# Create profiles file
|
|
@@ -155,5 +165,6 @@ def update_api_key(key_name: str, key_value: str):
|
|
|
155
165
|
|
|
156
166
|
with open(config_path, "w") as f:
|
|
157
167
|
yaml.dump(config, f, default_flow_style=False)
|
|
158
|
-
|
|
168
|
+
config_path.chmod(0o600)
|
|
169
|
+
|
|
159
170
|
console.print(f"[green]✓[/green] Updated {key_name}")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "osintkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "OSINT CLI for personal digital footprint analysis",
|
|
5
5
|
"bin": {
|
|
6
6
|
"osintkit": "./bin/osintkit.js"
|
|
@@ -24,8 +24,16 @@
|
|
|
24
24
|
"cli",
|
|
25
25
|
"recon"
|
|
26
26
|
],
|
|
27
|
-
"author": "",
|
|
27
|
+
"author": "diesesschnitzel",
|
|
28
28
|
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/diesesschnitzel/osintkit.git"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/diesesschnitzel/osintkit#api-keys-all-optional",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/diesesschnitzel/osintkit/issues"
|
|
36
|
+
},
|
|
29
37
|
"engines": {
|
|
30
38
|
"node": ">=16",
|
|
31
39
|
"python": ">=3.10"
|