osintkit 0.1.0
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/README.md +86 -0
- package/bin/osintkit.js +7 -0
- package/osintkit/__init__.py +3 -0
- 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/cli.py +613 -0
- package/osintkit/config.py +51 -0
- package/osintkit/modules/__init__.py +6 -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/breach.py +82 -0
- package/osintkit/modules/brokers.py +56 -0
- package/osintkit/modules/certs.py +42 -0
- package/osintkit/modules/dark_web.py +51 -0
- package/osintkit/modules/gravatar.py +50 -0
- package/osintkit/modules/harvester.py +56 -0
- package/osintkit/modules/hibp.py +40 -0
- package/osintkit/modules/hibp_kanon.py +66 -0
- package/osintkit/modules/holehe.py +39 -0
- package/osintkit/modules/libphonenumber_info.py +79 -0
- package/osintkit/modules/paste.py +55 -0
- package/osintkit/modules/phone.py +32 -0
- package/osintkit/modules/sherlock.py +48 -0
- package/osintkit/modules/social.py +58 -0
- package/osintkit/modules/stage2/__init__.py +1 -0
- package/osintkit/modules/stage2/github_api.py +65 -0
- package/osintkit/modules/stage2/hunter.py +64 -0
- package/osintkit/modules/stage2/leakcheck.py +58 -0
- package/osintkit/modules/stage2/numverify.py +62 -0
- package/osintkit/modules/stage2/securitytrails.py +65 -0
- package/osintkit/modules/wayback.py +70 -0
- package/osintkit/output/__init__.py +1 -0
- 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/html_writer.py +36 -0
- package/osintkit/output/json_writer.py +31 -0
- package/osintkit/output/md_writer.py +115 -0
- package/osintkit/output/templates/report.html +74 -0
- package/osintkit/profiles.py +116 -0
- package/osintkit/risk.py +42 -0
- package/osintkit/scanner.py +240 -0
- package/osintkit/setup.py +157 -0
- package/package.json +25 -0
- package/pyproject.toml +44 -0
- package/requirements.txt +9 -0
package/osintkit/cli.py
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
"""osintkit CLI - OSINT tool for personal digital footprint analysis."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
from rich.prompt import Prompt, Confirm
|
|
15
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
16
|
+
|
|
17
|
+
from osintkit.scanner import Scanner
|
|
18
|
+
from osintkit.config import load_config, Config, APIKeys
|
|
19
|
+
from osintkit.profiles import Profile, ProfileStore, ScanHistory
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(help="OSINT CLI for personal digital footprint analysis")
|
|
22
|
+
console = Console()
|
|
23
|
+
store = ProfileStore()
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _print_ethics_banner():
|
|
28
|
+
"""Print the ethics and legal notice before every scan."""
|
|
29
|
+
console.print(Panel(
|
|
30
|
+
"[yellow]Only use osintkit on targets you have explicit permission to investigate.\n"
|
|
31
|
+
"GDPR applies to EU subjects. Unauthorized use may be illegal.[/yellow]",
|
|
32
|
+
title="[bold red]Ethics Notice[/bold red]",
|
|
33
|
+
border_style="red",
|
|
34
|
+
))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def validate_and_format_phone(phone_str: str) -> Optional[str]:
|
|
38
|
+
"""Parse and validate a phone number string, returning E.164 format.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
phone_str: Raw phone number string entered by the user.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
E.164 formatted string (e.g. '+15555550100') if valid, None otherwise.
|
|
45
|
+
"""
|
|
46
|
+
if not phone_str:
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
import phonenumbers
|
|
50
|
+
parsed = phonenumbers.parse(phone_str, None)
|
|
51
|
+
if phonenumbers.is_valid_number(parsed):
|
|
52
|
+
return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
|
|
53
|
+
# Try with US as default region
|
|
54
|
+
parsed = phonenumbers.parse(phone_str, "US")
|
|
55
|
+
if phonenumbers.is_valid_number(parsed):
|
|
56
|
+
return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
|
|
57
|
+
logger.warning(f"Phone number appears invalid: {phone_str!r}")
|
|
58
|
+
return None
|
|
59
|
+
except Exception:
|
|
60
|
+
try:
|
|
61
|
+
import phonenumbers
|
|
62
|
+
parsed = phonenumbers.parse(phone_str, "US")
|
|
63
|
+
if phonenumbers.is_valid_number(parsed):
|
|
64
|
+
return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
logger.warning(f"Could not parse phone number: {phone_str!r}")
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
# ============ SETUP ============
|
|
71
|
+
|
|
72
|
+
def check_first_time():
|
|
73
|
+
"""Check if this is first run and prompt for API keys."""
|
|
74
|
+
config_path = Path.home() / ".osintkit" / "config.yaml"
|
|
75
|
+
|
|
76
|
+
if not config_path.exists():
|
|
77
|
+
console.print("\n[bold cyan]═══ First Time Setup ═══[/bold cyan]\n")
|
|
78
|
+
console.print("osintkit needs API keys for full functionality.")
|
|
79
|
+
console.print("[dim]You can skip this and add keys later.[/dim]\n")
|
|
80
|
+
console.print(
|
|
81
|
+
"[dim]Tip: for social profile enumeration install optional tools:\n"
|
|
82
|
+
" pip install -r requirements-tools.txt (maigret, holehe, sherlock)[/dim]\n"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
keys = {}
|
|
86
|
+
api_key_list = [
|
|
87
|
+
("hibp", "HaveIBeenPwned", "Free tier available"),
|
|
88
|
+
("breachdirectory", "BreachDirectory", "Via RapidAPI"),
|
|
89
|
+
("leakcheck", "LeakCheck", "Free tier available"),
|
|
90
|
+
("intelbase", "Intelbase", "Dark web + paste search"),
|
|
91
|
+
("google_cse_key", "Google CSE Key", "Data broker search"),
|
|
92
|
+
("google_cse_cx", "Google CSE CX ID", "Engine ID"),
|
|
93
|
+
("numverify", "NumVerify", "Phone validation"),
|
|
94
|
+
("emailrep", "EmailRep", "Email reputation"),
|
|
95
|
+
("hunter", "Hunter.io (email finder)", "https://hunter.io"),
|
|
96
|
+
("github", "GitHub Personal Access Token", "https://github.com/settings/tokens"),
|
|
97
|
+
("securitytrails", "SecurityTrails", "https://securitytrails.com"),
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
for key_name, service_name, note in api_key_list:
|
|
101
|
+
value = Prompt.ask(f"{service_name} ({note})", default="")
|
|
102
|
+
keys[key_name] = value.strip()
|
|
103
|
+
|
|
104
|
+
# Create config
|
|
105
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
config_content = f"""# osintkit Configuration
|
|
107
|
+
output_dir: ~/osint-results
|
|
108
|
+
timeout_seconds: 120
|
|
109
|
+
|
|
110
|
+
api_keys:
|
|
111
|
+
hibp: "{keys.get('hibp', '')}"
|
|
112
|
+
breachdirectory: "{keys.get('breachdirectory', '')}"
|
|
113
|
+
leakcheck: "{keys.get('leakcheck', '')}"
|
|
114
|
+
intelbase: "{keys.get('intelbase', '')}"
|
|
115
|
+
google_cse_key: "{keys.get('google_cse_key', '')}"
|
|
116
|
+
google_cse_cx: "{keys.get('google_cse_cx', '')}"
|
|
117
|
+
numverify: "{keys.get('numverify', '')}"
|
|
118
|
+
emailrep: "{keys.get('emailrep', '')}"
|
|
119
|
+
resend: ""
|
|
120
|
+
hunter: "{keys.get('hunter', '')}"
|
|
121
|
+
github: "{keys.get('github', '')}"
|
|
122
|
+
securitytrails: "{keys.get('securitytrails', '')}"
|
|
123
|
+
epieos: ""
|
|
124
|
+
"""
|
|
125
|
+
config_path.write_text(config_content)
|
|
126
|
+
console.print(f"\n[green]✓[/green] Config saved to {config_path}")
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_profile_by_identifier(name=None, email=None, username=None, phone=None) -> Optional[Profile]:
|
|
133
|
+
"""Find profile by any identifier."""
|
|
134
|
+
profiles = store.list()
|
|
135
|
+
for p in profiles:
|
|
136
|
+
if name and p.name and name.lower().strip() == p.name.lower().strip():
|
|
137
|
+
return p
|
|
138
|
+
if email and p.email and email.lower().strip() == p.email.lower().strip():
|
|
139
|
+
return p
|
|
140
|
+
if username and p.username and username.lower().strip() == p.username.lower().strip():
|
|
141
|
+
return p
|
|
142
|
+
if phone and p.phone and phone.strip() == p.phone.strip():
|
|
143
|
+
return p
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def select_profile() -> Optional[Profile]:
|
|
148
|
+
"""Let user select a profile from list."""
|
|
149
|
+
profiles = store.list()
|
|
150
|
+
|
|
151
|
+
if not profiles:
|
|
152
|
+
console.print("[yellow]No profiles found. Use 'osintkit new' to create one.[/yellow]")
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
console.print("\n[bold]Select a profile:[/bold]\n")
|
|
156
|
+
|
|
157
|
+
for i, p in enumerate(profiles, 1):
|
|
158
|
+
scan_info = f"{len(p.scan_history)} scans" if p.scan_history else "no scans"
|
|
159
|
+
console.print(f" [cyan]{i}.[/cyan] {p.name or 'Unnamed'} - {p.email or p.username or 'no email'} ({scan_info})")
|
|
160
|
+
|
|
161
|
+
console.print()
|
|
162
|
+
choice = Prompt.ask("Enter number (or Enter to cancel)", default="")
|
|
163
|
+
|
|
164
|
+
if not choice:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
idx = int(choice) - 1
|
|
169
|
+
if 0 <= idx < len(profiles):
|
|
170
|
+
return profiles[idx]
|
|
171
|
+
except ValueError:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
console.print("[red]Invalid selection[/red]")
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def run_scan_for_profile(profile: Profile) -> dict:
|
|
179
|
+
"""Execute scan for a profile with progress display."""
|
|
180
|
+
_print_ethics_banner()
|
|
181
|
+
|
|
182
|
+
config_path = Path.home() / ".osintkit" / "config.yaml"
|
|
183
|
+
cfg = load_config(config_path)
|
|
184
|
+
|
|
185
|
+
# Create output directory (use ~/osint-results or config setting)
|
|
186
|
+
target = "_".join(filter(None, [profile.name, profile.email, profile.username, profile.phone])).replace(" ", "_")
|
|
187
|
+
date_str = datetime.now().strftime("%Y-%m-%d_%H%M%S")
|
|
188
|
+
|
|
189
|
+
# Use config output_dir or default to ~/osint-results
|
|
190
|
+
base_dir = Path(cfg.output_dir).expanduser() if cfg.output_dir else Path.home() / "osint-results"
|
|
191
|
+
output_dir = base_dir / f"{target}_{date_str}"
|
|
192
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
193
|
+
|
|
194
|
+
console.print(f"\n[bold]Scanning: {profile.name or profile.email or profile.username}[/bold]")
|
|
195
|
+
console.print(f"[dim]Output: {output_dir}[/dim]\n")
|
|
196
|
+
|
|
197
|
+
# Run scanner
|
|
198
|
+
scanner = Scanner(config=cfg, output_dir=output_dir, console=console)
|
|
199
|
+
inputs = {
|
|
200
|
+
"name": profile.name,
|
|
201
|
+
"email": profile.email,
|
|
202
|
+
"username": profile.username,
|
|
203
|
+
"phone": profile.phone,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
findings = scanner.run(inputs)
|
|
207
|
+
|
|
208
|
+
# Write outputs
|
|
209
|
+
json_path = scanner.write_json(findings)
|
|
210
|
+
html_path = scanner.write_html(findings)
|
|
211
|
+
md_path = scanner.write_md(findings)
|
|
212
|
+
|
|
213
|
+
# Show results
|
|
214
|
+
score = findings.get("risk_score", 0)
|
|
215
|
+
color = "red" if score >= 70 else "yellow" if score >= 40 else "green"
|
|
216
|
+
|
|
217
|
+
total_findings = sum(len(f) for f in findings.get("findings", {}).values())
|
|
218
|
+
|
|
219
|
+
console.print(f"\n[bold {color}]Risk Score: {score}/100[/bold {color}]")
|
|
220
|
+
console.print(f"Total findings: {total_findings}")
|
|
221
|
+
console.print(f"\n[green]✓[/green] JSON: {json_path}")
|
|
222
|
+
console.print(f"[green]✓[/green] HTML: {html_path}")
|
|
223
|
+
console.print(f"[green]✓[/green] Markdown: {str(md_path)}")
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
"findings": findings,
|
|
227
|
+
"json_path": json_path,
|
|
228
|
+
"html_path": html_path,
|
|
229
|
+
"md_path": md_path,
|
|
230
|
+
"score": score,
|
|
231
|
+
"total": total_findings,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ============ COMMANDS ============
|
|
236
|
+
|
|
237
|
+
@app.command()
|
|
238
|
+
def setup():
|
|
239
|
+
"""Configure API keys."""
|
|
240
|
+
config_path = Path.home() / ".osintkit" / "config.yaml"
|
|
241
|
+
|
|
242
|
+
if config_path.exists():
|
|
243
|
+
console.print(f"\n[yellow]Config already exists: {config_path}[/yellow]")
|
|
244
|
+
if not Confirm.ask("Overwrite?"):
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
console.print("\n[bold cyan]═══ API Key Setup ═══[/bold cyan]\n")
|
|
248
|
+
console.print("Enter your API keys (press Enter to skip).\n")
|
|
249
|
+
|
|
250
|
+
keys = {}
|
|
251
|
+
api_key_list = [
|
|
252
|
+
("hibp", "HaveIBeenPwned (hibp)", "https://haveibeenpwned.com/API/Key"),
|
|
253
|
+
("breachdirectory", "BreachDirectory", "https://rapidapi.com/"),
|
|
254
|
+
("leakcheck", "LeakCheck", "https://leakcheck.io/"),
|
|
255
|
+
("intelbase", "Intelbase", "https://intelbase.is/"),
|
|
256
|
+
("google_cse_key", "Google CSE API Key", "https://developers.google.com/custom-search/"),
|
|
257
|
+
("google_cse_cx", "Google CSE Engine ID", ""),
|
|
258
|
+
("numverify", "NumVerify", "https://numverify.com/"),
|
|
259
|
+
("emailrep", "EmailRep", "https://emailrep.io/"),
|
|
260
|
+
("hunter", "Hunter.io (email finder)", "https://hunter.io"),
|
|
261
|
+
("github", "GitHub Personal Access Token", "https://github.com/settings/tokens"),
|
|
262
|
+
("securitytrails", "SecurityTrails", "https://securitytrails.com"),
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
for key_name, label, url in api_key_list:
|
|
266
|
+
hint = f" ({url})" if url else ""
|
|
267
|
+
value = Prompt.ask(f"{label}{hint}", default="")
|
|
268
|
+
keys[key_name] = value.strip()
|
|
269
|
+
|
|
270
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
271
|
+
config_content = f"""# osintkit Configuration
|
|
272
|
+
output_dir: ~/osint-results
|
|
273
|
+
timeout_seconds: 120
|
|
274
|
+
|
|
275
|
+
api_keys:
|
|
276
|
+
hibp: "{keys.get('hibp', '')}"
|
|
277
|
+
breachdirectory: "{keys.get('breachdirectory', '')}"
|
|
278
|
+
leakcheck: "{keys.get('leakcheck', '')}"
|
|
279
|
+
intelbase: "{keys.get('intelbase', '')}"
|
|
280
|
+
google_cse_key: "{keys.get('google_cse_key', '')}"
|
|
281
|
+
google_cse_cx: "{keys.get('google_cse_cx', '')}"
|
|
282
|
+
numverify: "{keys.get('numverify', '')}"
|
|
283
|
+
emailrep: "{keys.get('emailrep', '')}"
|
|
284
|
+
resend: ""
|
|
285
|
+
hunter: "{keys.get('hunter', '')}"
|
|
286
|
+
github: "{keys.get('github', '')}"
|
|
287
|
+
securitytrails: "{keys.get('securitytrails', '')}"
|
|
288
|
+
epieos: ""
|
|
289
|
+
"""
|
|
290
|
+
config_path.write_text(config_content)
|
|
291
|
+
console.print(f"\n[green]✓[/green] Config saved: {config_path}")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@app.command()
|
|
295
|
+
def new():
|
|
296
|
+
"""Create a new person profile."""
|
|
297
|
+
check_first_time()
|
|
298
|
+
|
|
299
|
+
console.print("\n[bold cyan]═══ New Profile ═══[/bold cyan]\n")
|
|
300
|
+
|
|
301
|
+
# Step-by-step input
|
|
302
|
+
name = Prompt.ask("1. Full name").strip()
|
|
303
|
+
email = Prompt.ask("2. Email (optional)", default="").strip()
|
|
304
|
+
username = Prompt.ask("3. Username (optional)", default="").strip()
|
|
305
|
+
phone_raw = Prompt.ask("4. Phone (optional)", default="").strip()
|
|
306
|
+
if phone_raw:
|
|
307
|
+
phone = validate_and_format_phone(phone_raw)
|
|
308
|
+
if phone is None:
|
|
309
|
+
console.print(f"[yellow]Warning: Could not parse phone '{phone_raw}' — storing as-is[/yellow]")
|
|
310
|
+
phone = phone_raw
|
|
311
|
+
else:
|
|
312
|
+
phone = ""
|
|
313
|
+
|
|
314
|
+
if not any([name, email, username, phone]):
|
|
315
|
+
console.print("[red]Error: Need at least name, email, username, or phone[/red]")
|
|
316
|
+
raise typer.Exit(1)
|
|
317
|
+
|
|
318
|
+
# Check for existing
|
|
319
|
+
existing = get_profile_by_identifier(name, email, username, phone)
|
|
320
|
+
if existing:
|
|
321
|
+
console.print(f"\n[yellow]⚠ Profile already exists:[/yellow] {existing.id}")
|
|
322
|
+
console.print(f" Name: {existing.name or '—'}")
|
|
323
|
+
console.print(f" Email: {existing.email or '—'}")
|
|
324
|
+
if Confirm.ask("\nUpdate this profile?"):
|
|
325
|
+
profile = existing
|
|
326
|
+
if name:
|
|
327
|
+
profile.name = name
|
|
328
|
+
if email:
|
|
329
|
+
profile.email = email
|
|
330
|
+
if username:
|
|
331
|
+
profile.username = username
|
|
332
|
+
if phone:
|
|
333
|
+
profile.phone = phone
|
|
334
|
+
store.update(profile)
|
|
335
|
+
console.print(f"[green]✓[/green] Updated profile: {profile.id}")
|
|
336
|
+
else:
|
|
337
|
+
console.print("[red]Cancelled[/red]")
|
|
338
|
+
return
|
|
339
|
+
else:
|
|
340
|
+
profile = Profile(
|
|
341
|
+
name=name or None,
|
|
342
|
+
email=email or None,
|
|
343
|
+
username=username or None,
|
|
344
|
+
phone=phone or None,
|
|
345
|
+
)
|
|
346
|
+
store.create(profile)
|
|
347
|
+
console.print(f"\n[green]✓[/green] Created profile: {profile.id}")
|
|
348
|
+
console.print(f" Name: {name or '—'}")
|
|
349
|
+
console.print(f" Email: {email or '—'}")
|
|
350
|
+
console.print(f" Username: {username or '—'}")
|
|
351
|
+
console.print(f" Phone: {phone or '—'}")
|
|
352
|
+
|
|
353
|
+
# Ask to scan
|
|
354
|
+
if Confirm.ask("\nRun scan now?"):
|
|
355
|
+
result = run_scan_for_profile(profile)
|
|
356
|
+
|
|
357
|
+
# Save to history
|
|
358
|
+
scan_record = ScanHistory(
|
|
359
|
+
scan_id=datetime.now().strftime("%Y%m%d_%H%M%S"),
|
|
360
|
+
timestamp=datetime.now().isoformat(),
|
|
361
|
+
inputs={"name": profile.name, "email": profile.email, "username": profile.username, "phone": profile.phone},
|
|
362
|
+
risk_score=result["score"],
|
|
363
|
+
findings_count=result["total"],
|
|
364
|
+
findings_file=str(result["json_path"]),
|
|
365
|
+
html_file=str(result["html_path"]),
|
|
366
|
+
)
|
|
367
|
+
store.add_scan_result(profile.id, scan_record)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@app.command()
|
|
371
|
+
def list():
|
|
372
|
+
"""List all profiles."""
|
|
373
|
+
profiles = store.list()
|
|
374
|
+
|
|
375
|
+
if not profiles:
|
|
376
|
+
console.print("\n[yellow]No profiles found.[/yellow]")
|
|
377
|
+
console.print("Use [cyan]osintkit new[/cyan] to create one.\n")
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
console.print(f"\n[bold]Profiles ({len(profiles)}):[/bold]\n")
|
|
381
|
+
|
|
382
|
+
table = Table()
|
|
383
|
+
table.add_column("#", width=3)
|
|
384
|
+
table.add_column("ID", style="cyan")
|
|
385
|
+
table.add_column("Name")
|
|
386
|
+
table.add_column("Email")
|
|
387
|
+
table.add_column("Username")
|
|
388
|
+
table.add_column("Scans")
|
|
389
|
+
table.add_column("Risk")
|
|
390
|
+
|
|
391
|
+
for i, p in enumerate(profiles, 1):
|
|
392
|
+
last_scan = p.scan_history[-1] if p.scan_history else None
|
|
393
|
+
table.add_row(
|
|
394
|
+
str(i),
|
|
395
|
+
p.id,
|
|
396
|
+
p.name or "—",
|
|
397
|
+
p.email or "—",
|
|
398
|
+
p.username or "—",
|
|
399
|
+
str(len(p.scan_history)),
|
|
400
|
+
str(last_scan.risk_score) if last_scan else "—",
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
console.print(table)
|
|
404
|
+
console.print()
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@app.command()
|
|
408
|
+
def refresh(profile_ref: str = typer.Argument(None, help="Profile ID or name")):
|
|
409
|
+
"""Refresh scan for a profile."""
|
|
410
|
+
if profile_ref:
|
|
411
|
+
# Try to find by ID or name
|
|
412
|
+
profile = store.get(profile_ref)
|
|
413
|
+
if not profile:
|
|
414
|
+
profiles = store.list()
|
|
415
|
+
for p in profiles:
|
|
416
|
+
if p.name and p.name.lower() == profile_ref.lower():
|
|
417
|
+
profile = p
|
|
418
|
+
break
|
|
419
|
+
if not profile:
|
|
420
|
+
console.print(f"[red]Profile not found: {profile_ref}[/red]")
|
|
421
|
+
raise typer.Exit(1)
|
|
422
|
+
else:
|
|
423
|
+
profile = select_profile()
|
|
424
|
+
if not profile:
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
console.print(f"\n[bold]Refreshing: {profile.name or profile.email or profile.id}[/bold]")
|
|
428
|
+
|
|
429
|
+
result = run_scan_for_profile(profile)
|
|
430
|
+
|
|
431
|
+
# Save to history
|
|
432
|
+
scan_record = ScanHistory(
|
|
433
|
+
scan_id=datetime.now().strftime("%Y%m%d_%H%M%S"),
|
|
434
|
+
timestamp=datetime.now().isoformat(),
|
|
435
|
+
inputs={"name": profile.name, "email": profile.email, "username": profile.username, "phone": profile.phone},
|
|
436
|
+
risk_score=result["score"],
|
|
437
|
+
findings_count=result["total"],
|
|
438
|
+
findings_file=str(result["json_path"]),
|
|
439
|
+
html_file=str(result["html_path"]),
|
|
440
|
+
)
|
|
441
|
+
store.add_scan_result(profile.id, scan_record)
|
|
442
|
+
console.print(f"\n[green]✓[/green] Scan saved to profile history")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@app.command()
|
|
446
|
+
def open(profile_ref: str = typer.Argument(None, help="Profile ID or name")):
|
|
447
|
+
"""Show profile details."""
|
|
448
|
+
if profile_ref:
|
|
449
|
+
profile = store.get(profile_ref)
|
|
450
|
+
if not profile:
|
|
451
|
+
profiles = store.list()
|
|
452
|
+
for p in profiles:
|
|
453
|
+
if p.name and p.name.lower() == profile_ref.lower():
|
|
454
|
+
profile = p
|
|
455
|
+
break
|
|
456
|
+
if not profile:
|
|
457
|
+
console.print(f"[red]Profile not found: {profile_ref}[/red]")
|
|
458
|
+
raise typer.Exit(1)
|
|
459
|
+
else:
|
|
460
|
+
profile = select_profile()
|
|
461
|
+
if not profile:
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
console.print(f"\n[bold cyan]═══ Profile: {profile.name or profile.id} ═══[/bold cyan]\n")
|
|
465
|
+
console.print(f" [bold]ID:[/bold] {profile.id}")
|
|
466
|
+
console.print(f" [bold]Name:[/bold] {profile.name or '—'}")
|
|
467
|
+
console.print(f" [bold]Email:[/bold] {profile.email or '—'}")
|
|
468
|
+
console.print(f" [bold]Username:[/bold] {profile.username or '—'}")
|
|
469
|
+
console.print(f" [bold]Phone:[/bold] {profile.phone or '—'}")
|
|
470
|
+
console.print(f" [bold]Notes:[/bold] {profile.notes or '—'}")
|
|
471
|
+
console.print(f" [bold]Created:[/bold] {profile.created_at[:10] if profile.created_at else '—'}")
|
|
472
|
+
|
|
473
|
+
if profile.scan_history:
|
|
474
|
+
console.print(f"\n[bold]Scan History ({len(profile.scan_history)} scans):[/bold]\n")
|
|
475
|
+
|
|
476
|
+
table = Table()
|
|
477
|
+
table.add_column("Date")
|
|
478
|
+
table.add_column("Risk")
|
|
479
|
+
table.add_column("Findings")
|
|
480
|
+
table.add_column("Report")
|
|
481
|
+
|
|
482
|
+
for scan in reversed(profile.scan_history[-10:]):
|
|
483
|
+
report_name = Path(scan.html_file).name if scan.html_file else "—"
|
|
484
|
+
table.add_row(
|
|
485
|
+
scan.timestamp[:10],
|
|
486
|
+
str(scan.risk_score),
|
|
487
|
+
str(scan.findings_count),
|
|
488
|
+
report_name,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
console.print(table)
|
|
492
|
+
|
|
493
|
+
# Open latest report
|
|
494
|
+
if profile.scan_history and profile.scan_history[-1].html_file:
|
|
495
|
+
latest = profile.scan_history[-1]
|
|
496
|
+
if Confirm.ask(f"\nOpen latest report?"):
|
|
497
|
+
import webbrowser
|
|
498
|
+
webbrowser.open(f"file://{latest.html_file}")
|
|
499
|
+
else:
|
|
500
|
+
console.print("\n[yellow]No scans yet. Use 'osintkit refresh' to run a scan.[/yellow]")
|
|
501
|
+
|
|
502
|
+
console.print()
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
@app.command()
|
|
506
|
+
def export(
|
|
507
|
+
profile_ref: str = typer.Argument(None, help="Profile ID or name"),
|
|
508
|
+
format: str = typer.Option("json", "--format", "-f", help="Output format: json or md"),
|
|
509
|
+
output: Path = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
510
|
+
):
|
|
511
|
+
"""Export profile data."""
|
|
512
|
+
if profile_ref:
|
|
513
|
+
profile = store.get(profile_ref)
|
|
514
|
+
if not profile:
|
|
515
|
+
profiles = store.list()
|
|
516
|
+
for p in profiles:
|
|
517
|
+
if p.name and p.name.lower() == profile_ref.lower():
|
|
518
|
+
profile = p
|
|
519
|
+
break
|
|
520
|
+
if not profile:
|
|
521
|
+
console.print(f"[red]Profile not found: {profile_ref}[/red]")
|
|
522
|
+
raise typer.Exit(1)
|
|
523
|
+
else:
|
|
524
|
+
profile = select_profile()
|
|
525
|
+
if not profile:
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
# Load latest findings
|
|
529
|
+
if not profile.scan_history:
|
|
530
|
+
console.print("[yellow]No scans to export. Run 'osintkit refresh' first.[/yellow]")
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
latest_scan = profile.scan_history[-1]
|
|
534
|
+
|
|
535
|
+
if format == "json":
|
|
536
|
+
if output is None:
|
|
537
|
+
output = Path.cwd() / f"{profile.name or profile.id}_export.json"
|
|
538
|
+
|
|
539
|
+
# Load the findings
|
|
540
|
+
if latest_scan.findings_file and Path(latest_scan.findings_file).exists():
|
|
541
|
+
import shutil
|
|
542
|
+
shutil.copy(latest_scan.findings_file, output)
|
|
543
|
+
console.print(f"[green]✓[/green] Exported JSON: {output}")
|
|
544
|
+
else:
|
|
545
|
+
console.print("[red]Findings file not found[/red]")
|
|
546
|
+
|
|
547
|
+
elif format == "md":
|
|
548
|
+
if output is None:
|
|
549
|
+
output = Path.cwd() / f"{profile.name or profile.id}_export.md"
|
|
550
|
+
|
|
551
|
+
# Create markdown report
|
|
552
|
+
findings_data = {}
|
|
553
|
+
if latest_scan.findings_file and Path(latest_scan.findings_file).exists():
|
|
554
|
+
with open(latest_scan.findings_file) as f:
|
|
555
|
+
findings_data = json.load(f)
|
|
556
|
+
|
|
557
|
+
md_content = f"""# osintkit Report: {profile.name or profile.id}
|
|
558
|
+
|
|
559
|
+
**Generated:** {datetime.now().isoformat()}
|
|
560
|
+
|
|
561
|
+
## Profile Information
|
|
562
|
+
|
|
563
|
+
- **Name:** {profile.name or '—'}
|
|
564
|
+
- **Email:** {profile.email or '—'}
|
|
565
|
+
- **Username:** {profile.username or '—'}
|
|
566
|
+
- **Phone:** {profile.phone or '—'}
|
|
567
|
+
|
|
568
|
+
## Risk Score: {latest_scan.risk_score}/100
|
|
569
|
+
|
|
570
|
+
## Findings Summary
|
|
571
|
+
|
|
572
|
+
"""
|
|
573
|
+
for module_name, findings in findings_data.get("findings", {}).items():
|
|
574
|
+
if findings:
|
|
575
|
+
md_content += f"### {module_name}\n\n"
|
|
576
|
+
for finding in findings:
|
|
577
|
+
md_content += f"- **{finding.get('type', 'Unknown')}** via {finding.get('source', 'unknown')}\n"
|
|
578
|
+
if finding.get('url'):
|
|
579
|
+
md_content += f" - URL: {finding['url']}\n"
|
|
580
|
+
md_content += "\n"
|
|
581
|
+
|
|
582
|
+
output.write_text(md_content)
|
|
583
|
+
console.print(f"[green]✓[/green] Exported Markdown: {output}")
|
|
584
|
+
|
|
585
|
+
else:
|
|
586
|
+
console.print(f"[red]Unknown format: {format}[/red]")
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@app.command()
|
|
590
|
+
def delete(profile_ref: str = typer.Argument(None, help="Profile ID or name")):
|
|
591
|
+
"""Delete a profile."""
|
|
592
|
+
if profile_ref:
|
|
593
|
+
profile = store.get(profile_ref)
|
|
594
|
+
else:
|
|
595
|
+
profile = select_profile()
|
|
596
|
+
|
|
597
|
+
if not profile:
|
|
598
|
+
return
|
|
599
|
+
|
|
600
|
+
if Confirm.ask(f"\nDelete profile '{profile.name or profile.id}'?"):
|
|
601
|
+
store.delete(profile.id)
|
|
602
|
+
console.print(f"[green]✓[/green] Deleted")
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
@app.command()
|
|
606
|
+
def version():
|
|
607
|
+
"""Show version."""
|
|
608
|
+
from osintkit import __version__
|
|
609
|
+
console.print(f"osintkit v{__version__}")
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
if __name__ == "__main__":
|
|
613
|
+
app()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Configuration loader for osintkit."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
import yaml
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class APIKeys(BaseModel):
|
|
10
|
+
"""Optional API keys for premium modules."""
|
|
11
|
+
|
|
12
|
+
emailrep: str = ""
|
|
13
|
+
breachdirectory: str = ""
|
|
14
|
+
leakcheck: str = ""
|
|
15
|
+
google_cse_key: str = ""
|
|
16
|
+
google_cse_cx: str = ""
|
|
17
|
+
intelbase: str = ""
|
|
18
|
+
numverify: str = ""
|
|
19
|
+
resend: str = ""
|
|
20
|
+
hibp: str = ""
|
|
21
|
+
hunter: str = ""
|
|
22
|
+
github: str = ""
|
|
23
|
+
securitytrails: str = ""
|
|
24
|
+
epieos: str = ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Config(BaseModel):
|
|
28
|
+
"""osintkit configuration."""
|
|
29
|
+
|
|
30
|
+
output_dir: str = "~/osint-results"
|
|
31
|
+
timeout_seconds: int = 120
|
|
32
|
+
api_keys: APIKeys = Field(default_factory=APIKeys)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_config(config_path: Path) -> Config:
|
|
36
|
+
"""Load configuration from YAML file.
|
|
37
|
+
|
|
38
|
+
Returns default config if file doesn't exist.
|
|
39
|
+
"""
|
|
40
|
+
if not config_path.exists():
|
|
41
|
+
return Config()
|
|
42
|
+
|
|
43
|
+
with open(config_path) as f:
|
|
44
|
+
data = yaml.safe_load(f) or {}
|
|
45
|
+
|
|
46
|
+
api_keys_data = data.pop("api_keys", {})
|
|
47
|
+
|
|
48
|
+
return Config(
|
|
49
|
+
**data,
|
|
50
|
+
api_keys=APIKeys(**api_keys_data) if api_keys_data else APIKeys(),
|
|
51
|
+
)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|