osintkit 0.8.0__tar.gz → 0.9.0__tar.gz
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.
- {osintkit-0.8.0 → osintkit-0.9.0}/CHANGELOG.md +9 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/PKG-INFO +7 -4
- {osintkit-0.8.0 → osintkit-0.9.0}/README.md +5 -3
- {osintkit-0.8.0 → osintkit-0.9.0}/docs/REFERENCE.md +10 -1
- {osintkit-0.8.0 → osintkit-0.9.0}/pyproject.toml +2 -1
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/__init__.py +1 -1
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/cli.py +51 -5
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/collectors/__init__.py +1 -0
- osintkit-0.9.0/src/osintkit/collectors/identifiers.py +65 -0
- osintkit-0.9.0/src/osintkit/confidence.py +55 -0
- osintkit-0.9.0/src/osintkit/data/common-passwords.txt +10001 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/models.py +1 -0
- osintkit-0.9.0/src/osintkit/passwords_offline.py +29 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/pipeline.py +16 -1
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/render.py +28 -4
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/synthesis.py +2 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/targets.py +13 -1
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_cli.py +23 -0
- osintkit-0.9.0/tests/test_confidence.py +42 -0
- osintkit-0.9.0/tests/test_identifiers.py +62 -0
- osintkit-0.9.0/tests/test_passwords_offline.py +17 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/uv.lock +12 -1
- {osintkit-0.8.0 → osintkit-0.9.0}/.editorconfig +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/.github/CODEOWNERS +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/.github/dependabot.yml +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/.github/pull_request_template.md +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/.github/workflows/ci.yml +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/.github/workflows/codeql.yml +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/.github/workflows/publish.yml +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/.github/workflows/release.yml +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/.github/workflows/smoke.yml +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/.gitignore +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/.mailmap +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/.pre-commit-config.yaml +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/CODE_OF_CONDUCT.md +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/CONTRIBUTING.md +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/LICENSE +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/SECURITY.md +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/docs/architecture.md +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/docs/demo.svg +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/docs/sample-output.md +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/docs/social-preview.png +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/examples/README.md +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/examples/targets.csv +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/catalogue.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/collectors/accounts.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/collectors/base.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/collectors/breaches.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/collectors/ct.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/collectors/dns.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/collectors/github.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/collectors/infostealer.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/collectors/pastes.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/collectors/ports.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/collectors/subdomains.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/collectors/whois_lookup.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/collectors/xposed_analytics.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/config.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/diff.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/doctor.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/errors.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/http.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/llm.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/logging_store.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/normalise.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/offline.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/passwords.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/py.typed +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/retention.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/userconfig.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/verify.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/src/osintkit/watch.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/conftest.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/fixtures/model_reply.json +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_accounts.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_breaches.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_catalogue.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_collectors.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_config.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_ct.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_diff.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_doctor.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_e2e.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_http.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_infostealer.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_llm.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_normalise.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_offline.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_passwords.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_pastes.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_pipeline.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_render.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_retention.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_synthesis.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_targets.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_userconfig.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_verify.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_watch.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_whois.py +0 -0
- {osintkit-0.8.0 → osintkit-0.9.0}/tests/test_xposed_analytics.py +0 -0
|
@@ -6,6 +6,15 @@ All notable changes are recorded here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.9.0 — 2026-11-02
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- phone targets with offline libphonenumber metadata (region, carrier, line type, timezones)
|
|
14
|
+
- Bitcoin address targets with balance and transaction history from Blockstream
|
|
15
|
+
- per-section confidence, capped by the number of backing sources
|
|
16
|
+
- `password --offline` against a bundled 10k common-password list, no network
|
|
17
|
+
|
|
9
18
|
## 0.8.0 — 2026-11-02
|
|
10
19
|
|
|
11
20
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: osintkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Public-data OSINT aggregator for emails, usernames, domains, and companies
|
|
5
5
|
Project-URL: Homepage, https://github.com/sjdsjadjajdjsa/osintkit
|
|
6
6
|
Project-URL: Issues, https://github.com/sjdsjadjajdjsa/osintkit/issues
|
|
@@ -19,6 +19,7 @@ Classifier: Typing :: Typed
|
|
|
19
19
|
Requires-Python: >=3.12
|
|
20
20
|
Requires-Dist: dnspython>=2.6
|
|
21
21
|
Requires-Dist: httpx>=0.27
|
|
22
|
+
Requires-Dist: phonenumbers>=8.13
|
|
22
23
|
Requires-Dist: pydantic>=2.7
|
|
23
24
|
Requires-Dist: python-whois>=0.9
|
|
24
25
|
Requires-Dist: rich>=13.7
|
|
@@ -38,7 +39,7 @@ Description-Content-Type: text/markdown
|
|
|
38
39
|
[](LICENSE)
|
|
39
40
|
[](https://github.com/astral-sh/ruff)
|
|
40
41
|
|
|
41
|
-
Public-data OSINT for one email, username, domain, or
|
|
42
|
+
Public-data OSINT for one email, username, domain, company, phone number, or Bitcoin address: gathered from free sources,
|
|
42
43
|
briefed by an LLM acting as a defensive analyst, rendered as a colour-coded report.
|
|
43
44
|
|
|
44
45
|
`osintkit` uses only public sources: certificate transparency, public DNS, WHOIS, public GitHub
|
|
@@ -102,10 +103,10 @@ Everything else needs no credentials.
|
|
|
102
103
|
|
|
103
104
|
| Command | Purpose |
|
|
104
105
|
| --- | --- |
|
|
105
|
-
| `lookup` | One target, one report (`--email`, `--username`, `--domain`, `--company`) |
|
|
106
|
+
| `lookup` | One target, one report (`--email`, `--username`, `--domain`, `--company`, `--phone`, `--crypto`) |
|
|
106
107
|
| `batch` | A CSV of targets, run concurrently, reported in order |
|
|
107
108
|
| `diff` | What changed between two archived runs |
|
|
108
|
-
| `password` | Is a password in Pwned Passwords (
|
|
109
|
+
| `password` | Is a password in Pwned Passwords (`--offline` uses a bundled list, no network) |
|
|
109
110
|
| `watch` | Re-check on an interval, print only changes, optionally notify |
|
|
110
111
|
| `history` | Past runs, replayable by id |
|
|
111
112
|
| `doctor` | Key, endpoint, and directory checks (`--deep` verifies auth) |
|
|
@@ -147,6 +148,8 @@ the model fail.
|
|
|
147
148
|
undercut.
|
|
148
149
|
- **Cached everything.** 24h default, shorter for volatile sources; repeat prompts cost nothing.
|
|
149
150
|
- **Validated output.** Strict JSON schema, one repair round-trip, then a clean failure.
|
|
151
|
+
- **Confidence per section.** Each section states `low`, `medium`, or `high`, capped by how many
|
|
152
|
+
sources actually back it, so a single weak source never reads as certain.
|
|
150
153
|
- **Self-checked claims.** Every breach, date, host, and account URL is compared back to the
|
|
151
154
|
collected data; anything unsupported is called out.
|
|
152
155
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/astral-sh/ruff)
|
|
8
8
|
|
|
9
|
-
Public-data OSINT for one email, username, domain, or
|
|
9
|
+
Public-data OSINT for one email, username, domain, company, phone number, or Bitcoin address: gathered from free sources,
|
|
10
10
|
briefed by an LLM acting as a defensive analyst, rendered as a colour-coded report.
|
|
11
11
|
|
|
12
12
|
`osintkit` uses only public sources: certificate transparency, public DNS, WHOIS, public GitHub
|
|
@@ -70,10 +70,10 @@ Everything else needs no credentials.
|
|
|
70
70
|
|
|
71
71
|
| Command | Purpose |
|
|
72
72
|
| --- | --- |
|
|
73
|
-
| `lookup` | One target, one report (`--email`, `--username`, `--domain`, `--company`) |
|
|
73
|
+
| `lookup` | One target, one report (`--email`, `--username`, `--domain`, `--company`, `--phone`, `--crypto`) |
|
|
74
74
|
| `batch` | A CSV of targets, run concurrently, reported in order |
|
|
75
75
|
| `diff` | What changed between two archived runs |
|
|
76
|
-
| `password` | Is a password in Pwned Passwords (
|
|
76
|
+
| `password` | Is a password in Pwned Passwords (`--offline` uses a bundled list, no network) |
|
|
77
77
|
| `watch` | Re-check on an interval, print only changes, optionally notify |
|
|
78
78
|
| `history` | Past runs, replayable by id |
|
|
79
79
|
| `doctor` | Key, endpoint, and directory checks (`--deep` verifies auth) |
|
|
@@ -115,6 +115,8 @@ the model fail.
|
|
|
115
115
|
undercut.
|
|
116
116
|
- **Cached everything.** 24h default, shorter for volatile sources; repeat prompts cost nothing.
|
|
117
117
|
- **Validated output.** Strict JSON schema, one repair round-trip, then a clean failure.
|
|
118
|
+
- **Confidence per section.** Each section states `low`, `medium`, or `high`, capped by how many
|
|
119
|
+
sources actually back it, so a single weak source never reads as certain.
|
|
118
120
|
- **Self-checked claims.** Every breach, date, host, and account URL is compared back to the
|
|
119
121
|
collected data; anything unsupported is called out.
|
|
120
122
|
|
|
@@ -7,6 +7,8 @@ Everything the README leaves out: every flag, every collector, storage, and fail
|
|
|
7
7
|
| Collector | Targets | Source | Needs key |
|
|
8
8
|
| --- | --- | --- | --- |
|
|
9
9
|
| `infostealer` | email / username / domain | Hudson Rock stealer logs | no |
|
|
10
|
+
| `phone_info` | phone | libphonenumber metadata, offline | no |
|
|
11
|
+
| `btc_address` | crypto | Blockstream address history | no |
|
|
10
12
|
| `hibp_breaches`, `hibp_pastes` | email | Have I Been Pwned | yes |
|
|
11
13
|
| `xposed_or_not`, `xposed_analytics`, `leakcheck_public` | email | free public breach indexes | no |
|
|
12
14
|
| `gravatar` | email | Gravatar | no |
|
|
@@ -62,6 +64,12 @@ Explicit `--base-url` and `--model` beat both the provider and the prefix. Tempe
|
|
|
62
64
|
Per-command: `--json`, `--markdown`, `-o/--out`, `--raw`, `--offline`, `--dry-run`, `--only`, `--skip`,
|
|
63
65
|
`--ports`, `--fail-fast`, `-c/--concurrency`, `--limit`, `--show`, `--days`, `--clear`, `--deep`.
|
|
64
66
|
|
|
67
|
+
## Confidence
|
|
68
|
+
|
|
69
|
+
Every section carries `low`, `medium`, or `high`. The value is derived from how many contributing
|
|
70
|
+
sources returned data (two or more is high, one is medium) and the model may be more pessimistic
|
|
71
|
+
but never more optimistic. A section backed by one index therefore cannot claim high confidence.
|
|
72
|
+
|
|
65
73
|
## Watching
|
|
66
74
|
|
|
67
75
|
`watch` repeats a lookup on an interval (`--every`, minimum 60s), prints a baseline, and then
|
|
@@ -94,7 +102,8 @@ domains, and record counts. At most 15 enriched rows go into the prompt; the tru
|
|
|
94
102
|
|
|
95
103
|
## Password checks
|
|
96
104
|
|
|
97
|
-
`osintkit password
|
|
105
|
+
`osintkit password --offline` checks the bundled 10k most-common list and never opens a socket;
|
|
106
|
+
without `--offline` it hashes locally, sends only the five-character SHA-1 prefix to
|
|
98
107
|
`api.pwnedpasswords.com`, and reports the count and a commonness rating. Exit code `1` means
|
|
99
108
|
pwned. `--json` for machines. Nothing about the password beyond the prefix leaves the machine.
|
|
100
109
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "osintkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.0"
|
|
8
8
|
description = "Public-data OSINT aggregator for emails, usernames, domains, and companies"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -29,6 +29,7 @@ dependencies = [
|
|
|
29
29
|
"rich>=13.7",
|
|
30
30
|
"dnspython>=2.6",
|
|
31
31
|
"python-whois>=0.9",
|
|
32
|
+
"phonenumbers>=8.13",
|
|
32
33
|
]
|
|
33
34
|
|
|
34
35
|
[project.optional-dependencies]
|
|
@@ -18,7 +18,18 @@ from rich.console import Console
|
|
|
18
18
|
from rich.panel import Panel
|
|
19
19
|
from rich.table import Table
|
|
20
20
|
|
|
21
|
-
from . import
|
|
21
|
+
from . import (
|
|
22
|
+
__version__,
|
|
23
|
+
diff,
|
|
24
|
+
doctor,
|
|
25
|
+
passwords,
|
|
26
|
+
passwords_offline,
|
|
27
|
+
pipeline,
|
|
28
|
+
render,
|
|
29
|
+
retention,
|
|
30
|
+
userconfig,
|
|
31
|
+
watch,
|
|
32
|
+
)
|
|
22
33
|
from .collectors import REGISTRY
|
|
23
34
|
from .config import (
|
|
24
35
|
DEFAULT_RESOLVERS,
|
|
@@ -139,6 +150,8 @@ def _pick_target(
|
|
|
139
150
|
username: str | None,
|
|
140
151
|
domain: str | None,
|
|
141
152
|
company: str | None,
|
|
153
|
+
phone: str | None = None,
|
|
154
|
+
crypto: str | None = None,
|
|
142
155
|
) -> tuple[str, str | None]:
|
|
143
156
|
supplied = [
|
|
144
157
|
(name, value)
|
|
@@ -147,16 +160,18 @@ def _pick_target(
|
|
|
147
160
|
("username", username),
|
|
148
161
|
("domain", domain),
|
|
149
162
|
("company", company),
|
|
163
|
+
("phone", phone),
|
|
164
|
+
("crypto", crypto),
|
|
150
165
|
)
|
|
151
166
|
if value
|
|
152
167
|
]
|
|
153
168
|
if len(supplied) > 1:
|
|
154
|
-
raise TargetParseError("pass only one of --email/--username/--domain/--company")
|
|
169
|
+
raise TargetParseError("pass only one of --email/--username/--domain/--company/--phone/--crypto")
|
|
155
170
|
if supplied:
|
|
156
171
|
kind, value = supplied[0]
|
|
157
172
|
return value, kind
|
|
158
173
|
if not target:
|
|
159
|
-
raise TargetParseError("give a target or one of --email/--username/--domain/--company")
|
|
174
|
+
raise TargetParseError("give a target or one of --email/--username/--domain/--company/--phone/--crypto")
|
|
160
175
|
return target, None
|
|
161
176
|
|
|
162
177
|
|
|
@@ -249,6 +264,8 @@ def lookup(
|
|
|
249
264
|
username: str | None = typer.Option(None, "--username", help="Username target."),
|
|
250
265
|
domain: str | None = typer.Option(None, "--domain", help="Domain target."),
|
|
251
266
|
company: str | None = typer.Option(None, "--company", help="Company target."),
|
|
267
|
+
phone: str | None = typer.Option(None, "--phone", help="Phone target (E.164, e.g. +15551234567)."),
|
|
268
|
+
crypto: str | None = typer.Option(None, "--crypto", help="Bitcoin address target."),
|
|
252
269
|
json_out: bool = typer.Option(False, "--json", help="Raw structured JSON on stdout."),
|
|
253
270
|
markdown: bool = typer.Option(False, "--markdown", help="Write a Markdown report."),
|
|
254
271
|
out_path: Path | None = typer.Option(None, "--out", "-o", help="Markdown destination."),
|
|
@@ -277,7 +294,7 @@ def lookup(
|
|
|
277
294
|
if out_path:
|
|
278
295
|
settings.extra["markdown_path"] = str(out_path)
|
|
279
296
|
try:
|
|
280
|
-
raw, declared = _pick_target(target, email, username, domain, company)
|
|
297
|
+
raw, declared = _pick_target(target, email, username, domain, company, phone, crypto)
|
|
281
298
|
except TargetParseError as exc:
|
|
282
299
|
out.print(f"[red]{exc}[/red]")
|
|
283
300
|
raise typer.Exit(2) from exc
|
|
@@ -646,6 +663,8 @@ def watch_cmd(
|
|
|
646
663
|
username: str | None = typer.Option(None, "--username", help="Username target."),
|
|
647
664
|
domain: str | None = typer.Option(None, "--domain", help="Domain target."),
|
|
648
665
|
company: str | None = typer.Option(None, "--company", help="Company target."),
|
|
666
|
+
phone: str | None = typer.Option(None, "--phone", help="Phone target (E.164, e.g. +15551234567)."),
|
|
667
|
+
crypto: str | None = typer.Option(None, "--crypto", help="Bitcoin address target."),
|
|
649
668
|
every: str = typer.Option("1d", "--every", help="Interval: 30m, 6h, 1d."),
|
|
650
669
|
cycles: int = typer.Option(0, "--cycles", min=0, help="Stop after N runs; 0 means forever."),
|
|
651
670
|
full: bool = typer.Option(False, "--full", help="Print the whole report on each change."),
|
|
@@ -658,7 +677,7 @@ def watch_cmd(
|
|
|
658
677
|
out = _console()
|
|
659
678
|
try:
|
|
660
679
|
interval = watch.parse_duration(every)
|
|
661
|
-
raw, declared = _pick_target(target, email, username, domain, company)
|
|
680
|
+
raw, declared = _pick_target(target, email, username, domain, company, phone, crypto)
|
|
662
681
|
except (ValueError, TargetParseError) as exc:
|
|
663
682
|
out.print(f"[red]{exc}[/red]")
|
|
664
683
|
raise typer.Exit(2) from exc
|
|
@@ -694,6 +713,7 @@ def watch_cmd(
|
|
|
694
713
|
def password(
|
|
695
714
|
value: str | None = typer.Argument(None, help="Password to check; prompt if omitted."),
|
|
696
715
|
json_out: bool = typer.Option(False, "--json", help="JSON output."),
|
|
716
|
+
offline: bool = typer.Option(False, "--offline", help="Use only the bundled list; no network."),
|
|
697
717
|
) -> None:
|
|
698
718
|
"""Check a password against Pwned Passwords, without sending it."""
|
|
699
719
|
out = _console()
|
|
@@ -707,6 +727,32 @@ def password(
|
|
|
707
727
|
out.print("[red]no password given[/red]")
|
|
708
728
|
raise typer.Exit(2)
|
|
709
729
|
|
|
730
|
+
local_rank = passwords_offline.check(secret)
|
|
731
|
+
if offline:
|
|
732
|
+
if local_rank:
|
|
733
|
+
if json_out:
|
|
734
|
+
print(
|
|
735
|
+
json.dumps(
|
|
736
|
+
{
|
|
737
|
+
"pwned": True,
|
|
738
|
+
"rank": local_rank,
|
|
739
|
+
"list_size": passwords_offline.size(),
|
|
740
|
+
"checked_by": "bundled list",
|
|
741
|
+
}
|
|
742
|
+
)
|
|
743
|
+
)
|
|
744
|
+
else:
|
|
745
|
+
out.print(
|
|
746
|
+
f"[red]common[/red] rank #{local_rank:,} of {passwords_offline.size():,} "
|
|
747
|
+
"(bundled list, no network)"
|
|
748
|
+
)
|
|
749
|
+
raise typer.Exit(1)
|
|
750
|
+
if json_out:
|
|
751
|
+
print(json.dumps({"pwned": False, "checked_by": "bundled list"}))
|
|
752
|
+
else:
|
|
753
|
+
out.print(f"not in the bundled top {passwords_offline.size():,}; not proof of strength")
|
|
754
|
+
return
|
|
755
|
+
|
|
710
756
|
settings: Settings = STATE["settings"] # type: ignore[assignment]
|
|
711
757
|
|
|
712
758
|
async def run() -> dict[str, object]:
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Phone metadata (offline) and Bitcoin address history."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import phonenumbers
|
|
6
|
+
from phonenumbers import PhoneNumberType, carrier, geocoder, timezone
|
|
7
|
+
|
|
8
|
+
from .base import Context, collector
|
|
9
|
+
|
|
10
|
+
BTC = "https://blockstream.info/api/address"
|
|
11
|
+
LINE_TYPES = {
|
|
12
|
+
value: name
|
|
13
|
+
for name, value in vars(PhoneNumberType).items()
|
|
14
|
+
if isinstance(value, int) and not name.startswith("_")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@collector("phone_info", ("phone",), "Phone region, carrier, and line type", order=15, ttl=2592000)
|
|
19
|
+
async def phone_info(ctx: Context) -> dict:
|
|
20
|
+
try:
|
|
21
|
+
parsed = phonenumbers.parse(ctx.target, None)
|
|
22
|
+
except phonenumbers.NumberParseException as exc:
|
|
23
|
+
return {"valid": False, "note": str(exc)}
|
|
24
|
+
return {
|
|
25
|
+
"valid": phonenumbers.is_valid_number(parsed),
|
|
26
|
+
"possible": phonenumbers.is_possible_number(parsed),
|
|
27
|
+
"region": phonenumbers.region_code_for_number(parsed),
|
|
28
|
+
"country": geocoder.description_for_number(parsed, "en") or None,
|
|
29
|
+
"carrier": carrier.name_for_number(parsed, "en") or None,
|
|
30
|
+
"line_type": LINE_TYPES.get(phonenumbers.number_type(parsed), "UNKNOWN"),
|
|
31
|
+
"timezones": list(timezone.time_zones_for_number(parsed)),
|
|
32
|
+
"e164": phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164),
|
|
33
|
+
"international": phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.INTERNATIONAL),
|
|
34
|
+
"source": "libphonenumber",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@collector("btc_address", ("crypto",), "Bitcoin address balances and transaction history", order=16, ttl=3600)
|
|
39
|
+
async def btc_address(ctx: Context) -> dict:
|
|
40
|
+
response = await ctx.http.get(
|
|
41
|
+
f"{BTC}/{ctx.target}",
|
|
42
|
+
namespace="blockstream",
|
|
43
|
+
empty_on=(400, 404),
|
|
44
|
+
timeout=ctx.http_timeout or 30,
|
|
45
|
+
)
|
|
46
|
+
if response.status_code != 200:
|
|
47
|
+
return {"found": False, "note": f"HTTP {response.status_code}"}
|
|
48
|
+
try:
|
|
49
|
+
body = response.json()
|
|
50
|
+
except ValueError:
|
|
51
|
+
return {"found": False, "note": "non-JSON"}
|
|
52
|
+
|
|
53
|
+
chain = body.get("chain_stats") or {}
|
|
54
|
+
mempool = body.get("mempool_stats") or {}
|
|
55
|
+
funded = int(chain.get("funded_txo_sum") or 0)
|
|
56
|
+
spent = int(chain.get("spent_txo_sum") or 0)
|
|
57
|
+
return {
|
|
58
|
+
"found": True,
|
|
59
|
+
"balance_sats": funded - spent,
|
|
60
|
+
"balance_btc": round((funded - spent) / 100_000_000, 8),
|
|
61
|
+
"tx_count": int(chain.get("tx_count") or 0),
|
|
62
|
+
"received_sats": funded,
|
|
63
|
+
"mempool_tx_count": int(mempool.get("tx_count") or 0),
|
|
64
|
+
"source": "blockstream.info",
|
|
65
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Per-section confidence, capped by how many sources actually back a section."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .models import CollectorResult
|
|
8
|
+
|
|
9
|
+
SECTIONS = ("breaches", "accounts", "infrastructure", "subdomains")
|
|
10
|
+
ORDER = ("low", "medium", "high")
|
|
11
|
+
|
|
12
|
+
CONTRIBUTORS: dict[str, frozenset[str]] = {
|
|
13
|
+
"breaches": frozenset({"breach_index"}),
|
|
14
|
+
"accounts": frozenset({"username_presence", "github_profile", "github_commit_search"}),
|
|
15
|
+
"infrastructure": frozenset(
|
|
16
|
+
{
|
|
17
|
+
"dns_records",
|
|
18
|
+
"dns_mail_posture",
|
|
19
|
+
"dns_wildcard",
|
|
20
|
+
"whois_registration",
|
|
21
|
+
"crt_sh_certificates",
|
|
22
|
+
"tcp_ports",
|
|
23
|
+
"infostealer",
|
|
24
|
+
"phone_info",
|
|
25
|
+
"btc_address",
|
|
26
|
+
}
|
|
27
|
+
),
|
|
28
|
+
"subdomains": frozenset({"crt_sh_subdomains", "passive_subdomains"}),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _rank(value: str) -> int:
|
|
33
|
+
return ORDER.index(value) if value in ORDER else 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def derive(results: list[CollectorResult]) -> dict[str, str]:
|
|
37
|
+
"""Confidence implied by the data: 2+ contributing sources is high, 1 medium, 0 low."""
|
|
38
|
+
out: dict[str, str] = {}
|
|
39
|
+
for section, names in CONTRIBUTORS.items():
|
|
40
|
+
count = sum(1 for result in results if result.collector in names and result.has_data)
|
|
41
|
+
if section == "breaches":
|
|
42
|
+
rows = next((r.data for r in results if r.collector == "breach_index" and r.has_data), [])
|
|
43
|
+
count = min(len(rows), 2) if rows else 0
|
|
44
|
+
out[section] = "high" if count >= 2 else "medium" if count else "low"
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def apply(chosen: dict[str, Any] | None, derived: dict[str, str]) -> dict[str, str]:
|
|
49
|
+
"""Keep the model's honest pessimism, but never let it outrank the evidence."""
|
|
50
|
+
final: dict[str, str] = {}
|
|
51
|
+
for section in SECTIONS:
|
|
52
|
+
claimed = str((chosen or {}).get(section) or "").strip().lower()
|
|
53
|
+
floor = derived.get(section, "low")
|
|
54
|
+
final[section] = claimed if claimed in ORDER and _rank(claimed) <= _rank(floor) else floor
|
|
55
|
+
return final
|