Commit Graph
134 Commits
Author SHA1 Message Date
SoxojandGitHub d7f759a239 Keep runtime activation tokens out of the sites database (#2973)
* Keep runtime activation tokens out of the sites database

* Document the activation token cache
2026-08-15 12:43:30 +02:00
felipeandGitHub b84d61331f ci: publish dev builds as nightly prereleases (#2959) (#2963) 2026-08-14 22:10:53 +02:00
felipeandGitHub 77b565cfd5 fix: normalize SOCKS5 proxy scheme per HTTP transport (#2955) (#2966) 2026-08-12 17:04:32 +02:00
SoxojandGitHub fbda9cdfcc fix: retry transient network/proxy errors in site checkers (#2954)
Rotating/residential proxies often drop a connection mid-request
(truncated body, closed socket, failed handshake) instead of failing
outright. These were previously lumped into "Unexpected" and never
retried, silently discarding otherwise-successful checks.

- Retry once on ClientPayloadError, ServerDisconnectedError, and
  aiohttp_socks' ProxyConnectionError/ProxyTimeoutError (aiohttp
  checker), and on CurlError (curl_cffi checker) — covers truncated
  responses, dropped connections, CONNECT-tunnel 502s, and TLS
  handshake failures.
- Fix the proxy-error except clause: it caught python_socks'
  ProxyError, but aiohttp_socks (the connector actually in use)
  raises its own unrelated same-named exceptions, so it never fired.
- Generic ProxyError (e.g. bad credentials) is classified but NOT
  retried — it fails identically every attempt.
2026-08-11 13:16:13 +02:00
SoxojandGitHub af13058cfe Update sponsorship information (#2953) 2026-08-11 11:14:46 +02:00
Julio César SuásteguiandGitHub 34a1669974 Fix XMind reports for modern readers (#2930)
* fix(report): add XMind manifest for modern readers

* docs: document modern XMind reader compatibility
2026-08-10 22:54:35 +02:00
Sanjay SanthanamandGitHub e08310da87 fix: don't let the SUPPORTED_IDS branch re-add a rejected username in extract_ids_from_page (#2924)
extract_ids_from_page() has the same bug #2907 fixed in
checking.py::parse_usernames(). "username" is itself in SUPPORTED_IDS, so
the `if k in SUPPORTED_IDS` loop stores a bare `username` value directly,
bypassing the is_plausible_username() guard that extract_usernames()
applies in its own separate pass. A URL or email returned by
socid_extractor under a bare `username` key therefore becomes a recursive
search target via `maigret --parse-url`, reproducing the #1403
false-error cascade.

Skip keys containing "username" in the SUPPORTED_IDS loop; those are
already owned (and validated) by extract_usernames(). Every other
SUPPORTED_ID (gaia_id, vk_id, orcid, ...) is unaffected since none of
those keys contain "username".

Add regression tests for the bare `username` key with a URL value and for
the unchanged handling of other supported IDs.

Closes #2911
2026-08-03 08:43:50 +09:00
SoxojandGitHub d1b8b9fa5c Web interface update: live scan, settings perseistence, history, nagivation (#2925) 2026-08-03 08:41:07 +09:00
AshvinandGitHub 0fd5bf66e8 fix: block SSRF / local-file reads via report image URLs during PDF generation (#2908)
* fix: block SSRF and local-file reads via report image URLs in PDF generation

save_pdf_report() rendered scraped profile image URLs (ids_data['image'])
straight into xhtml2pdf, which fetches <img src> while building the PDF.
The image field is attacker-influenced and pisaDocument ran with no
link_callback, so a profile carrying image = "file:///etc/passwd" or an
intranet/metadata URL turned report generation into a local file read or
an SSRF from the machine running maigret. In the web UI this is
server-side and fires on every search, since save_pdf_report is always
called.

Add a link_callback that only lets public http(s) images through and
diverts everything else (file://, data:, other schemes, and hosts that
resolve to loopback/private/link-local/reserved addresses) to a bundled
1x1 placeholder, so no fetch or read happens. Diverting rather than
raising keeps report generation working when a scanned profile carries a
hostile image URL.

Tests cover the URL classifier, the callback's placeholder diversion, and
an end-to-end check that PDF generation does not fetch an internal image.

* fix: use is_global to also block CGNAT (100.64.0.0/10) report image hosts

The flag chain missed 100.64.0.0/10, which is neither is_private nor
is_global and is routable inside many cloud and k8s networks. is_global
covers it along with private, loopback, link-local and unspecified.

Multicast and reserved stay explicit: both are still is_global on
CPython, and 64:ff9b::/96 reaches IPv4 through a NAT64 gateway.
2026-07-27 17:57:08 +05:00
AshvinandGitHub 76c994f7bc fix: don't let the SUPPORTED_IDS branch re-add a rejected username (#2907)
parse_usernames() validates *_username fields with is_plausible_username
(the #1403 guard), but the trailing `if k in SUPPORTED_IDS` branch runs
unconditionally. Since "username" is itself in SUPPORTED_IDS, the bare
`username` key bypasses the guard: a URL/email/path value is dropped by
the plausibility check and then silently re-added, so it becomes a
recursive search target again and reproduces the #1403 false-error
cascade.

Make the branch an elif. The bare `username` key is already handled with
validation by the first branch; every other SUPPORTED_ID (gaia_id,
vk_id, orcid, ...) still gets added as before since none of those keys
contain "username".

Add a regression test for the bare `username` key with URL/email/path
values; the existing parse_usernames tests only covered *_username
variants, which aren't in SUPPORTED_IDS.
2026-07-27 12:36:30 +05:00
SoxojandGitHub aa6c16f1dc Tag 2210 sites by country and topic (#2892)
- Countries: +783 sites (ccTLD-deterministic + LLM content classification),
cleaned 9 anomalous tags (uk->gb, eu region, domain hacks)
- Topics: +2165 sites; vocab 69->75 (+parenting, psychology, cooking,
esoteric, religion, firearms)
- Add tests/test_country_tags.py (ISO validity, no uk/eu, hack regression)
2026-07-20 23:45:04 +02:00
SoxojandGitHub aa79917052 Updated AI report prompt for analysis and default model (#2888)
* Updated AI report prompt for analysis and default model

* Fix test_cli.py DEFAULT_ARGS to match updated default AI model (gpt-4o → gpt-5.4)
2026-07-20 20:38:09 +02:00
SoxojandGitHub d5cf4b15ef Add QQ search via qq_id id type; per-site encoding config (#2890) 2026-07-20 19:47:33 +02:00
SoxojandGitHub 3c7a0e0c43 Enrichment via socid_extractor url mutations (#2880) 2026-07-19 21:40:56 +02:00
caed0bf602 fix: reduce false positives for non-ASCII (Chinese) usernames + add CSDN site (#2876)
* refactor: drop dead 'if not dictionary' guards across report paths

Closes #2665.

Every site results entry flows through make_site_result (checking.py:788),
which initializes results_site = {} and then unconditionally populates it
(site/username/keywords/parsing_enabled/url_main/cookies up front, checker
at the end, plus status or url_user+future on every branch). It has no
return path that yields an empty dict. check_site_for_username wraps that
result and likewise never returns a falsy entry, and maigret.py does not
construct site-result dicts by hand.

So the four downstream 'if not dictionary: continue' guards (all tagged
'# TODO: fix no site data issue') were dead code — the entries they
guarded against cannot occur. Removed:

  - maigret/maigret.py:101  (extract_ids_from_results)
  - maigret/report.py:151   — 'if not dictionary or dictionary.get("is_similar")'
                              trimmed to just the is_similar check (real logic)
  - maigret/report.py:467  (extended-report builder)
  - maigret/report.py:609  (generate_txt_report)
  - maigret/report.py:627  — 'if not site_result or not site_result.get("status")'
                              trimmed to just the status check (real logic)
  - maigret/report.py:687 (generate_json_report)

plus the four '# TODO: fix no site data issue' comments.

No behavior change for well-formed inputs; downstream .get() calls are
safe because entries are always populated dicts. 292 tests pass.

* feat: add Chinese name false-positive fix + CSDN site

- #2633: When searching non-ASCII usernames, skip presence detection
  if the response body doesn't contain the username at all, preventing
  false positives on sites that return generic error pages.
- #2634: Add CSDN (blog.csdn.net) — major Chinese dev blog with
  40M+ users, alexa rank #45 in China.

* fix: add tls_fingerprint to CSDN + add tests for non-ASCII false positive fix

Addresses PR #2876 review feedback from soxoj:
- Add tls_fingerprint to CSDN protection (curl_cffi required)
- Add 2 test cases for #2633 non-ASCII username false positive fix:
  - non-ASCII username not in response → no false CLAIMED
  - non-ASCII username in response → normal CLAIMED behavior

---------

Co-authored-by: aznikline <aznikline@users.noreply.github.com>
2026-07-17 19:07:08 +02:00
SoxojandGitHub 9d29061e2a Fix site checks: 2 fixed, 1 new added + proton activation method (#2869) 2026-07-15 12:30:00 +02:00
b74a063f49 fix: explicit SKIPPED status for Tor/I2P/DNS sites when no proxy is configured (#2815)
Fixes #2664

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-13 20:02:35 +02:00
SoxojandGitHub 6ced77f894 Bump to 0.6.3 (#2863) 2026-07-13 18:06:51 +02:00
SoxojandGitHub 897769dcc2 Fix site checks: 82 fixed, 11 disabled + DiscourseJson engine (#2836)
* Fix site checks: 82 fixed, 11 disabled + DiscourseJson engine

* Fix test_hackernews_requires_profile_marker: update test body to JSON format matching Firebase API probe
2026-07-05 13:28:46 +02:00
SoxojandGitHub c74bad1ebf Fix site checks: 13 fixed, 7 disabled + CN sites: Tuchong, GitCode, Yuque, LeetCode CN, Bangumi, NGA, Moegirl (#2835) 2026-07-04 22:51:50 +02:00
SoxojandGitHub cfb28b492a Bump to 0.6.2 (#2827)
* Bump to 0.6.2

* Release process automation
2026-07-01 17:32:50 +02:00
jichaowang02-langandGitHub af253d7dc3 Make include-tag site filter case-insensitive (match exclude filter) (#2811)
In MaigretDatabase.ranked_sites_dict, the include (whitelist) tag filter
compared the site's raw tags against the lowercased query tags:

    is_tags_ok = lambda x: set(x.tags).intersection(set(normalized_tags))

while the exclude (blacklist) filter and every sibling lambda
(name/source/engine) lowercase the site-side value:

    is_excluded_by_tag = lambda x: set(map(str.lower, x.tags)).intersection(
        set(normalized_excluded_tags)
    )

So a site stored with an upper/mixed-case tag (e.g. a custom or submitted
site tagged 'US') was excluded by `--exclude-tags us` but NOT found by
`--tags us` — an asymmetry between the two filters. Lowercase the site
tags in the include filter too, matching the rest of the method.

Adds a regression test asserting tags=['us'] finds a site tagged 'US'.
2026-06-29 22:05:00 +02:00
AliandGitHub e41c4d7ee3 Add Neo4j Cypher export (--neo4j) (#2774)
Serialize the maigret graph (the same one --graph builds) into an idempotent Cypher script importable into Neo4j. Reuses MaigretGraph via an extracted _build_maigret_graph() helper, leaving save_graph_report behavior unchanged; no new runtime dependency. Adds the --neo4j flag, the neo4j_report setting, docs, and a unit test.

Closes #2630
2026-06-29 15:41:57 +02:00
Sushanth012andGitHub 0a414c2e1c fix: preserve report error reasons (#2802) 2026-06-29 15:06:03 +02:00
jichaowang02-langandGitHub f8c35b09e3 Fix URLMatcher eating leading host chars (unescaped dots in prefix regex) (#2808)
URLMatcher._HTTP_URL_RE_STR was "^https?://(www.|m.)?(.+)$". The dots in
the optional (www.|m.)? subdomain-prefix group were unescaped, so each `.`
matched ANY character. For a host that simply starts with `m` or `www`
(not the literal `m.`/`www.` prefix), the group greedily ate the first
characters:

    extract_main_part('https://medium.com/alice')  -> 'dium.com/alice'
    extract_main_part('https://myspace.com/bob')   -> 'space.com/bob'

extract_main_part feeds make_profile_url_regexp / get_url_template, so the
generated site-detection regexp was corrupted too (medium.com -> dium.com),
and the loose prefix could match bogus hosts like `xmedium.com`. 170+ hosts
in resources/data.json start with `m`.

Fix: escape the dots with a raw string, r"^https?://(www\.|m\.)?(.+)$".
Genuine `m.`/`www.` prefixes are still stripped
(m.wikipedia.org -> wikipedia.org). The two existing tests that pinned the
unescaped pattern string are updated to the corrected form, and a
regression test covers hosts beginning with m/www.
2026-06-29 14:59:31 +02:00
jichaowang02-langandGitHub b0b5bf68d2 Compare raw error percentage against threshold (don't round first) (#2809)
extract_and_group stored the per-error-type percentage already rounded to
2 decimals, and is_important() compares that stored value against the
threshold. Rounding before the comparison can push a rate that is strictly
below the threshold up to it:

    8 Captcha errors / 267 sites = 2.99625%
    round(2.99625, 2) == 3.0  ->  is_important() True  ->  spurious
    'Too many errors of type "Captcha" (3.0%)' warning

This is the residual of the same class fixed in #2788 (scale-then-round):
scaling to a percentage first stopped 2.5% from becoming 3.0%, but rounding
the percentage still feeds the threshold check. The display sites already
round (round(e["perc"], 2)), so storing the raw percentage keeps the shown
values unchanged while making the threshold comparison exact.

Genuine at-or-above-threshold rates still fire (3/100 = 3.0%, DNS 10%).
Adds a regression test for the 2.996% -> 3.0% rounding-up case.
2026-06-28 20:14:51 +02:00
jichaowang02-langandGitHub ac25cd71bb Fix get_dict_ascii_tree ignoring new_line=False (#2805)
`get_dict_ascii_tree(items, prepend="", new_line=True)` immediately reassigned
`new_line` to the horizontal box-drawing glyph ("─"), shadowing the boolean
parameter. The trailing `if not new_line: text = text[1:]` — meant to strip the
leading newline — therefore tested a non-empty string (always truthy) and never
ran. Callers passing `new_line=False` (e.g. `maigret.py` printing a user's
identity data) got a stray leading blank line.

Rename the glyph local to `h_line` so the `new_line` parameter is preserved and
its strip takes effect. The tree drawing is unchanged.

Adds a regression test asserting `new_line=False` drops the leading newline
while keeping the rest of the tree identical.
2026-06-28 20:11:46 +02:00
e90ca42aac test: cover #2666 TODO-test paths (cookie_jar forwarding, extract_ids_from_results) (#2795)
Closes #2666.

Issue #2666 tracked four paths flagged `# TODO: tests`. Two of them
(`executors.py` increment_progress / stop_progress) were removed by the
#2789 refactor and no longer exist. This PR covers the two that remain:

1. maigret/checking.py:247 — the ClientSession cookie_jar forwarding.
   SimpleAiohttpChecker.check() builds ClientSession(cookie_jar=self.cookie_jar
   if self.cookie_jar else None), but nothing asserted the jar handed to the
   checker actually reached the outgoing session. Added two tests using the
   existing constructor-capture pattern (_capture_clientsession): one asserts
   a configured jar is forwarded verbatim, the other asserts None is passed
   when no jar is configured (the else branch).

2. maigret/maigret.py:946 — extract_ids_from_results. The pre-existing
   test_extract_ids_from_results was a bare expression with no `assert` (so
   it silently passed regardless of return value) AND pinned its expectation
   against test_db (tests/db.json), whose site set never matched the Reddit
   URL and silently dropped the ids_links result. Fixed: real assertion against
   default_db, plus three branch tests (cross-site username merge, empty-site
   skip, empty input).

No new dependencies: aioresponses was suggested in the issue but is not in
the project — the existing monkeypatch capture pattern fits the codebase
better and adds zero deps.

Removed both `# TODO: tests` comments from the now-covered lines.

Co-authored-by: aznikline <aznikline@users.noreply.github.com>
2026-06-22 19:22:37 +02:00
SoxojandGitHub 3eaec01523 Code cleanup and refactoring after ponytail audit (#2789) 2026-06-21 12:51:01 +02:00
jichaowang02-langandGitHub 7936ce267f Fix error-percentage rounding tripping thresholds at whole-percent granularity (#2788)
`extract_and_group` computed the per-error-type percentage as
`round(count / len(search_res), 2) * 100` — it rounded the *fraction* to 2
decimals (i.e. to the nearest 1%) and only then scaled by 100, so `perc`
always had whole-percent granularity and was mis-rounded.

A genuine 2.5% error rate (1 error out of 40 sites) became
`round(0.025, 2) * 100 = 3.0%` and tripped the 3% "important" threshold in
`is_important()`, firing a spurious "Too many errors of type ..." warning.
Likewise 9.5% DNS errors rounded to 10.0% and crossed the DNS override.

Scale to a percentage *before* rounding: `round(count / len(search_res) * 100, 2)`.
The existing tests use integer-landing ratios (25/100, 5/100, 3/100) that are
identical under both formulas, so none of them change.

Adds a regression test for a sub-threshold non-integer rate (2.5%) staying silent.
2026-06-21 12:50:27 +02:00
5888f81235 Fix HackerNews and Rajce.net false positives (#2780)
* Fix HackerNews and Rajce.net false positives

* Fix HackerNews and Rajce.net false positives

---------

Co-authored-by: Codex <codex@openai.com>
2026-06-18 19:42:41 +02:00
SoxojandGitHub f2e4f8d3a2 Merge commit from fork 2026-06-17 15:15:32 +02:00
Xinmin ZengandGitHub 13f43b3420 Fix async activation retry handling (#2765)
Convert activation HTTP calls to aiohttp coroutines, await activation before retrying, and allocate independent protocol checkers per site check so concurrent retries do not overwrite shared checker state.
2026-06-13 16:34:24 +02:00
AshvinandGitHub 0e9ced3168 fix: pass value v not key k to is_country_tag in generate_report_context (#2753)
is_country_tag checks for a 2-letter alpha code. The field names
'country' and 'locale' are 6-7 characters and never match, so the
direct alpha_2 lookup branch was dead code and all country values fell
through to search_fuzzy unconditionally.

Change is_country_tag(k) -> is_country_tag(v) so that 2-letter ISO
codes (e.g. 'US', 'RU') use the fast direct lookup while full country
names continue to use fuzzy search.

Fixes #2752
2026-06-11 14:48:21 +02:00
AshvinandGitHub 4c938751ea fix: update_site() now replaces list element instead of local variable (#2751)
update_site() used `s = site` inside a for-loop, which rebinds only the
local loop variable and leaves self._sites[i] unchanged. The method
returned self silently appearing to succeed while the database list
retained the original entry. This broke --auto-disable for any site
already present in the database.

Fix with enumerate so the actual list element is replaced.

Fixes #2750
2026-06-10 20:05:18 +02:00
c7edebb57a Refactor error detection and username extraction (#2701)
Co-authored-by: Soxoj <31013580+soxoj@users.noreply.github.com>
2026-06-07 13:38:51 +02:00
a85b0152fe Warn on engine dict merge conflicts (#2737)
Co-authored-by: pratyushjaiswal0806-dot <pratyushjaiswal0806@gmail.com>
2026-06-04 23:55:05 +02:00
SoxojandGitHub 528b75336c Added sponsor logo and CLI proxy recommendation (#2729) 2026-06-02 18:28:15 +02:00
SoxojandGitHub 4cc803bb39 Graceful Ctrl+C + error UX improvements (#2719) 2026-05-30 16:14:57 +02:00
SoxojandGitHub a89b7f09a7 Fix DNS resolver failures: classify aiodns errors, add --dns-resolver threaded fallback (#2688) (#2717)
* Fix DNS resolver failures: classify aiodns errors, add --dns-resolver threaded fallback (#2688)

* test: include dns_resolver in CLI default args
2026-05-30 16:09:54 +02:00
Dimitris MarakomihelakisandGitHub 4ce8d99352 feature: add keywords parameter and filter by its matching #979 (#2702)
* feature: add keywords parameter and filter by its matching #979

* fixes & nits
2026-05-27 19:27:48 +02:00
SoxojandGitHub 9a2cce4d85 Add Cloudflare bypass (FlareSolverr) support to web UI + docs (#2700) 2026-05-25 18:53:18 +02:00
SoxojandGitHub ec7f57eab7 Windows version improvements and docs (#2690) 2026-05-21 12:12:17 +02:00
SebastionandGitHub e6f53862f5 fix(web): sanitize username in report file paths to prevent path traversal (#2678)
* fix: sanitize username in report file paths to prevent path traversal (CWE-22)

* address review: add unit tests for sanitize_username_for_path()
2026-05-20 23:55:17 +02:00
SoxojandGitHub ceed9aa9cc fix(Instagram): refresh rate-limit marker for stale Login title (#2674) 2026-05-16 18:23:13 +02:00
SoxojandGitHub 51a5169987 fix(checking): reject URLs and emails extracted as usernames (#2673)
Closes #1403
2026-05-16 17:52:00 +02:00
SoxojandGitHub a7338e97f3 Make xhtml2pdf optional, fix install on Linux without libcairo (#2659)
* Make xhtml2pdf optional, fix install on Linux without libcairo

Move xhtml2pdf to the new [pdf] extra so default `pip install maigret`
no longer pulls pycairo (which has no Linux/macOS wheels and breaks the
build without libcairo2-dev). save_pdf_report now raises a clear
RuntimeError pointing to `pip install 'maigret[pdf]'`, and the CLI
turns it into a friendly warning instead of a crash. Adds tests
covering the missing-extra path, plus per-OS install docs.

Fix for #2657, #2534

* Make arabic-reshaper and python-bidi optional; idempotent update of db_meta.json and sites.md

* Regenerated poerty.lock

* Update CI workflow to cover minimal installation without PDF deps
2026-05-15 14:33:55 +02:00
Raz Ben YaishandGitHub 650f990547 test(checking): cover CurlCffiChecker proxy forwarding (#2652) 2026-05-13 13:33:17 +02:00
f413603431 fix(security): harden /reports path containment via send_from_directory (#2635)
The previous /reports/<path:filename> handler resolved the filename with
os.path.normpath and gated send_file on file_path.startswith(REPORTS_FOLDER).
Plain ../ traversal was rejected because the resolved path no longer started
with REPORTS_FOLDER, but a sibling-prefix variant slipped through: a request
of the form ..%2F<reports_root_basename>2/<file> resolves to a path like
/tmp/maigret_reports2/<file>, which still starts with /tmp/maigret_reports
and was served back to the caller.

Replace the manual normpath+startswith check with Flask's send_from_directory,
which delegates to werkzeug.security.safe_join. safe_join enforces a real
boundary against the resolved directory, rejects absolute paths, and refuses
.. segments that escape the root.

Tests: 4 new test_download_report_* cases in tests/test_web.py covering the
happy path, ../ traversal, the sibling-prefix bypass (regression test —
fails on the pre-fix code, passes on the new code), and absolute paths.

Detected by Aeon + manual review of maigret.web.app.
Severity: low (web UI defaults to FLASK_HOST=127.0.0.1; the Docker `web`
target binds 0.0.0.0; exploitation reads files from sibling /tmp directories,
which is bounded by who can place files there).
CWE-22.

Co-authored-by: aeonframework <aeon-bot@aaronjmars.com>
2026-05-10 17:12:51 +03:00
SoxojandGitHub 264bae34d6 Add test for CheckError bug (#2631) 2026-05-09 16:11:07 +03:00