osintkit 0.5.0__tar.gz → 0.6.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.5.0 → osintkit-0.6.0}/CHANGELOG.md +7 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/PKG-INFO +6 -1
- {osintkit-0.5.0 → osintkit-0.6.0}/README.md +5 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/docs/REFERENCE.md +10 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/pyproject.toml +1 -1
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/__init__.py +1 -1
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/collectors/__init__.py +1 -0
- osintkit-0.6.0/src/osintkit/collectors/infostealer.py +71 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/normalise.py +4 -2
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/offline.py +18 -1
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/pipeline.py +5 -1
- osintkit-0.6.0/tests/test_infostealer.py +64 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_normalise.py +9 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/.editorconfig +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/.github/CODEOWNERS +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/.github/dependabot.yml +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/.github/pull_request_template.md +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/.github/workflows/ci.yml +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/.github/workflows/codeql.yml +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/.github/workflows/publish.yml +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/.github/workflows/release.yml +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/.github/workflows/smoke.yml +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/.gitignore +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/.mailmap +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/.pre-commit-config.yaml +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/CODE_OF_CONDUCT.md +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/CONTRIBUTING.md +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/LICENSE +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/SECURITY.md +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/docs/architecture.md +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/docs/demo.svg +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/docs/sample-output.md +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/docs/social-preview.png +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/examples/README.md +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/examples/targets.csv +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/catalogue.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/cli.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/collectors/accounts.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/collectors/base.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/collectors/breaches.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/collectors/ct.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/collectors/dns.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/collectors/github.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/collectors/pastes.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/collectors/ports.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/collectors/subdomains.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/collectors/whois_lookup.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/config.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/diff.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/doctor.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/errors.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/http.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/llm.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/logging_store.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/models.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/passwords.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/py.typed +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/render.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/retention.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/synthesis.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/targets.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/src/osintkit/userconfig.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/conftest.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/fixtures/model_reply.json +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_accounts.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_breaches.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_catalogue.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_cli.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_collectors.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_config.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_ct.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_diff.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_doctor.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_e2e.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_http.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_llm.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_offline.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_passwords.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_pastes.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_pipeline.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_render.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_retention.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_synthesis.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_targets.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_userconfig.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/tests/test_whois.py +0 -0
- {osintkit-0.5.0 → osintkit-0.6.0}/uv.lock +0 -0
|
@@ -6,6 +6,13 @@ All notable changes are recorded here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.6.0 — 2026-11-02
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `infostealer` collector for Hudson Rock stealer logs across email, username, and domain
|
|
14
|
+
- any stealer infection forces a high risk floor and a rotate-everything recommendation
|
|
15
|
+
|
|
9
16
|
## 0.5.0 — 2026-11-02
|
|
10
17
|
|
|
11
18
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: osintkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -138,6 +138,8 @@ the model fail.
|
|
|
138
138
|
and XposedOrNot catalogues. Those catalogues are free; only HIBP *account* checks need a key.
|
|
139
139
|
- **Email domains follow the address.** An email lookup also checks the domain's DNS, mail
|
|
140
140
|
posture, and WHOIS, marked as derived.
|
|
141
|
+
- **Info-stealer logs.** Hudson Rock reports machines infected by stealers that saved
|
|
142
|
+
credentials, with dates and hostnames; one infection forces a high risk floor.
|
|
141
143
|
- **Risk floor.** Breach count and credential types set a floor the model may exceed but never
|
|
142
144
|
undercut.
|
|
143
145
|
- **Cached everything.** 24h default, shorter for volatile sources; repeat prompts cost nothing.
|
|
@@ -159,6 +161,9 @@ against the public HIBP and XposedOrNot catalogues for date, exposed fields, and
|
|
|
159
161
|
the report can say "LinkedIn, 2021-06, emails and password hashes" instead of a bare name. Add a
|
|
160
162
|
`HIBP_API_KEY` (about $4/month) to get per-account membership with the same metadata, plus pastes.
|
|
161
163
|
|
|
164
|
+
Stealer logs matter more than most breaches: the credentials were live on an infected machine,
|
|
165
|
+
not just leaked years ago. They are queried for every target type, keyless.
|
|
166
|
+
|
|
162
167
|
Passwords are checked separately with `osintkit password`: Pwned Passwords covers hundreds of
|
|
163
168
|
millions of leaked passwords, needs no key, and only the first five SHA-1 characters leave the
|
|
164
169
|
machine.
|
|
@@ -106,6 +106,8 @@ the model fail.
|
|
|
106
106
|
and XposedOrNot catalogues. Those catalogues are free; only HIBP *account* checks need a key.
|
|
107
107
|
- **Email domains follow the address.** An email lookup also checks the domain's DNS, mail
|
|
108
108
|
posture, and WHOIS, marked as derived.
|
|
109
|
+
- **Info-stealer logs.** Hudson Rock reports machines infected by stealers that saved
|
|
110
|
+
credentials, with dates and hostnames; one infection forces a high risk floor.
|
|
109
111
|
- **Risk floor.** Breach count and credential types set a floor the model may exceed but never
|
|
110
112
|
undercut.
|
|
111
113
|
- **Cached everything.** 24h default, shorter for volatile sources; repeat prompts cost nothing.
|
|
@@ -127,6 +129,9 @@ against the public HIBP and XposedOrNot catalogues for date, exposed fields, and
|
|
|
127
129
|
the report can say "LinkedIn, 2021-06, emails and password hashes" instead of a bare name. Add a
|
|
128
130
|
`HIBP_API_KEY` (about $4/month) to get per-account membership with the same metadata, plus pastes.
|
|
129
131
|
|
|
132
|
+
Stealer logs matter more than most breaches: the credentials were live on an infected machine,
|
|
133
|
+
not just leaked years ago. They are queried for every target type, keyless.
|
|
134
|
+
|
|
130
135
|
Passwords are checked separately with `osintkit password`: Pwned Passwords covers hundreds of
|
|
131
136
|
millions of leaked passwords, needs no key, and only the first five SHA-1 characters leave the
|
|
132
137
|
machine.
|
|
@@ -6,6 +6,7 @@ Everything the README leaves out: every flag, every collector, storage, and fail
|
|
|
6
6
|
|
|
7
7
|
| Collector | Targets | Source | Needs key |
|
|
8
8
|
| --- | --- | --- | --- |
|
|
9
|
+
| `infostealer` | email / username / domain | Hudson Rock stealer logs | no |
|
|
9
10
|
| `hibp_breaches`, `hibp_pastes` | email | Have I Been Pwned | yes |
|
|
10
11
|
| `xposed_or_not`, `leakcheck_public` | email | free public breach indexes | no |
|
|
11
12
|
| `gravatar` | email | Gravatar | no |
|
|
@@ -61,6 +62,15 @@ Explicit `--base-url` and `--model` beat both the provider and the prefix. Tempe
|
|
|
61
62
|
Per-command: `--json`, `--markdown`, `-o/--out`, `--raw`, `--offline`, `--dry-run`, `--only`, `--skip`,
|
|
62
63
|
`--ports`, `--fail-fast`, `-c/--concurrency`, `--limit`, `--show`, `--days`, `--clear`, `--deep`.
|
|
63
64
|
|
|
65
|
+
## Info-stealer logs
|
|
66
|
+
|
|
67
|
+
`infostealer` queries Hudson Rock's keyless endpoint for infections that saved credentials on a
|
|
68
|
+
compromised machine. It returns the infection count, dates, hostnames, IPs, antivirus state, and
|
|
69
|
+
masked logins and passwords. Any infection forces a `high` risk floor; masked values are shown as
|
|
70
|
+
the source provides them and are not usable as credentials.
|
|
71
|
+
|
|
72
|
+
The address goes to Hudson Rock; nothing else does.
|
|
73
|
+
|
|
64
74
|
## Breach catalogue enrichment
|
|
65
75
|
|
|
66
76
|
Breach names from free indexes are joined against `haveibeenpwned.com/api/v3/breaches` (keyless
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Info-stealer log exposure via Hudson Rock's keyless endpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
from .base import Context, collector
|
|
8
|
+
|
|
9
|
+
BASE = "https://cavalier.hudsonrock.com/api/json/v2/osint-tools/search-by"
|
|
10
|
+
MAX_STEALERS = 5
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _clean(value: object) -> str | None:
|
|
14
|
+
text = str(value or "").strip()
|
|
15
|
+
if not text or text.lower() == "not found":
|
|
16
|
+
return None
|
|
17
|
+
return text[:120]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _summary(body: object) -> dict:
|
|
21
|
+
if not isinstance(body, dict):
|
|
22
|
+
return {"infected": False, "infections": 0, "stealers": []}
|
|
23
|
+
raw = body.get("stealers") or []
|
|
24
|
+
stealers = []
|
|
25
|
+
for row in raw[:MAX_STEALERS]:
|
|
26
|
+
if not isinstance(row, dict):
|
|
27
|
+
continue
|
|
28
|
+
stealers.append(
|
|
29
|
+
{
|
|
30
|
+
"date": (row.get("date_compromised") or "")[:10] or None,
|
|
31
|
+
"computer": _clean(row.get("computer_name")),
|
|
32
|
+
"os": _clean(row.get("operating_system")),
|
|
33
|
+
"ip": _clean(row.get("ip")),
|
|
34
|
+
"antiviruses": [av for av in row.get("antiviruses") or [] if av != "None"][:5],
|
|
35
|
+
"logins": [str(item) for item in (row.get("top_logins") or [])[:5]],
|
|
36
|
+
"passwords": [str(item) for item in (row.get("top_passwords") or [])[:5]],
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
return {
|
|
40
|
+
"infected": bool(stealers),
|
|
41
|
+
"infections": len(raw),
|
|
42
|
+
"corporate_services": body.get("total_corporate_services"),
|
|
43
|
+
"user_services": body.get("total_user_services"),
|
|
44
|
+
"stealers": stealers,
|
|
45
|
+
"source": "hudsonrock",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _lookup(ctx: Context, kind: str) -> dict:
|
|
50
|
+
response = await ctx.http.get(
|
|
51
|
+
f"{BASE}-{kind}",
|
|
52
|
+
namespace="hudsonrock",
|
|
53
|
+
params={kind: ctx.target},
|
|
54
|
+
empty_on=(404, 429),
|
|
55
|
+
timeout=ctx.http_timeout or 45,
|
|
56
|
+
)
|
|
57
|
+
if response.status_code != 200:
|
|
58
|
+
return {"infected": False, "infections": 0, "stealers": [], "note": f"HTTP {response.status_code}"}
|
|
59
|
+
try:
|
|
60
|
+
return _summary(response.json())
|
|
61
|
+
except ValueError:
|
|
62
|
+
return {"infected": False, "infections": 0, "stealers": [], "note": "non-JSON"}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@collector("infostealer", ("email", "username", "domain"), "Info-stealer log infections", order=5, ttl=43200)
|
|
66
|
+
async def infostealer(ctx: Context) -> dict:
|
|
67
|
+
kind = {"email": "email", "username": "username", "domain": "domain"}[ctx.target_type]
|
|
68
|
+
result = await _lookup(ctx, kind)
|
|
69
|
+
result["target_kind"] = kind
|
|
70
|
+
result["checked"] = quote(ctx.target)
|
|
71
|
+
return result
|
|
@@ -14,7 +14,7 @@ SEVERITY = ("low", "medium", "high")
|
|
|
14
14
|
MAX_PROMPT_BREACHES = 15
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def signals_for(merged: list[dict[str, Any]]) -> dict[str, Any]:
|
|
17
|
+
def signals_for(merged: list[dict[str, Any]], infections: int = 0) -> dict[str, Any]:
|
|
18
18
|
"""Compute risk signals the model can check itself against."""
|
|
19
19
|
credential = [
|
|
20
20
|
entry
|
|
@@ -22,10 +22,12 @@ def signals_for(merged: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
22
22
|
if any(CREDENTIAL_RE.search(str(kind)) for kind in entry.get("data_types", []))
|
|
23
23
|
]
|
|
24
24
|
count = len(merged)
|
|
25
|
+
floor = "high" if infections else _floor(count, len(credential))
|
|
25
26
|
return {
|
|
26
27
|
"breach_count": count,
|
|
27
28
|
"credential_bearing": len(credential),
|
|
28
|
-
"
|
|
29
|
+
"stealer_infections": infections,
|
|
30
|
+
"risk_floor": floor,
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
|
|
@@ -91,6 +91,15 @@ def _infrastructure(results: list[CollectorResult], report_notes: list[str]) ->
|
|
|
91
91
|
if isinstance(wildcard, dict) and wildcard.get("wildcard"):
|
|
92
92
|
notes.append("Wildcard DNS is enabled, so unknown labels resolve")
|
|
93
93
|
|
|
94
|
+
stealer = _data(results, "infostealer")
|
|
95
|
+
if isinstance(stealer, dict) and stealer.get("infections"):
|
|
96
|
+
first = (stealer.get("stealers") or [{}])[0]
|
|
97
|
+
notes.append(
|
|
98
|
+
f"{stealer['infections']} info-stealer infection(s)"
|
|
99
|
+
+ (f", latest {first.get('date')}" if first.get("date") else "")
|
|
100
|
+
+ (f" on {first.get('computer')}" if first.get("computer") else "")
|
|
101
|
+
)
|
|
102
|
+
|
|
94
103
|
whois = _data(results, "whois_registration")
|
|
95
104
|
if isinstance(whois, dict) and whois.get("registrar"):
|
|
96
105
|
parts = [f"registrar {whois['registrar']}"]
|
|
@@ -117,9 +126,16 @@ def _infrastructure(results: list[CollectorResult], report_notes: list[str]) ->
|
|
|
117
126
|
|
|
118
127
|
|
|
119
128
|
def _recommendations(
|
|
120
|
-
creds: int,
|
|
129
|
+
creds: int,
|
|
130
|
+
mail: dict | None,
|
|
131
|
+
wildcard: bool,
|
|
132
|
+
ports: list[int],
|
|
133
|
+
breach_count: int,
|
|
134
|
+
infections: int = 0,
|
|
121
135
|
) -> list[str]:
|
|
122
136
|
recs: list[str] = []
|
|
137
|
+
if infections:
|
|
138
|
+
recs.append("Change every password saved on the infected machine, then sign out all sessions")
|
|
123
139
|
if creds:
|
|
124
140
|
recs.append("Rotate passwords for the affected services and enable MFA where offered")
|
|
125
141
|
elif breach_count:
|
|
@@ -204,5 +220,6 @@ def build(
|
|
|
204
220
|
wildcard,
|
|
205
221
|
risky,
|
|
206
222
|
signals["breach_count"],
|
|
223
|
+
int((_data(results, "infostealer") or {}).get("infections") or 0),
|
|
207
224
|
),
|
|
208
225
|
)
|
|
@@ -186,7 +186,11 @@ async def execute(
|
|
|
186
186
|
logger.event("breaches_merged", duplicates=duplicates)
|
|
187
187
|
|
|
188
188
|
merged_breaches = next((r.data for r in prompt_results if r.collector == "breach_index"), [])
|
|
189
|
-
|
|
189
|
+
stealer = next((r.data for r in prompt_results if r.collector == "infostealer"), {})
|
|
190
|
+
infections = int((stealer or {}).get("infections") or 0)
|
|
191
|
+
signals = normalise.signals_for(merged_breaches, infections)
|
|
192
|
+
if infections:
|
|
193
|
+
report.notes.append(f"{infections} info-stealer infection(s) observed")
|
|
190
194
|
|
|
191
195
|
bundle = PromptBundle(
|
|
192
196
|
target=target,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import respx
|
|
3
|
+
|
|
4
|
+
from osintkit.collectors import infostealer
|
|
5
|
+
|
|
6
|
+
URL = "https://cavalier.hudsonrock.com/api/json/v2/osint-tools/search-by-email"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@respx.mock
|
|
10
|
+
async def test_infections_are_summarised(make_context):
|
|
11
|
+
ctx = make_context("a@b.co", "email")
|
|
12
|
+
respx.get(URL).mock(
|
|
13
|
+
return_value=httpx.Response(
|
|
14
|
+
200,
|
|
15
|
+
json={
|
|
16
|
+
"stealers": [
|
|
17
|
+
{
|
|
18
|
+
"date_compromised": "2026-10-31T00:00:00.000Z",
|
|
19
|
+
"computer_name": "DESKTOP-1 (David)",
|
|
20
|
+
"operating_system": "Not Found",
|
|
21
|
+
"ip": "105.118.***.**",
|
|
22
|
+
"antiviruses": ["None"],
|
|
23
|
+
"top_logins": ["a***n"],
|
|
24
|
+
"top_passwords": ["v**************E"],
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"total_corporate_services": 3,
|
|
28
|
+
"total_user_services": 9,
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
result = await infostealer.infostealer(ctx)
|
|
33
|
+
assert result["infected"] is True
|
|
34
|
+
assert result["infections"] == 1
|
|
35
|
+
assert result["stealers"][0]["date"] == "2026-10-31"
|
|
36
|
+
assert result["stealers"][0]["os"] is None
|
|
37
|
+
assert result["stealers"][0]["antiviruses"] == []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@respx.mock
|
|
41
|
+
async def test_clean_target(make_context):
|
|
42
|
+
ctx = make_context("a@b.co", "email")
|
|
43
|
+
respx.get(URL).mock(return_value=httpx.Response(200, json={"stealers": []}))
|
|
44
|
+
result = await infostealer.infostealer(ctx)
|
|
45
|
+
assert result["infected"] is False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@respx.mock
|
|
49
|
+
async def test_rate_limited_is_not_an_error(make_context):
|
|
50
|
+
ctx = make_context("a@b.co", "email")
|
|
51
|
+
respx.get(URL).mock(return_value=httpx.Response(429))
|
|
52
|
+
result = await infostealer.infostealer(ctx)
|
|
53
|
+
assert result["infections"] == 0
|
|
54
|
+
assert "429" in result["note"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@respx.mock
|
|
58
|
+
async def test_domain_uses_domain_endpoint(make_context):
|
|
59
|
+
ctx = make_context("example.com", "domain")
|
|
60
|
+
route = respx.get("https://cavalier.hudsonrock.com/api/json/v2/osint-tools/search-by-domain").mock(
|
|
61
|
+
return_value=httpx.Response(200, json={"stealers": []})
|
|
62
|
+
)
|
|
63
|
+
await infostealer.infostealer(ctx)
|
|
64
|
+
assert route.calls[0].request.url.params["domain"] == "example.com"
|
|
@@ -116,3 +116,12 @@ def test_dict_shaped_sources_are_merged():
|
|
|
116
116
|
row = next(r for r in kept if r.collector == "breach_index").data[0]
|
|
117
117
|
assert row["sources"] == ["leakcheck_public", "xposedornot"]
|
|
118
118
|
assert row["date"] == "2020-01-01"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_infections_force_a_high_floor():
|
|
122
|
+
assert normalise.signals_for([], infections=1)["risk_floor"] == "high"
|
|
123
|
+
assert normalise.signals_for([], infections=0)["risk_floor"] == "low"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_infections_are_counted_in_signals():
|
|
127
|
+
assert normalise.signals_for([], infections=3)["stealer_infections"] == 3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|