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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Markdown report writer for osintkit scan results."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _scrub_keys(content: str, api_keys: Optional[Dict[str, str]] = None) -> str:
|
|
9
|
+
"""Replace any API key values in content with [REDACTED].
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
content: The string to scrub.
|
|
13
|
+
api_keys: Dict mapping key name -> key value. Values longer than
|
|
14
|
+
6 characters are scrubbed; shorter ones are skipped to
|
|
15
|
+
avoid over-scrubbing short common strings.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Scrubbed string.
|
|
19
|
+
"""
|
|
20
|
+
if not api_keys:
|
|
21
|
+
return content
|
|
22
|
+
for _name, value in api_keys.items():
|
|
23
|
+
if value and len(value) > 6:
|
|
24
|
+
content = content.replace(value, "[REDACTED]")
|
|
25
|
+
return content
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def write_md(
|
|
29
|
+
findings: Dict[str, Any],
|
|
30
|
+
output_dir: Path,
|
|
31
|
+
api_keys: Optional[Dict[str, str]] = None,
|
|
32
|
+
) -> Path:
|
|
33
|
+
"""Generate a Markdown report from scan findings.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
findings: The findings dict produced by Scanner.run().
|
|
37
|
+
output_dir: Directory where findings.md will be written.
|
|
38
|
+
api_keys: Optional dict of API key values to scrub from output.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Path to the written file.
|
|
42
|
+
"""
|
|
43
|
+
inputs = findings.get("inputs", {})
|
|
44
|
+
scan_date = findings.get("scan_date", datetime.now().isoformat())
|
|
45
|
+
risk_score = findings.get("risk_score", 0)
|
|
46
|
+
modules_meta = findings.get("modules", {})
|
|
47
|
+
findings_by_module = findings.get("findings", {})
|
|
48
|
+
|
|
49
|
+
name = inputs.get("name") or inputs.get("username") or inputs.get("email") or "Unknown"
|
|
50
|
+
|
|
51
|
+
lines = [
|
|
52
|
+
f"# osintkit Report: {name}",
|
|
53
|
+
"",
|
|
54
|
+
f"**Generated:** {scan_date}",
|
|
55
|
+
f"**Date:** {scan_date[:10]}",
|
|
56
|
+
"",
|
|
57
|
+
"## Target Information",
|
|
58
|
+
"",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
for field in ("name", "email", "username", "phone"):
|
|
62
|
+
value = inputs.get(field) or "—"
|
|
63
|
+
lines.append(f"- **{field.capitalize()}:** {value}")
|
|
64
|
+
|
|
65
|
+
lines += [
|
|
66
|
+
"",
|
|
67
|
+
f"## Risk Score: {risk_score}/100",
|
|
68
|
+
"",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
if risk_score >= 70:
|
|
72
|
+
lines.append("> **HIGH RISK** — significant digital footprint detected.")
|
|
73
|
+
elif risk_score >= 40:
|
|
74
|
+
lines.append("> **MEDIUM RISK** — moderate digital footprint detected.")
|
|
75
|
+
else:
|
|
76
|
+
lines.append("> **LOW RISK** — limited digital footprint detected.")
|
|
77
|
+
|
|
78
|
+
lines += ["", "## Module Results", ""]
|
|
79
|
+
|
|
80
|
+
for module_name, meta in modules_meta.items():
|
|
81
|
+
status = meta.get("status", "unknown")
|
|
82
|
+
count = meta.get("count", 0)
|
|
83
|
+
error = meta.get("error", "")
|
|
84
|
+
status_str = f"done ({count} findings)" if status == "done" else f"failed: {error}"
|
|
85
|
+
lines.append(f"- **{module_name}**: {status_str}")
|
|
86
|
+
|
|
87
|
+
lines += ["", "## Findings", ""]
|
|
88
|
+
|
|
89
|
+
for module_name, module_findings in findings_by_module.items():
|
|
90
|
+
if not module_findings:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
lines.append(f"### {module_name}")
|
|
94
|
+
lines.append("")
|
|
95
|
+
|
|
96
|
+
for finding in module_findings:
|
|
97
|
+
ftype = finding.get("type", "unknown")
|
|
98
|
+
source = finding.get("source", "unknown")
|
|
99
|
+
url = finding.get("url")
|
|
100
|
+
data = finding.get("data", {})
|
|
101
|
+
|
|
102
|
+
lines.append(f"**{ftype}** (source: {source})")
|
|
103
|
+
if url:
|
|
104
|
+
lines.append(f"- URL: {url}")
|
|
105
|
+
if data:
|
|
106
|
+
for key, val in data.items():
|
|
107
|
+
lines.append(f"- {key}: {val}")
|
|
108
|
+
lines.append("")
|
|
109
|
+
|
|
110
|
+
content = "\n".join(lines)
|
|
111
|
+
content = _scrub_keys(content, api_keys)
|
|
112
|
+
|
|
113
|
+
output_file = output_dir / "findings.md"
|
|
114
|
+
output_file.write_text(content, encoding="utf-8")
|
|
115
|
+
return output_file
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>osintkit Report</title>
|
|
7
|
+
<style>
|
|
8
|
+
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 20px auto; padding: 20px; }
|
|
9
|
+
h1 { color: #333; border-bottom: 2px solid #ddd; padding-bottom: 10px; }
|
|
10
|
+
.risk-gauge { padding: 20px; border-radius: 8px; text-align: center; margin: 20px 0; }
|
|
11
|
+
.risk-high { background: #fee; color: #c00; }
|
|
12
|
+
.risk-medium { background: #ffe; color: #960; }
|
|
13
|
+
.risk-low { background: #efe; color: #060; }
|
|
14
|
+
.module-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; margin: 20px 0; }
|
|
15
|
+
.module-card { padding: 15px; border: 1px solid #ddd; border-radius: 4px; }
|
|
16
|
+
.module-done { background: #efe; }
|
|
17
|
+
.module-failed { background: #fee; }
|
|
18
|
+
.module-skipped { background: #eee; color: #888; }
|
|
19
|
+
.findings-section { margin: 20px 0; }
|
|
20
|
+
.finding-item { padding: 10px; border-bottom: 1px solid #eee; }
|
|
21
|
+
</style>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<h1>osintkit OSINT Report</h1>
|
|
25
|
+
<p><strong>Scan Date:</strong> {{ scan_date }}</p>
|
|
26
|
+
<p><strong>Target:</strong> {{ inputs.name or inputs.email or inputs.username or inputs.phone or 'Unknown' }}</p>
|
|
27
|
+
|
|
28
|
+
{% set score = risk_score %}
|
|
29
|
+
{% if score >= 70 %}
|
|
30
|
+
<div class="risk-gauge risk-high">
|
|
31
|
+
{% elif score >= 40 %}
|
|
32
|
+
<div class="risk-gauge risk-medium">
|
|
33
|
+
{% else %}
|
|
34
|
+
<div class="risk-gauge risk-low">
|
|
35
|
+
{% endif %}
|
|
36
|
+
<h2>Risk Score: {{ score }}/100</h2>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<h2>Module Status</h2>
|
|
40
|
+
<div class="module-grid">
|
|
41
|
+
{% for module_name, module_data in modules.items() %}
|
|
42
|
+
<div class="module-card module-{{ module_data.status }}">
|
|
43
|
+
<strong>{{ module_name }}</strong><br>
|
|
44
|
+
{% if module_data.status == 'done' %}
|
|
45
|
+
✓ {{ module_data.count }} findings
|
|
46
|
+
{% elif module_data.status == 'failed' %}
|
|
47
|
+
✗ {{ module_data.error }}
|
|
48
|
+
{% else %}
|
|
49
|
+
— skipped
|
|
50
|
+
{% endif %}
|
|
51
|
+
</div>
|
|
52
|
+
{% endfor %}
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<h2>Findings</h2>
|
|
56
|
+
{% for module_name, module_findings in findings.items() %}
|
|
57
|
+
{% if module_findings %}
|
|
58
|
+
<div class="findings-section">
|
|
59
|
+
<h3>{{ module_name }} ({{ module_findings|length }})</h3>
|
|
60
|
+
{% for finding in module_findings %}
|
|
61
|
+
<div class="finding-item">
|
|
62
|
+
<strong>{{ finding.type }}</strong>
|
|
63
|
+
<span style="color:#888">via {{ finding.source }}</span>
|
|
64
|
+
{% if finding.url %}
|
|
65
|
+
<a href="{{ finding.url }}" target="_blank">[link]</a>
|
|
66
|
+
{% endif %}
|
|
67
|
+
<br><small>Confidence: {{ finding.confidence }}</small>
|
|
68
|
+
</div>
|
|
69
|
+
{% endfor %}
|
|
70
|
+
</div>
|
|
71
|
+
{% endif %}
|
|
72
|
+
{% endfor %}
|
|
73
|
+
</body>
|
|
74
|
+
</html>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Profile management for saving and rerunning scans."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ScanHistory(BaseModel):
|
|
12
|
+
"""Record of a single scan run."""
|
|
13
|
+
scan_id: str
|
|
14
|
+
timestamp: str
|
|
15
|
+
inputs: Dict[str, Optional[str]]
|
|
16
|
+
risk_score: int
|
|
17
|
+
findings_count: int
|
|
18
|
+
findings_file: Optional[str] = None
|
|
19
|
+
html_file: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Profile(BaseModel):
|
|
23
|
+
"""A saved target profile with scan history."""
|
|
24
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
|
|
25
|
+
name: Optional[str] = None
|
|
26
|
+
email: Optional[str] = None
|
|
27
|
+
username: Optional[str] = None
|
|
28
|
+
phone: Optional[str] = None
|
|
29
|
+
notes: str = ""
|
|
30
|
+
tags: List[str] = Field(default_factory=list)
|
|
31
|
+
created_at: str = Field(default_factory=lambda: datetime.now().isoformat())
|
|
32
|
+
updated_at: str = Field(default_factory=lambda: datetime.now().isoformat())
|
|
33
|
+
scan_history: List[ScanHistory] = Field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ProfileStore:
|
|
37
|
+
"""Manages profile storage in JSON file."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, store_path: Path = None):
|
|
40
|
+
if store_path is None:
|
|
41
|
+
store_path = Path.home() / ".osintkit" / "profiles.json"
|
|
42
|
+
self.store_path = store_path
|
|
43
|
+
self.store_path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
|
|
45
|
+
def _load(self) -> Dict[str, Dict]:
|
|
46
|
+
"""Load all profiles from store."""
|
|
47
|
+
if not self.store_path.exists():
|
|
48
|
+
return {}
|
|
49
|
+
with open(self.store_path) as f:
|
|
50
|
+
return json.load(f)
|
|
51
|
+
|
|
52
|
+
def _save(self, profiles: Dict[str, Dict]):
|
|
53
|
+
"""Save all profiles to store."""
|
|
54
|
+
with open(self.store_path, "w") as f:
|
|
55
|
+
json.dump(profiles, f, indent=2, default=str)
|
|
56
|
+
|
|
57
|
+
def create(self, profile: Profile) -> Profile:
|
|
58
|
+
"""Create a new profile."""
|
|
59
|
+
profiles = self._load()
|
|
60
|
+
profiles[profile.id] = profile.model_dump()
|
|
61
|
+
self._save(profiles)
|
|
62
|
+
return profile
|
|
63
|
+
|
|
64
|
+
def get(self, profile_id: str) -> Optional[Profile]:
|
|
65
|
+
"""Get a profile by ID."""
|
|
66
|
+
profiles = self._load()
|
|
67
|
+
if profile_id in profiles:
|
|
68
|
+
return Profile(**profiles[profile_id])
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
def list(self, tag: str = None) -> List[Profile]:
|
|
72
|
+
"""List all profiles, optionally filtered by tag."""
|
|
73
|
+
profiles = self._load()
|
|
74
|
+
result = [Profile(**p) for p in profiles.values()]
|
|
75
|
+
if tag:
|
|
76
|
+
result = [p for p in result if tag in p.tags]
|
|
77
|
+
return sorted(result, key=lambda p: p.updated_at, reverse=True)
|
|
78
|
+
|
|
79
|
+
def update(self, profile: Profile) -> Profile:
|
|
80
|
+
"""Update an existing profile."""
|
|
81
|
+
profile.updated_at = datetime.now().isoformat()
|
|
82
|
+
profiles = self._load()
|
|
83
|
+
profiles[profile.id] = profile.model_dump()
|
|
84
|
+
self._save(profiles)
|
|
85
|
+
return profile
|
|
86
|
+
|
|
87
|
+
def delete(self, profile_id: str) -> bool:
|
|
88
|
+
"""Delete a profile."""
|
|
89
|
+
profiles = self._load()
|
|
90
|
+
if profile_id in profiles:
|
|
91
|
+
del profiles[profile_id]
|
|
92
|
+
self._save(profiles)
|
|
93
|
+
return True
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
def add_scan_result(self, profile_id: str, scan: ScanHistory) -> bool:
|
|
97
|
+
"""Add a scan result to a profile's history."""
|
|
98
|
+
profile = self.get(profile_id)
|
|
99
|
+
if not profile:
|
|
100
|
+
return False
|
|
101
|
+
profile.scan_history.append(scan)
|
|
102
|
+
self.update(profile)
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
def search(self, query: str) -> List[Profile]:
|
|
106
|
+
"""Search profiles by name, email, username, or notes."""
|
|
107
|
+
profiles = self._load()
|
|
108
|
+
query_lower = query.lower()
|
|
109
|
+
result = []
|
|
110
|
+
for p in profiles.values():
|
|
111
|
+
if (query_lower in (p.get("name") or "").lower() or
|
|
112
|
+
query_lower in (p.get("email") or "").lower() or
|
|
113
|
+
query_lower in (p.get("username") or "").lower() or
|
|
114
|
+
query_lower in (p.get("notes") or "").lower()):
|
|
115
|
+
result.append(Profile(**p))
|
|
116
|
+
return result
|
package/osintkit/risk.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Risk score calculation for osintkit."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def calculate_risk_score(findings: Dict[str, List]) -> int:
|
|
7
|
+
"""Calculate risk score 0-100 based on findings.
|
|
8
|
+
|
|
9
|
+
Formula:
|
|
10
|
+
- Breach findings: 3 pts each, max 30 (cap at 10)
|
|
11
|
+
- Social profiles: 2 pts each, max 20 (cap at 10)
|
|
12
|
+
- Data brokers: 4 pts each, max 20 (cap at 5)
|
|
13
|
+
- Dark web/paste: 5 pts each, max 15 (cap at 3)
|
|
14
|
+
|
|
15
|
+
Returns: int 0-100
|
|
16
|
+
"""
|
|
17
|
+
score = 0
|
|
18
|
+
|
|
19
|
+
# Breach exposure (30 points max)
|
|
20
|
+
breach_count = len(findings.get("breach_exposure", []))
|
|
21
|
+
score += min(30, breach_count * 3)
|
|
22
|
+
|
|
23
|
+
# Social profiles (20 points max)
|
|
24
|
+
social_count = len(findings.get("social_profiles", []))
|
|
25
|
+
score += min(20, social_count * 2)
|
|
26
|
+
|
|
27
|
+
# Data brokers (20 points max)
|
|
28
|
+
broker_count = len(findings.get("data_brokers", []))
|
|
29
|
+
score += min(20, broker_count * 4)
|
|
30
|
+
|
|
31
|
+
# Dark web + paste sites (15 points max)
|
|
32
|
+
dark_count = len(findings.get("dark_web", [])) + len(findings.get("paste_sites", []))
|
|
33
|
+
score += min(15, dark_count * 5)
|
|
34
|
+
|
|
35
|
+
# Password exposure - typically 0 since we don't have passwords
|
|
36
|
+
# If data contains count, scale it
|
|
37
|
+
password_data = findings.get("password_exposure", [])
|
|
38
|
+
if password_data and isinstance(password_data[0], dict) and password_data[0].get("data", {}).get("count"):
|
|
39
|
+
pw_count = password_data[0]["data"]["count"]
|
|
40
|
+
score += min(15, pw_count // 1000)
|
|
41
|
+
|
|
42
|
+
return min(100, score)
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Scanner orchestrator - runs all OSINT modules in parallel with progress."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Callable, Dict, List
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.progress import Progress
|
|
9
|
+
|
|
10
|
+
from osintkit.config import Config
|
|
11
|
+
from osintkit.output.json_writer import write_json
|
|
12
|
+
from osintkit.output.html_writer import write_html
|
|
13
|
+
from osintkit.output.md_writer import write_md
|
|
14
|
+
from osintkit.risk import calculate_risk_score
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Scanner:
|
|
18
|
+
"""Orchestrates OSINT module execution with progress display."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: Config, output_dir: Path, console: Console):
|
|
21
|
+
self.config = config
|
|
22
|
+
self.output_dir = output_dir
|
|
23
|
+
self.console = console
|
|
24
|
+
self.modules = self._load_modules()
|
|
25
|
+
|
|
26
|
+
def _load_modules(self) -> List[tuple]:
|
|
27
|
+
"""Load all available OSINT modules (Stage 1 + Stage 2 if keys present)."""
|
|
28
|
+
modules = [
|
|
29
|
+
("social_profiles", self._run_social_profiles, "Social media profiles"),
|
|
30
|
+
("email_accounts", self._run_email_accounts, "Email accounts"),
|
|
31
|
+
("password_exposure", self._run_password_exposure, "Password breaches"),
|
|
32
|
+
("web_presence", self._run_web_presence, "Web presence"),
|
|
33
|
+
("cert_transparency", self._run_cert_transparency, "SSL certificates"),
|
|
34
|
+
("breach_exposure", self._run_breach_exposure, "Data breaches"),
|
|
35
|
+
("dark_web", self._run_dark_web, "Dark web"),
|
|
36
|
+
("paste_sites", self._run_paste_sites, "Paste sites"),
|
|
37
|
+
("data_brokers", self._run_data_brokers, "Data brokers"),
|
|
38
|
+
("phone", self._run_phone, "Phone info"),
|
|
39
|
+
# New Stage 1 modules
|
|
40
|
+
("sherlock", self._run_sherlock, "Social profiles (Sherlock)"),
|
|
41
|
+
("gravatar", self._run_gravatar, "Gravatar profile"),
|
|
42
|
+
("wayback", self._run_wayback, "Wayback Machine"),
|
|
43
|
+
("phone_info", self._run_phone_info, "Phone analysis"),
|
|
44
|
+
("hibp_kanon", self._run_hibp_kanon, "Password k-anonymity check"),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# Stage 2 modules — only included when corresponding API key is set
|
|
48
|
+
api_keys = self.config.api_keys
|
|
49
|
+
stage2_map = [
|
|
50
|
+
("leakcheck", api_keys.leakcheck, self._run_stage2_leakcheck, "LeakCheck breach lookup"),
|
|
51
|
+
("hunter", api_keys.hunter, self._run_stage2_hunter, "Hunter email verify"),
|
|
52
|
+
("numverify", api_keys.numverify, self._run_stage2_numverify, "NumVerify phone"),
|
|
53
|
+
("github_api", api_keys.github, self._run_stage2_github, "GitHub profile"),
|
|
54
|
+
(
|
|
55
|
+
"securitytrails",
|
|
56
|
+
api_keys.securitytrails,
|
|
57
|
+
self._run_stage2_securitytrails,
|
|
58
|
+
"SecurityTrails subdomains",
|
|
59
|
+
),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
for name, key, func, desc in stage2_map:
|
|
63
|
+
if key and key.strip():
|
|
64
|
+
modules.append((name, func, desc))
|
|
65
|
+
|
|
66
|
+
return modules
|
|
67
|
+
|
|
68
|
+
# ---- Stage 1 module runners ----
|
|
69
|
+
|
|
70
|
+
async def _run_social_profiles(self, inputs: Dict) -> List[Dict]:
|
|
71
|
+
from osintkit.modules.social import run_social_profiles
|
|
72
|
+
return await run_social_profiles(inputs, self.config.timeout_seconds)
|
|
73
|
+
|
|
74
|
+
async def _run_email_accounts(self, inputs: Dict) -> List[Dict]:
|
|
75
|
+
from osintkit.modules.holehe import run_email_accounts
|
|
76
|
+
return await run_email_accounts(inputs, self.config.timeout_seconds)
|
|
77
|
+
|
|
78
|
+
async def _run_password_exposure(self, inputs: Dict) -> List[Dict]:
|
|
79
|
+
from osintkit.modules.hibp import run_password_exposure
|
|
80
|
+
return await run_password_exposure(inputs)
|
|
81
|
+
|
|
82
|
+
async def _run_web_presence(self, inputs: Dict) -> List[Dict]:
|
|
83
|
+
from osintkit.modules.harvester import run_web_presence
|
|
84
|
+
return await run_web_presence(inputs, self.config.timeout_seconds)
|
|
85
|
+
|
|
86
|
+
async def _run_cert_transparency(self, inputs: Dict) -> List[Dict]:
|
|
87
|
+
from osintkit.modules.certs import run_cert_transparency
|
|
88
|
+
return await run_cert_transparency(inputs)
|
|
89
|
+
|
|
90
|
+
async def _run_breach_exposure(self, inputs: Dict) -> List[Dict]:
|
|
91
|
+
from osintkit.modules.breach import run_breach_exposure
|
|
92
|
+
return await run_breach_exposure(inputs, self.config.api_keys)
|
|
93
|
+
|
|
94
|
+
async def _run_dark_web(self, inputs: Dict) -> List[Dict]:
|
|
95
|
+
from osintkit.modules.dark_web import run_dark_web
|
|
96
|
+
return await run_dark_web(inputs, self.config.api_keys)
|
|
97
|
+
|
|
98
|
+
async def _run_paste_sites(self, inputs: Dict) -> List[Dict]:
|
|
99
|
+
from osintkit.modules.paste import run_paste_sites
|
|
100
|
+
return await run_paste_sites(inputs, self.config.api_keys)
|
|
101
|
+
|
|
102
|
+
async def _run_data_brokers(self, inputs: Dict) -> List[Dict]:
|
|
103
|
+
from osintkit.modules.brokers import run_data_brokers
|
|
104
|
+
return await run_data_brokers(inputs, self.config.api_keys)
|
|
105
|
+
|
|
106
|
+
async def _run_phone(self, inputs: Dict) -> List[Dict]:
|
|
107
|
+
from osintkit.modules.phone import run_phone
|
|
108
|
+
return await run_phone(inputs, self.config.api_keys)
|
|
109
|
+
|
|
110
|
+
async def _run_sherlock(self, inputs: Dict) -> List[Dict]:
|
|
111
|
+
from osintkit.modules.sherlock import run_sherlock
|
|
112
|
+
return await run_sherlock(inputs, self.config.timeout_seconds)
|
|
113
|
+
|
|
114
|
+
async def _run_gravatar(self, inputs: Dict) -> List[Dict]:
|
|
115
|
+
from osintkit.modules.gravatar import run_gravatar
|
|
116
|
+
return await run_gravatar(inputs)
|
|
117
|
+
|
|
118
|
+
async def _run_wayback(self, inputs: Dict) -> List[Dict]:
|
|
119
|
+
from osintkit.modules.wayback import run_wayback
|
|
120
|
+
return await run_wayback(inputs)
|
|
121
|
+
|
|
122
|
+
async def _run_phone_info(self, inputs: Dict) -> List[Dict]:
|
|
123
|
+
from osintkit.modules.libphonenumber_info import run_libphonenumber
|
|
124
|
+
return await run_libphonenumber(inputs)
|
|
125
|
+
|
|
126
|
+
async def _run_hibp_kanon(self, inputs: Dict) -> List[Dict]:
|
|
127
|
+
from osintkit.modules.hibp_kanon import run_hibp_kanon
|
|
128
|
+
return await run_hibp_kanon(inputs)
|
|
129
|
+
|
|
130
|
+
# ---- Stage 2 module runners ----
|
|
131
|
+
|
|
132
|
+
async def _run_stage2_leakcheck(self, inputs: Dict) -> List[Dict]:
|
|
133
|
+
from osintkit.modules.stage2.leakcheck import run
|
|
134
|
+
return await run(inputs, self.config.api_keys.leakcheck)
|
|
135
|
+
|
|
136
|
+
async def _run_stage2_hunter(self, inputs: Dict) -> List[Dict]:
|
|
137
|
+
from osintkit.modules.stage2.hunter import run
|
|
138
|
+
return await run(inputs, self.config.api_keys.hunter)
|
|
139
|
+
|
|
140
|
+
async def _run_stage2_numverify(self, inputs: Dict) -> List[Dict]:
|
|
141
|
+
from osintkit.modules.stage2.numverify import run
|
|
142
|
+
return await run(inputs, self.config.api_keys.numverify)
|
|
143
|
+
|
|
144
|
+
async def _run_stage2_github(self, inputs: Dict) -> List[Dict]:
|
|
145
|
+
from osintkit.modules.stage2.github_api import run
|
|
146
|
+
return await run(inputs, self.config.api_keys.github)
|
|
147
|
+
|
|
148
|
+
async def _run_stage2_securitytrails(self, inputs: Dict) -> List[Dict]:
|
|
149
|
+
from osintkit.modules.stage2.securitytrails import run
|
|
150
|
+
return await run(inputs, self.config.api_keys.securitytrails)
|
|
151
|
+
|
|
152
|
+
# ---- Execution ----
|
|
153
|
+
|
|
154
|
+
def run(self, inputs: Dict) -> Dict[str, Any]:
|
|
155
|
+
"""Run all modules without progress display."""
|
|
156
|
+
findings = {
|
|
157
|
+
"scan_date": datetime.now().isoformat(),
|
|
158
|
+
"inputs": inputs,
|
|
159
|
+
"modules": {},
|
|
160
|
+
"findings": {},
|
|
161
|
+
"risk_score": 0,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async def run_one(name: str, func: Callable):
|
|
165
|
+
try:
|
|
166
|
+
result = await func(inputs)
|
|
167
|
+
findings["modules"][name] = {"status": "done", "count": len(result)}
|
|
168
|
+
findings["findings"][name] = result
|
|
169
|
+
except Exception as e:
|
|
170
|
+
findings["modules"][name] = {"status": "failed", "error": str(e)}
|
|
171
|
+
findings["findings"][name] = []
|
|
172
|
+
|
|
173
|
+
async def main():
|
|
174
|
+
await asyncio.gather(*[run_one(n, f) for n, f, _ in self.modules])
|
|
175
|
+
|
|
176
|
+
asyncio.run(main())
|
|
177
|
+
findings["risk_score"] = calculate_risk_score(findings["findings"])
|
|
178
|
+
return findings
|
|
179
|
+
|
|
180
|
+
def run_with_progress(self, inputs: Dict, progress: Progress) -> Dict[str, Any]:
|
|
181
|
+
"""Run all modules with progress display."""
|
|
182
|
+
findings = {
|
|
183
|
+
"scan_date": datetime.now().isoformat(),
|
|
184
|
+
"inputs": inputs,
|
|
185
|
+
"modules": {},
|
|
186
|
+
"findings": {},
|
|
187
|
+
"risk_score": 0,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
tasks = {}
|
|
191
|
+
for name, func, desc in self.modules:
|
|
192
|
+
task_id = progress.add_task(f"[cyan]{desc}...", total=None)
|
|
193
|
+
tasks[name] = {"func": func, "task_id": task_id, "desc": desc}
|
|
194
|
+
|
|
195
|
+
async def run_one(name: str):
|
|
196
|
+
task_info = tasks[name]
|
|
197
|
+
try:
|
|
198
|
+
result = await task_info["func"](inputs)
|
|
199
|
+
findings["modules"][name] = {"status": "done", "count": len(result)}
|
|
200
|
+
findings["findings"][name] = result
|
|
201
|
+
progress.update(
|
|
202
|
+
task_info["task_id"],
|
|
203
|
+
completed=True,
|
|
204
|
+
description=f"[green]done {task_info['desc']} ({len(result)})[/green]",
|
|
205
|
+
)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
findings["modules"][name] = {"status": "failed", "error": str(e)}
|
|
208
|
+
findings["findings"][name] = []
|
|
209
|
+
progress.update(
|
|
210
|
+
task_info["task_id"],
|
|
211
|
+
completed=True,
|
|
212
|
+
description=f"[yellow]failed {task_info['desc']}[/yellow]",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
async def main():
|
|
216
|
+
await asyncio.gather(*[run_one(name) for name in tasks])
|
|
217
|
+
|
|
218
|
+
asyncio.run(main())
|
|
219
|
+
findings["risk_score"] = calculate_risk_score(findings["findings"])
|
|
220
|
+
return findings
|
|
221
|
+
|
|
222
|
+
def write_json(self, findings: Dict) -> Path:
|
|
223
|
+
api_keys = self._get_api_key_values()
|
|
224
|
+
return write_json(findings, self.output_dir, api_keys=api_keys)
|
|
225
|
+
|
|
226
|
+
def write_html(self, findings: Dict) -> Path:
|
|
227
|
+
api_keys = self._get_api_key_values()
|
|
228
|
+
return write_html(findings, self.output_dir, api_keys=api_keys)
|
|
229
|
+
|
|
230
|
+
def write_md(self, findings: Dict) -> Path:
|
|
231
|
+
api_keys = self._get_api_key_values()
|
|
232
|
+
return write_md(findings, self.output_dir, api_keys=api_keys)
|
|
233
|
+
|
|
234
|
+
def _get_api_key_values(self) -> Dict[str, str]:
|
|
235
|
+
"""Return dict of all non-empty API key values for scrubbing."""
|
|
236
|
+
keys = {}
|
|
237
|
+
for field, value in self.config.api_keys.model_dump().items():
|
|
238
|
+
if value and isinstance(value, str):
|
|
239
|
+
keys[field] = value
|
|
240
|
+
return keys
|