Campagne internationale pour la sécurité informatique : audit automatique des projets

Campagne internationale pour la sécurité informatique, partie 2

La partie 1 traitait des dépendances logicielles et des failles officielles.

Plutôt que d’attendre la découverte d’une faille (et sa divulgation), il est quand même plus sage d’auditer le code que l’on utilise. Pouvoir lire le code est quand même une des bases de l’open source.

Un humain aura du mal à lire tout le code qu’il utilise (puis les modifications), et il aura donc besoin de l’aide de robots.

Imaginer une lecture exhaustive est utopique, mais ça ne veut pas dire qu’il ne faut pas essayer.

La Dent parachée

Quantifier la qualité du code

Il est difficile d’auditer en continu le code que l’on écrit, le moindre mal étant la revue de code dans les demandes de fusion, mais il n’est pas humainement possible d’auditer tout le code open source utilisé, et encore moins de recommencer à chaque mise à jour.

L’idée est de confier cette tâche à des robots, comme assistant, et de manière autonome.

Typage

Le typage fort (tellement pratique pour la complétion dans les IDEs) s’avère être aussi très utile pour faire de l’analyse statique (ou même dynamique) de code. Le typage fort, longtemps méprisé par les langages de scripting, est maintenant partout : Javascript était tellement mou qu’il a fallu crée TypeScript (mais il y a maintenant la proposition d’intégrer le typage dans Ecmascript), Python a son typing et même Ruby, pourtant fan de monkey patching, a son sorbet pour gérer le typage.

Le typage permet d’avoir des informations sur l’intention du développeur, indépendamment de l’implémentation qu’il propose. Ce serait dommage de s’en passer.

Analyse statique

L’analyse statique de code existe depuis longtemps, et ça a même été une des fiertés de Java : les vraies professionnelles ont plein de tableaux de bord avec des graphiques qui vont en s’améliorant (sauf la consommation de mémoire, mais ce n’est qu’un détail).

Quand l’analyse touche la sécurité, on parle de SAST, Static Application Security Testing.

SonarQube regroupe divers outils d’analyse de codes pour créer de chouettes tableaux de bord dont est friand le management moderne.

Règles

La recherche d’erreur récurrente fait maintenant partie des outils de développement des différents langages. Ces outils mélangent souvent les règles de formatage, les remarques blessantes sur l’absence de commentaires, de vraies obligations de bonnes pratiques dans le code. Les outils sont souvent dispersés, et utilisés via un métaoutil “pour les gouverner tous” :

Pour aller plus loin que cette sélection, allez jeter un œil sur analysis-tools.dev que l’on pourrait renommer “awesome static analysis”.

Ces outils sont souvent utilisés via les IDE, mais ils peuvent se brancher dans l’intégration continue pour avoir un résultat systématique et homogène. L’intégration la plus brutale est de refuser le commit, puis de vautrer la CI, le plus diplomate étant de commenter la demande de fusion (et de bloquer cette demande).

Github intègre les résultats d’analyse de code via le format SARIF, crée par Microsoft. Le format, basé sur JSON, est normalisé par OASIS, et utilisé pas différents outils.

Gitlab a réfléchi pendant 4 ans à son intégration, pour finalement proposer une moulinette qui transforme un rapport SARIF vers leur format maison, implémenté dans leur projet report sous licence MIT (dans sarif.go pour être précis).

Les outils de lint fournissent des listes de règles qui se pensent universelles et on peut les désactiver une à une (attention, c’est une lourde responsabilité).

Les règles génériques constituent une base, elles sont écrites et recommandées par des entités de renoms, ce qui permet d’éviter de longs ergotages sur la pertinence de tel ou tel point.

La découverte d’une nouvelle faille ou soucis de performance peut être généralisé sous la forme d’un motif, que l’on doit pouvoir rechercher autre part.

Chaque application/groupe/entreprise est unique, et il y aura des motifs récurrents en son sein : des bonnes pratiques, et des erreurs. Ici aussi, des outils doivent permettre de travailler avec des motifs maison.

