1.0.8 — seconde tentative sur les sites en echec
CI / Backend — lint, types, tests (push) Successful in 31s
CI / Interface — types et compilation (push) Successful in 14s
CI / Construction des images (sans publication) (push) Successful in 1m33s
Publication / Images vers Harbor (push) Successful in 2m40s

This commit is contained in:
hcornet committed 2026-09-13 10:37:32 +02:00
1 parent a59c0b370b
commit 12973dab5d
11 files changed
+166 -14

No files matched your search

+37
View File
@@ -2,6 +2,43 @@
Format inspiré de [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/). Format inspiré de [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/).
## [1.0.8] — 2026-09-12
### Modifié
- **Les en-têtes de navigateur passent en seconde tentative.** La première
passe conserve le comportement natif de Maigret ; seuls les sites l'ayant
refusée sont rejoués en se présentant comme un navigateur. Le surcoût ne
porte donc que sur les échecs, et les sites qui répondent normalement ne sont
jamais sollicités deux fois. Le nombre de résultats récupérés par cette
seconde passe est annoncé dans l'interface.
### Corrigé
- **Les en-têtes étaient écrits sur les objets de la base en cache**, donc de
façon permanente et partagée entre les recherches simultanées. La seconde
passe travaille désormais sur des copies.
## [1.0.7] — 2026-09-12
### Ajouté
- **En-têtes de navigateur sur les requêtes sortantes.** Les sites qui ne
déclarent pas leurs propres en-têtes dans la base reçoivent désormais un jeu
complet — `Sec-Fetch-*`, `Upgrade-Insecure-Requests`, `DNT`, `Priority`,
User-Agent Firefox. Un client qui n'envoie qu'un User-Agent se distingue
immédiatement d'un navigateur. Désactivable par
`LIMIER_OUTBOUND_BROWSER_HEADERS=false`.
Trois en-têtes du jeu Firefox complet sont écartés : `Host` est propre à
chaque requête, `Connection` et `TE` relèvent du transport. `Accept-Encoding`
omet `zstd`, qu'aiohttp ne sait pas décoder — l'annoncer reproduirait le
défaut Brotli corrigé en 1.0.3.
Les sites déclarant déjà leurs en-têtes (LinkedIn et quelques autres) sont
laissés intacts : leurs valeurs ont été choisies pour eux.
### Corrigé
- `LIMIER_OUTBOUND_USER_AGENT` ne s'appliquait qu'aux moteurs e-mail et domaine.
Il vaut désormais aussi pour le moteur pseudonyme.
## [1.0.6] — 2026-09-12 ## [1.0.6] — 2026-09-12
### Corrigé ### Corrigé
+1 -1
View File
@@ -1 +1 @@
1.0.6 1.0.8
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "limier" name = "limier"
version = "1.0.6" version = "1.0.8"
description = "Plateforme OSINT auto-hébergée : pseudonyme, nom, e-mail, domaine" description = "Plateforme OSINT auto-hébergée : pseudonyme, nom, e-mail, domaine"
requires-python = ">=3.12,<3.14" requires-python = ">=3.12,<3.14"
license = { text = "MIT" } license = { text = "MIT" }
+1 -1
View File
@@ -1,3 +1,3 @@
"""Limier — plateforme OSINT auto-hébergée.""" """Limier — plateforme OSINT auto-hébergée."""
__version__ = "1.0.6" __version__ = "1.0.8"
+15 -2
View File
@@ -124,10 +124,23 @@ class Settings(BaseSettings):
"""Ex. ``http://flaresolverr:8191``. Vide => bypass Cloudflare désactivé.""" """Ex. ``http://flaresolverr:8191``. Vide => bypass Cloudflare désactivé."""
outbound_user_agent: str = ( outbound_user_agent: str = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0"
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
) )
outbound_browser_headers: bool = True
"""Présenter un jeu d'en-têtes de navigateur sur les requêtes sortantes.
Appliqué uniquement aux sites qui ne déclarent pas déjà leurs propres
en-têtes dans la base Maigret : LinkedIn, par exemple, garde les siens.
Trois en-têtes du jeu Firefox complet sont volontairement écartés :
``Host`` est propre à chaque requête et casserait toutes les autres,
``Connection`` et ``TE`` sont gérés par la couche de transport et provoquent
des erreurs s'ils sont imposés. ``Accept-Encoding`` omet ``zstd``, qu'aiohttp
ne sait pas décoder ici — l'annoncer reproduirait le défaut Brotli corrigé
en 1.0.3.
"""
# ------------------------------------------------------------------- RGPD # ------------------------------------------------------------------- RGPD
retention_days: int = 30 retention_days: int = 30
"""Durée de conservation des recherches et de leurs résultats.""" """Durée de conservation des recherches et de leurs résultats."""
@@ -124,6 +124,36 @@ def charger_base(forcer: bool = False):
return db return db
def entetes_navigateur() -> dict[str, str]:
"""Jeu d'en-têtes présenté aux sites qui n'en déclarent pas.
Un client qui n'envoie qu'un User-Agent se distingue immédiatement d'un
navigateur : ce sont les en-têtes ``Sec-Fetch-*`` et ``Upgrade-Insecure-
Requests`` qu'un navigateur émet systématiquement et qu'un script omet
presque toujours.
À tempérer : les protections modernes s'appuient d'abord sur l'empreinte
TLS, que ces en-têtes ne changent pas. Le gain réel est mesurable site par
site, il n'est pas garanti.
"""
from limier.config import get_settings
return {
"User-Agent": get_settings().outbound_user_agent,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "fr,fr-FR;q=0.9,en-US;q=0.8,en;q=0.7",
"Accept-Encoding": "gzip, deflate, br",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"DNT": "1",
"Sec-GPC": "1",
"Priority": "u=0, i",
}
def selectionner_sites( def selectionner_sites(
*, top: int, tags: list[str] | None = None, id_type: str = "username" *, top: int, tags: list[str] | None = None, id_type: str = "username"
) -> dict: ) -> dict:
@@ -143,6 +173,28 @@ def selectionner_sites(
) )
def dupliquer_avec_entetes(sites: dict) -> dict:
"""Copie des sites donnés, avec les en-têtes de navigateur ajoutés.
**Duplication et non modification sur place.** Les objets rendus par
``ranked_sites_dict`` proviennent de la base en cache, partagée par tous les
travaux du processus : les modifier contaminerait durablement les recherches
suivantes et celles qui tournent en parallèle.
Les sites qui déclarent déjà leurs propres en-têtes dans la base sont
laissés intacts — leurs valeurs ont été choisies pour eux.
"""
from maigret.sites import MaigretSite
entetes = entetes_navigateur()
copies = {}
for nom, site in sites.items():
copie = MaigretSite(site.name, dict(site.json))
copie.headers = dict(site.headers) if site.headers else dict(entetes)
copies[nom] = copie
return copies
def statistiques() -> dict: def statistiques() -> dict:
"""Résumé de la base, exposé par ``/api/catalogue``.""" """Résumé de la base, exposé par ``/api/catalogue``."""
db = charger_base() db = charger_base()
@@ -224,6 +224,7 @@ class MoteurPseudonyme(Engine):
deja_vus: set[tuple[str, str]], deja_vus: set[tuple[str, str]],
pseudos_decouverts: dict[str, str], pseudos_decouverts: dict[str, str],
origine: str | None = None, origine: str | None = None,
seconde_chance: bool = False,
): ):
"""Exécute une recherche Maigret et émet ses évènements au fil de l'eau. """Exécute une recherche Maigret et émet ses évènements au fil de l'eau.
@@ -258,6 +259,9 @@ class MoteurPseudonyme(Engine):
) )
erreurs_reseau = 0 erreurs_reseau = 0
# Sites dont la vérification a échoué : ce sont eux, et eux seuls, que
# la seconde passe réinterrogera avec des en-têtes de navigateur.
echecs: set[str] = set()
while True: while True:
if tache.done() and file.empty(): if tache.done() and file.empty():
@@ -273,6 +277,9 @@ class MoteurPseudonyme(Engine):
statut = charge.get("status", "") statut = charge.get("status", "")
if statut.startswith("Unknown"): if statut.startswith("Unknown"):
erreurs_reseau += 1 erreurs_reseau += 1
nom_echec = charge.get("site", "").split(" [")[0].strip()
if nom_echec in sites:
echecs.add(nom_echec)
if compteur["faits"] % 10 == 0: if compteur["faits"] % 10 == 0:
# Maigret relance les sites en échec : le nombre de # Maigret relance les sites en échec : le nombre de
# vérifications dépasse alors le total estimé. On borne # vérifications dépasse alors le total estimé. On borne
@@ -329,6 +336,44 @@ class MoteurPseudonyme(Engine):
# Le bilan des blocages est cumulé puis émis une seule fois en fin de # Le bilan des blocages est cumulé puis émis une seule fois en fin de
# recherche : le répéter par variante produisait cinq avertissements # recherche : le répéter par variante produisait cinq avertissements
# quasi identiques pour une même cause. # quasi identiques pour une même cause.
# ------------------------------------------------------ seconde passe
# Stratégie en deux temps : on interroge d'abord chaque site avec le
# comportement natif de Maigret, puis on rejoue les seuls échecs en se
# présentant comme un navigateur. Le surcoût ne porte donc que sur les
# sites qui ont refusé — les autres ne sont jamais sollicités deux fois.
if not seconde_chance and self.settings.outbound_browser_headers and echecs:
a_rejouer = {n: s for n, s in sites.items() if n in echecs}
yield NoticeEvent(
module=self.name,
level="info",
message=(
f"Seconde tentative sur {len(a_rejouer)} site(s) en échec, "
"avec des en-têtes de navigateur."
),
)
# Le total inclut désormais cette seconde passe, sinon la barre de
# progression dépasserait son maximum.
compteur["total"] += len(a_rejouer)
recuperes_avant = len(deja_vus)
async for evenement in self._chercher_un(
pseudo=pseudo,
sites=database.dupliquer_avec_entetes(a_rejouer),
compteur=compteur,
label=f"{label} — seconde tentative",
deja_vus=deja_vus,
pseudos_decouverts=pseudos_decouverts,
origine=origine,
seconde_chance=True,
):
yield evenement
recuperes = len(deja_vus) - recuperes_avant
if recuperes:
yield NoticeEvent(
module=self.name,
level="info",
message=(f"{recuperes} résultat(s) récupéré(s) grâce à la seconde tentative."),
)
compteur["bloques"] = compteur.get("bloques", 0) + erreurs_reseau compteur["bloques"] = compteur.get("bloques", 0) + erreurs_reseau
compteur["interroges"] = compteur.get("interroges", 0) + len(sites) compteur["interroges"] = compteur.get("interroges", 0) + len(sites)
+6 -1
View File
@@ -7,7 +7,7 @@
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
REGISTRY=registry.tips-of-mine.com REGISTRY=registry.tips-of-mine.com
TAG=1.0.6 TAG=1.0.8
LIMIER_HOST=limier.tips-of-mine.com LIMIER_HOST=limier.tips-of-mine.com
LIMIER_PUBLIC_URL=https://limier.tips-of-mine.com LIMIER_PUBLIC_URL=https://limier.tips-of-mine.com
LIMIER_ENVIRONMENT=prod LIMIER_ENVIRONMENT=prod
@@ -71,4 +71,9 @@ LIMIER_HIBP_API_KEY=
# --- Divers ---------------------------------------------------------------- # --- Divers ----------------------------------------------------------------
LIMIER_DNS_RESOLVERS=1.1.1.1,9.9.9.9 LIMIER_DNS_RESOLVERS=1.1.1.1,9.9.9.9
# Seconde tentative avec des en-têtes de navigateur sur les seuls sites ayant
# refusé la première requête. Mettre à false pour s'en tenir au comportement
# natif de Maigret et comparer les taux de blocage.
LIMIER_OUTBOUND_BROWSER_HEADERS=true
LIMIER_WORKER_MAX_JOBS=4 LIMIER_WORKER_MAX_JOBS=4
+4 -4
View File
@@ -56,7 +56,7 @@ x-limier-env: &limier-env
services: services:
limier-api: limier-api:
image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-backend:${TAG:-1.0.6} image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-backend:${TAG:-1.0.8}
container_name: limier-api container_name: limier-api
restart: unless-stopped restart: unless-stopped
command: ["limier-api"] command: ["limier-api"]
@@ -99,7 +99,7 @@ services:
- "homepage.description=Plateforme OSINT" - "homepage.description=Plateforme OSINT"
limier-worker: limier-worker:
image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-backend:${TAG:-1.0.6} image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-backend:${TAG:-1.0.8}
container_name: limier-worker container_name: limier-worker
restart: unless-stopped restart: unless-stopped
command: ["limier-worker"] command: ["limier-worker"]
@@ -131,7 +131,7 @@ services:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"
limier-web: limier-web:
image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-frontend:${TAG:-1.0.6} image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-frontend:${TAG:-1.0.8}
container_name: limier-web container_name: limier-web
restart: unless-stopped restart: unless-stopped
volumes: volumes:
@@ -214,7 +214,7 @@ services:
# ``--profile``, pour que l'installation par défaut reste minimale. # ``--profile``, pour que l'installation par défaut reste minimale.
limier-holehe: limier-holehe:
image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-holehe:${TAG:-1.0.6} image: ${REGISTRY:-registry.tips-of-mine.com}/limier/limier-holehe:${TAG:-1.0.8}
container_name: limier-holehe container_name: limier-holehe
restart: unless-stopped restart: unless-stopped
profiles: ["holehe"] profiles: ["holehe"]
+3 -3
View File
@@ -12,7 +12,7 @@ Ni base de données, ni Redis, ni Authentik. On appelle le moteur directement.
cd /opt # ou n'importe où, il suffit d'avoir essai_moteur.py sous la main cd /opt # ou n'importe où, il suffit d'avoir essai_moteur.py sous la main
docker run --rm -v "$PWD/essai_moteur.py:/tmp/essai.py" \ docker run --rm -v "$PWD/essai_moteur.py:/tmp/essai.py" \
registry.tips-of-mine.com/limier/limier-backend:1.0.6 \ registry.tips-of-mine.com/limier/limier-backend:1.0.8 \
python /tmp/essai.py domain tips-of-mine.com python /tmp/essai.py domain tips-of-mine.com
``` ```
@@ -24,7 +24,7 @@ Le moteur pseudonyme, en version courte :
```bash ```bash
docker run --rm -v "$PWD/essai_moteur.py:/tmp/essai.py" \ docker run --rm -v "$PWD/essai_moteur.py:/tmp/essai.py" \
-e LIMIER_USERNAME_TOP_SITES=80 \ -e LIMIER_USERNAME_TOP_SITES=80 \
registry.tips-of-mine.com/limier/limier-backend:1.0.6 \ registry.tips-of-mine.com/limier/limier-backend:1.0.8 \
python /tmp/essai.py username soxoj python /tmp/essai.py username soxoj
``` ```
@@ -33,7 +33,7 @@ Et la recherche par nom, qui montre les variantes déduites :
```bash ```bash
docker run --rm -v "$PWD/essai_moteur.py:/tmp/essai.py" \ docker run --rm -v "$PWD/essai_moteur.py:/tmp/essai.py" \
-e LIMIER_USERNAME_TOP_SITES=60 \ -e LIMIER_USERNAME_TOP_SITES=60 \
registry.tips-of-mine.com/limier/limier-backend:1.0.6 \ registry.tips-of-mine.com/limier/limier-backend:1.0.8 \
python /tmp/essai.py name "Hubert Cornet" python /tmp/essai.py name "Hubert Cornet"
``` ```
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "limier-frontend", "name": "limier-frontend",
"version": "1.0.6", "version": "1.0.8",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {