Quand j’ai commence a developper des outils pour l’audit de code source, mon besoin principal etait de suivre les flux de donnees corrompues (tainted) a travers des bases de code complexes lors de revues de code manuelles. Au depart, je me suis tourne vers Tree-Sitter, qui s’est avere excellent pour l’analyse de fichiers individuels grace a ses capacites de parsing rapide et incrementiel. Cependant, en passant a des bases de code plus volumineuses avec des dependances inter-fichiers complexes et des flux de donnees, l’approche AST-only de Tree-Sitter est devenue limitante. Le defi n’etait pas simplement de parser des fichiers individuels. Il s’agissait de comprendre comment les donnees circulent entre les fonctions, a travers les modules et via differents chemins d’execution lors d’evaluations de securite manuelles approfondies.
Imaginez la scene : moi, un debutant, persuade que Tree-Sitter allait resoudre tous mes reves d’audit de code. “C’est rapide ! C’est incrementiel ! Ca parse tout !” disais-je, en analysant joyeusement des fichiers un par un comme en 1999.

Ma tache “simple” de suivi des entrees utilisateur a travers une application web s’est transformee en cauchemar : sauter manuellement entre les fichiers, perdre le fil des appels de fonctions et pleurer dans mon cafe. Les changements de contexte etaient brutaux. J’avais un visualiseur d’AST sur un ecran, un editeur de texte sur un autre et un tableur tentaculaire essayant de cartographier les appels de fonctions et les transformations de variables. C’etait lent, source d’erreurs et destructeur pour le moral. Cette approche fragmentee est la ou les bugs se cachent, dans les coutures entre les differentes vues du code. Les outils traditionnels fournissent des cartes isolees – une carte syntaxique, une carte de flux de controle – mais ce dont on avait desesperement besoin, c’etait un GPS entierement integre qui comprenne les routes, le trafic et le terrain en meme temps. Tree-Sitter, malgre toute sa beaute, ne me donnait que l’arbre syntaxique. C’etait comme avoir une carte de chaque arbre dans une foret sans la moindre idee de comment naviguer entre eux.
Apres des semaines de frustration et environ 47 tentatives ratees de construire mon propre outil d’analyse inter-fichiers (spoiler : c’etait horrible), je suis tombe sur les Code Property Graphs. Les anges ont chante, les nuages se sont ecartes, et soudainement je pouvais suivre les flux de donnees a travers des bases de code entieres avec de simples requetes. C’etait le moment “eureka”. Le defi fondamental de l’analyse de code n’est pas simplement de comprendre les pieces individuelles, mais de comprendre leurs relations.
Les CPGs promettaient le Graal de l’analyse de code : syntaxe, flux de controle et dependances de donnees, le tout dans une structure unique et interrogeable. Plus besoin de jongler entre trois outils differents et un tableur pour suivre mes decouvertes lors des revues manuelles. Ce rapport est l’aboutissement de ce parcours. Nous allons deconstruire les anciennes methodes (AST, CFG, PDG) pour comprendre leurs defauts fatals, assister a leur renaissance dans le CPG unifie, apprendre a maitriser ce nouveau pouvoir avec le REPL Joern, et enfin, le replacer dans le contexte de l’ecosysteme de securite au sens large.
Les trois mousquetaires : AST, CFG et PDG
Avant de plonger dans la magie du CPG, faisons connaissance avec les trois comperes qui rendent tout cela possible. Voyez-les comme les ingredients de votre cocktail d’analyse de securite prefere. Comprendre leurs forces individuelles et, surtout, leurs faiblesses collectives est la cle pour apprecier pourquoi les CPGs representent un bond en avant aussi monumental.

