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.
Files changed (65) hide show
  1. package/README.md +86 -0
  2. package/bin/osintkit.js +7 -0
  3. package/osintkit/__init__.py +3 -0
  4. package/osintkit/__pycache__/__init__.cpython-311.pyc +0 -0
  5. package/osintkit/__pycache__/cli.cpython-311.pyc +0 -0
  6. package/osintkit/__pycache__/config.cpython-311.pyc +0 -0
  7. package/osintkit/__pycache__/profiles.cpython-311.pyc +0 -0
  8. package/osintkit/__pycache__/risk.cpython-311.pyc +0 -0
  9. package/osintkit/__pycache__/scanner.cpython-311.pyc +0 -0
  10. package/osintkit/cli.py +613 -0
  11. package/osintkit/config.py +51 -0
  12. package/osintkit/modules/__init__.py +6 -0
  13. package/osintkit/modules/__pycache__/__init__.cpython-311.pyc +0 -0
  14. package/osintkit/modules/__pycache__/breach.cpython-311.pyc +0 -0
  15. package/osintkit/modules/__pycache__/brokers.cpython-311.pyc +0 -0
  16. package/osintkit/modules/__pycache__/certs.cpython-311.pyc +0 -0
  17. package/osintkit/modules/__pycache__/dark_web.cpython-311.pyc +0 -0
  18. package/osintkit/modules/__pycache__/gravatar.cpython-311.pyc +0 -0
  19. package/osintkit/modules/__pycache__/harvester.cpython-311.pyc +0 -0
  20. package/osintkit/modules/__pycache__/hibp.cpython-311.pyc +0 -0
  21. package/osintkit/modules/__pycache__/hibp_kanon.cpython-311.pyc +0 -0
  22. package/osintkit/modules/__pycache__/holehe.cpython-311.pyc +0 -0
  23. package/osintkit/modules/__pycache__/libphonenumber_info.cpython-311.pyc +0 -0
  24. package/osintkit/modules/__pycache__/paste.cpython-311.pyc +0 -0
  25. package/osintkit/modules/__pycache__/phone.cpython-311.pyc +0 -0
  26. package/osintkit/modules/__pycache__/sherlock.cpython-311.pyc +0 -0
  27. package/osintkit/modules/__pycache__/social.cpython-311.pyc +0 -0
  28. package/osintkit/modules/__pycache__/wayback.cpython-311.pyc +0 -0
  29. package/osintkit/modules/breach.py +82 -0
  30. package/osintkit/modules/brokers.py +56 -0
  31. package/osintkit/modules/certs.py +42 -0
  32. package/osintkit/modules/dark_web.py +51 -0
  33. package/osintkit/modules/gravatar.py +50 -0
  34. package/osintkit/modules/harvester.py +56 -0
  35. package/osintkit/modules/hibp.py +40 -0
  36. package/osintkit/modules/hibp_kanon.py +66 -0
  37. package/osintkit/modules/holehe.py +39 -0
  38. package/osintkit/modules/libphonenumber_info.py +79 -0
  39. package/osintkit/modules/paste.py +55 -0
  40. package/osintkit/modules/phone.py +32 -0
  41. package/osintkit/modules/sherlock.py +48 -0
  42. package/osintkit/modules/social.py +58 -0
  43. package/osintkit/modules/stage2/__init__.py +1 -0
  44. package/osintkit/modules/stage2/github_api.py +65 -0
  45. package/osintkit/modules/stage2/hunter.py +64 -0
  46. package/osintkit/modules/stage2/leakcheck.py +58 -0
  47. package/osintkit/modules/stage2/numverify.py +62 -0
  48. package/osintkit/modules/stage2/securitytrails.py +65 -0
  49. package/osintkit/modules/wayback.py +70 -0
  50. package/osintkit/output/__init__.py +1 -0
  51. package/osintkit/output/__pycache__/__init__.cpython-311.pyc +0 -0
  52. package/osintkit/output/__pycache__/html_writer.cpython-311.pyc +0 -0
  53. package/osintkit/output/__pycache__/json_writer.cpython-311.pyc +0 -0
  54. package/osintkit/output/__pycache__/md_writer.cpython-311.pyc +0 -0
  55. package/osintkit/output/html_writer.py +36 -0
  56. package/osintkit/output/json_writer.py +31 -0
  57. package/osintkit/output/md_writer.py +115 -0
  58. package/osintkit/output/templates/report.html +74 -0
  59. package/osintkit/profiles.py +116 -0
  60. package/osintkit/risk.py +42 -0
  61. package/osintkit/scanner.py +240 -0
  62. package/osintkit/setup.py +157 -0
  63. package/package.json +25 -0
  64. package/pyproject.toml +44 -0
  65. 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
@@ -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