Skip to main content

adler_core/
lib.rs

1//! Core engine for the [Adler](https://github.com/commit3296/adler)
2//! OSINT username-search tool — runtime-agnostic, embed-friendly.
3//!
4//! The CLI lives in `adler-cli`; this crate is what you reach for to
5//! drive username detection from your own Rust code (a Discord bot
6//! that checks usernames, a security tool that flags exposed
7//! identities across a watchlist, a CI gate that asserts a name
8//! isn't claimed elsewhere, …).
9//!
10//! ## Quick start
11//!
12//! Scan the embedded 1,900-entry main registry for one username and print
13//! the hits:
14//!
15//! ```no_run
16//! use adler_core::{Client, ExecutorOptions, MatchKind, Registry, Username, executor};
17//!
18//! # async fn run() -> adler_core::Result<()> {
19//! let registry = Registry::default_embedded()?;
20//!
21//! // filter(include, exclude, tags, exclude_tags, include_nsfw)
22//! // — empty slices = no name/tag filter; `false` keeps the
23//! // default NSFW auto-exclusion (matches Sherlock's `--nsfw`
24//! // opt-in). Pass `true` (or `&["nsfw".into()]` as tags) to
25//! // scan adult-content sites.
26//! let sites = registry.filter(&[], &[], &[], &[], false);
27//!
28//! let username = Username::new("torvalds")?;
29//! let client = Client::builder().build()?;
30//!
31//! let outcomes =
32//!     executor::run(&client, &sites, &username, ExecutorOptions::default()).await;
33//!
34//! for outcome in outcomes.iter().filter(|o| o.kind == MatchKind::Found) {
35//!     println!("{} → {}", outcome.site, outcome.url);
36//! }
37//! # Ok(())
38//! # }
39//! ```
40//!
41//! ## Map of the public API
42//!
43//! Detection plumbing:
44//!
45//! - [`Registry`] — loaded, validated collection of sites. Build from
46//!   the embedded [`default_embedded`](Registry::default_embedded),
47//!   from a JSON string ([`from_json_str`](Registry::from_json_str)),
48//!   or from disk ([`load_from_path`](Registry::load_from_path)).
49//! - [`Site`], [`Signal`], [`UrlTemplate`], [`Extractor`],
50//!   [`KnownPresent`] — site-registry value types. `Site` is
51//!   serde-(de)serialisable; the JSON Schema lives in `docs/sites.schema.json`.
52//! - [`Username`] — validated search target. Constructed via
53//!   [`Username::new`](Username::new); invalid characters / overlong
54//!   names are rejected at construction time.
55//! - [`Client`], [`ClientBuilder`] — `reqwest`-backed probe issuer.
56//!   Knobs the builder exposes: timeout, redirect limit, per-host /
57//!   global throttle, retry policy, user-agent rotation pool, proxy,
58//!   `robots.txt` cache, browser backend, browser budget.
59//! - [`CheckOutcome`], [`MatchKind`], [`UncertainReason`] — verdict
60//!   types. The signal pipeline is *negative-priority*: any
61//!   `NotFound` vote wins over `Found`; no votes → `Uncertain`. A
62//!   per-site `regex_check` mismatch short-circuits with
63//!   [`UncertainReason::UsernameNotAllowed`] before any HTTP request.
64//! - [`executor`] — bounded-concurrency fan-out runner. Pass an
65//!   [`ExecutorOptions`] to control concurrency, deadline, and
66//!   progress callback.
67//!
68//! Optional analysis:
69//!
70//! - [`correlate`] — group accounts that look like the same person
71//!   across sites via [`enriched`](crate::correlate::correlate)
72//!   profile fields.
73//! - [`permute`] — generate username variants
74//!   (alice → alice1, alice.dev, …) via [`MAX_VARIANTS`] /
75//!   [`PermuteLevel`].
76//! - [`WatchlistConfig`] — serde-compatible watchlist configuration
77//!   for usernames, aliases, and optional site/tag scopes.
78//! - [`doctor`] — registry health check
79//!   ([`check_site`](crate::doctor::check_site)), signature
80//!   derivation ([`suggest_fix`](crate::doctor::suggest_fix)),
81//!   known-present discovery
82//!   ([`discover_known_present`](crate::doctor::discover_known_present)),
83//!   site scaffolding ([`scaffold_site`](crate::doctor::scaffold_site)).
84//!
85//! Bot-protected sites (Instagram, X/Twitter today):
86//!
87//! - [`BrowserBackend`] trait — abstract real-Chrome driver.
88//!   Configurable on the [`Client`] via
89//!   [`ClientBuilder::browser`](ClientBuilder::browser). Built-in
90//!   implementations: [`browser::local::LocalBackend`] (free, via
91//!   `chromiumoxide`) and
92//!   [`browser::browserbase::BrowserbaseBackend`] (cloud, residential
93//!   IPs, in-tree raw async CDP client). [`BrowserBudget`] caps
94//!   browser-routed fetches per scan to keep cost predictable.
95//!
96//! ## Cache
97//!
98//! [`Cache`] persists per-(site, username, signal-signature) verdicts
99//! between runs. Compose with [`Client`] via the builder or skip
100//! entirely for one-shot scans.
101//!
102//! ## Error model
103//!
104//! [`Result`] is a `Result<T, Error>` alias; [`Error`] is a single
105//! crate-level `thiserror` enum. The probe path *never* surfaces
106//! errors — transient network failures become
107//! [`MatchKind::Uncertain`] with a typed [`UncertainReason`], so
108//! you get a partial result for every site even when the network is
109//! flaky. Loader errors (malformed registry JSON, invalid CSS
110//! selectors, regex compile failures) come back as `Err`.
111//!
112//! ## Version history
113//!
114//! Pre-1.0 `SemVer`. Breaking changes since 0.1:
115//!
116//! - **0.2.0** — added [`Site::request_headers`] (`BTreeMap<String,
117//!   String>`); [`BrowserBackend::fetch`] gained the `headers`
118//!   parameter; [`browser`] module became `pub`.
119//! - **0.3.0** — [`Site::known_present`] changed from
120//!   `Option<String>` to `Option<KnownPresent>` (the new enum
121//!   accepts string-or-array via untagged serde);
122//!   [`DoctorReport::Healthy::present`] and
123//!   `Unhealthy::present` changed from `Option<CheckOutcome>` to
124//!   `Vec<(String, CheckOutcome)>` (one entry per probed candidate).
125//! - **0.4.0** — [`Registry::filter`] gained a fifth
126//!   `include_nsfw: bool` parameter (default-exclude adult sites);
127//!   [`UncertainReason`] gained `UsernameNotAllowed`;
128//!   [`Site::regex_check`] field added (per-site username regex).
129//!
130//! Each change has a migration block in [the
131//! CHANGELOG](https://github.com/commit3296/adler/blob/main/CHANGELOG.md).
132
133mod access;
134mod avatar;
135mod ban;
136mod cache;
137mod check;
138mod client;
139mod confidence;
140mod correlate;
141pub mod doctor;
142mod enrich;
143mod error;
144mod escalation;
145pub mod executor;
146mod history;
147mod identity;
148mod permute;
149mod profile;
150mod registry;
151mod report;
152mod report_render;
153mod retry;
154mod robots;
155mod site;
156pub mod telemetry;
157#[cfg(test)]
158mod test_fixtures;
159mod throttle;
160mod transport;
161mod username;
162mod watchlist;
163
164pub mod browser;
165
166pub use access::{
167    AccessPolicy, CountryCode, EgressKind, EgressSpec, EgressSummary, Session, SessionStore,
168};
169pub use avatar::{
170    AVATAR_HASH_ALGORITHM, AvatarHashError, AvatarHashOptions, DEFAULT_AVATAR_HASH_MAX_BYTES,
171    DEFAULT_AVATAR_HASH_TIMEOUT, avatar_hash_from_bytes, fetch_avatar_hash,
172};
173pub use browser::{BrowserBackend, BrowserBudget, RenderedPage};
174pub use cache::Cache;
175pub use check::{CheckOutcome, MatchKind, UncertainReason};
176pub use client::{
177    BOT_PROTECTED_TAG, Client, ClientBuilder, DEFAULT_BROWSER_BUDGET, DEFAULT_ESCALATION_BUDGET,
178    RawResponse,
179};
180pub use confidence::{ConfidenceLabel, ConfidenceReason, ConfidenceScore};
181pub use correlate::{Cluster, CorrelationReport, LINK_THRESHOLD, correlate};
182pub use doctor::{DoctorReport, ExtractSuggestion, FixSuggestion};
183pub use error::{Error, Result};
184pub use escalation::{EscalationBudget, TransportTier};
185pub use executor::ExecutorOptions;
186pub use history::{HistoricalScanRef, historical_consistency_counts};
187pub use identity::{
188    ClusterReason, IdentityCluster, ObservedProfile, build_identity_clusters,
189    build_identity_clusters_with_history,
190};
191pub use permute::{MAX_VARIANTS, PermuteLevel, permute};
192pub use profile::{
193    EvidenceAccessPath, EvidenceOrigin, EvidenceSource, ProfileEvidence, ProfileEvidenceKind,
194};
195pub use registry::{Registry, SiteFilter};
196pub use report::{
197    INVESTIGATION_REPORT_SCHEMA_VERSION, InvestigationReport, InvestigationReportBuilder,
198    ReportAccount, ReportDisabledSite, ReportEvidence, ReportLimitation, ReportLimitationKind,
199    ReportSummary, ReportTimelineEvent, ReportTimelineEventKind, ReportUncertainAccount,
200};
201pub use report_render::{render_investigation_report_html, render_investigation_report_markdown};
202pub use site::{
203    Engine, Extractor, HttpMethod, KnownPresent, ProtectionKind, Signal, Site, UrlTemplate,
204};
205pub use username::Username;
206pub use watchlist::{
207    ScanSchedule, WATCHLIST_CONFIG_SCHEMA_VERSION, WatchScanTarget, WatchScope, WatchTarget,
208    WatchlistConfig, WatchlistError,
209};