PyDA Course
Intermédiaire Semi-guidé ⏱ 60 minutes ⚡ +100 XP +15 XP par étape

Scanner de Vulnérabilités

securitycliautomation

Les scanners de sécurité semblent magiques jusqu’à ce que tu voies les pièces : une liste de versions connues-mauvaises (c’est la base de données CVE), un contrôle « ma version est-elle dans une mauvaise plage ? » (c’est le calcul de version), et un passage pour les motifs qui ne devraient pas être dans le code livré (c’est le SAST). Ce projet construit ces trois pièces en Python pur, analyse requirements.txt en dépendances structurées, les fait correspondre à une petite base de données CVE avec sévérité, grep les fichiers de code pour des anti-modèles dangereux, et émet un rapport unique classé par sévérité avec des suggestions de mise à niveau. Il ne couvrira pas toute ta chaîne d’approvisionnement ; il démythifiera exactement comment un tel scanner pense.

Cela suppose Python 101 et un peu de regex, rien d’Analyse de Données n’est requis. C’est optionnel et non noté ; voir Projets du monde réel pour la liste complète et croissante.

🎯 Ce que tu vas faire

  1. Analyser un requirements.txt épinglé en dépendances structurées.
  2. Modéliser une petite base de données CVE avec des plages de versions affectées et des sévérités.
  3. Faire correspondre les versions installées aux plages et collecter les constats.
  4. Exécuter un SAST basé sur les regex sur les fichiers de code pour des motifs dangereux.
  5. Combiner les deux en un rapport classé par sévérité avec des suggestions de correction.

Où exécuter ceci

En local avec uv est le chemin principal. Le scanner est en bibliothèque standard pure, mais sa vraie valeur est de le pointer vers le requirements.txt et src/ de ton projet, des fichiers qui vivent sur un disque que tu possèdes.

Google Colab, Kaggle Notebooks et Binder exécutent chaque cellule de manière identique (stdlib uniquement), et le notebook d’exemple embarque le requirements.txt et fragile.py d’exemple à l’intérieur, donc tu vois le scan complet contre un projet d’exemple fixe. L’honnêteté impose de préciser : un notebook scannant les propres dépendances du dépôt du cours te montrerait le même moteur contre la vraie chose, mais son système de fichiers éphémère fait de « scanner mon projet » un geste local uniquement. Utilise les badges pour voir le moteur ; exécute uv pour le vrai audit.

Open In Colab Open In Kaggle Binder

Configuration

Crée le projet. Le scanner n’utilise que la bibliothèque standard, json, re, pathlib, et un assistant de découpage de version que tu écriras car « quelle version est la plus récente » est un vrai algorithme.

uv init vulnerability-scanner
cd vulnerability-scanner
uv run python -c "import json, re; from pathlib import Path; print('ok')"

json stocke la base de données CVE comme données, re alimente les motifs SAST, et pathlib parcourt ton arbre src/. Tu écriras toi-même la logique de comparaison de versions à l’Étape 3 plutôt que d’importer une bibliothèque de versions, parce que cette comparaison est l’une des deux idées que ce projet enseigne.

✅ Liste de vérification

  • uv init vulnerability-scanner a créé un dossier avec un pyproject.toml.
  • ✅ Le contrôle d’import affiche ok, zéro paquet ajouté.

Étape 1 : Analyse les dépendances en spécifications structurées

Chaque scan commence par « qu’avons-nous réellement d’installé ? ». Un requirements.txt est une vérité épinglée, mais seulement si tu transformes chaque ligne en une comparaison, pas en une chaîne.

1.1 Écris parse_version et parse_requirements

👟 Indice de départ : Découpe une chaîne de version comme 2.28.1 en un tuple numérique, Python compare les tuples dans le bon ordre gratuitement, puis regex chaque ligne d’exigence en name, operator, et version.

# scanner.py
import json
import re
from dataclasses import dataclass
from pathlib import Path

