1.0.8 — seconde tentative sur les sites en echec
This commit is contained in:
1 parent
a59c0b370b
commit
12973dab5d
11 files changed
+166
-14
No files matched your search
@@ -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é
|
||||||
|
|||||||
@@ -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,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"
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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,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": {
|
||||||
|
|||||||
Reference in new issue
Block a user