Per-source-type epistemic ceilings for OSINT results - anti-overclaim as code.
Experimental — feature-complete and CI-tested with a zero-dependency core, but not yet published to PyPI, and the confidence anchors are hand-calibrated tradecraft heuristics rather than corpus-validated metrics (see What this is not).
OSINT tooling loves to render a green checkmark. A username "found" on 40 sites, a phone number "traced" to a carrier, an email "confirmed" - all presented with the same confident UI as a cryptographically real breach hit. The problem is that most OSINT signals cannot support that confidence, and the uncertainty usually lives only in a human analyst's head (or a footnote nobody reads).
This package moves that uncertainty into the type system of the result. Every
lookup is wrapped in a {"result": ..., "trust": ...} envelope whose verdict
is capped at the highest level the source type can honestly support - and
that cap is written into the code, not left to caller discipline.
from osint_trust_envelope import wrap_phone
env = wrap_phone({
"parsed": {"valid": True, "country_code": "+1", "enrichment_source": "libphonenumber"},
"social_checks": [{"platform": "WhatsApp", "possible": True}],
})
env["trust"]["verdict"] # -> "inferred" (never "verified", by design)
env["trust"]["confidence"] # -> 0.66
env["trust"]["warnings"] # -> ["number_portability_not_reflected",
# "ownership_not_determinable_from_number_alone", ...]A phone number wrapper structurally cannot return verified, no matter how
rich the input. That is the whole point.
Four levels, most trustworthy to least:
| Verdict | Meaning | Confidence band |
|---|---|---|
verified |
A real, authoritative source confirmed it (HIBP k-anonymity, an RDAP/DNS resolve, EXIF parsed from a local file). | 0.85 - 1.00 |
inferred |
Real data was retrieved, but the interpretation is indirect (an MX record exists; an avatar URL returned 200). | 0.55 - 0.80 |
heuristic |
Pattern/regex/404-scraping. False positives are expected. | 0.25 - 0.55 |
unverified |
The check was attempted but nothing came back, or the input was malformed. The honest "we don't know". | 0.00 - 0.20 |
confidence is a separate 0-1 number that tracks the verdict but lets you
order results within a band.
Note on the word
verified: it is a verdict label meaning "an authoritative upstream source confirmed this", assigned from the raw data you pass in. The library performs no network calls and makes no independent claim about your data - it records the ceiling the source type allows.
This table is the library. Each wrapper enforces a maximum verdict because of a concrete tradecraft reason the source type can't escape.
| Wrapper | Max verdict | Why it can't go higher |
|---|---|---|
wrap_phone |
inferred | Number portability (MNP) breaks prefix-to-carrier inference; messenger presence proves reachability, not ownership; a paid reverse-lookup gives a current carrier, never an identity. |
wrap_email |
inferred | An MX record proves the domain accepts mail, not that this mailbox exists or is read; aliases, forwarders and catch-all rules are invisible from outside; SPF/DMARC describe handling policy, not ownership. |
wrap_username_scan |
heuristic (-> inferred only with cross-platform corroboration or a historical track record) | HTTP-status / 404-based detection is structurally fragile - false positives are expected. Only independent agreement across enough platforms, or a per-site reliability history, earns a promotion. |
wrap_company |
inferred | A GitHub org is a real API hit, but the social-presence half is 404-scraped. |
wrap_avatar |
inferred | "A profile image exists at this URL" is not "owned by the target"; correlation is probabilistic. |
wrap_paste |
inferred | Hits require manual relevance review; the presence of a string is not attribution. |
wrap_ip |
verified (<= 0.92; <= 0.95 for a Tor exit) | Geo + RDAP + reverse-DNS can corroborate each other, but geolocation is ISP-level, never user-level. |
wrap_domain |
verified (<= 0.96) | DNS + RDAP + SSL + HTTP are authoritative for the domain; registrar/WHOIS data is frequently privacy-redacted. |
wrap_breach |
verified (<= 0.97) | The HIBP k-anonymity password check is cryptographically real; the email-breach path needs a paid key. |
wrap_whois / wrap_ssl / wrap_metadata |
verified | RDAP API, a TLS handshake, and a local binary parse are authoritative for what they measure (EXIF can still be spoofed or stripped). |
wrap_pipeline |
= weakest sub-module | A pipeline is only as trustworthy as its least-trustworthy link. |
Mandatory honesty disclaimers ride along in trust.warnings and are always
present for the relevant source type - e.g. a phone result always carries
number_portability_not_reflected; an email always carries
mailbox_existence_not_proven; an IP always carries
ip_geolocation_is_isp_level_not_user_level. They cannot be configured off.
Being honest about the tool is the same discipline the tool encodes:
- The confidence anchors and ceilings are hand-calibrated tradecraft heuristics, not measured precision/recall. They have not been validated against a labelled external corpus. The numbers express a relative epistemic ordering ("an MX record is worth more than a regex match, less than a DNSSEC resolve"), not a probability you should bet on. No accuracy figure is claimed.
- It performs no lookups. It does not call any API, resolve any DNS, or touch the network. You bring the raw result from your own adapters; this layer only assigns the trust envelope.
- It does not make a person-level identity determination. Every wrapper
that touches identity caps below
verifiedprecisely because identity cannot be established from these signals.
If you wire this into a product, surface the verdict and the warnings - not a bare green checkmark.
wrap_username_scan can lift a verdict from heuristic toward inferred when
it has a historical reliability score per site. That data source is a
pluggable seam, not bundled (the core stays zero-dependency):
import osint_trust_envelope.trust as trust
# username -> {site: reliability_score 0..1}
def my_history(username: str) -> dict[str, float]:
return load_scores_for(username)
trust._get_site_confidences = my_history
# optional: trust._detect_site_anomaly = my_anomaly_detectorThe shipped defaults are no-ops, so out of the box the scanner uses only the static ladder. Wiring a provider is entirely opt-in.
pip install osint-trust-envelope # once published
# or, from a checkout:
pip install .Zero runtime dependencies. Python 3.10+.
python demo.pypip install ".[dev]"
pytestMIT - see LICENSE.
- mcp-objauthz-lab — vulnerable-by-design MCP server for learning BOLA/IDOR
- devguard-scan — 100% client-side secret scanner
Full index → github.com/WRG-11