@dataclass
class Dependency:
    name: str
    operator: str          # "==" | ">=" | ">" | "<" | "<=" | "any"
    version: tuple[int, ...]


def parse_version(v: str) -> tuple[int, ...]:
    return tuple(int(part) for part in v.split("."))

def parse_requirements(path: str = "requirements.txt") -> list[Dependency]:
    deps = []
    pattern = re.compile(r"([\w\-\.]+)\s*(==|>=|<=|>|<)\s*([0-9\.]+)")
    for line in Path(path).read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        match = pattern.match(line)
        if not match:
            continue
        deps.append(Dependency(name=match.group(1),
                               operator=match.group(2),
                               version=parse_version(match.group(3))))
    return deps

Path("requirements.txt").write_text(
    "requests==2.28.1\nflask==2.2.5\nurllib3==1.26.0\nmakedata==0.9.0\n"
)
for dep in parse_requirements():
    print(dep)

parse_version est le héros discret : en découpant 2.28.1 en (2, 28, 1), la comparaison de tuples native de Python fait le travail difficile, (2, 28, 1) < (2, 31, 0) est vrai, et c’est précisément ainsi qu’on répond à « cette version est-elle vulnérable » à l’Étape 3. Le @dataclass donne à chaque dépendance un nom et un contrat de comparaison au lieu d’une chaîne à analyser manuellement plus tard. La regex empêche les espaces blancs et les commentaires de devenir des fausses dépendances : les lignes # pinned sont sautées, et seules les lignes avec un nom, un opérateur et une version pointillée valides deviennent des objets Dependency.

🎯 Résultat attendu : Quatre lignes Dependency(name=..., operator='==', version=(2, 28, 1)), une par exigence réelle, commentaires et blancs ignorés.

🩹 Si ça ne marche pas : Si la version d’une dépendance revient comme un tuple vide, c’est que parse_version n’a jamais tourné (un groupe None de la regex a atteint le dataclass). Si des lignes comme flask --hash=... plantent, le match de la regex échoue et continue les verrouille, mais un match() strict sur ([\w\-\.]+) laisse aussi tomber des paquets légitimes avec _ dans le nom ; élargis la classe à [\w\.\-]. Si les exigences se parsent depuis le mauvais dossier, Path("requirements.txt") est relatif au répertoire de travail.

1.2 Vérifie l’analyse

✅ Liste de vérification

  • ✅ Quatre dépendances se parsent depuis le fichier d’exemple ; les commentaires sont ignorés.
  • ✅ Une ligne flask>=3.0.0 donne operator='>=' et version=(3, 0, 0).
  • ✅ Les tuples de version se comparent correctement : (2, 28, 1) < (2, 31, 0) est True.

🤔 Question(s) socratique(s)

  • Une version comme 1.10.0 se comparerait plus basse que 1.9.0 si tu la stockais comme un seul nombre (int("1.10.0"), littéralement impossible). Quelle partie de cette conception fait sortir 1.10.0 > 1.9.0 correctement, et où une version de paquet comme "2026.9.2rc1" la casserait-elle ?
  • La regex ignore les lignes qu’elle ne peut pas faire correspondre silencieusement. Quand sauter silencieusement une exigence malformée est-il pire que de générer une erreur, et que journaliserais-tu pour rendre le saut visible ?

Étape 2 : Modélise une base de données CVE

Un scanner n’est aussi malin que sa base de données. Cette étape encode quelques CVE comme données, chacun avec une plage affectée et une sévérité, et les charge depuis JSON pour que le scanner les « connaisse » de la même façon qu’un vrai outil connaît son flux.

2.1 Charge la base de données CVE

👟 Indice de départ : Garde les CVE comme un petit fichier JSON et charge-les une fois avec json.load, les paires operator/version réutilisent exactement le contrat de comparaison construit à l’Étape 1.