Idéalement, il faudrait pouvoir corriger les problèmes remontés par les outils d’analyses, à défaut, il est possible de se contenter d’une note, et attendre les effets de la pression sociale pour que ça s’améliore (d’ailleurs, ce blog a fièrement un A+ pour son SSL).

Recherche

La recherche “full text” classique, naïve en fait, est “courte des pattes” pour faire des recherches dans de grandes quantités de codes. La normalisation des mots (comme la lemmatisation) n’est pas pertinente pour du code, et c’est surtout le débit de modification sature les index inversés.

Github a utilisé Solr puis Elasticsearch, qui n’a pas pu suivre la croissance du nombre de projets et de modifications. Au sommet, Github a utilisé un cluster Elasticsearch de 162 nœuds, 5184 vCPUs, 40To de RAM, 1.25Po de disques, une moyenne de 200 requêtes par seconde, sur 53 milliards de fichiers. Des dimensions mythologiques.

Expression régulière

grep est l’ami du développeur, enfin, ripgrep de nos jours. Ils lisent quantité de fichiers pour y rechercher des motifs, ce qui ne va pas bien fonctionner sur de grandes quantités de textes et beaucoup d’utilisateurs.

Google explique comment combiner indexation et expressions régulières : c’est ainsi que fonctionnait (fonctionne encore?) leur CodeSearch interne. C’est à priori ce même CodeSearch qui est mis à disposition pour rechercher dans ses projets open source.

La recette secrète de CodeSearch est de pouvoir appliquer des expressions régulières (enfin, un sous-ensemble qui garantit un temps de réponse raisonnable, Re2) sur des quantités gargantuesques de code, indexé par trigrammes. On commence par extraire les trigrammes de l’expression de recherche (tout ce qui n’est pas signe cabalistique), ce qui permet de faire une première sélection de documents contenant ces trigrammes, et de ne lancer la couteuse expression régulière que dans ce sous-ensemble.

Russ Cox (cofondateur de Go et “ingénieur distingué” chez Google) explique précisément le principe de la recherche par expression régulière sur des index avec du vieux code archivé comme démonstration.

Sourcegraph maintient Zoekt, projet initié par Google, qui implémente (et cite) l’article regexp/trigram, pour des quantités raisonnables de code (2Go), en utilisant un simple btree pour le stockage. Attention à la taille des index, qui peut atteindre 3 fois la taille du code.

Ling Zang, doctorante au sein du Gray Systems Lab propose une implémentation contemporaine en C++ : Blare (BLAzing fast REgex) avec le white paper qui va bien.

La recherche de Github est basée sur l’article de Cox, mais avec un pinaillage sur la taille des ngrams (les sparse-ngrams), car le code contient trop de mots trop courts (for, if, not…). Oui, leur explication technique sur les ngrams clairsemés est nébuleuse.

Arbre symbolique abstrait

Le code n’est pas de la prose.

Quand on cherche un élément, on ne veut pas mélanger les commentaires, fonction, argument ou autres informations typées. De manière similaire, les commentaires (annotations, types…) sont attachés à un élément.

Pour indexer du code, il est pertinent de commencer par le parser, pour créer un AST (Abstract Symbolic Tree), qui lui, sera indexé.

Chaque langage civilisé a dans sa bibliothèque standard ce qu’il faut pour lire sa propre syntaxe. Ils existent de multiples approches pour parser des formats structurés, et le domaine évolue au fil du temps, et de nouveaux outils apparaissent régulièrement, l’hégémonie de lex/yacc n’est plus.

Tree-sitter

L’ambitieux Tree-Sitter se positionne comme le générateur universelle de parsers :

  • gestion d’un nombre farfelu de formats avec ses parsers (137 cités dans la doc)
  • des bindings dans 19 langages
  • tree-sitter sait créer une bibliothèque partagée (un .so, quoi) à partir d’une grammaire, et la plupart des bindings utiliseront ce format, il y aura donc du C dans votre langage préféré. Le parser est suffisamment rapide pour reparser le buffer d’un éditeur de texte à chaque clic d’une touche.
  • les s-expressions (les listes imbriquées de Lisp) sont utilisées à différents endroits : comme format de sortie, comme prédicat pour filtrer le parcours d’un arbre.

La navigation de code de Github utilise tree-sitter, ainsi que son moteur de recherche.

