Tutorials

Journaux d'audit CAPTCHA : tracer chaque résolution pour la conformité

Un auditeur SOC 2, ou une demande d'accès au titre du RGPD, pose toujours la même question : pouvez-vous prouver qui a déclenché chaque résolution de CAPTCHA, pour quel site et à quel coût ? Sans piste d'audit, la réponse n'existe pas. Un journal d'audit enregistre chaque demande envoyée à CaptchaAI — déclencheur, horodatage, résultat et temps de résolution — pour rendre chaque appel traçable et rattachable à une opération précise. Ce guide montre comment construire cette journalisation en Python et en Node.js, l'interroger, puis la conserver selon votre volume.

Ce que doit contenir un journal d'audit CAPTCHA

Une piste d'audit exploitable ne se résume pas à « résolu / échoué » : elle doit permettre de reconstituer, des mois plus tard, le contexte exact d'une résolution. Enregistrez au minimum les champs suivants :

Champ Objectif Exemple
timestamp Quand la demande a été faite 2026-04-04T14:30:00Z
request_id Identifiant unique pour cette résolution uuid4()
captcha_type Méthode CAPTCHA utilisée userrecaptcha
target_site URL de la page en cours de résolution https://example.com/login
task_id ID de tâche CaptchaAI 73829451
status Résultat solved, failed, timeout
solve_time_ms Temps entre la soumission et le résultat 18432
error_code Erreur en cas d'échec ERROR_CAPTCHA_UNSOLVABLE
initiator Qui ou quoi a déclenché la résolution scraper-job-42
cost Coût estimé 0.003

À ne jamais journaliser, sous peine d'alourdir votre exposition RGPD :

  • vos clés API ;
  • les tokens CAPTCHA, temporaires et sans valeur d'audit une fois expirés ;
  • toute donnée personnelle issue des sites cibles : adresse e-mail, contenu de formulaire, identifiant de compte.

Ce dernier point relève directement du RGPD : le principe de minimisation impose de ne conserver que ce qui sert réellement à l'audit. Un initiator et un task_id suffisent pour la traçabilité ; l'adresse e-mail ou le contenu d'un formulaire, non.

Implémentation en Python

Le point clé est d'isoler la journalisation d'audit de vos logs applicatifs : un fichier dédié au format JSON Lines (une ligne JSON par résolution), avec rotation automatique. Le RotatingFileHandler de la bibliothèque standard suffit, et un enregistrement est écrit qu'une résolution réussisse, échoue ou expire.

# audit_solver.py
import os
import uuid
import time
import json
import logging
from datetime import datetime, timezone
import requests

API_KEY = os.environ.get("CAPTCHAAI_KEY", "YOUR_API_KEY")

# Configure audit logger — separate from application logs
audit_logger = logging.getLogger("captcha_audit")
audit_logger.setLevel(logging.INFO)

# File handler with rotation
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler(
    "captcha_audit.jsonl",
    maxBytes=50_000_000,  # 50 MB per file
    backupCount=10,
)
handler.setFormatter(logging.Formatter("%(message)s"))
audit_logger.addHandler(handler)

def log_audit(record):
    """Write a structured audit record."""
    audit_logger.info(json.dumps(record, default=str))

def solve_with_audit(sitekey, pageurl, captcha_type="userrecaptcha",
                      initiator="unknown"):
    """Solve a CAPTCHA with full audit logging."""
    request_id = str(uuid.uuid4())
    start = time.time()

    audit_record = {
        "request_id": request_id,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "captcha_type": captcha_type,
        "target_site": pageurl,
        "initiator": initiator,
        "status": "submitted",
    }

    session = requests.Session()

    try:
        # Submit
        resp = session.get("https://ocr.captchaai.com/in.php", params={
            "key": API_KEY,
            "method": captcha_type,
            "googlekey": sitekey,
            "pageurl": pageurl,
            "json": "1",
        })
        result = resp.json()

        if result.get("status") != 1:
            audit_record.update({
                "status": "submit_failed",
                "error_code": result.get("request"),
                "solve_time_ms": int((time.time() - start) * 1000),
            })
            log_audit(audit_record)
            return None

        task_id = result["request"]
        audit_record["task_id"] = task_id

        # Poll
        time.sleep(15)
        for _ in range(25):
            poll = session.get("https://ocr.captchaai.com/res.php", params={
                "key": API_KEY, "action": "get",
                "id": task_id, "json": "1",
            })
            poll_result = poll.json()

            if poll_result.get("status") == 1:
                solve_time = int((time.time() - start) * 1000)
                audit_record.update({
                    "status": "solved",
                    "solve_time_ms": solve_time,
                    "cost_estimate": 0.003,  # Adjust per your rate
                })
                log_audit(audit_record)
                return poll_result["request"]

            if poll_result.get("request") != "CAPCHA_NOT_READY":
                audit_record.update({
                    "status": "failed",
                    "error_code": poll_result.get("request"),
                    "solve_time_ms": int((time.time() - start) * 1000),
                })
                log_audit(audit_record)
                return None

            time.sleep(5)

        audit_record.update({
            "status": "timeout",
            "solve_time_ms": int((time.time() - start) * 1000),
        })
        log_audit(audit_record)
        return None

    except Exception as e:
        audit_record.update({
            "status": "error",
            "error_code": str(e)[:200],
            "solve_time_ms": int((time.time() - start) * 1000),
        })
        log_audit(audit_record)
        raise