# scanner.py (continuation)
CVE_DB_PATH = Path("cve_db.json")
CVE_DB_PATH.write_text(json.dumps([
    {"id": "CVE-2026-0001", "package": "requests", "operator": "<",
     "version": "2.31.0", "severity": "high",
     "summary": "SSL verification bypass on redirect"},
    {"id": "CVE-2025-1234", "package": "flask", "operator": "<=",
     "version": "2.2.5", "severity": "critical",
     "summary": "RCE reachable in debug mode"},
    {"id": "CVE-2026-1000", "package": "makedata", "operator": "<",
     "version": "1.0.0", "severity": "medium",
     "summary": "Slowloris-style memory leak"},
], indent=2))

def load_cves(path: str | None = None) -> list[dict]:
    with open(path or CVE_DB_PATH) as f:
        return json.load(f)

print([c["id"] for c in load_cves()])

La décision de modélisation critique est qu’un CVE porte un opérateur plus une version, ("<", "2.31.0") signifie « toute version en dessous de 2.31.0 est affectée », donc y faire correspondre une dépendance à l’Étape 3 consiste simplement à appliquer la même comparaison de tuples que tu as déjà écrite pour parse_version. Garder severity comme données (pas comme code) signifie que le trier plus tard (Étape 5) est un tri, pas une forêt de if-else. Parce que la base de données vit dans JSON plutôt qu’un fichier Python, la mettre à jour est une modification de données, pas de code.

🎯 Résultat attendu : ['CVE-2026-0001', 'CVE-2025-1234', 'CVE-2026-1000'].

🩹 Si ça ne marche pas : Si la liste est vide, load_cves a ouvert un fichier vide ou json.load a avalé un décalage de chemin. Si un rang montre des entiers (1, 2), le fichier stockait des numériques mais le schéma attend une sévérité comme une chaîne exacte "high". Si un nouveau CVE « ne s’applique pas quelle que soit la version », c’est que ses champs operator/version sont mal écrits.

2.2 Vérifie la base de données

✅ Liste de vérification

  • load_cves() retourne les trois CVE avec id, package, operator, version, severity, summary.
  • ✅ Les valeurs severity sont exactement critical / high / medium (le tri de l’Étape 5 en dépend).
  • ✅ Éditer cve_db.json change la connaissance du scanner sans toucher au code.

🤔 Question(s) socratique(s)

  • Chaque CVE ici cible un paquet. Les vrais CVE utilisent des plages de versions (>=1.0, <1.5). Que se passe-t-il avec ton modèle à opérateur unique quand un correctif livre une 1.5.0 qui réintroduit le bug, et quel changement de schéma exprimerait deux opérateurs ?
  • Le score CVSS qui décide de severity dans la vraie vie est calculé depuis le vecteur d’attaque et l’exploitabilité. Si tu le stockais comme un nombre au lieu de critical/high/medium, que pourrait faire ton rapport qu’une sévérité chaîne ne peut pas ?

Étape 3 : Fais correspondre les dépendances à la base de données

Avec les dépendances analysées et les CVE chargés, le scan lui-même est une fonction de comparaison : « cette version installée est-elle dans la plage affectée de ce CVE ? ». Cette étape l’applique à chaque paire dépendance/CVE.

3.1 Écris la logique de correspondance

👟 Indice de départ : Écris un assistant in_range(dep_version, cve) utilisant la chaîne d’opérateur comme un dict de lambdas de comparaison, puis boucle chaque dep × chaque CVE.

# scanner.py (continuation)
COMPARE = {
    "<": lambda a, b: a < b,
    "<=": lambda a, b: a <= b,
    ">": lambda a, b: a > b,
    ">=": lambda a, b: a >= b,
    "==": lambda a, b: a == b,
}

def in_range(dep: Dependency, cve: dict) -> bool:
    if dep.name != cve["package"]:
        return False
    cve_version = parse_version(cve["version"])
    return COMPARE[cve["operator"]](dep.version, cve_version)