Sourcegraph aussi aime bien tree-sitter:

Bien d’autres applications utilisent tree-sitter, dont certaines seront évoquées plus bas dans l’article.

Datalog

Ni les moteurs de recherche full text, ni les bases de données relationnelles ne sont adaptés pour indexer des projets avec leur code.

Il faut donc étendre la recherche dans les modèles exotiques de bases de données. Avant l’invention de la base de données relationnelle, il y a eu les bases de données déductives, inspirées par Prolog.

Le concept de ces bases est proche de celui des bases orientées graphes, les données sont modélisées par des triplets : 2 nœuds liés par une relation.

Le modèle déductif a Datalog comme langage de requête tout comme le modèle relationnel a SQL.

Embeded code

Rien à voir avec les ordinateurs trop petits de l’embarqué, embeded désigne la technique de représenter les mots (enfin, des tokens) sous forme de vecteurs, ainsi que les documents, permettant ainsi de faire des recherches de proximité (en calculant la distance entre deux vecteurs). C’est la base des LLM, les Larges Languages Model.

Vous reprendrez bien un louche d’IA?

Github a documenté ses travaux pour proposer de la recherche sémantique de code (avec les liens vers les papiers de recherche), pour proposer l’année suivante (2019) un concours sur le même thème avec encore des citations de publications sympathiques comme embarquement de texte et de code par préentrainement contrasté.

Tout ça a fini par devenir Copilot, leur magicien de l’autocomplétion de code.

Les LLM peuvent rendre de grands services, mais pas (encore?) de chasser le bogue.

Sourcegraph

Sourcegraph essaye de se positionner comme le leader de la recherche de code, bien que seule une petite portion de son code soit libre. On peut installer le service sur son poste, pour avoir la première dose gratuite.

Gitlab peut utiliser Sourcegraph pour naviguer dans le code, depuis l’interface web, et effectuer des recherches, ça en fait presque un standard de fait.

La recherche de Sourcegraph s’appuie sur un salmigondis de bases de données (du trigramme/regexp avec Zoepkt, du vecteur avec Qdrant, du relationnel avec Postgres et Sqlite). On peut ajouter à ça un peu de full text (j’ai vu passer des bibliothèques de lemmatisation), et de l’IA avec son Cody.

Sourcegraph a essayé d’étendre Langserver (le LSP des IDE) avec LSIF pour normaliser l’indexation de code, pour finalement abandonner et déprécier ses contributions, pour se rediriger vers son SCIP, qui crée des index que son serveur peut manger. Un outil permet de dumper les index en JSON, pour l’utiliser en dehors de Sourcegraph.

Sourcegraph propose des outils pour rechercher puis patcher du code, mais l’implémentation actuelle est brutale : il faut écrire un bout de bash qui sera lancé dans un conteneur sur toutes les occurrences de la recherche.

L’écosystème Sourcegraph fournit des briques intéressantes, mais rien de bouleversant pour aller à la chasse au mauvais code.

CodeQL

Microsoft, en rachetant Github s’est retrouvé, de fait, comme responsable d’une bonne partie des sources du logiciel libre. C’est assez cocasse quand on connait la position de Steve Ballmer quelques décennies plus tôt.

Gardien des sources, des bugs mais aussi de la flemme de tous les jours.

Microsoft est bien au courant de la valeur d’une CVE, il a donc ajouté des outils pour boucher le code troué.

Dependabot (dont certaines briques sont libres) est un robot qui crée des demandes de fusion quand la correction est triviale (mettre à jour une dépendance officiellement trouée) ou envoie des alertes s’il faut prendre une décision pour corriger.

Personnellement, je pense que Dependabot est aussi un complot pour faire regretter d’avoir choisi Nodejs, qui avec sa palanquée de microbibliothèques, va statistiquement trouver au moins une faille par semaine, quelque part au fond d’une branche du graphe de vos dépendances.

La vraie puissance de Dependabot n’est pas juste de lire et patcher les listes de dépendances figées, mais la recherche de motif de code avec CodeQL en fournissant un lot de règles CodeQL, qui ont la gentillesse d’être sous licence MIT.