# Usage
token = solve_with_audit(
    sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
    pageurl="https://www.google.com/recaptcha/api2/demo",
    initiator="price-scraper-v2",
)

Format de sortie JSONL

Chaque résolution produit une ligne autonome, facile à filtrer avec grep, à ingérer dans un agrégateur ou à charger dans un tableur. Exemple d'une résolution réussie :

{"request_id":"a1b2c3d4-...","timestamp":"2026-04-04T14:30:00+00:00","captcha_type":"userrecaptcha","target_site":"https://www.google.com/recaptcha/api2/demo","initiator":"price-scraper-v2","status":"solved","task_id":"73829451","solve_time_ms":18432,"cost_estimate":0.003}

Implémentation en JavaScript (Node.js)

La même logique se transpose dans une pile Node.js : fs.appendFileSync ajoute une ligne par résolution ; pour un fort débit, remplacez-le par un flux d'écriture (fs.createWriteStream) afin d'éviter les écritures synchrones bloquantes.

// audit_solver.js
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const axios = require('axios');

const API_KEY = process.env.CAPTCHAAI_KEY || 'YOUR_API_KEY';
const AUDIT_FILE = 'captcha_audit.jsonl';

function logAudit(record) {
  fs.appendFileSync(AUDIT_FILE, JSON.stringify(record) + '\n');
}

async function solveWithAudit(sitekey, pageurl, initiator = 'unknown') {
  const requestId = uuidv4();
  const start = Date.now();
  const record = {
    request_id: requestId,
    timestamp: new Date().toISOString(),
    captcha_type: 'userrecaptcha',
    target_site: pageurl,
    initiator,
    status: 'submitted',
  };

  try {
    const submit = await axios.get('https://ocr.captchaai.com/in.php', {
      params: {
        key: API_KEY, method: 'userrecaptcha',
        googlekey: sitekey, pageurl, json: '1',
      },
    });

    if (submit.data.status !== 1) {
      record.status = 'submit_failed';
      record.error_code = submit.data.request;
      record.solve_time_ms = Date.now() - start;
      logAudit(record);
      return null;
    }

    record.task_id = submit.data.request;
    await new Promise(r => setTimeout(r, 15000));

    for (let i = 0; i < 25; i++) {
      const poll = await axios.get('https://ocr.captchaai.com/res.php', {
        params: { key: API_KEY, action: 'get', id: submit.data.request, json: '1' },
      });

      if (poll.data.status === 1) {
        record.status = 'solved';
        record.solve_time_ms = Date.now() - start;
        record.cost_estimate = 0.003;
        logAudit(record);
        return poll.data.request;
      }
      if (poll.data.request !== 'CAPCHA_NOT_READY') {
        record.status = 'failed';
        record.error_code = poll.data.request;
        record.solve_time_ms = Date.now() - start;
        logAudit(record);
        return null;
      }
      await new Promise(r => setTimeout(r, 5000));
    }

    record.status = 'timeout';
    record.solve_time_ms = Date.now() - start;
    logAudit(record);
    return null;
  } catch (e) {
    record.status = 'error';
    record.error_code = e.message.slice(0, 200);
    record.solve_time_ms = Date.now() - start;
    logAudit(record);
    throw e;
  }
}

Interroger vos journaux d'audit CAPTCHA

Un journal n'a de valeur que si vous pouvez le lire. Comme chaque ligne est un objet JSON, quelques lignes de Python suffisent à produire le récapitulatif qu'un responsable conformité attend :

  • le volume de résolutions du jour ;
  • la répartition des statuts (solved, failed, timeout) ;
  • le coût estimé cumulé ;
  • le temps de résolution médian.

Résumé quotidien

import json
from collections import Counter
from datetime import date