def scan_dependencies(deps: list[Dependency], cves: list[dict]) -> list[dict]:
    findings = []
    for dep in deps:
        for cve in cves:
            if in_range(dep, cve):
                findings.append({
                    "type": "dependency",
                    "package": dep.name,
                    "installed": ".".join(str(p) for p in dep.version),
                    "cve": cve["id"],
                    "severity": cve["severity"],
                    "summary": cve["summary"],
                })
    return findings

for f in scan_dependencies(parse_requirements(), load_cves()):
    print(f["severity"], f["package"], f["installed"], f["cve"])

COMPARE comme dict de lambdas est le switch-state que Python n’a pas : la chaîne d’opérateur est la branche de code, donc un CVE arrivant avec "<=" fonctionne sans éditer le matcher. Le garde-fou dep.name != cve["package"] court-circuite les décalages de paquet avant tout calcul de version, ce qui garde la double boucle (deps × CVEs) bon marché à l’échelle réelle. Le dict de constat est le contrat que chaque étape ultérieure consomme, il porte la sévérité pour le tri de l’Étape 5 et le résumé pour la lisibilité humaine.

🎯 Résultat attendu : Trois constats triés par les données, pas par chance : requests 2.28.1 CVE-2026-0001, flask 2.2.5 CVE-2025-1234, makedata 0.9.0 CVE-2026-1000, celui de flask rapportant la sévérité critical.

🩹 Si ça ne marche pas : Si requests ne correspond jamais malgré être < 2.31.0, c’est que in_range compare dep.version contre une chaîne qui n’a pas été parse_version-ée. Si chaque dépendance correspond à chaque CVE, c’est que le garde-fou de paquet manque. Si un KeyError se déclenche sur COMPARE[...], un CVE a un opérateur que la carte de cinq ne couvre pas, ajoute-le à COMPARE ou valide la base de données au chargement.

3.2 Vérifie le scan de dépendances

✅ Liste de vérification

  • ✅ Trois constats correspondent exactement aux épingles vulnérables de l’exemple ; urllib3 n’en produit aucun.
  • CVE-2026-0001 (< 2.31.0) ne se déclenche pas pour un hypothétique requests==2.31.0.
  • ✅ Les données de classement de sévérité (critical/high/medium) sont présentes sur chaque constat.

🤔 Question(s) socratique(s)

  • Les dépendances épinglées avec >= au lieu de == déclarent un minimum, pas une installation exacte. Que peut réellement affirmer un scanner d’une ligne flask>=2.0.0 par rapport à une ligne flask==2.2.5, et laquelle est le sujet honnête d’un contrôle de version ?
  • Le matcher suppose que tu as la version installée exacte. Où les lockfiles (uv.lock, package-lock.json) s’insèrent-ils, que t’achète le scan d’un lockfile que le scan de requirements.txt ne peut pas ?

Étape 4 : Scanne la source pour des anti-modèles avec SAST

Les versions de dépendances sont un mode d’échec ; le code est l’autre. Le test de sécurité des applications statiques (SAST) scanne la source pour des motifs qui ne devraient pas être livrés, eval, chaînes shell, secrets codés en dur, sans exécuter le programme. Cette étape exécute un petit passage SAST sur chaque fichier .py d’un dossier.

4.1 Écris la liste de motifs et le scanner

👟 Indice de départ : Garde les motifs comme tuples (regex, label, severity), parcours les fichiers .py avec Path.rglob, et cherche chaque ligne, signalant les numéros de ligne pour que le rapport soit actionnable.