CodeQL a fait le pari de Datalog pour écrire des requêtes. Choisir un langage ésotérique, inventé avant le Minitel, est audacieux. Charge à Github d’utiliser correctement ses index, le cache et la parallélisation pour exécuter efficacement ces recherches déclenchées par la CI.

Règles métiers

Il existe divers outils pour décrire des patchs génériques à appliquer sur quantités de projets (ou dans un bon gros monorepo).

Ils sont souvent conçus pour créer des patchs jetables, et se contentent pour la plupart d’une utilisation en ligne de commande, quelques-uns proposent une intégration plus fine, avec des consoles interactives ou de l’intégration continue.

Agnostique

Semgrep, écrit en OCaml qui aime tant le parsing, propose d’appliquer des règles décrites en YAML. Il y a une version libre en LGPL, et pour avoir le parsing multifichier et d’autres bonus, il faut passer à la caisse. On notera la possibilité d’obtenir une réponse en SARIF, gage d’intégration avec d’autres produits.

Comby, encore en OCaml, et sous licence Apache 2, se positionne comme roi du one liner, pour remplacer le traditionnel grep+sed. Il positionne comme outil de patch universel, plutôt que comme linter.

Infer, de chez Facebook, et toujours en OCaml, se contente de chasser le bogue en C/C++/Objective-C et Java, mais il est utilisé par de grands noms.

ast-grep, écrit en Rust avec du tree-sitter, se positionne comme grep du code, alors qu’il permet aussi de remplacer, en plus de rechercher des motifs.

Python

pyparsing a longtemps été la brique de base pour lire du code (pour ensuite râler ou proposer une correction). Yelp a utilisé Undebt pour faire de la chasse au code déprécié. La chasse a été bonne, mais le projet a été archivé.

Plus classique, issu de l’équipe sécurité d’OpenStack, puis maintenu par la Python Code Quality Authority, bandit parcours l’AST d’un fichier Python pour y trouver des mauvaises pratiques. Bandit propose une liste de règles, mais il est aisé d’en créer de nouvelles. Une règle s’abonne à un évènement (souvent un appel de fonction), qui déclenchera une vérification, pouvant renvoyer un Incident. Les règles fournies sont simples (voir naïves), bandit ne permet pas une grande expressivité.

RedBaron propose de naviguer dans l’arbre abstrait de code python, de manière interactive, avec la possibilité de modifier des éléments, et de revenir à un code source avec juste ce qu’il faut de modifications. RedBaron a tout ce qu’il faut pour créer des patchs génériques, mais pour l’instant, il n’a pas exploré la recherche de “mauvais motif” avec une correction; il est plus conçu pour créer du patch du sur mesure, pas du prêt-à-porter.

Facebook a sa productive équipe OCaml et propose Pyre, parfois appelé Pysa (pour PYre Security Audit). Pyre file la métaphore des hydrologues, avec des source qui sont tainted (des inputs confiés à un utilisateur) et des sink (des zones à surveiller, comme l’exécution d’une commande) qui seront teintés ou non. Pyre s’appuie sur le typage de Python.

Golang

Gopatch reprend l’astuce de Coccinelle, et utilise l’antique syntaxe de patch de diff pour décrire des correctifs.

Java

Java propose des outils étranges et grandiloquents, comme OpenRewrite et Rascal. Les patchs, c’est pour les petits bras, Java mérite de la méta programmation.

Fuzzing

L’analyse statique, c’est bien, mais l’analyse dynamique peut apporter des informations complémentaires.

Le fuzzing est une des stratégies possibles d’analyse dynamique, il en existe d’autres.

