Comprendre la note qualité : trois handlers, trois notes
Chaque déploiement de code repart avec une note sur 100 et une lettre. Plutôt que d'expliquer le barème dans le vide, on regarde le même webhook en trois versions — 47/D, 81/B, 100/A — et ce que chaque point retiré désigne réellement dans le code.
Chaque déploiement de code sur Zerolith repart désormais avec une note : un nombre sur 100 et une lettre de A à E. Elle apparaît dans la colonne « Note qualité » de votre liste de fonctions et en tête de la page de chaque fonction.
Avant tout le reste, ce qu'elle n'est pas : ce n'est pas un audit de sécurité, elle n'a jamais empêché une mise en ligne, et elle ne change rien à ce qui s'exécute. C'est un indicateur. Il existe parce que le code déployé ici est souvent écrit vite, parfois écrit par un agent, et que les fautes qui coûtent le plus cher sont aussi les plus faciles à repérer mécaniquement.
Le meilleur moyen de comprendre ce qu'elle mesure, c'est de regarder trois versions du même handler — un récepteur de webhook qui vérifie qu'une commande existe — et les notes qu'elles obtiennent réellement.
47/D — le webhook écrit vite
import hashlib
import os
import libsql_client
API_TOKEN = "sk_live_9f2c1a77b430"
def handler(request):
payload = request.json()
signature = hashlib.md5(request.body).hexdigest()
client = libsql_client.create_client_sync(
url=os.environ["DATABASE_URL"],
auth_token=os.environ["DATABASE_AUTH_TOKEN"],
)
rows = []
try:
rows = client.execute(f"SELECT id FROM orders WHERE ref = '{payload['ref']}'").rows
except Exception:
pass
return {"ok": True, "sig": signature, "found": bool(rows)}Ce code marche. Il se déploie, il répond, il rend le bon JSON. L'analyse en sort quatre problèmes :
| Ligne | Gravité | Ce qui est signalé |
|---|---|---|
| 19 | élevée | La requête SQL est assemblée dans une f-string |
| 6 | moyenne | Un identifiant ressemble à un secret écrit en dur |
| 11 | faible | MD5 |
| 18 | faible | Un except qui ne fait rien |
100 − 30 − 15 − 4 − 4 = 47, soit un D.
Le seul problème qui compte vraiment est le premier. payload["ref"] vient du corps de la requête,
il est interpolé dans la chaîne SQL, et n'importe qui pouvant appeler ce webhook choisit donc la fin
de la requête. C'est le genre de ligne qu'on écrit sans y penser un mardi après-midi, et c'est
exactement ce que la note est là pour rendre visible.
MD5, à l'inverse, ne sert ici qu'à fabriquer une empreinte d'affichage. Ce n'est pas un trou, et ça ne coûte que quatre points : la graduation est faite pour qu'un problème faible se voie sans écraser la note.
81/B — presque propre, mais pas tout à fait
import hashlib
import requests
def handler(request):
ref = request.query.get("ref", "")
upstream = requests.get(
"https://partner.example.com/orders/" + ref,
timeout=5,
verify=False,
)
etag = hashlib.md5(upstream.content).hexdigest()
return {"order": upstream.json(), "etag": etag}Deux problèmes : verify=False (moyenne, 15 points) et MD5 (faible, 4 points). 100 − 15 − 4 = 81,
soit un B.
C'est la note la plus intéressante des trois, parce que c'est celle qu'on obtient sans rien faire de
scandaleux. verify=False arrive presque toujours de la même façon : un certificat récalcitrant en
développement, une ligne ajoutée pour débloquer l'après-midi, et personne ne l'a retirée. La note ne
dit pas que ce code est mauvais ; elle dit qu'il reste dedans une ligne dont vous ne vous souvenez
plus.
100/A — la même fonction, corrigée
import hashlib
import hmac
import logging
import os
import libsql_client
logger = logging.getLogger("orders")
def handler(request):
expected = hmac.new(
os.environ["WEBHOOK_SECRET"].encode(), request.body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Signature", "")):
return 401, {"error": "bad signature"}
payload = request.json()
client = libsql_client.create_client_sync(
url=os.environ["DATABASE_URL"],
auth_token=os.environ["DATABASE_AUTH_TOKEN"],
)
try:
rows = client.execute("SELECT id FROM orders WHERE ref = ?", [payload["ref"]]).rows
except libsql_client.LibsqlError:
logger.exception("lookup failed")
return 503, {"error": "storage unavailable"}
finally:
client.close()
return {"ok": True, "found": bool(rows)}Quatre changements, aucun difficile. La valeur passe en paramètre lié plutôt que dans la chaîne. Le
secret devient une variable d'environnement de type secret — elle part vers le cluster et n'est
jamais relisible, ni par l'interface ni par l'API. SHA-256 remplace MD5. Et le except attrape une
erreur précise, la journalise, et renvoie un code honnête.
Ce dernier point n'est pas cosmétique. Un except Exception: pass autour d'un accès à la base
transforme une panne réelle en réponse {"ok": true} : votre client voit un succès là où il n'y en
a pas eu.
Comment le nombre est calculé
On part de 100 et on retranche un poids par problème, selon sa gravité :
| Gravité | Poids | Exemples |
|---|---|---|
| élevée | 30 | eval, os.system, shell=True, pickle.loads, SQL construit en chaîne |
| moyenne | 15 | secret en dur, random pour un jeton, debug=True |
| faible | 4 | MD5/SHA-1, exception avalée, tempfile.mktemp |
La lettre suit le nombre : A à partir de 90, B à partir de 75, C à partir de 55, D à partir de 30, E en dessous.
Deux choix méritent d'être expliqués, parce qu'ils sont contre-intuitifs.
Le calcul ne divise pas par la taille du fichier. Une version antérieure le faisait, et ça a l'air plus juste : une faute dans deux mille lignes serait moins grave qu'une faute dans vingt. Sauf qu'une injection reste une injection quelle que soit la longueur du fichier, et que diviser par la taille offre à chacun un moyen d'améliorer sa note en rallongeant son code. La densité est une bonne métrique pour trier une base existante ; c'est la mauvaise métrique pour un handler dont on vous dit qu'il est dangereux.
Un problème de gravité élevée ne peut jamais être présenté comme un A ni un B. Avec les poids actuels l'arithmétique s'en charge toute seule — un seul problème élevé fait tomber à 70, donc à un C — mais la règle est écrite à part, pour qu'elle survive à un futur ajustement des poids.
Le même code, l'autre langage
Le webhook du premier exemple, traduit en Node avec le client libSQL et crypto, ressort avec
exactement la même note : 47/D, les mêmes quatre problèmes aux mêmes gravités.
Ce n'est pas une coïncidence : c'est la raison pour laquelle Python et JavaScript sont analysés par un seul moteur et un seul jeu de règles, plutôt que par un outil spécialisé de chaque côté. Deux outils, ce sont deux échelles de gravité — et une note qui n'est pas comparable d'un langage à l'autre n'est pas une note.
Ce qu'un A ne dit pas
Un A veut dire « aucune règle n'a réagi ». Il ne veut pas dire « ce code est sûr ».
L'analyse est statique : elle lit votre source sans l'exécuter, ne connaît pas vos données et ne
suit pas une valeur à travers dix fonctions. Elle passe donc à côté de choses — une autorisation
oubliée, une logique métier fausse, un secret lu depuis un fichier — et elle en signale parfois à
tort, quand une variable s'appelle token sans en être un.
Trois détails pratiques, pour finir :
- La note décrit toujours le code actuellement déployé. Redéployez, une nouvelle analyse est mise en file ; si vous redéployez pendant qu'une analyse tourne, celle de l'ancien code est abandonnée plutôt que recollée sur le nouveau.
- Rien ne rattrape le passé. Une fonction déployée avant l'arrivée de la fonctionnalité n'a pas de note, et sa cellule reste vide — pas de tiret, pas de E — jusqu'à son prochain déploiement.
- Votre source n'est jamais conservée. L'analyseur la relit au moment du scan et n'en garde que la ligne citée par un problème.
Le détail complet est dans la documentation, et le reste du modèle de sécurité de la plateforme — isolation par locataire, secrets en écriture seule, authentification au bord du réseau — est sur la page sécurité.
Essayez sur votre propre compte
L'inscription vient avec du crédit, de quoi déployer, planifier et mesurer tout ce qui précède sans sortir de carte bancaire.
Démarrer gratuitement »