# scanner.py (continuation)
ANTI_PATTERNS = [
    (re.compile(r"\beval\s*\("), "eval() on untrusted data", "high"),
    (re.compile(r"\bshell\s*=\s*True"), "subprocess with shell=True", "high"),
    (re.compile(r"password\s*=\s*['\"][^'\"]+['\"]"), "Hardcoded password", "critical"),
    (re.compile(r"\bassert\s+"), "assert used for runtime checks", "low"),
    (re.compile(r"\bTODO\b|\bFIXME\b"), "Unresolved marker", "low"),
]

def scan_source(path: str = "src") -> list[dict]:
    findings = []
    for file in Path(path).rglob("*.py"):
        for lineno, line in enumerate(Path(file).read_text().splitlines(), 1):
            for pattern, label, severity in ANTI_PATTERNS:
                if pattern.search(line):
                    findings.append({
                        "type": "sast",
                        "file": str(file),
                        "line": lineno,
                        "severity": severity,
                        "summary": label,
                    })
    return findings

src = Path("src")
src.mkdir(exist_ok=True)
(src / "fragile.py").write_text(
    "import subprocess\n"
    "data = eval(input('code: '))\n"
    "password = 'hunter2'\n"
    "def run(cmd):\n"
    "    return subprocess.run(cmd, shell=True)\n"
    "assert password != ''\n"
    "# TODO: remove before ship\n"
)

for f in scan_source():
    print(f["line"], f["severity"], f["summary"])

La conception motif-comme-données (regex, label, severity) signifie qu’ajouter un contrôle est d’ajouter un tuple, pas de réécrire le scanner, exactement comment les vrais outils te laissent injecter des règles personnalisées. rglob("*.py") trouve les fichiers dans les dossiers imbriqués, et itérer splitlines() avec enumerate(..., 1) donne des numéros de ligne humains. Le constat porte le fichier et la ligne, ce qui transforme une liste de problèmes en une revue diff-able. assert et TODO sont délibérément de basse sévérité : ce sont surtout de l’hygiène, inclus pour que tu puisses voir la sévérité avoir de l’étendue.

🎯 Résultat attendu : Cinq constats avec des numéros de ligne 2-7, le eval (high) à la ligne 2, un mot de passe codé en dur (critical) à la ligne 3, shell=True (high) à la ligne 5, et le assert et le TODO (low) sur leurs lignes.