L’idée du fuzzing est de jeter du pâté dans le ventilateur (“spam hits the fan” en VO).

  • le code est instrumenté, depuis l’intérieur en le recompilant (ou une technique équivalente pour du script), ou depuis l’extérieur via le kernel (souvent via QEMU). Des désinfecteurs (sanitizer en VO) vont surveiller des comportements anormaux, tant sur le plan logiciel que matériel.
  • On désigne un point d’entrée (une fonction)
  • Il est possible de modifier les appels système (avec preeny par exemple) pour que le code ait un fonctionnement plus déterministe, voire même de remplacer un échange réseau par une discussion sur STDIN/STDOUT, les fuzzer préfèrent appeler une fonction, plutôt que de démarrer un serveur complet.
  • À partir d’exemples, ou de rien, un corpus va être créé, en essayant de maximiser la couverture de code. Ce corpus devient un artéfact et sera réutilisé.
  • Le point d’entrée est testé avec des données issues du corpus, auquel on applique des mutations. En cas d’erreurs (ce que l’on cherche), le test sera réduit (shrinking en VO) pour n’avoir qu’un changement mineur entre une valeur qui passe, et celle qui ne passe pas.
  • Historiquement, les données en entrée sont un gros binaire, ce qui correspond aux cas classiques de la lecture d’un fichier ou d’un échange réseau. Il est aussi possible de définir des générateurs (ou des grammaires), pour fournir du contenu typé.

Générer du contenu complètement aléatoire sera peu efficace, il existe différentes stratégies faisant référence à la génétique, avec des notions de mutations, qui ne sont pas retenues si elles ne génèrent pas d’erreur ou n’améliorent pas la couverture de code.

Bien sûr, il est possible d’utiliser de l’IA pour fuzzer encore plus fort.

Les fuzzers subissent une forte sélection darwinienne, les bonnes idées sont conservées pour la génération suivante, les autres idées finissent en fossile.

AFL, considéré comme standard de fait, mute en AFL++ pour finalement donner HonggFuzz, et je vous fais grâce de l’explosion cambrienne listée dans Awesome-AFL.

Gitlab propose de s’interfacer avec du fuzzing, mais dans sa version ultimate (la plus chère), en vous laissant l’approche DIY dans votre très classique CI, avec des exemples dans différents langages

Oss-fuzz

Google a testé ses outils de fuzzing créés pour Chrome sur des logiciels opens sources, et a trouvé beaucoup d’erreurs, certaines avec des implications au niveau sécurité. Comme les erreurs découvertes se chiffrent en milliers, Google s’est associé à l’OSSF pour créer oss-fuzz, un service de fuzzing continue dédié à l’open source.

Attention, on entre dans le monde merveilleux du code Google libéré : il faut s’attendre à des acronymes aléatoires de 4 lettres, des projets remplaçant le précédent (parfois 3 ou 4 remplacements) et des README partiellement à jour.

L’outil, ClusterFuzz (avec sa variante allégée, ClusterFuzzLite), est libre (licence Apache 2). Il est conseillé pour fuzzer du code non libre (sur une instance interne).

ClusterFuzz gère différent fuzzers historiques, et globalement, les fuzzers spécifiques se rapprochent de LibFuzzer dans leurs comportements.

On notera que l’attaque sur OpenSSH via xz a commencé par désactiver le fuzzing d’Oss-fuzz sur la partie qu’il a ensuite contaminée : témoignage de crainte face à Oss-fuzz. OK, Oss-fuzz n’a pas découvert cette faille, car le cambrioleur a coupé l’alarme de la salle du coffre, mais il a pris la peine de le faire.

Sanitizers

Les désinfecteurs vont surveiller ce qu’il se passe au niveau de l’allocation de la mémoire et des threads.

Les désinfecteurs peuvent être utilisés en instrumentant le code compilé (avec LLVM et même gcc), puis le code est exécuté avec un malloc spécifique.

Le kernel Linux est maintenant outillé pour surveiller ce qu’il se passe au niveau de la mémoire et des threads (qui font quand même partie de sa responsabilité). Ces modules ont été intégrés au fur et à mesure dans le kernel : ce sont les K*SAN. Ils ne gèreront pas forcément toutes les architectures, et pour certains, il faudra utiliser un noyau compilé avec LLVM plutôt que gcc (ne le dites pas à Stallman). Les kernels instrumentés ne visent pas la production, mais une VM temporaire pour secouer du code dans une cuve pleine de capteurs.

Fuzzers

D’autres langages disposent d’une instrumentation compatible avec oss-fuzz : C/C++, Rust, Go, Python, Java/JVM, Swift et JavaScript (dont typescript), pour les autres, ce sera uniquement via LLVM.

