osintkit 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +28 -24
  3. package/bin/osintkit.js +32 -20
  4. package/osintkit/__init__.py +1 -1
  5. package/osintkit/__pycache__/__init__.cpython-311.pyc +0 -0
  6. package/osintkit/__pycache__/cli.cpython-311.pyc +0 -0
  7. package/osintkit/__pycache__/config.cpython-311.pyc +0 -0
  8. package/osintkit/__pycache__/profiles.cpython-311.pyc +0 -0
  9. package/osintkit/__pycache__/risk.cpython-311.pyc +0 -0
  10. package/osintkit/__pycache__/scanner.cpython-311.pyc +0 -0
  11. package/osintkit/__pycache__/setup.cpython-311.pyc +0 -0
  12. package/osintkit/cli.py +59 -38
  13. package/osintkit/config.py +13 -0
  14. package/osintkit/modules/__init__.py +10 -0
  15. package/osintkit/modules/__pycache__/__init__.cpython-311.pyc +0 -0
  16. package/osintkit/modules/__pycache__/breach.cpython-311.pyc +0 -0
  17. package/osintkit/modules/__pycache__/brokers.cpython-311.pyc +0 -0
  18. package/osintkit/modules/__pycache__/certs.cpython-311.pyc +0 -0
  19. package/osintkit/modules/__pycache__/dark_web.cpython-311.pyc +0 -0
  20. package/osintkit/modules/__pycache__/gravatar.cpython-311.pyc +0 -0
  21. package/osintkit/modules/__pycache__/harvester.cpython-311.pyc +0 -0
  22. package/osintkit/modules/__pycache__/hibp.cpython-311.pyc +0 -0
  23. package/osintkit/modules/__pycache__/hibp_kanon.cpython-311.pyc +0 -0
  24. package/osintkit/modules/__pycache__/holehe.cpython-311.pyc +0 -0
  25. package/osintkit/modules/__pycache__/libphonenumber_info.cpython-311.pyc +0 -0
  26. package/osintkit/modules/__pycache__/paste.cpython-311.pyc +0 -0
  27. package/osintkit/modules/__pycache__/phone.cpython-311.pyc +0 -0
  28. package/osintkit/modules/__pycache__/sherlock.cpython-311.pyc +0 -0
  29. package/osintkit/modules/__pycache__/social.cpython-311.pyc +0 -0
  30. package/osintkit/modules/__pycache__/wayback.cpython-311.pyc +0 -0
  31. package/osintkit/modules/gravatar.py +0 -0
  32. package/osintkit/modules/libphonenumber_info.py +0 -0
  33. package/osintkit/modules/sherlock.py +0 -0
  34. package/osintkit/modules/stage2/__init__.py +0 -0
  35. package/osintkit/modules/stage2/__pycache__/__init__.cpython-311.pyc +0 -0
  36. package/osintkit/modules/stage2/__pycache__/github_api.cpython-311.pyc +0 -0
  37. package/osintkit/modules/stage2/__pycache__/hunter.cpython-311.pyc +0 -0
  38. package/osintkit/modules/stage2/__pycache__/leakcheck.cpython-311.pyc +0 -0
  39. package/osintkit/modules/stage2/__pycache__/numverify.cpython-311.pyc +0 -0
  40. package/osintkit/modules/stage2/__pycache__/securitytrails.cpython-311.pyc +0 -0
  41. package/osintkit/modules/stage2/github_api.py +12 -9
  42. package/osintkit/modules/stage2/hunter.py +7 -5
  43. package/osintkit/modules/stage2/leakcheck.py +7 -5
  44. package/osintkit/modules/stage2/numverify.py +7 -5
  45. package/osintkit/modules/stage2/securitytrails.py +7 -5
  46. package/osintkit/output/__pycache__/__init__.cpython-311.pyc +0 -0
  47. package/osintkit/output/__pycache__/html_writer.cpython-311.pyc +0 -0
  48. package/osintkit/output/__pycache__/json_writer.cpython-311.pyc +0 -0
  49. package/osintkit/output/__pycache__/md_writer.cpython-311.pyc +0 -0
  50. package/osintkit/output/md_writer.py +0 -0
  51. package/osintkit/output/templates/report.html +313 -50
  52. package/osintkit/scanner.py +25 -2
  53. package/osintkit/setup.py +21 -10
  54. package/package.json +10 -2
  55. package/postinstall.js +69 -50
  56. package/pyproject.toml +1 -1
  57. package/osintkit/__pycache__/__main__.cpython-311.pyc +0 -0
@@ -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
- 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; }
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
- <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
-
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-gauge risk-high">
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-gauge risk-medium">
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-gauge risk-low">
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
- <h2>Risk Score: {{ score }}/100</h2>
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
- <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 %}
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
- {% endfor %}
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
- <h2>Findings</h2>
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
- {% 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>
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
- <br><small>Confidence: {{ finding.confidence }}</small>
68
- </div>
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
- {% endif %}
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>
@@ -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"[yellow]failed {task_info['desc']}[/yellow]",
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(config, f, default_flow_style=False)
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",
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"