🩹 Si ça ne marche pas : Si rien ne s’affiche, rglob("*.py") n’a trouvé aucun fichier, vérifie le chemin du dossier src. Si la règle de mot de passe codé en dur se déclenche sur une variable nommée password = getenv(...), c’est que la regex ['\"][^'\"]+ matche aussi un appel de fonction, exige un littéral de guillemet. Si les constats comptent deux fois une ligne, plusieurs motifs ont correspondu à la même ligne (légitime) mais tu veux un constat représentatif par ligne, dédoublonne par (file, line).

4.2 Vérifie le SAST

✅ Liste de vérification

  • scan_source("src") retourne cinq constats avec fichier, ligne, sévérité, résumé.
  • ✅ La règle de mot de passe codé en dur rapporte critical.
  • ✅ Ajouter un nouveau tuple (regex, label, severity) à ANTI_PATTERNS produit immédiatement des constats sur les lignes correspondantes.

🤔 Question(s) socratique(s)

  • Le SAST par regex voit shell=True dans un commentaire et une docstring aussi, parce qu’il n’exécute jamais le code. Quelle classe de faux positif cela produit-il, et que devrait faire un outil réel basé sur un parseur (un AST) pour distinguer un commentaire du code ?
  • eval est signalé high, jamais critical, mais un eval sur des entrées d’attaquant est facilement critique. Quelle information un scan de ligne manque-t-il qui te permettrait d’élever cette sévérité de manière responsable ?

Étape 5 : Construis le rapport classé par sévérité avec corrections

La dernière étape rend le scanner utile : fusionne les constats de dépendances et SAST, les classe par sévérité, attache une suggestion de mise à niveau là où elle existe, imprime un résumé humain, et écrit le tout dans report.json.

5.1 Écris build_report et le point d’entrée principal

👟 Indice de départ : Trie par une carte de rang de sévérité (critical <-1 → low), ajoute la recommendation depuis une carte de version corrigée, imprime les comptes + constats principaux, et vide la liste fusionnée en JSON.

# scanner.py (continuation)
import sys

SEVERITY_RANK = {"critical": 0, "high": 1, "medium": 2, "low": 3}
FIXED_VERSIONS = {"requests": "2.32.0", "flask": "3.0.0", "makedata": "1.0.1"}
FIX_HINT = "upgrade to >= {}"

def build_report(deps: list[Dependency], cves: list[dict], source_dir: str = "src") -> list[dict]:
    findings = scan_dependencies(deps, cves) + scan_source(source_dir)
    for f in findings:
        if f["type"] == "dependency" and f["package"] in FIXED_VERSIONS:
            f["recommendation"] = FIX_HINT.format(FIXED_VERSIONS[f["package"]])
    findings.sort(key=lambda f: SEVERITY_RANK.get(f["severity"], 9))
    return findings

def main() -> None:
    report = build_report(parse_requirements(), load_cves())
    Path("report.json").write_text(json.dumps(report, indent=2))
    print(f"report.json: {len(report)} findings")
    for f in report:
        rec = f.get("recommendation", "")
        print(f"  [{f['severity']:>8}] {f['summary']:<40} {f['package'] if f['type']=='dependency' else f['file']}  {rec}")

if __name__ == "__main__":
    main()

build_report fusionne les deux scanners et remet la liste à un seul tri de sévérité, SEVERITY_RANK.get(severity, 9) retombe sur un grand nombre pour qu’une sévérité inattendue se trie en dernier au lieu de planter. La recommendation est attachée comme données seulement là où une version corrigée connue existe, gardant la colonne « comment corriger cela ? » honnête plutôt que devinée. sys n’apparaît que pour garder main derrière __name__, pour que import scanner dans un test n’exécute jamais le scan. Le JSON à la fin est le contrat machine-readable qu’un pipeline CI (le consommateur naturel d’un scanner) lirait.

🎯 Résultat attendu : report.json contient 8 constats ; la liste imprimée commence par les deux éléments critical (le RCE de flask et le mot de passe codé en dur), puis les highs, puis les lows, avec des recommandations de mise à niveau sur les trois constats de dépendances.

🩹 Si ça ne marche pas : Si le rapport commence par les lows, c’est que les recherches de SEVERITY_RANK échouent et que chaque sévérité s’est triée dans le compartiment 9. Si recommendation n’apparaît jamais, c’est que FIXED_VERSIONS a une clé de paquet sans rapport avec les constats (les cas diffèrent, normaliser les noms à l’analyse le corrige). Si report.json s’écrit mais qu’un parseur CI s’étouffe dessus, c’est qu’un constat manque l’un des champs attendus par le parseur, garde le contrat de dict identique entre les deux types de scanner.

5.2 Vérifie le scanner fini

✅ Liste de vérification

  • uv run python scanner.py écrit report.json avec 8 constats, les criticals en premier.
  • ✅ Les trois constats de dépendances portent chacun une recommandation de mise à niveau concrète.
  • ✅ Les constats SAST portent file/line ; les constats de dépendances portent package/installed.
  • ✅ Exécuter le scanner sur son propre src ajoute exactement les constats attendus, un scanner qui se signale lui-même fonctionne.

🤔 Question(s) socratique(s)

  • Le rapport trie par sévérité mais garde l’atteignabilité d’une vulnérabilité hors de son classement. Qu’est-ce qui compte le plus au triage, la sévérité seule, ou la sévérité × « est-ce même dans le chemin chaud » ? Quelle colonne ajouterais-tu pour encoder cela ?
  • Un scanner qui signale tout entraîne les équipes à ignorer tout. Que (dans les données de ce rapport) présenterais-tu différemment pour une équipe qui reçoit 200 constats par mois par rapport à une qui en reçoit 2, et pourquoi l’interface de triage décide-t-elle si un scanner vit ou meurt ?

⚠️ Pièges courants

  • Des tuples de version qui ne se parsent pas. Une version chaîne comme "1.10.0rc1" tue int(part) et tout le scan plante. Correction : retire les suffixes (split("-")[0], jette un rcN final) avant de convertir, et laisse les épingles malformées journaliser plutôt qu’abandonner.
  • Dérive de casse sur les noms de paquets. Requests vs requests vs requests[socks] échouent tous au garde-fou de correspondance exacte et ratent silencieusement des CVE. Correction : normalise les noms (et jette les extras comme [socks]) une fois à l’analyse.
  • Se fier à requirements.txt pour la vérité installée. Les épingles expriment l’intention, pas nécessairement la version en cours. Correction : si l’environnement a un lockfile ou une sortie pip freeze, scanne ça à la place, c’est l’inventaire réel.
  • Le SAST regex qui crie au loup sur les commentaires. shell=True dans un commentaire est un fichier de politique, pas une faille. Correction : signale-le mais laisse un humain le peser, et préfère des contrôles de type AST (arbres de noms, littéraux de chaîne) pour tout ce sur quoi tu agirais automatiquement.
  • Des sévérités non classées qui se trient en dernier par accident. Un CVE avec un "critial" mal écrit coule en bas au lieu du haut. Correction : SEVERITY_RANK.get(severity, 9) est le repli sûr, plus un avertissement quand une sévérité inconnue est vue.

Ce que tu viens de construire

Un scanner de vulnérabilités fonctionnel, en bibliothèque standard : analyse structurée des dépendances, base de données CVE JSON avec correspondance de plages, SAST par regex sur la source, et un rapport unique classé par sévérité avec recommandations de mise à niveau écrit en JSON. La compétence transférable est transformer la « sécurité » en opérations de données : comparaison de plages de versions, correspondance de motifs et tri de sévérités sont exactement les mêmes gestes derrière les bots de dépendances, les règles de lint et chaque outil « vérifie mon projet », tu en as maintenant construit trois à partir de zéro.

Exécute une version plus complète sans aucune configuration locale

examples/vulnerability-scanner/ dans le dépôt du cours est une version plus complète du code ci-dessus, avec un support de lockfile et un projet d’exemple plus riche à scanner. Clone-le, ou ouvre tout le dépôt dans un GitHub Codespace, et exécute-le depuis là.

Où aller à partir d’ici

  • Étends parse_requirements pour lire les tables [project] du pyproject.toml, pour que le scanner couvre les projets uv/pip, pas seulement les anciens requirements.txt.
  • Ajoute un mode --ast qui parcourt l’AST au lieu des lignes et signale eval/exec seulement quand ils pourraient atteindre des entrées non fiables, moins de faux positifs, même couverture.
  • Branche un score de sévérité CVE (via l’idée CVSS numérique de la question de l’Étape 2) et imprime un total de risque à l’échelle du projet comme titre du résumé.
  • Fais retourner à main un code de sortie non nul quand un constat critical/high existe, pour qu’un travail CI qui exécute le scanner fasse réellement échouer une build.

Partage ton projet avec la classe

Tu as construit quelque chose dont tu es fier ? examples/student-projects/ est une galerie de projets soumis par d’autres élèves, et son README a un tutoriel complet et adapté aux débutants pour ajouter le tien via une pull request, même si tu n’as jamais utilisé git avant : forker le dépôt, créer une branche, commiter tes fichiers, et ouvrir la PR, une étape à la fois. Aucune expérience préalable avec git n’est supposée.

Bienvenue dans l’écriture de Python en dehors du navigateur. 🎓

Terminez chaque étape puis marquez ce projet comme terminé pour gagner ses points.