Abstract Syntax Tree (AST) : le plan statique du code (et ses angles morts)
L’Abstract Syntax Tree (AST) est le gardien de la grammaire de la programmation. Il decompose votre code en une structure arborescente, capturant chaque parenthese, l’intention de chaque point-virgule (meme si le point-virgule lui-meme finit a la poubelle). Genere par un parser, il represente la structure syntaxique du code selon la grammaire du langage. Il est excellent pour vous dire CE QUE votre code dit, mais n’a aucune idee de COMMENT il s’execute.
Considerez cette adorable fonction factorielle :
int factorial(int n) {
if (n <= 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
L’AST le voit comme ceci :
graph TD
A["Function: factorial"] --> B["Param: int n"]
A --> C["Block"]
C --> D["IfStatement"]
D --> E["Condition: n <= 1"]
D --> F["Return 1"]
D --> G["Return n * factorial(n - 1)"]
L’AST est merveilleux pour trouver des patterns comme “toutes les fonctions qui commencent par ‘unsafe_’”, mais inutile pour repondre a “est-ce que l’entree utilisateur peut atteindre cette requete SQL ?” C’est comme demander a un dictionnaire de planifier votre road trip. C’est l’angle mort critique de l’AST : il voit la structure, pas le flux.
Pour illustrer, considerez une transformation de donnees un peu plus complexe :
void process_request(char* user_input) {
char temp_buffer[256];
char final_buffer[256];
// Some intermediate processing
transform_data(user_input, temp_buffer);
// Copy to final destination
strcpy(final_buffer, temp_buffer); // Vulnerable line
}
Un outil base sur l’AST peut facilement trouver l’appel a strcpy. Il peut vous dire que ses arguments sont final_buffer et temp_buffer. Ce qu’il est fondamentalement incapable de faire, c’est de vous dire que les donnees dans temp_buffer proviennent de user_input. Cet ecart entre le potentiel et la realite est la source principale des taux eleves de faux positifs qui plombent les outils traditionnels de Static Application Security Testing (SAST). Un AST represente chaque construction syntaxique possible, mais il ne represente pas un seul chemin d’execution. Il montre le potentiel d’utilisation des donnees d’une certaine maniere, mais ne peut pas confirmer la realite d’un flux de donnees dangereux. Cela en fait une premiere etape necessaire, fournissant le “quoi”, mais dangereusement incomplete pour l’analyse de securite car il lui manque le “comment” et le “d’ou”.

Control Flow Graph (CFG) : la feuille de route d’execution du code (mais ou est la cargaison ?)
Le Control Flow Graph (CFG) est le GPS de votre code. Il est construit a partir de blocs de base (sequences de code sans sauts entrants ou sortants) et de branchements conditionnels. Il montre tous les chemins possibles que l’execution peut prendre, y compris ce cas limite bizarre que vous avez oublie a 3h du matin. Le CFG se fiche de votre belle syntaxe ; il veut juste savoir ce qui se passe ensuite.
Voici notre fonction factorielle vue par le CFG :
graph TD
Entry["Entry"] --> Check["Check: n <= 1"]
Check -->|Yes| Ret1["Return 1"]
Check -->|No| Compute["Compute: n * factorial(n - 1)"]
Compute --> RetResult["Return result"]
Ret1 --> Exit["Exit"]
RetResult --> Exit
Mais attendez, ce n’est pas tout ! Regardons une fonction avec une boucle, parce que qui n’aime pas une bonne possibilite de boucle infinie :
void process_array(int* arr, int size) {
int i = 0;
while (i < size) {
if (arr[i] > 0) {
printf("Positive: %d\n", arr[i]);
}
i++;
}
}
graph TD
Entry["Entry"] --> Init["i = 0"]
Init --> While["While: i < size"]
While -->|True| IfCheck["If: arr[i] > threshold"]
IfCheck -->|Yes| Process["Process arr[i]"]
IfCheck -->|No| Skip["Skip"]
Process --> Inc["i++"]
Skip --> Inc
Inc --> While
While -->|False| Exit["Exit"]
Le CFG de cette fonction montre clairement la structure de la boucle, avec une arete revenant de la fin du bloc while vers la verification de la condition.
Le CFG ajoute la dimension cruciale de l’ordre d’execution, mais il souffre d’un defaut fatal : il est sensible aux chemins mais agnostique vis-a-vis des donnees. Il peut repondre a “Cette ligne peut-elle etre atteinte ?” mais pas a “Cette ligne peut-elle etre atteinte avec des donnees corrompues (tainted) ?”
Considerez ce scenario :
void handle_data(char* data) {
char sanitized_data[256];
if (is_safe(data)) {
sanitize(data, sanitized_data);
} else {
// Path where data is NOT sanitized
strcpy(sanitized_data, data);
}
execute_query(sanitized_data); // Sink
}
Le CFG montrera deux chemins convergeant vers execute_query. Il n’a aucun mecanisme pour differencier l’etat de sanitized_data arrivant au sink. Il connait les routes, mais il est aveugle a la cargaison. Cette limitation est mise en evidence par la recherche sur le Control-Flow Integrity (CFI), qui demontre que meme en appliquant strictement un CFG valide, on ne previent pas tous les exploits. Un attaquant ne manipule pas seulement le chemin ; il manipule la charge utile (les donnees). Le CFG est comme un videur qui verifie que vous etes sur la liste des invites mais ne verifie pas le contenu de votre sac a dos.
Program Dependence Graph (PDG) : la toile d’influence (et son probleme de precision)
Le Program Dependence Graph (PDG) est la ou les choses deviennent pimentees. Il suit a la fois les dependances de controle (ce qui decide si le code s’execute) et les dependances de donnees (quelles valeurs circulent ou). C’est comme avoir une vision aux rayons X pour votre code.
Dependance de controle : une instruction S2 depend du controle d’un predicat S1 si le resultat de S1 determine si S2 s’execute. Dependance de donnees : une instruction S2 depend des donnees de S1 si S1 definit une variable que S2 utilise.
Voici une fonction potentiellement vulnerable pour illustrer :
int vulnerable_function(char* user_input) {
char buffer[100];
int length = strlen(user_input);
if (length < 100) {
strcpy(buffer, user_input); // What could go wrong? 🙈
return process(buffer);
}
return -1;
}
Le PDG de cette fonction montrerait :
- Une arete de dependance de controle du predicat if (length < 100) vers l’appel a strcpy.
- Une arete de dependance de donnees du parametre user_input vers l’appel a strlen.
- Une arete de dependance de donnees de l’appel a strlen vers la variable length.
- Une arete de dependance de donnees du parametre user_input vers l’appel a strcpy.
- Une arete de dependance de donnees de l’appel a strcpy vers la variable buffer.
- Une arete de dependance de donnees de buffer vers l’appel a process.
graph TD
Param["user_input"] -->|data| Strlen["strlen(user_input)"]
Strlen -->|data| Length["length"]
Param -->|data| Strcpy["strcpy(buffer, user_input)"]
Strcpy -->|data| Buffer["buffer"]
Buffer -->|data| ProcessCall["process(buffer)"]
IfCheck["if (length < 100)"] -->|control| Strcpy
IfCheck -->|control| ProcessCall
Length -->|data| IfCheck
Vous voyez ce strcpy ? C’est un buffer overflow classique en puissance. Le PDG nous montre exactement comment les donnees corrompues circulent de l’entree vers la catastrophe. Conceptuellement, le PDG est le plus proche des trois de ce dont un analyste de securite a besoin, car il modelise l’influence.
Cependant, son utilite pratique est souvent handicapee par le principe du “garbage in, garbage out” des dependances. La qualite d’un PDG est entierement tributaire de la precision des analyses sous-jacentes utilisees pour le construire. Dans des langages comme C/C++ avec de l’arithmetique de pointeurs complexe, ou dans des systemes modernes comme les smart contracts avec des appels dynamiques, construire un PDG precis necessite une analyse de pointeurs (points-to analysis) precise. Si cette analyse est imprecise – par exemple, elle pense qu’un pointeur pourrait pointer vers A, B ou C alors qu’il ne peut pointer que vers A – elle generera un reseau de fausses aretes de dependance de donnees. Cela pollue le graphe de faux positifs et rend le suivi du veritable flux de donnees quasi impossible.
| Representation | Ce qu’elle modelise | Forces cles | Defaut fatal pour l’analyse de securite |
|---|---|---|---|
| AST | Structure syntaxique | Rapide, excellent pour le linting et le pattern matching simple. | Aveugle au flux de controle et de donnees. Ne peut pas confirmer si une vulnerabilite est atteignable. |
| CFG | Ordre d’execution | Montre tous les chemins d’execution possibles, modelise les boucles et les branchements. | Sensible aux chemins mais agnostique vis-a-vis des donnees. Ne peut pas suivre l’etat des donnees le long des chemins. |
| PDG | Influence des donnees et du controle | Connecte explicitement le code lie par le calcul, rendant l’influence visible. | La qualite est tributaire de la precision des analyses sous-jacentes (ex. analyse de pointeurs), generant du bruit. |
Qu’est-ce qu’un Code Property Graph ?
Passons maintenant au plat principal. Un Code Property Graph (CPG) est ce qui se passe quand l’AST, le CFG et le PDG ont un beau bebe ensemble. C’est un graphe unique qui contient TOUTES les informations, interrogeable avec un langage unifie. Plus besoin de passer d’un outil a l’autre comme un ecureuil cafeine.
Techniquement, un CPG est un multigraphe oriente, a aretes etiquetees et attribue. Decomposons cela :
- Oriente : les aretes ont une direction (par exemple, d’un appelant vers un appele).
- A aretes etiquetees : chaque arete a un type (par exemple, AST, CFG, REACHING_DEF) qui definit la relation qu’elle represente.
- Attribue (Property Graph) : les noeuds (representant des constructions de code comme CALL ou METHOD) et les aretes peuvent avoir des paires cle-valeur (proprietes) attachees. Cette flexibilite est cruciale, nous permettant de stocker des informations riches comme CODE, LINE_NUMBER, NAME, etc., directement sur les elements du graphe.
La grande unification du CPG n’est pas un simple “ecrasement” des trois graphes de base. C’est une superimposition plus elegante. Les noeuds du graphe representent principalement les instructions et expressions de l’AST. Les differents types de graphes sont ensuite superposes en tant qu’ensembles differents d’aretes connectant ces memes noeuds. Une seule instruction if est un noeud unique, mais elle possede :
- Des aretes AST la reliant a sa condition et son corps.
- Des aretes CFG la reliant aux instructions precedentes et aux chemins de branchement suivants.
- Des aretes CDG (Control Dependence Graph) la reliant aux instructions dont elle controle l’execution.
- Des aretes REACHING_DEF la reliant aux definitions de variables utilisees dans sa condition.
Cette approche par couches est la solution technique au probleme de fragmentation. Elle cree une source unique de verite ou un analyste peut pivoter de maniere fluide entre differentes vues du code dans une seule requete.
Pensez aux approches d’analyse traditionnelles comme l’utilisation d’applications differentes pour les cartes, la meteo et le trafic. Le CPG, c’est comme avoir Google Maps avec tout integre. Vous pouvez poser des questions comme “Montre-moi tous les chemins de l’entree utilisateur aux requetes SQL qui ne passent pas par une fonction de validation” et obtenir effectivement une reponse.
L’ancienne methode :
graph TD
subgraph "AST Tool"
A1["AST Analysis"] --> A2["Syntax Results"]
end
subgraph "CFG Tool"
B1["CFG Analysis"] --> B2["Control Flow Results"]
end
subgraph "PDG Tool"
C1["PDG Analysis"] --> C2["Data Flow Results"]
end
A2 -.-x NoLink["No Unified View"]
B2 -.-x NoLink
C2 -.-x NoLink
La methode CPG :
graph TD
Code["Source Code"] --> CPG["Unified CPG"]
CPG --- AST["AST Edges"]
CPG --- CFG["CFG Edges"]
CPG --- PDG["PDG Edges"]
AST --> Query["Single Query Language"]
CFG --> Query
PDG --> Query
Query --> Results["Unified Results"]
Le CPG est un couteau suisse de l’analyse de code qui tient vraiment dans votre poche.
Construire et utiliser des CPGs
Creer un CPG, c’est comme preparer une bonne soupe – il faut les bons ingredients, une preparation adequate et de la patience. Le processus de construction est un pipeline multi-couches qui enrichit progressivement une representation basique du code avec des informations semantiques plus profondes.

Le pipeline de construction, tel qu’implemente par des outils comme Joern, ressemble a ceci :
graph LR
Parse["Parsing Frontend"] --> AST["AST Layer"]
AST --> CFG["CFG Layer"]
CFG --> Dom["Dominator Tree"]
Dom --> PDG["PDG Layer"]
PDG --> CG["Call Graph + Shortcuts"]
Parsing (Frontend) : le processus commence par un frontend specifique au langage. Un avantage cle de nombreux outils CPG est l’utilisation de parseurs “indulgents” (forgiving), qui peuvent generer un AST utilisable meme a partir de code qui ne compile pas entierement en raison de dependances manquantes ou d’erreurs de syntaxe mineures. C’est un avantage pratique considerable par rapport aux outils qui exigent un projet parfaitement compilable. Chaque langage necessite son propre parseur :
- C/C++ utilise Clang (quand il ne segfault pas).
- Java utilise Soot ou JavaParser (oui, Soot est vraiment son nom).
- JavaScript utilise le compilateur TypeScript (meme pour du JS classique, parce que l’ironie).
- Python utilise son module AST integre (Python etant raisonnable, comme d’habitude).
Construction de la couche AST : le frontend produit l’Abstract Syntax Tree fondamental, qui sert de squelette au CPG. Toutes les instructions, expressions et declarations deviennent des noeuds connectes par des aretes AST.
Construction de la couche CFG : le pipeline parcourt ensuite l’AST et ajoute des aretes CFG entre les noeuds, les connectant dans l’ordre d’execution. Cette couche modelise le flux de controle comme les branchements if-else, les boucles et les appels de fonctions.
Calcul de l’arbre des dominateurs : pour determiner les dependances de controle, le CFG est analyse pour produire des arbres de post-dominateurs. Un noeud A post-domine le noeud B si chaque chemin de B vers la sortie de la fonction passe par A.
Construction de la couche PDG : avec les arbres de dominateurs et l’analyse de flux de donnees, la couche PDG est ajoutee. Cela implique de tracer des aretes CDG (pour la dependance de controle) et des aretes REACHING_DEF (pour la dependance de donnees), qui suivent explicitement le flux de valeurs entre les definitions de variables et leurs utilisations.
Graphe d’appels et raccourcis : enfin, des aretes de commodite de plus haut niveau sont ajoutees. Les aretes CALL connectent les sites d’appel aux methodes qu’ils invoquent, creant un graphe d’appels inter-procedural. Des aretes raccourcis comme CONTAINS sont egalement ajoutees pour relier un noeud directement a la methode qui le contient, simplifiant de nombreuses requetes courantes.
La magie se produit dans cette phase de fusion. Le constructeur de CPG regarde chaque instruction et dit “Toi, tu as un noeud AST ! Et toi, tu as une arete CFG ! Tout le monde est connecte !” C’est comme Oprah pour l’analyse de code. Le resultat n’est pas une fusion de trois graphes egaux, mais plutot un AST qui a ete progressivement enrichi avec le flux de controle, le flux de donnees et d’autres relations semantiques, le tout dans une structure unique et interrogeable.
Une fois construit, vous pouvez interroger votre CPG comme s’il s’agissait de Google pour votre base de code. Vous voulez trouver tous les endroits ou l’entree utilisateur peut atteindre un appel a system() ? Il suffit de demander :
cpg.method.parameter
.reachableBy(cpg.call.name("system"))
C’est presque trop facile. Presque comme de la triche. Mais ce n’est pas de la triche si ca trouve de vraies vulnerabilites, pas vrai ?
Analyse de securite avec les CPGs
C’est ici que les CPGs brillent vraiment. Vous vous souvenez d’avoir passe des heures a tracer manuellement du code pour voir si l’entree utilisateur pouvait atteindre cette requete SQL douteuse ? Les CPGs transforment ca en une requete de 30 secondes. Le flux de travail interactif et exploratoire reflete le processus de pensee d’un auditeur manuel : commencer large, identifier les patterns interessants, puis zoomer pour confirmer les decouvertes.
Plongee dans la taint analysis
Le fond de commerce de l’analyse de securite basee sur les CPGs est le suivi de contamination (taint tracking). L’idee centrale est d’identifier les sources de donnees non fiables et de suivre leur flux vers des sinks sensibles.
Sources : les points ou des donnees externes, potentiellement malveillantes, entrent dans le programme. Dans un CPG, cela est souvent represente par les parametres de methodes, en particulier dans les fonctions qui gerent les requetes web ou les entrees utilisateur.
def source = cpg.method.parameter.name(".*userInput.*|.*requestBody.*")
Sinks : les fonctions ou operations dangereuses qui, si elles sont atteintes par des donnees corrompues, pourraient mener a une vulnerabilite. Les exemples incluent les fonctions de requetes de base de donnees, les fonctions d’execution de commandes ou les fonctions de copie memoire.
def sink = cpg.call.name("strcpy|system|executeQuery")
La requete : l’etape reachableBy est le cheval de bataille de la taint analysis, trouvant tous les chemins de flux de donnees d’un ensemble de sources vers un ensemble de sinks.
sink.reachableBy(source)
Trouver des candidats a l’injection SQL
Trouvons tous les appels de requetes de base de donnees dont les arguments peuvent etre influences par un parametre de methode ressemblant a une entree utilisateur.
// Define sources as parameters of methods that might handle user data
def sources = cpg.method.parameter.where(_.method.name(".*User.*|.*Request.*"))
// Define sinks as the arguments to common query execution functions
def sinks = cpg.call.name(".*[Qq]uery.*|.*[Ee]xec.*").argument
// Find flows from sources to sinks
sinks.reachableBy(sources).l
Detection de buffer overflow
Une vulnerabilite strcpy classique survient lorsque des donnees d’un buffer source circulent vers un buffer de destination sans verification de taille. On peut trouver des candidats en cherchant des flux d’un buffer plus grand (ou d’une entree utilisateur) vers le deuxieme argument de strcpy.
// strcpy: The function that refuses to die
def sources = cpg.method.parameter.name("user_input")
def sinks = cpg.call.name("strcpy").argument(1) // Second argument of strcpy is the source
sinks.reachableBy(sources).p //.p pretty-prints the paths
Chasse aux contournements d’authentification
Les failles de logique concernent la violation de workflows prevus. Ici, nous pouvons chercher des “mauvais chemins” ou des “verifications manquantes”. Par exemple, trouvons les fonctions d’administration qui peuvent etre appelees sans passer par une verification d’authentification. Cette requete exploite la comprehension du graphe d’appels par le CPG.
// Find admin functions with a suspicious lack of auth checks
cpg.method.name(".*[Aa]dmin.*")
.whereNot(
_.caller.call.name(".*[Aa]uth.*")
).name.l
Cette requete trouve toutes les methodes avec “Admin” dans leur nom qui ne sont pas appelees par une methode contenant egalement un appel a une fonction “Auth”. C’est une heuristique, mais c’est un moyen puissant d’identifier rapidement les chemins de code suspects qui meritent une revue manuelle.
Pattern avance : injection de second ordre
Les vulnerabilites de second ordre, ou des donnees corrompues sont stockees (par exemple, dans une base de donnees) puis utilisees de maniere non securisee, sont notoirement difficiles a detecter pour les outils SAST traditionnels. Les CPGs peuvent modeliser cela en chainant deux requetes de flux de donnees.
Flux 1 : source vers stockage. Trouver les chemins de l’entree utilisateur vers une fonction d’ecriture en base de donnees.
Flux 2 : stockage vers sink. Trouver les chemins d’une fonction de lecture en base de donnees vers un sink dangereux comme l’execution d’une requete.
// Step 1: Identify where tainted data might be stored
def sources = cpg.method.parameter
def storageSinks = cpg.call.name(".*save.*|.*persist.*|.*insert.*").argument
def taintedStorageLocations = storageSinks.reachableBy(sources)
// Step 2: Identify where stored data is read and used unsafely
def storageSources = cpg.call.name(".*load.*|.*find.*|.*select.*")
def executionSinks = cpg.call.name(".*query.*").argument
// Find flows from storage reads to dangerous sinks
// This is a conceptual query; a real implementation would correlate the storage locations
executionSinks.reachableBy(storageSources).l
Bien qu’un CPG ne modelise pas l’etat de la base de donnees lui-meme, il modelise les points d’interaction du code avec cet etat. En definissant les ecritures en base comme des sinks intermediaires et les lectures comme des sources intermediaires, un analyste peut modeliser cette transition d’etat a travers le code, un bond significatif au-dela du simple pattern matching sans etat.
La beaute reside dans l’interactivite. Commencez large (“montre-moi toutes les entrees utilisateur”), trouvez des patterns interessants, puis zoomez comme un enqueteur des Experts, sauf que votre bouton “ameliorer” fonctionne vraiment. Lors des revues manuelles, j’utilise les CPGs pour :
- Cartographier toute la surface d’attaque (15 minutes au lieu de 5 heures).
- Tracer toutes les entrees utilisateur vers les operations sensibles.
- Verifier que les controles de securite sont reellement appeles (spoiler : souvent ils ne le sont pas).
- Trouver ce chemin de code bizarre que tout le monde a oublie.
Pourquoi les CPGs surpassent les outils traditionnels
Laissez-moi enumerer les facons dont les CPGs font ressembler l’analyse statique traditionnelle a des tablettes de pierre :
Source unique de verite : plus besoin de correler les rapports de 17 outils differents. Un graphe pour les gouverner tous. Le CPG integre la syntaxe, le flux de controle et le flux de donnees, eliminant les changements de contexte et la correlation manuelle qui plombent les approches d’analyse fragmentees.
Interrogation agnostique du langage : que vous examiniez du Java, du Python ou ce maudit code PHP legacy, les concepts de requetes restent les memes. Bien que les frontends soient specifiques au langage, la structure CPG resultante et le langage de requete sont universels. C’est comme avoir un traducteur universel pour les vulnerabilites.
Exploration interactive : c’est la fonctionnalite qui change la donne pour les auditeurs manuels. Le SAST traditionnel est une boite noire “fire and forget” qui produit un rapport statique. L’analyse CPG est une conversation. Vous commencez avec une hypothese (“je parie que l’entree utilisateur atteint cette fonction”), vous la formulez en requete et vous obtenez une reponse immediate. Vous pouvez ensuite affiner votre requete de maniere iterative, creusant plus profondement dans la structure du code. Ce flux de travail transforme l’analyse d’une revue passive en une investigation active.
Sensibilite aux chemins (avec un bemol) : bien qu’une seule requete de flux de donnees CPG ne soit pas strictement sensible aux chemins au meme titre que l’execution symbolique, le graphe combine permet un raisonnement sensible aux chemins. Vous pouvez combiner des traversees CFG avec des requetes de flux de donnees pour poser des questions comme “Montre-moi les flux de donnees de A a B, mais seulement le long des chemins qui passent aussi par C.” Cela permet a un analyste de filtrer les chemins non pertinents et de se concentrer sur ceux qui repondent a des criteres specifiques de flux de controle.
graph LR
H["Hypothesis"] --> F["Formulate Query"]
F --> E["Execute on CPG"]
E --> R["Results"]
R --> Refine["Refine Query"]
Refine --> F
R --> Report["Report Finding"]
Joern : votre compagnon CPG
Joern est le couteau suisse des outils CPG – si les couteaux suisses etaient gratuits, open-source et capables de trouver des buffer overflows. C’est l’implementation originale du concept de Code Property Graph et il a ete le fondement d’une vaste quantite de recherche academique et industrielle dans la decouverte de vulnerabilites.

Demarrer est plus facile que d’expliquer a la direction pourquoi vous avez besoin d’un nouvel outil de securite :
# Download and install the latest version
wget https://github.com/joernio/joern/releases/latest/download/joern-install.sh
chmod +x joern-install.sh
./joern-install.sh
# Congrats, you're now a CPG wizard
# Parse your victim... I mean, target codebase
# This generates the CPG and saves it as cpg.bin
joern-parse /path/to/code
# Enter the matrix (the interactive shell)
joern
Une fois dans le REPL de Joern, vous interagissez avec un shell Scala qui a votre CPG pre-charge dans la variable cpg. Vous pouvez commencer a poser des questions comme un detective obsede par la securite :
// "Show me all your vulnerabilities!"
// Find all calls to functions with 'unsafe' in their name
cpg.call.name(".*unsafe.*").l
// "Where does user input go to die?"
// Find data flows from parameters named 'input' to command execution sinks
def source = cpg.method.parameter.name(".*[Ii]nput.*")
def sink = cpg.call.name("exec.*|system")
sink.reachableBy(source).p
// "Find me all the SQL construction shenanigans"
// A simple heuristic for string-concatenated SQL queries
cpg.call.code(".*\\+.*sql.*\\+.*").l
Le meilleur ? Joern parle plusieurs langues, grace a son architecture frontend modulaire :
- C/C++ (y compris tout le comportement indefini)
- Java / JVM Bytecode (verbeux mais supporte)
- JavaScript/TypeScript (callback hell inclus)
- Python (meme la folie du duck-typing)
- Go (goroutines et tout le reste)
- LLVM Bitcode et binaires x86 (via Ghidra)
Conseils de “pro” pour utiliser Joern :
- Commencez simple : n’ecrivez pas Guerre et Paix comme premiere requete. Commencez par cpg.method.name.l ou cpg.call.name.l pour prendre le pouls de la base de code.
- Utilisez .l pour lister : le terminateur .l (abreviation de .toList) execute la traversee et affiche les resultats. Sans lui, vous ne faites que construire un objet de requete en memoire.
- Exportez vos decouvertes : utilisez run.toJson ou des scripts personnalises pour sauvegarder vos resultats avant de fermer accidentellement le REPL.
- Empruntez liberalement : la communaute Joern maintient une base de donnees de requetes avec des requetes preconstruites pour les vulnerabilites courantes. Utilisez-les comme point de depart.
Joern s’integre merveilleusement aux workflows de revue manuelle. Exportez vos decouvertes en JSON, generez des visualisations de graphes avec .dotAst ou .dotCfg pour impressionner la direction, ou utilisez-le simplement pour gagner des debats sur la question de savoir si ce chemin de code est vraiment atteignable (spoiler : il l’est generalement).
Glossaire
- AST (Abstract Syntax Tree) : l’arbre genealogique de votre code, montrant qui herite de qui, mais pas qui parle a qui a l’execution.
- Basic Block (bloc de base) : un morceau de code qui s’execute ensemble sans aucun saut entrant ou sortant, comme une clique qui traine toujours ensemble.
- CFG (Control Flow Graph) : une carte de tous les chemins d’execution possibles, y compris ceux qui menent a la catastrophe.
- CPG (Code Property Graph) : le multigraphe ultime d’analyse de code qui rend les autres outils jaloux.
- CPGQL : le langage de requete pour les CPGs – comme SQL mais pour trouver des bugs au lieu de donnees clients.
- Data Dependence (dependance de donnees) : quand un morceau de code a besoin de donnees d’un autre, comme une addiction au cafe mais pour les variables.
- Control Dependence (dependance de controle) : quand l’execution du code depend d’une condition, comme votre humeur depend de la disponibilite du cafe.
- Edge (arete) : une connexion dans le graphe, pas le guitariste de U2.
- Frontend : le parseur qui lit votre code et juge vos choix de nommage de variables.
- Node (noeud) : un point dans le graphe representant des elements de code, pas un runtime JavaScript (ce qui prete a confusion).
- PDG (Program Dependence Graph) : montre les dependances de controle et de donnees, comme un diagramme de statut relationnel pour votre code.
- Property (propriete) : information supplementaire attachee aux noeuds, comme des metadonnees mais reellement utiles.
- Reaching Definition : quand l’assignation d’une variable reussit a voyager a travers le code pour atteindre son utilisation, representee par une arete REACHING_DEF.
- Sink : la ou les donnees potentiellement dangereuses finissent, comme les appels a system() ou les requetes SQL – l’equivalent code d’un trou noir.
- Source : la ou les donnees externes entrent dans votre programme, apportant generalement des cadeaux de XSS et d’injection SQL.
- Taint analysis : le suivi de donnees corrompues a travers votre code propre, comme suivre des traces de boue sur un tapis blanc.
- Traversal (traversee) : parcourir le graphe, comme explorer un donjon mais avec plus de pointeurs null et moins de dragons.
References et lectures complementaires
Vous voulez plonger plus profondement dans le terrier du CPG ? Voici les ressources qui m’ont aide dans mon parcours de la frustration Tree-Sitter a l’illumination CPG :
Lectures essentielles
Wikipedia: Code Property Graph - Commencez ici pour les fondements academiques et les definitions formelles.
L’article original sur les CPG (2014) - Le travail fondateur de Yamaguchi et al. qui a tout lance. Cet article a recu le IEEE Test-of-Time Award en 2024 pour son impact durable.
Specification CPG - La specification formelle de la structure, des couches, des noeuds et des aretes du CPG, telle qu’implementee par Joern.
Documentation et tutoriels Joern
Documentation officielle de Joern - Guide complet des concepts CPG et de l’utilisation de Joern.
Tutoriel du langage de requete CPG - Tutoriel interactif pour apprendre CPGQL avec des exemples de vulnerabilites reelles.
Blog Joern - Mises a jour regulieres sur les nouvelles fonctionnalites, les techniques avancees et les etudes de cas de vulnerabilites.
Tutoriels video
- Introduction aux Code Property Graphs - Explication visuelle des concepts CPG. - Demonstrations pratiques de chasse aux vulnerabilites avec Joern.
Implementations alternatives
- CPG Github - Une implementation alternative de CPG avec un fort accent sur le support multi-langages et les specifications formelles.
Ressources communautaires
- Exemples de requetes CPG - Une base de donnees de requetes de detection de vulnerabilites contribuees par la communaute pour Joern.
N’oubliez pas : la meilleure facon d’apprendre les CPGs, c’est de commencer a interroger.