Tech Hub

@ Solution Architecture Works

Sécurité Avancée sur GitHub – Partie 2 sur 2

Exécuter CodeQL sur une base de données

Temps estimé :11 minutes 55 vues

Une fois votre code extrait dans une base de données, vous pouvez l’analyser à l’aide de requêtes CodeQL. Les requêtes par défaut sont écrites et maintenues par des experts GitHub, des chercheurs en sécurité et des contributeurs de la communauté. Vous pouvez également écrire vos propres requêtes.

Les requêtes CodeQL sont utilisées dans l’analyse de code pour détecter des problèmes dans votre code source, y compris des vulnérabilités potentielles. Vous pouvez aussi écrire des requêtes personnalisées pour identifier des problèmes spécifiques à chaque langage utilisé dans votre projet.

Il existe deux types importants de requêtes :

  • Requêtes d’alerte : elles mettent en évidence des problèmes à des emplacements précis dans votre code.
  • Requêtes de chemin (path queries) : elles décrivent le flux d’informations entre une source et une cible (sink) dans votre code.

Requête CodeQL simple

La structure de base d’une requête CodeQL est enregistrée dans un fichier avec l’extension .ql et contient une clause select. 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 ... */

Métadonnées de requête

L’utilisation de CodeQL avec l’analyse de code (code scanning) transforme les résultats de manière à mettre en évidence les problèmes potentiels que les requêtes sont conçues pour détecter. Les requêtes contiennent des propriétés de métadonnées qui indiquent comment interpréter les résultats. Les métadonnées de requête permettent de :

  • Identifier vos requêtes personnalisées lorsque vous les ajoutez à votre dépôt GitHub.
  • Fournir des informations sur l’objectif de la requête.

Les métadonnées peuvent inclure :

  • une description de la requête,
  • un identifiant unique,
  • le type de problème détecté (alerte ou chemin).

Les métadonnées spécifient également comment interpréter et afficher les résultats de la requête.

GitHub propose un guide de style recommandé pour les métadonnées de requête. Vous pouvez le consulter dans la documentation de CodeQL.

Voici un exemple de métadonnées pour l’une des requêtes standard en Java :

(L’exemple n’était pas inclus dans votre message. Souhaitez-vous que je vous en montre un ?)

CodeQL n’interprète pas les requêtes sans métadonnées

Les requêtes qui ne contiennent pas de métadonnées ne sont pas interprétées par CodeQL. Les résultats sont alors affichés sous forme de tableau, sans mise en évidence dans le code source.

Syntaxe QL

QL est un langage de requête déclaratif et orienté objet. Il est optimisé pour permettre une analyse efficace des structures de données hiérarchiques, en particulier les bases de données représentant des artefacts logiciels.

La syntaxe de QL est similaire à celle de SQL, mais sa sémantique repose sur Datalog, un langage de programmation logique déclaratif souvent utilisé comme langage de requête. Étant un langage logique, toutes les opérations en QL sont des opérations logiques. QL hérite également des prédicats récursifs de Datalog et ajoute la prise en charge des agrégats pour rendre les requêtes complexes plus concises et simples.

Le langage QL est composé de formules logiques. Il utilise des connecteurs logiques courants comme andornot, ainsi que des quantificateurs comme forall et exists. Grâce aux prédicats récursifs, vous pouvez écrire des requêtes récursives complexes en utilisant la syntaxe de base de QL et des agrégats comme countsum et average.

Pour plus d’informations sur le langage QL, consultez la documentation de CodeQL.

Requêtes de chemin (Path Queries)

Le flux d’information dans un programme est crucial. Des données apparemment inoffensives peuvent circuler de manière inattendue et être utilisées de façon malveillante.

Les requêtes de chemin permettent de visualiser ce flux d’information dans une base de code. Une requête peut suivre le chemin que prennent les données depuis leurs points de départ possibles (sources) jusqu’à leurs points d’arrivée possibles (sinks). Pour modéliser ces chemins, votre requête doit définir :

  • une source,
  • un sink,
  • les étapes de flux de données qui les relient.

Le moyen le plus simple de commencer à écrire une requête de chemin est d’utiliser une requête existante comme modèle. Pour obtenir ces requêtes selon les langages pris en charge, consultez la documentation de CodeQL.

Votre requête de chemin doit inclure certaines métadonnées, des prédicats de requête, et une structure d’instruction select. La plupart des requêtes de chemin intégrées dans CodeQL suivent une structure de base, qui dépend de la manière dont CodeQL modélise le langage analysé.

Voici un exemple de modèle de requête de chemin :

(L’exemple de code suit généralement ce paragraphe dans la documentation.)

