Exécuter CodeQL dans une base de données
Avec votre code extrait dans une base de données, vous pouvez désormais l’analyser à l’aide de requêtes CodeQL. Les experts GitHub, les chercheurs en sécurité et les contributeurs de la communauté écrivent et gèrent les requêtes CodeQL par défaut. Vous pouvez également écrire vos propres requêtes.
Vous pouvez utiliser des requêtes CodeQL dans l’analyse du code pour rechercher des problèmes dans votre code source et identifier les vulnérabilités de sécurité potentielles. Vous pouvez également écrire des requêtes personnalisées pour identifier les problèmes liés à chaque langage que vous utilisez dans votre code source.
Il existe deux types de requêtes importants :
- Les requêtes d’alerte mettent en évidence les problèmes dans des emplacements spécifiques de votre code.
- Les requêtes de chemin décrivent le flux d’informations entre une source et un récepteur dans votre code.
Requête CodeQL simple
La structure de requête CodeQL de base a l’extension .ql de fichier et contient une select clause. Voici un exemple de structure de requête :
/**
*
* Query metadata
*
*/
import /* ... CodeQL libraries or modules ... */
/* ... Optional, define CodeQL classes and predicates ... */
from /* ... variable declarations ... */
where /* ... logical formula ... */
select /* ... expressions ... */
Personnalisation des requêtes
L’analyse CodeQL est pilotée par les requêtes. Bien que vous puissiez utiliser les requêtes standard fournies par GitHub, vous pouvez également personnaliser l’analyse en écrivant vos propres requêtes et en les organisant dans des packs de requêtes.
Les requêtes sont généralement regroupées dans des packs de requêtes, qui sont des répertoires qui contiennent des requêtes, des bibliothèques partagées et des fichiers de configuration. Un pack de requêtes vous permet de définir un ensemble réutilisable de règles d’analyse pour vos projets. Dans un pack, vous pouvez inclure des fichiers individuels .ql , des bibliothèques d’assistance qui définissent une logique réutilisable et des suites de requêtes qui regroupent plusieurs requêtes.
Une suite de requêtes (.qls fichier) est utilisée pour contrôler les requêtes exécutées pendant l’analyse. Au lieu d’exécuter des requêtes un par un, vous définissez une suite qui répertorie toutes les requêtes que vous souhaitez exécuter. Par exemple:
- description: Custom security queries
- queries:
- ./queries/hardcoded-credentials.ql
- ./queries/insecure-config.ql
Cette suite regroupe plusieurs requêtes afin qu’elles puissent s’exécuter ensemble dans le cadre d’une analyse unique.
Vous pouvez créer vos propres requêtes en rédigeant des fichiers .ql. Une requête décrit un modèle dans votre code que vous souhaitez détecter. Il importe généralement une bibliothèque de langues, définit des conditions et retourne des résultats à l’aide d’une select instruction.
Par exemple, la requête suivante recherche des littéraux de chaîne qui peuvent contenir des informations d’identification codées en dur :
/**
* @name Hardcoded credential detection
* @description Finds string literals that may contain passwords
* @kind problem
* @id example/hardcoded-credentials
* @severity warning
*/
import javascript
from Literal l
where l.getValue().toString().matches("%password%")
select l, "Possible hardcoded credential"
Dans cette requête :
- L’instruction
importcharge le modèle de langage pour JavaScript. - La
fromclause définit les données en cours d’analyse. - La
whereclause filtre les modèles correspondants. - L’instruction
selectdéfinit les résultats retournés.
Vous pouvez créer des requêtes personnalisées en commençant par des requêtes standard et en modifiant leurs conditions ou sorties.
Pour utiliser une requête avec GitHub analyse du code, vous devez inclure des métadonnées de requête. Les métadonnées sont définies dans un bloc de commentaires en haut du fichier et contrôlent la façon dont les résultats sont interprétés et affichés.
Au minimum, les métadonnées doivent inclure :
- Identificateur unique (
@id) - Un nom (
@name) - Une description (
@description) - Type de résultat (
@kindpar exemple, ouproblempath-problem)
Des propriétés supplémentaires telles que @severity et @precision permettent de déterminer comment les alertes apparaissent dans GitHub.
Les métadonnées sont requises pour l’intégration à l’analyse du code. Lorsque les métadonnées sont présentes, les résultats sont affichés sous forme d’alertes dans le référentiel. Si les métadonnées sont manquantes, CodeQL exécute toujours la requête, mais les résultats sont affichés uniquement en tant que sortie brute et ne sont pas exposés en tant qu’alertes d’analyse du code.
Une fois que vous avez défini vos requêtes ou suite de requêtes, vous pouvez les inclure dans votre configuration d’analyse. Dans GitHub Actions, vous spécifiez les requêtes pendant l’étape d’initialisation :
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
queries: ./path/to/query-suite.qls
Pendant le flux de travail :
- CodeQL crée la base de données.
- Exécute les requêtes sélectionnées.
- Génère des résultats au format SARIF.
- Charge les résultats dans GitHub.
Les résultats de requête personnalisés s’affichent en même temps que les résultats CodeQL standard sous l’onglet Sécurité. Cela vous permet d'étendre l'analyse par défaut avec des vérifications spécifiques à votre codebase tout en bénéficiant toujours des jeux de requêtes gérés par GitHub.
Métadonnées de requête
Dans la section précédente, vous avez ajouté des métadonnées à une requête afin qu’elle puisse être utilisée dans l’analyse du code. Cette section explique comment les métadonnées sont utilisées et comment elles affectent les résultats des requêtes.
Les métadonnées de requête sont définies dans un bloc de commentaires en haut d’un .ql fichier. Il fournit des informations sur la requête et contrôle la façon dont les résultats sont interprétés et affichés.
Les métadonnées sont utilisées par CodeQL et l’analyse du code de GitHub pour :
- Identifiez la requête et son objectif.
- Déterminez comment les résultats sont classés (par exemple,
problemoupath). - Attribuez des niveaux de gravité et de précision.
- Mettre en forme les résultats pour l’affichage dans le référentiel.
Par exemple, une requête peut inclure des métadonnées telles que celles-ci :
/**
* @name Hardcoded credential detection
* @description Finds string literals that may contain passwords
* @kind problem
* @id example/hardcoded-credentials
* @severity warning
*/
Lorsque ces métadonnées sont présentes :
- Les résultats sont convertis au format SARIF.
- Les alertes sont affichées dans GitHub analyse du code.
- Les résultats incluent un contexte tel que la gravité et la description.
Lorsque les métadonnées sont manquantes :
- La requête s’exécute toujours.
- Les résultats ne sont pas affichés en tant qu’alertes.
- La sortie n’est affichée que sous forme de tables brutes.
Les métadonnées déterminent également la façon dont les résultats sont regroupés et suivis entre les analyses. Par exemple, la requête @id est utilisée pour faire correspondre les alertes entre les exécutions.
GitHub a un guide de style recommandé pour les métadonnées de requête. Vous pouvez le trouver dans la documentation CodeQL.
Cet exemple montre les métadonnées pour l’une des requêtes Java standard :
CodeQL n’interprète pas les requêtes qui n’ont pas de métadonnées. Il affiche ces résultats sous forme de table et ne les affiche pas dans le code source.
Écriture, test et exécution de requêtes
Après avoir créé des requêtes personnalisées, l’étape suivante consiste à les tester, à les exécuter dans vos flux de travail et à les gérer au fil du temps.
Lorsque vous écrivez une requête, vous définissez un modèle que CodeQL doit détecter dans votre codebase. Le moyen le plus efficace de développer des requêtes consiste à itérer localement avant de les ajouter à votre référentiel.
Tester les requêtes localement
Vous pouvez tester des requêtes à l’aide de l’interface CLI CodeQL ou de l’extension Visual Studio Code.
Avec l’interface CLI CodeQL, vous exécutez des requêtes sur une base de données que vous avez déjà créée :
codeql database analyze <database> <query.ql>
Cette commande exécute la requête et génère des résultats que vous pouvez examiner dans SARIF ou un autre format de sortie.
Vous pouvez également exécuter :
codeql query run <query.ql> --database=<database>
Le test local vous permet de :
- Vérifiez que la requête retourne les résultats attendus.
- Affinez la logique de requête.
- Identifiez les faux positifs ou les cas manquants.
L’extension Visual Studio Code offre une expérience plus interactive. Vous pouvez:
- Ouvrez une base de données.
- Exécutez des requêtes directement à partir de l’éditeur.
- Affichez les résultats en même temps que le code source.
Cela facilite la compréhension du comportement de votre requête et son ajustement rapide.
Exécuter des requêtes dans l’analyse du code de GitHub
Une fois que votre requête produit les résultats attendus, vous pouvez l’inclure dans votre workflow d’analyse du code.
Dans GitHub Actions, les requêtes sont configurées à l’étape d’initialisation :
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
queries: ./path/to/query-suite.qls
Lorsque le flux de travail s’exécute :
- CodeQL crée une base de données pour le référentiel.
- Exécute les requêtes sélectionnées.
- Convertit les résultats en SARIF.
- Charge les résultats dans GitHub.
Les résultats s’affichent sous forme d’alertes sous l’onglet Sécurité , en même temps que les résultats CodeQL standard.
L’exécution de requêtes dans les workflows garantit que :
- L’analyse s’exécute automatiquement sur les pull requests et les branches.
- De nouveaux problèmes sont détectés en tant que modifications de code.
- Les résultats sont visibles pour l’équipe.
Gérer et mettre à jour des requêtes
Après avoir ajouté une requête personnalisée à votre flux de travail, vous remarquerez peut-être que les résultats ne sont pas toujours ce que vous attendez.
Par exemple:
- Une requête peut retourner trop de résultats (faux positifs).
- Il peut manquer des cas que vous attendiez qu’il détecte.
- Il se peut que les nouveaux modèles de code de votre référentiel ne soient pas couverts.
Dans ces cas, vous mettez à jour la requête pour améliorer sa précision.
Commencez par exécuter la requête localement et examiner les résultats. Examinez les emplacements de code qui ont été marqués et déterminez s’ils représentent des problèmes réels. Si ce n’est pas le cas, affinez les conditions de la where clause pour affiner les résultats.
Par exemple, vous pouvez :
- Ajoutez des conditions supplémentaires pour exclure des modèles sécurisés.
- Ajustez la logique de correspondance de chaîne ou de flux de données.
- Réutilisez les prédicats des bibliothèques existantes pour améliorer la précision.
Après avoir mis à jour la requête, réexécutez-la sur votre base de données pour confirmer que les résultats ont été améliorés.
Lorsque vous validez la requête mise à jour, elle s’exécute automatiquement dans votre workflow d’analyse de code. Cela signifie :
- Les alertes existantes peuvent être mises à jour ou supprimées.
- De nouvelles alertes peuvent apparaître en fonction de la logique mise à jour.
Au fil du temps, vous répétez ce processus à mesure que votre codebase évolue. La maintenance des requêtes est une tâche en cours qui permet de garantir que votre analyse reste précise et pertinente.
Syntaxe QL
QL est un langage de requête déclaratif orienté objet. Il est optimisé pour permettre une analyse efficace des structures de données hiérarchiques, et notamment des bases de données qui représentent des artefacts logiciels.
La syntaxe de QL est similaire à SQL, mais la sémantique de QL est basée sur Datalog. Datalog est un langage de programmation logique déclaratif, souvent utilisé comme langage de requête. Étant donné que QL est principalement un langage logique, toutes les opérations dans QL sont des opérations logiques. QL hérite également des prédicats récursifs de Datalog. QL ajoute la prise en charge des agrégats pour rendre même les requêtes complexes concises et simples.
Le langage QL se compose de formules logiques. Il utilise des conjonctifs logiques courants tels que and, oret not, ainsi que des quantificateurs tels que forall et exists. Étant donné que QL hérite des prédicats récursifs, vous pouvez également écrire des requêtes récursives complexes à l’aide de la syntaxe QL de base et des agrégats tels que count, sumet average.
Pour plus d’informations sur le langage QL, consultez la documentation CodeQL.
Requêtes de chemin
La façon dont les informations transitent par un programme est importante. Les données qui semblent bénignes peuvent circuler de manière inattendue qui lui permettent d’être utilisées de manière malveillante.
La création de requêtes de chemin d’accès peut vous aider à visualiser le flux d’informations via une base de code. Une requête peut suivre le parcours qu’empruntent les données depuis leurs points de départ possibles (source) jusqu’à leurs points d’arrivée possibles (sink). Pour modéliser les chemins d’accès, votre requête doit fournir des informations sur la source, le récepteur et les étapes de flux de données qui les lient.
Le moyen le plus simple de commencer à écrire votre propre requête de chemin consiste à utiliser l’une des requêtes existantes en tant que modèle. Pour obtenir ces requêtes pour les langages pris en charge, consultez la documentation CodeQL.
Votre requête de chemin nécessite certaines métadonnées, des prédicats de requête, et des structures d’instructions select. La plupart des requêtes de chemin d’accès intégrés dans CodeQL suivent une structure de base. La structure dépend de la façon dont CodeQL modélise le langage que vous analysez.
Voici un exemple de modèle pour une requête de chemin d’accès :
/**
* ...
* @kind path-problem
* ...
*/
import <language>
// For some languages (Java/C++/Python/Swift), you need to explicitly
// import the data-flow library, such as:
// import semmle.code.java.dataflow.DataFlow
// import codeql.swift.dataflow.DataFlow
...
module Flow = DataFlow::Global<MyConfiguration>;
import Flow::PathGraph
from Flow::PathNode source, Flow::PathNode sink
where Flow::flowPath(source, sink)
select sink.getNode(), source, sink, "<message>"
Dans ce modèle :
-
MyConfigurationest un module qui contient les prédicats qui définissent la façon dont les données circulent entre la source et le récepteur. -
Flowest le résultat du calcul de flux de données basé surMyConfiguration. -
Flow::PathGraphest le module de graphe de flux de données résultant que vous devez importer pour inclure des explications de chemin d’accès dans la requête. -
sourceetsinksont des nœuds dans le graphique tel que défini dans la configuration, etFlow::PathNodeest leur type. -
DataFlow::Global<..>est un appel de flux de données. Vous pouvez utiliserTaintTracking::Global<..>à la place pour inclure un ensemble par défaut d’étapes de contamination.
Comment écrire une requête de chemin d’accès
Votre requête doit calculer un graphique de chemin pour générer des explications de chemin. Pour ce faire, définissez un prédicat de requête appelé edges. Un prédicat de requête est un prédicat non membre avec une annotation de requête. Cette annotation retourne tous les tuples évalués par le prédicat.
Le edges prédicat définit les relations de périphérie du graphique que vous calculez. Il est utilisé pour calculer les chemins liés à chaque résultat généré par votre requête. Vous pouvez également importer un prédicat prédéfini edges à partir d’un module de graphique de chemin d’accès dans l’une des bibliothèques de flux de données standard.
Les bibliothèques de flux de données contiennent les autres classes, prédicats et modules couramment utilisés dans l’analyse de flux de données, en plus du module de graphe de chemin d’accès. Les bibliothèques de flux de données CodeQL fonctionnent en modélisant le graphe de flux de données ou en implémentant une analyse de flux de données. Les bibliothèques de flux de données normales sont utilisées pour analyser le flux d’informations dans lequel les valeurs de données sont conservées à chaque étape.
Voici un exemple d’instruction qui importe le PathGraph module à partir de la bibliothèque de flux de données (DataFlow.qll), dans laquelle edges est défini :
import DataFlow::PathGraph
Vous pouvez importer de nombreuses autres bibliothèques incluses dans CodeQL. Vous pouvez également importer des bibliothèques conçues spécifiquement pour implémenter l’analyse de flux de données dans différents frameworks et environnements courants.
La classe PathNode est conçue pour implémenter l’analyse de flux de données. Il s’agit de Node, enrichi d’un contexte d’appel (sauf pour les sinks), d’un chemin d’accès et d’une configuration. Seules PathNode les valeurs accessibles à partir d’une source sont générées.
Voici un exemple de chemin d’importation :
import semmle.code.cpp.ir.dataflow.internal.DataFlowImpl
Vous pouvez éventuellement définir un nodes prédicat de requête, qui spécifie les nœuds du graphique de chemin d’accès pour toutes les langues. Lorsque vous définissez nodes, les nœuds sélectionnés définissent uniquement des arêtes avec des points de terminaison. Lorsque vous ne définissez nodespas, vous devez sélectionner tous les points de terminaison possibles des arêtes.
Analyse de base de données
Lorsque vous utilisez des requêtes pour analyser une base de données CodeQL, vous recevez des résultats significatifs dans le contexte du code source. Les résultats sont mis en forme sous forme d’alertes ou de chemins d’accès dans SARIF ou dans un autre format interprété.
Voici un exemple de commande de base de données CodeQL qui analyse la base de données en exécutant des requêtes sélectionnées sur celle-ci et en interprétant les résultats :
codeql database analyze \
--format=<format> \
--output=<output> \
[--threads=<num>] \
[--ram=<MB>] \
<options>... \
-- <database> <query|dir|suite>...
Cette commande combine l’effet des commandes de plomberie codeql database run-queries et codeql database interpret-results.
Vous pouvez également exécuter des requêtes qui ne répondent pas aux exigences en matière d’interprétation en tant qu’alertes de code source. Pour ce faire, utilisez :
codeql database run-queriescodeql query run
Utilisez ensuite :
codeql bqrs decode
pour convertir les résultats bruts en notation lisible.
Vous pouvez obtenir une liste complète des commandes CLI CodeQL disponibles dans le manuel de l’interface CLI CodeQL.
Utiliser un fichier SARIF avec des catégories
CodeQL prend en charge SARIF pour le partage des résultats d’analyse statique. SARIF est conçu pour représenter la sortie d’un large éventail d’outils d’analyse statique.
Vous devez spécifier une catégorie lors de l’utilisation de la sortie SARIF pour l’analyse CodeQL. Les catégories peuvent distinguer plusieurs analyses effectuées sur le même référentiel de validation et sur différentes langues ou dans différentes parties du code. Toutefois, les fichiers SARIF avec la même catégorie se remplacent mutuellement.
Vous pouvez analyser chaque fichier de sortie SARIF à l’aide de CodeQL pour analyser différents langages dans la même base de code lorsque la valeur de catégorie est cohérente entre les exécutions d’analyse. Nous vous recommandons d’utiliser la langue analysée en tant qu’identificateur pour la catégorie.
Par exemple, la valeur de catégorie apparaît (avec une barre oblique de fin ajoutée si elle n’est pas déjà présente) comme suit :
-
<run>.automationIddans SARIF v1 -
<run>.automationLogicalIddans SARIF v2 -
<run>.automationDetails.iddans SARIF v2.1.0
Publier les résultats SARIF sur GitHub
Une fois la base de données prête, vous pouvez l’interroger de manière interactive. Vous pouvez également exécuter une suite de requêtes pour générer un ensemble de résultats au format SARIF et charger les résultats dans un référentiel cible sur GitHub.com :
codeql github upload-results \
--sarif=<file> \
[--github-auth-stdin] \
[--github-url=<url>] \
[--repository=<repository-name>] \
[--ref=<ref>] \
[--commit=<commit>] \
[--checkout-path=<path>] \
<options>...
Pour charger les résultats sur GitHub, assurez-vous que chaque serveur d’intégration continue (CI) dispose d’une application GitHub ou d’un jeton d’accès personnel pour que l’interface CLI CodeQL soit utilisée. Vous devez utiliser un jeton d’accès ou une application GitHub avec l’autorisation d’écriture security_events .
Vous pouvez potentiellement autoriser l’interface CLI CodeQL à utiliser le même jeton si les serveurs CI utilisent déjà un jeton avec cette étendue pour extraire les référentiels à partir de GitHub. Sinon, créez un nouveau jeton avec l'autorisation d'écriture security_events et ajoutez ce jeton au stockage des secrets du système CI.
En guise de bonne pratique de sécurité, utilisez l’indicateur --github-auth-stdin et transmettez le jeton à la commande via une entrée standard.
Charger les résultats SARIF
Pour que l’analyse du code affiche les résultats d’un outil d’analyse statique non Microsoft dans votre dépôt GitHub, vos résultats doivent être stockés dans un fichier SARIF qui prend en charge un sous-ensemble spécifique du schéma JSON SARIF 2.1.0. Vous pouvez charger les résultats à l’aide de l’API d’analyse du code ou de l’interface CLI CodeQL.
Chaque fois que vous chargez les résultats d’une nouvelle analyse de code, CodeQL traite les résultats et ajoute des alertes au référentiel. Pour éviter les alertes en double pour le même problème, l’analyse du code utilise la propriété SARIF partialFingerprints pour faire correspondre les résultats entre les exécutions afin qu’elles n’apparaissent qu’une seule fois dans la dernière exécution de la branche sélectionnée.
L’élimination des doublons permet de faire correspondre les alertes à la ligne de code correcte lorsque les fichiers sont modifiés.
L’ID de règle d’un résultat doit être identique entre les analyses. Les données d’empreinte digitale sont automatiquement incluses dans les fichiers SARIF créés via le flux de travail d’analyse CodeQL ou l’exécuteur CodeQL.
Les spécifications SARIF utilisent le nom de propriété JSON partialFingerprints, qui est un dictionnaire reliant les types d’empreintes digitales nommés à leur empreinte digitale respective. Cette propriété contient, au minimum, une valeur pour primaryLocationLineHashlaquelle fournit une empreinte digitale basée sur le contexte de l’emplacement principal.
GitHub tente de remplir le partialFingerprints champ à partir des fichiers sources si vous chargez un fichier SARIF à l’aide de l’action upload-sarif et que ces données sont manquantes.
En outre, si vous chargez un fichier SARIF sans données d’empreinte digitale à l’aide du point de terminaison de l’API /code-scanning/sarifs , les utilisateurs peuvent voir des alertes en double lorsque les alertes d’analyse du code sont traitées et affichées.
Pour éviter les alertes en double lors de l’utilisation d’outils d’analyse statique, calculez les données d’empreinte digitale et remplissez la partialFingerprints propriété avant de charger le fichier SARIF. Un point de départ utile consiste à utiliser le même script que l’action upload-sarif .