def daily_summary(log_file, target_date=None):
    """Generate a daily summary from audit logs."""
    target = target_date or date.today().isoformat()
    statuses = Counter()
    total_cost = 0
    solve_times = []

    with open(log_file) as f:
        for line in f:
            record = json.loads(line)
            if record["timestamp"].startswith(target):
                statuses[record["status"]] += 1
                total_cost += record.get("cost_estimate", 0)
                if record.get("solve_time_ms"):
                    solve_times.append(record["solve_time_ms"])

    print(f"Date: {target}")
    print(f"Total requests: {sum(statuses.values())}")
    print(f"Statuses: {dict(statuses)}")
    print(f"Estimated cost: ${total_cost:.2f}")
    if solve_times:
        print(f"Median solve time: {sorted(solve_times)[len(solve_times)//2]}ms")

daily_summary("captcha_audit.jsonl")

Ce récapitulatif sert aussi de contrôle croisé : ses totaux de coût doivent correspondre à ceux de votre tableau de bord CaptchaAI. Un écart signale une résolution non journalisée ou une erreur d'estimation du coût unitaire.

Rétention et stockage

Le volume du journal augmente linéairement avec le nombre de résolutions. Ne conservez pas tout indéfiniment : dimensionnez le stockage selon votre débit réel et purgez selon une durée définie à l'avance.

Volume Taille du journal quotidien Stockage mensuel Recommandation
100 résolutions/jour ~30 Ko ~1 Mo Fichier local
1 000 résolutions/jour ~300 Ko ~10 Mo Fichier local + rotation
10 000 résolutions/jour ~3 Mo ~100 Mo Expédier vers un agrégateur de logs
100 000 résolutions/jour ~30 Mo ~1 Go Journalisation centralisée (ELK, Datadog)

Pour une équipe basée en Europe, expédier ces journaux vers une région comme eu-west-3 (Paris) garde les données près de vos autres traitements et simplifie l'argumentaire de résidence des données en audit.

Trois règles évitent les dérives de rétention :

  • fixez une durée de conservation avant de commencer à journaliser, pas après coup ;
  • automatisez la purge (logrotate, politique de cycle de vie du bucket) plutôt que de la déclencher à la main ;
  • chiffrez les journaux au repos dès que le débit justifie un stockage centralisé.

Dépannage

Problème Cause Correctif
Le fichier journal devient trop volumineux Aucune rotation configurée Utilisez RotatingFileHandler ou logrotate
Des enregistrements d'audit manquent Exception levée avant l'écriture Journalisez dans un bloc finally
Écritures lentes à fort volume Entrées/sorties fichier synchrones Passez aux écritures asynchrones ou à un tampon
Horodatages incohérents Dérive de l'horloge système Synchronisez via NTP ; journalisez en UTC

FAQ

Le RGPD s'applique-t-il à un journal d'audit de résolution CAPTCHA ?

Cela dépend de son contenu. Un journal limité au task_id, au statut et à l'initiator technique ne contient pas de donnée personnelle. Il y tombe dès qu'il enregistre une adresse IP, un identifiant de compte ou le contenu d'un formulaire — d'où la règle de ne journaliser que les champs nécessaires à la traçabilité.

Faut-il séparer les journaux d'audit des logs applicatifs ?

Oui. Un journal d'audit a une finalité, un format et une durée de conservation propres. Le mélanger à vos logs de debug complique les recherches en audit et rend la purge sélective quasi impossible. Utilisez un fichier ou un flux dédié, comme ci-dessus.

Comment relier une résolution à l'opération qui l'a déclenchée ?

Renseignez le champ initiator à chaque appel — nom du job, identifiant de worker ou version de script (price-scraper-v2). C'est lui qui transforme une liste de résolutions anonymes en piste d'audit traçable, capable de répondre à « qui a lancé ceci ? ».

Puis-je rapprocher ces journaux de ma facturation CaptchaAI ?

Oui. La facturation CaptchaAI repose sur le nombre de threads simultanés — par exemple BASIC ($15/mois, 5 threads) — et non sur le nombre de résolutions. Vos journaux ne remplacent pas la facture, mais ils aident à vérifier le débit réel et à ventiler le coût par job ou par équipe.

Combien de temps faut-il conserver ces journaux ?

90 jours est une durée courante pour des journaux opérationnels. Pour une journalisation orientée conformité (SOC 2, RGPD, HIPAA), alignez la rétention sur les exigences de votre secteur et documentez votre règle de purge.

Articles connexes

Prochaines étapes

Ajoutez de la traçabilité à chaque résolution de CAPTCHA — récupérez votre clé API CaptchaAI.

Guides associés :

Les commentaires sont désactivés pour cet article.