/**
 * ...
 * @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 or 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 :

  • MyConfiguration est un module contenant les prédicats qui définissent comment les données circulent entre la source et le puits (sink).
  • Flow est le résultat du calcul de flux de données basé sur MyConfiguration.
  • Flow::PathGraph est le module de graphe de flux de données à importer pour inclure les explications de chemin dans la requête.
  • source et sink sont des nœuds du graphe définis dans la configuration, et Flow::PathNode est leur type.
  • DataFlow::Global<...> est une invocation du flux de données. Vous pouvez utiliser TaintTracking::Global<...> à la place pour inclure un ensemble par défaut d’étapes de contamination (taint steps).

Comment écrire une requête de chemin

Votre requête doit calculer un graphe de chemin pour générer des explications de chemin. Pour cela, vous devez définir un prédicat de requête appelé edges. Un prédicat de requête est un prédicat non-membre avec une annotation @query. Cette annotation retourne tous les tuples que le prédicat évalue.

Le prédicat edges définit les relations d’arêtes du graphe que vous calculez. Il est utilisé pour déterminer les chemins associés à chaque résultat généré par votre requête. Vous pouvez aussi importer un prédicat edges prédéfini depuis un module de graphe de chemin dans l’une des bibliothèques de flux de données standard.

Les bibliothèques de flux de données contiennent d’autres classes, prédicats et modules couramment utilisés pour l’analyse de flux de données, en plus du module de graphe de chemin. Elles modélisent le graphe de flux de données ou implémentent l’analyse de flux. Les bibliothèques classiques sont utilisées pour analyser les flux où les valeurs de données sont conservées à chaque étape.

Voici un exemple d’instruction pour importer le module PathGraph depuis la bibliothèque DataFlow.qll, où edges est défini : import DataFlow::PathGraph

Vous pouvez importer de nombreuses autres bibliothèques incluses avec CodeQL, y compris celles conçues pour des frameworks ou environnements spécifiques.

La classe PathNode

La classe PathNode est conçue pour implémenter l’analyse de flux de données. Elle est basée sur Node, enrichie d’un contexte d’appel (sauf pour les sinks), d’un chemin d’accès et d’une configuration. Seuls les PathNode atteignables depuis une source sont générés.

Exemple de chemin d’importation : import semmle.code.cpp.ir.dataflow.internal.DataFlowImpl

Vous pouvez aussi définir un prédicat de requête nodes, qui spécifie les nœuds du graphe de chemin pour tous les langages. Si vous définissez nodes, seuls les nœuds sélectionnés formeront des arêtes. Si vous ne le définissez pas, vous devez sélectionner tous les points d’extrémité 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 obtenez des résultats pertinents dans le contexte du code source. Les résultats sont présentés sous forme d’alertes ou de chemins au format SARIF ou autre format interprété.

Exemple de commande pour analyser une base de données CodeQL : codeql database analyze –format= –output= [–threads=] [–ram=] … —  …
Cette commande combine les effets de codeql database run-queries et codeql database interpret-results.

Vous pouvez aussi exécuter des requêtes qui ne produisent pas d’alertes interprétables. Dans ce cas, utilisez : codeql database run-queries ou codeql query run

Puis convertissez les résultats bruts avec : codeql bqrs decode
Utiliser un fichier SARIF avec des catégories

CodeQL prend en charge SARIF pour partager les résultats d’analyse statique. SARIF permet de représenter les sorties de nombreux outils d’analyse.

Vous devez spécifier une catégorie lors de l’utilisation du format SARIF. Cela permet de distinguer plusieurs analyses sur le même dépôt ou sur différentes parties du code. Les fichiers SARIF avec la même catégorie se remplacent mutuellement.

Il est recommandé d’utiliser le langage analysé comme identifiant de catégorie.

Exemple : la valeur de catégorie apparaît dans :

  • run.automationId (SARIF v1),
  • run.automationLogicalId (SARIF v2),
  • run.automationDetails.id (SARIF v2.1.0).

Publier les résultats SARIF sur GitHub

Une fois la base prête, vous pouvez interroger la base ou exécuter une suite de requêtes pour générer un fichier SARIF, puis l’envoyer sur GitHub : codeql github upload-results –sarif= [–github-auth-stdin] [–github-url=] [–repository=] [–ref=] [–commit=] [–checkout-path=] …
Pour cela, chaque serveur CI doit disposer d’un GitHub App ou d’un jeton d’accès personnel avec la permission security_events: write.

Téléverser les résultats SARIF

Pour que GitHub affiche les résultats d’un outil d’analyse statique non-Microsoft, les résultats doivent être dans un fichier SARIF conforme au schéma JSON SARIF 2.1.0.

À chaque téléversement, CodeQL traite les résultats et ajoute des alertes. Pour éviter les doublons, CodeQL utilise la propriété partialFingerprints pour faire correspondre les résultats entre les exécutions.

Le champ partialFingerprints contient au minimum une valeur pour primaryLocationLineHash, qui fournit une empreinte basée sur le contexte de la ligne principale.

Si vous téléversez un fichier SARIF sans empreintes, GitHub tente de les générer automatiquement. Mais cela peut entraîner des alertes dupliquées.

Bonnes pratiques : calculez les empreintes et remplissez partialFingerprints avant le téléversement. Vous pouvez vous baser sur le script utilisé par l’action upload-sarif.

Pour éviter les alertes en double lors de l’utilisation d’outils d’analyse statique,

Calculez les empreintes numériques (fingerprints) et renseignez la propriété partialFingerprints avant de téléverser le fichier SARIF.

Un bon point de départ consiste à utiliser le même script que celui utilisé par l’action upload-sarif.

Share this Doc

Exécuter CodeQL sur une base de données

Or copy link

CONTENTS