Comme grand absent, on notera Ruby, tellement fière de la liberté de sa syntaxe et assument le monkey patching ne doit pas aider les fuzzers à se concentrer. Mais trêve de mauvaise langue, le projet Ruzzy reprend pour Ruby l’approche d’Atheris (le binding de libFuzzer pour Python).

Mutation de test

Le principe de mutation de données peut être utilisé sur de très classiques tests unitaires pour répondre à la question “Qui test les test?” (ou Quis custodiet ipsos custodes? pour les fans de péplums en VO).

La mutation de test est une idée de Richard Lipton, alors étudiant, en 1971, ce qui ne nous rajeunit pas (c’est plus vieux que vi). Il a attendu 1980 pour donner le sujet à un thésard en philosophie, Thimoty Alan Budd : Mutation Analysys of Program Test Data.

L’idée est de cochonner votre base de code avec des erreurs, et de voir si les tests passent encore. Les mutations correspondent à des fautes que ferait un développeur distrait (remplacer un > par une >=, un && par un ||, mettre un not devant un booléen, ce genre d’étourderies).

Le logiciel va créer un rapport et coller une note : le ratio entre le nombre de mutants attrapés et le nombre de mutants crée.

Si votre outil est un poil finaud, il va commencer par lancer les tests un à un, et noter la couverture de code à chaque fois. Ainsi, il ne va muter que le code couvert par les tests, et ne lancera que les tests concernés.

Les mutations de tests ne déclenchent pas un enthousiasme fou sur Github, mais il y a largement de quoi farfouiller.

Le leader de ce domaine est Java, avec Pitest, mais il existe aussi mutmut pour Python, Mutant.rs ou go-Gremlins.

Quantifier la qualité d’un projet

Nous venons de voir qu’il était possible de lancer des hordes de robots pour auditer le code de logiciels open source que vous souhaiteriez utiliser. Mais la qualité du code n’est qu’une partie de la qualité globale du projet.

L’exemple de l’attaque sur xz est frappant, sur un des miroirs (le projet officiel est banni), on voit que la qualité du code n’est qu’un détail :le payload de l’attaque, présenté comme une fixture, est chiffré, et le déclencheur n’est pas versionné dans le git, mais juste dans la tarball de la release. La vraie information, ce sont les contributions de xz, il y a UN mainteneur, Larzhu, une poignée de gens faisant quelques propositions de modifications, et un sauveur, l’infâme JiaT75, qui vient filer un coup de main.

ScoreCard.dev

L’OpenSource Security Foundation, a créé une moulinette, [ScoreCard] (https://scorecard.dev/) qui donne une note aux projets open source, dans une approche très scolaire, pour pousser les projets à suivre de bonnes pratiques.

Les différentes vérifications sont plus que légitimes, mais aussi des pousse-au-crime pour utiliser l’écosystème Github de bout en bout.

J’espère que ça va piailler, et qu’il va y avoir du lobbying pour que des services alternatifs (des gros services en ligne, ou des petits autohébergés) soient pris en compte pour le calcul du ScoreCard.

Deps.dev

Google, avec deps.dev pousse l’idée de ScoreCard.dev un cran plus loin en recyclant leur PageRank. la note finale d’un paquet est calculée en fonction du graphe de dépendance tirée par ce paquet. Un paquet avec une mauvaise note va faire baisser la note de ceux qui l’importent, directement ou indirectement.

Autre avantage de deps.dev est qu’il propose un chouette site web avec un moteur de recherche alors que scorecard.dev vous propose de télécharger des CSV de dimensions cyclopéennes.

Criticité d’un projet

L’OSSF a une autre moulinette (en bêta) pour donner une note à l’importance d’un paquet au sein de l’écosystème open source : le Criticality Score.

Il utilise le ScoreCard, l’âge, le nombre de projets l’utilisant, le nombre de contributeurs (individuels et organisations), le débit de contributions et de tickets, ce genre de choses.

Ce genre de classement devrait permettre de trouver les maillons faibles de la chaine d’approvisionnement, et justifier des financements pour des développeurs quelconques du Nebraska, comme le recommande XKCD.

Dependency by XKCD

blogroll

social