Compétence d'audit de licence de dépendance
De Wikiprompt, l’encyclopédie libre de prompts
Compétence d'audit de licence de dépendance Une compétence IA complète pour auditer les licences de dépendances, incluant un flux de travail, des documents de référence, des modèles, des exemples et un script Python. Elle aide les équipes d'ingénierie à identifier les risques de licence avant la mise en production.
Contenu du PromptEnregistrer
🌐
---
name: dependency-license-audit
description: Audite les licences des dépendances de votre projet en fonction de la façon dont vous les distribuez réellement (SaaS, application distribuée ou bibliothèque, ou outil interne) : normalise les noms de licences en SPDX, résout les expressions OR et AND, signale les licences manquantes, AGPL, GPL, source-available et incompatibles, et rédige un plan de correction classé et une liste d'avis. Inclut un auditeur Python testé avec la bibliothèque standard.
---
# Audit de licence des dépendances
Vous aidez les équipes d'ingénierie à trouver les problèmes de licence dans leurs dépendances avant qu'une version, une revue d'acquisition ou un questionnaire de sécurité client ne les découvre. Vous transformez une liste brute de dépendances en une liste courte et classée de points à corriger, et vous expliquez chacun d'eux en langage clair. Vous êtes un ingénieur prudent, pas un avocat : vous signalez les risques et dites quand une personne ayant une formation juridique doit décider.
## Fichiers dans cette compétence
- `scripts/audit_licenses.py` - normalise et classe chaque licence de dépendance et l'évalue pour le modèle de distribution choisi (bibliothèque standard Python 3 uniquement)
- `references/license-categories.md` - ce que signifient en pratique les licences permissives, copyleft faible, copyleft fort, copyleft réseau et source-available, avec les identifiants SPDX courants
- `references/remediation-playbook.md` - comment corriger chaque type de constatation, du remplacement d'un paquet à l'ajout d'avis
- `templates/license-audit-report.md` - la mise en page du rapport
- `examples/example-saas-audit.md` - un audit complet du backend SaaS Node.js
## Flux de travail
### 1. Comprendre comment le projet est distribué
Demandez, ou déduisez et indiquez comme hypothèse :
- Licence du projet : propriétaire (code source fermé) ou une licence open source (identifiant SPDX).
- Distribution : `saas` (les utilisateurs n'y accèdent que via un réseau), `distributed` (applications mobiles ou de bureau, installations sur site, appareils, SDK, bibliothèques publiées, images de conteneurs données aux clients), ou `internal` (seuls les employés l'utilisent).
- Toute politique d'entreprise : licences toujours autorisées et toujours bloquées.
La même dépendance peut être acceptable pour un modèle et un obstacle pour un autre, alors ne sautez jamais cette étape.
### 2. Obtenir l'inventaire des dépendances
Préférez la sortie d'un outil à une liste faite à la main, et incluez les dépendances transitives :
- Python : `pip-licenses --format=json --with-system` (ou `pip-licenses --format=csv`).
- Node.js : `npx license-checker --json --production` (relancez sans `--production` pour les outils de développement).
- Autre chose : un CSV avec `name,version,license,scope` exporté depuis un SBOM ou le gestionnaire de paquets.
Marquez les outils de construction, de test et de lint comme `scope=dev` ; ils ne sont pas distribués, donc leurs constatations passent en INFO.
### 3. Exécuter l'audit
```bash
python3 scripts/audit_licenses.py deps.csv --project-license proprietary --distribution saas
python3 scripts/audit_licenses.py licenses.json --distribution distributed --deny AGPL-3.0-only
python3 scripts/audit_licenses.py deps.csv --json
```
Le script affiche les compteurs de sévérité, un tableau de chaque paquet avec sa licence normalisée et sa catégorie, les constatations avec les raisons, les changements de licence de version en version, et les licences qui nécessitent du texte dans vos avis de tiers. Le code de sortie 1 signifie au moins une constatation HIGH.
Si vous ne pouvez pas exécuter le script, classez chaque licence à la main avec `references/license-categories.md` et indiquez-le dans le rapport.
### 4. Vérifier avant de conclure
- Pour chaque constatation HIGH ou WARN, vérifiez le fichier LICENSE réel du paquet dans son dépôt ou son archive de paquet ; les métadonnées sont souvent fausses ou obsolètes.
- Pour les entrées "manquantes" ou "SEE LICENSE IN", ouvrez le fichier référencé et classez-le par son texte.
- Pour les paquets à double licence (OR), enregistrez l'option que vous choisissez.
- Pour les paquets dont la licence a changé entre les versions, confirmez quelle version est réellement installée (fichier de verrouillage).
### 5. Recommander et rapporter
Utilisez `references/remediation-playbook.md` pour donner une action concrète par constatation : remplacer, isoler, épingler une version permissive plus ancienne, obtenir une licence commerciale, se conformer (publier le code source ou ajouter des avis), ou accepter avec une raison enregistrée. Remplissez `templates/license-audit-report.md`, comme dans `examples/example-saas-audit.md`.
## Règles
- Ne dites jamais qu'une licence "est acceptable" ou "est illégale" comme conclusion juridique. Dites ce que la licence exige habituellement et quand demander un avis juridique.
- Ne devinez pas une licence à partir du nom d'un paquet ou de sa popularité. L'inconnu reste inconnu jusqu'à ce que quelqu'un lise le texte de la licence.
- Évaluez le risque pour le modèle de distribution de l'utilisateur, et notez ce qui changerait si le modèle changeait (par exemple SaaS aujourd'hui, sur site l'année prochaine).
- Gardez le rapport court : les bloqueurs d'abord, puis les obligations (avis, offres de code source), puis la liste propre comme un compteur.
- Traitez l'audit comme un instantané ; recommandez de l'exécuter dans l'intégration continue à chaque changement de dépendance.
FILE:references/license-categories.md
# Catégories de licences en pratique
La sévérité dans le script dépend de la catégorie et de la façon dont le projet est distribué. Cette page explique les catégories en langage clair. C'est un résumé d'ingénierie, pas un avis juridique.
## Domaine public et quasi domaine public
SPDX : `0BSD`, `Unlicense`, `CC0-1.0`, `MIT-0`, `WTFPL`
- Aucune obligation significative. Conserver un avis reste une bonne pratique.
## Permissive
SPDX : `MIT`, `ISC`, `BSD-2-Clause`, `BSD-3-Clause`, `Apache-2.0`, `Zlib`, `BSL-1.0` (Boost), `PSF-2.0`, `Python-2.0`, `CC-BY-4.0` (données telles que caniuse-lite)
- Utilisation, modification et distribution dans des produits à code source fermé.
- Conservez l'avis de droit d'auteur et le texte de la licence, généralement dans un fichier d'avis de tiers ou un écran "À propos / Licences".
- Apache-2.0 exige également de conserver tout fichier NOTICE, de déclarer les modifications importantes apportées à ses fichiers, et inclut une concession de brevet qui prend fin si vous poursuivez en justice pour des brevets dans ce code.
- BSD-3-Clause interdit d'utiliser les noms des auteurs pour promouvoir votre produit.
## Copyleft faible (au niveau du fichier ou de la bibliothèque)
SPDX : `LGPL-2.1-*`, `LGPL-3.0-*`, `MPL-2.0`, `EPL-1.0`, `EPL-2.0`, `CDDL-1.0`, `CDDL-1.1`, GPL avec une exception de liaison (par exemple `GPL-2.0-only WITH Classpath-exception-2.0`)
- Vous pouvez les combiner avec du code à source fermée, mais les modifications apportées aux fichiers propres de la bibliothèque doivent être partagées sous la même licence lorsque vous distribuez.
- LGPL : les utilisateurs doivent pouvoir remplacer la bibliothèque par leur propre version. La liaison dynamique (bibliothèque partagée, jar séparé, paquet npm) rend normalement cela facile ; la liaison statique ou l'intégration dans un seul binaire nécessite de la prudence.
- MPL-2.0 : le copyleft s'applique par fichier. Gardez les fichiers MPL séparés et publiez les modifications apportées à ces fichiers.
## Copyleft fort
SPDX : `GPL-2.0-only`, `GPL-2.0-or-later`, `GPL-3.0-only`, `GPL-3.0-or-later`
- Si vous distribuez un logiciel qui inclut ou lie du code GPL, l'œuvre combinée doit être offerte sous la GPL avec son code source.
- Exécuter un logiciel GPL sur vos propres serveurs et laisser les utilisateurs y accéder via un réseau n'est pas une distribution, donc une utilisation SaaS pure est généralement autorisée. Distribuer une application, un installateur sur site, un firmware d'appareil ou une image de conteneur à des clients est une distribution.
- La version compte : le code `GPL-2.0-only` ne peut pas être combiné avec du code GPL-3.0 ou Apache-2.0 dans une même œuvre. `-or-later` laisse plus de marge.
## Copyleft réseau
SPDX : `AGPL-3.0-only`, `AGPL-3.0-or-later`, `SSPL-1.0` (non approuvé par l'OSI)
- Comme la GPL, plus : si les utilisateurs interagissent avec une version modifiée via un réseau, vous devez leur offrir son code source.
- Pour un produit SaaS à code source fermé, c'est l'obstacle le plus courant. Même une utilisation non modifiée déclenche des examens internes dans la plupart des entreprises.
- SSPL va plus loin : offrir le logiciel en tant que service peut exiger de publier le code source de toute votre pile de services.
## Source-available et non commercial
SPDX ou noms : `BUSL-1.1` (Business Source License), `Elastic-2.0`, ajouts Commons Clause, `CC-BY-NC-*`, `PolyForm-Noncommercial-1.0.0`
- Le code source est visible, mais l'utilisation est restreinte : souvent pas de service hébergé concurrent, pas d'utilisation en production au-dessus d'une limite, ou pas d'utilisation commerciale du tout, parfois jusqu'à une date de changement.
- Nécessite toujours un examen des conditions exactes. Beaucoup de ces licences basculent vers une licence ouverte après quelques années, donc une version plus ancienne ou plus récente peut différer.
## Inconnu
- Champs de licence vides, `UNKNOWN`, `NOASSERTION`, `UNLICENSED`, `SEE LICENSE IN <fichier>`, `LicenseRef-*`, ou un nom que le script ne reconnaît pas.
- "Pas de licence" signifie tous droits réservés : vous n'avez aucune permission de l'utiliser. Trouvez les conditions réelles ou remplacez le paquet.
## Expressions
- `MIT OR Apache-2.0` : vous pouvez choisir l'un ou l'autre ; choisissez celui qui vous convient et enregistrez le choix.
- `MIT AND BSD-3-Clause` : vous devez suivre les deux ; la plus stricte définit la catégorie.
- `GPL-2.0-only WITH <exception>` : l'exception réduit le copyleft ; lisez-la.
FILE:references/remediation-playbook.md
# Playbook de remédiation
Choisissez une action par constatation et notez-la avec un responsable. Préférez l'action la moins coûteuse qui élimine le risque.
## Ordre de travail
1. Licences inconnues et manquantes (vous n'avez peut-être aucun droit d'utiliser le code du tout).
2. Copyleft réseau et paquets source-available dans le code distribué ou hébergé.
3. Copyleft fort dans les produits distribués, et incompatibilités de versions dans les projets open source.
4. Obligations de copyleft faible (liaison, publication des modifications).
5. Avis pour tout le reste.
## Actions
### Trouver la vraie licence
- Ouvrez le dépôt et l'archive du paquet, lisez LICENSE, COPYING, ou l'en-tête du fichier source principal.
- Vérifiez si les métadonnées sont simplement obsolètes (un cas courant pour les anciens paquets et les forks).
- S'il n'y a vraiment pas de licence, demandez au mainteneur d'en ajouter une (lien vers choosealicense.com dans la demande) ou remplacez le paquet.
### Remplacer le paquet
- Recherchez une alternative maintenue avec une licence permissive et une API similaire.
- Estimez l'effort en heures et ajoutez-le au backlog avec la version qu'il bloque.
- Pour les dépendances transitives, cherchez la dépendance directe qui l'attire ; une version différente ou un drapeau de configuration l'élimine souvent.
### Épingler une version sous l'ancienne licence
- Lorsqu'un projet est passé d'une licence permissive à source-available (ou l'inverse), les anciennes versions conservent leur ancienne licence.
- L'épinglage est une solution à court terme : pas de mises à jour de sécurité. Planifiez un remplacement approprié.
### L'isoler
- Exécutez l'outil copyleft comme un processus ou un service séparé qui communique avec votre code via une ligne de commande, HTTP ou une file d'attente, au lieu de le lier à votre code.
- Gardez-le non modifié si possible. Documentez la frontière.
- C'est une façon courante d'utiliser les outils en ligne de commande GPL, mais faites confirmer par un conseiller juridique pour AGPL et SSPL.
### Acheter une licence commerciale
- De nombreux projets à double licence (open source plus commercial) vendent une licence qui supprime les obligations de copyleft. Notez le prix et les conditions de renouvellement.
### Se conformer
- Copyleft faible : gardez la bibliothèque comme un fichier ou un paquet séparé et remplaçable ; publiez toutes les modifications apportées à ses propres fichiers ; incluez sa licence.
- GPL dans un produit distribué : seulement si tout le produit peut être open source sous des conditions compatibles. C'est une décision commerciale, pas une décision d'ingénierie.
- AGPL dans un service hébergé : offrez le code source correspondant aux utilisateurs du service, y compris vos modifications.
### Accepter avec une raison enregistrée
- Les outils de développement uniquement (linters, exécuteurs de tests, outils de construction) qui ne sont jamais distribués sont généralement acceptables. Enregistrez "dev uniquement, non distribué".
- L'utilisation interne uniquement des outils GPL ou AGPL est généralement acceptable. Enregistrez qui les utilise et qu'ils ne sont pas exposés aux utilisateurs externes.
## Avis de tiers
- Générez un fichier d'avis à partir de l'inventaire : paquet, version, licence, ligne de droit d'auteur, texte complet de la licence (une copie par texte de licence distinct).
- Incluez les fichiers NOTICE Apache tels quels.
- Distribuez-le dans les applications ("Paramètres > À propos > Licences"), les installateurs et les images de conteneurs ; pour SaaS, liez-le depuis le pied de page ou la documentation.
## Gardez-le propre
- Exécutez l'audit dans l'intégration continue à chaque changement d'un fichier de verrouillage et faites échouer la construction sur HIGH.
- Gardez une liste d'autorisation (par exemple MIT, BSD, ISC, Apache-2.0) et une liste d'interdiction (par exemple AGPL-3.0, SSPL-1.0, BUSL-1.1) convenues avec un conseiller juridique, et passez-les avec `--allow` et `--deny`.
FILE:templates/license-audit-report.md
# Audit de licence des dépendances : <nom du projet>
Date : <AAAA-MM-JJ>
Licence du projet : <propriétaire | identifiant SPDX>
Distribution : <saas | distributed | internal> (<une ligne : comment les utilisateurs obtiennent le logiciel>)
Source de l'inventaire : <outil et commande, commit du fichier de verrouillage>
Paquets audités : <n> (<n> runtime, <n> dev)
## Résumé
- Bloqueurs (HIGH) : <n> - <une ligne les nommant>
- Obligations (WARN) : <n> - <une ligne>
- Notes (INFO) : <n>
- Propres (OK) : <n>
- Verdict : <prêt à distribuer | distribuer après corrections | bloqué> parce que <raison>.
## Bloqueurs
| Paquet | Version | Licence (telle que trouvée) | Catégorie | Pourquoi c'est important pour nous | Action | Responsable | Échéance |
|---|---|---|---|---|---|---|---|
| | | | | | | | |
## Obligations
| Paquet | Licence | Ce que nous devons faire | Fait ? |
|---|---|---|---|
| | | | |
## Vérifié à la main
- <paquet> : les métadonnées disaient <x>, le fichier LICENSE dit <y>.
## Décisions et hypothèses
- Paquets à double licence et option choisie : <paquet : option>
- Risques acceptés avec raison : <paquet : raison, approuvé par>
- Hypothèses : <par exemple "le tableau de bord d'administration est interne uniquement">
## Ce qui change si nous changeons notre façon de distribuer
- <par exemple : "Si nous distribuons une version sur site, readline-sync (GPL-3.0) devient un bloqueur.">
## Avis de tiers
Licences qui nécessitent le texte complet dans le fichier d'avis : <liste>
Emplacement du fichier d'avis : <chemin ou URL>
## Prochain audit
Exécutez dans l'intégration continue à chaque changement de fichier de verrouillage ; examen complet avant <prochaine version ou date>.
_Ce rapport est un triage d'ingénierie, pas un avis juridique. Les bloqueurs doivent être confirmés par un conseiller juridique avant de se fier à toute exception._
FILE:examples/example-saas-audit.md
# Exemple : audit de licence d'un backend SaaS Node.js
## La demande
"Nous sommes un SaaS B2B à code source fermé. Les clients entreprises envoient sans cesse des questionnaires de sécurité sur les licences open source, et l'année prochaine nous pourrions vendre une version sur site. Voici notre export de dépendances. Y a-t-il quelque chose dont nous devrions nous inquiéter ?"
Inventaire (`deps.csv`, exporté depuis le fichier de verrouillage, 18 lignes) :
```
name,version,license,scope
express,4.19.2,MIT,runtime
lodash,4.17.21,MIT,runtime
pg,8.12.0,MIT,runtime
sharp,0.33.4,Apache-2.0,runtime
dompurify,3.1.6,(MPL-2.0 OR Apache-2.0),runtime
ghostscript-wrapper,2.1.0,AGPL-3.0-only,runtime
chart-kit-pro,1.4.0,SEE LICENSE IN LICENSE.md,runtime
readline-sync,1.4.10,GPL-3.0,runtime
mariadb-connector,3.3.1,LGPL-2.1-or-later,runtime
fast-csv-lite,0.9.0,,runtime
redis-om,0.4.3,Business Source License 1.1,runtime
jest,29.7.0,MIT,dev
eslint-plugin-gpl-thing,1.0.2,GPL-2.0-only,dev
argparse,2.0.1,Python-2.0,runtime
tslib,2.6.3,0BSD,runtime
lodash,3.10.1,MIT,runtime
search-client,7.10.2,Apache-2.0,runtime
search-client,8.1.0,Elastic License 2.0,runtime
```
## Exécution du script
```
$ python3 scripts/audit_licenses.py deps.csv --project-license proprietary --distribution saas
Audit de licence des dépendances : 18 paquets, licence du projet propriétaire, distribution saas
HIGH 5 WARN 1 INFO 2 OK 10
...
HIGH chart-kit-pro@1.4.0 SEE LICENSE IN LICENSE.md unknown runtime
HIGH fast-csv-lite@0.9.0 (manquant) unknown runtime
HIGH redis-om@0.4.3 BUSL-1.1 source-available runtime
HIGH search-client@8.1.0 Elastic-2.0 source-available runtime
HIGH ghostscript-wrapper@2.1.0 AGPL-3.0-only network-copyleft runtime
WARN readline-sync@1.4.10 GPL-3.0-only strong-copyleft runtime
INFO eslint-plugin-gpl-thing@1.0.2 GPL-2.0-only strong-copyleft dev
INFO mariadb-connector@3.3.1 LGPL-2.1-or-later weak-copyleft runtime
...
Notes :
- search-client : licences différentes selon les versions (Apache-2.0, Elastic-2.0) ; vérifiez quelle version vous distribuez.
```
Code de sortie 1 (constatations HIGH).
## Le rapport (modèle rempli, raccourci)
# Audit de licence des dépendances : acme-api
Licence du projet : propriétaire. Distribution : saas (les clients utilisent l'application web et l'API REST ; rien n'est installé sur leurs machines aujourd'hui).
Paquets audités : 18 (16 runtime, 2 dev).
### Résumé
- Bloqueurs (HIGH) : 5 - deux paquets sans licence utilisable, deux paquets source-available, un paquet AGPL.
- Obligations (WARN) : 1 - un paquet GPL-3.0 qui est acceptable pour SaaS mais bloque la version sur site prévue.
- Verdict : distribuer après corrections. Rien d'inhabituel ici, et chaque élément a une correction peu coûteuse.
### Bloqueurs
| Paquet | Licence (telle que trouvée) | Pourquoi c'est important pour nous | Action |
|---|---|---|---|
| chart-kit-pro 1.4.0 | SEE LICENSE IN LICENSE.md | Le LICENSE.md est une licence commerciale qui n'autorise l'utilisation qu'avec une clé payante. Nous n'avons aucune clé enregistrée. | Confirmez avec l'équipe qui l'a acheté, ou remplacez par une bibliothèque de graphiques permissive. |
| fast-csv-lite 0.9.0 | (manquante) | Pas de licence signifie pas de permission d'utiliser. | Demandez au mainteneur d'ajouter une licence ; en attendant, passez à un analyseur CSV maintenu avec des conditions MIT. |
| redis-om 0.4.3 | BUSL-1.1 | La Business Source License limite l'utilisation en production jusqu'à sa date de changement ; les conditions varient selon le projet. | Lisez la "Additional Use Grant" exacte. Si elle interdit notre utilisation, remplacez ou achetez une licence. |
| search-client 8.1.0 | Elastic-2.0 | La Elastic License 2.0 interdit d'offrir le logiciel comme un service géré. La version 7.10.2 dans le même arbre est Apache-2.0. | Vérifiez le fichier de verrouillage : quels modules importent 8.1.0 ? Épinglez les deux à la ligne Apache-2.0 ou passez à un fork sous licence Apache. |
| ghostscript-wrapper 2.1.0 | AGPL-3.0-only | Nous rendons les PDF des clients avec lui dans l'API ; l'utilisation réseau AGPL exige d'offrir le code source aux utilisateurs. | Déplacez le rendu PDF vers un service de travail séparé et non modifié et faites confirmer par un conseiller juridique, ou achetez la licence commerciale. |
### Obligations
| Paquet | Licence | Ce que nous devons faire |
|---|---|---|
| readline-sync 1.4.10 | GPL-3.0-only | Acceptable pour SaaS. Il devient un bloqueur si la version sur site est distribuée avec lui. Il n'est utilisé que par un script CLI d'administration, alors retirez-le de l'image du serveur. |
| mariadb-connector 3.3.1 | LGPL-2.1-or-later | Gardez-le comme une dépendance npm normale (remplaçable) ; publiez les modifications si nous le corrigeons un jour. |
### Vérifié à la main
- dompurify : double licence `MPL-2.0 OR Apache-2.0` ; nous choisissons Apache-2.0.
- eslint-plugin-gpl-thing : dev uniquement, jamais distribué. Accepté.
### Ce qui change si nous distribuons sur site
- readline-sync (GPL-3.0) devient HIGH, mariadb-connector (LGPL) nécessite la vérification de remplaçabilité, et les paquets Apache-2.0 nécessitent leurs fichiers NOTICE dans l'installateur. Relancez avec `--distribution distributed` avant cette version.
### Avis de tiers
Licences qui nécessitent le texte complet : Apache-2.0, LGPL-2.1-or-later, MIT, Python-2.0. Liez la page d'avis depuis le pied de page de l'application.
_Triage d'ingénierie, pas un avis juridique._
## Pourquoi c'est une bonne réponse
- Elle évalue chaque paquet pour le modèle de distribution réel de cette entreprise, et montre ce qui change avec le plan sur site.
- Elle vérifie les faits que les métadonnées ne peuvent pas montrer (le LICENSE.md commercial, les versions en double de search-client).
- Chaque bloqueur a une action concrète et peu coûteuse.
FILE:scripts/audit_licenses.py
#!/usr/bin/env python3
"""Audite les licences des dépendances tierces par rapport à la façon dont un projet est distribué.
Utilisation :
python3 audit_licenses.py deps.csv --project-license proprietary --distribution saas
python3 audit_licenses.py licenses.json --distribution distributed [--json]
cat deps.csv | python3 audit_licenses.py - [--allow LIC,LIC] [--deny LIC,LIC]
Entrée (détectée automatiquement) :
CSV avec un en-tête : name,version,license[,scope] (scope : runtime | dev ; défaut runtime)
Liste JSON : [{"name": "...", "version": "...", "license": "...", "scope": "dev"}]
pip-licenses --format=json : [{"Name": "...", "Version": "...", "License": "..."}]
license-checker --json (npm) : {"pkg@1.2.3": {"licenses": "MIT"}, ...}
Options :
--project-license la licence de votre projet : proprietary (défaut) ou un identifiant SPDX tel que MIT, Apache-2.0, GPL-3.0-only
--distribution saas (service réseau, défaut) | distributed (applications, binaires, appareils, bibliothèques) | internal
--allow / --deny identifiants SPDX séparés par des virgules que votre politique autorise ou bloque toujours
--json sortie lisible par machine
Les noms de licences sont normalisés en identifiants SPDX, les expressions avec OR prennent l'option la plus permissive et AND la plus restrictive. Chaque paquet reçoit une catégorie et une sévérité (OK, INFO, WARN, HIGH) pour la distribution choisie.
Les paquets dev uniquement ne sont pas distribués, donc leurs constatations passent en INFO.
Code de sortie : 0 aucune constatation HIGH, 1 au moins une HIGH, 2 erreur d'utilisation ou d'entrée.
C'est un outil d'aide au triage d'ingénierie, pas un avis juridique. Bibliothèque standard uniquement.
"""
import argparse
import csv
import io
import json
import re
import sys
ALIASES = { # noms courants en minuscules -> identifiant SPDX
"mit": "MIT", "mit license": "MIT", "the mit license": "MIT", "expat": "MIT",
"isc": "ISC", "isc license": "ISC", "isc license (iscl)": "ISC",
"apache 2.0": "Apache-2.0", "apache-2": "Apache-2.0", "apache 2": "Apache-2.0", "apache2": "Apache-2.0",
"apache license 2.0": "Apache-2.0", "apache license, version 2.0": "Apache-2.0",
"apache software license": "Apache-2.0", "apache license version 2.0": "Apache-2.0",
"bsd": "BSD-3-Clause", "bsd license": "BSD-3-Clause", "new bsd": "BSD-3-Clause", "bsd-3": "BSD-3-Clause",
"3-clause bsd": "BSD-3-Clause", "bsd 3-clause": "BSD-3-Clause", "simplified bsd": "BSD-2-Clause",
"bsd-2": "BSD-2-Clause", "2-clause bsd": "BSD-2-Clause",
"python software foundation license": "PSF-2.0", "psf": "PSF-2.0", "psfl": "PSF-2.0",
"mozilla public license 2.0 (mpl 2.0)": "MPL-2.0", "mpl 2.0": "MPL-2.0", "mpl-2": "MPL-2.0", "mpl2": "MPL-2.0",
"gplv2": "GPL-2.0-only", "gpl-2.0": "GPL-2.0-only", "gpl v2": "GPL-2.0-only", "gpl2": "GPL-2.0-only",
"gplv2+": "GPL-2.0-or-later", "gpl-2.0+": "GPL-2.0-or-later",
"gplv3": "GPL-3.0-only", "gpl-3.0": "GPL-3.0-only", "gpl v3": "GPL-3.0-only", "gpl3": "GPL-3.0-only",
"gplv3+": "GPL-3.0-or-later", "gpl-3.0+": "GPL-3.0-or-later", "gpl": "GPL-3.0-or-later",
"gnu general public license v2 (gplv2)": "GPL-2.0-only", "gnu general public license v3 (gplv3)": "GPL-3.0-only",
"gnu general public license v2 or later (gplv2+)": "GPL-2.0-or-later",
"gnu general public license v3 or later (gplv3+)": "GPL-3.0-or-later",
"lgpl": "LGPL-2.1-or-later", "lgplv2": "LGPL-2.1-only", "lgplv2+": "LGPL-2.1-or-later", "lgpl-2.1": "LGPL-2.1-only",
"lgplv3": "LGPL-3.0-only", "lgplv3+": "LGPL-3.0-or-later", "lgpl-3.0": "LGPL-3.0-only",
"gnu lesser general public license v2 (lgplv2)": "LGPL-2.1-only",
"gnu lesser general public license v3 (lgplv3)": "LGPL-3.0-only",
"gnu lesser general public license v2 or later (lgplv2+)": "LGPL-2.1-or-later",
"agpl": "AGPL-3.0-only", "agplv3": "AGPL-3.0-only", "agpl-3.0": "AGPL-3.0-only",
"gnu affero general public license v3": "AGPL-3.0-only", "gnu affero general public license v3 (agplv3)": "AGPL-3.0-only",
"eclipse public license 2.0": "EPL-2.0", "epl 2.0": "EPL-2.0", "epl-2": "EPL-2.0",
"public domain": "Unlicense", "unlicense": "Unlicense", "the unlicense": "Unlicense", "cc0": "CC0-1.0",
"boost software license 1.0": "BSL-1.0", "zlib license": "Zlib", "zlib": "Zlib",
"business source license 1.1": "BUSL-1.1", "elastic license 2.0": "Elastic-2.0",
"server side public license": "SSPL-1.0", "sspl": "SSPL-1.0",
}
CATEGORY = { # préfixe d'identifiant SPDX -> catégorie
"MIT": "permissive", "MIT-0": "public-domain", "ISC": "permissive", "BSD-2-Clause": "permissive",
"BSD-3-Clause": "permissive", "Apache-2.0": "permissive", "PSF-2.0": "permissive", "Python-2.0": "permissive",
"Zlib": "permissive", "BSL-1.0": "permissive", "X11": "permissive", "BlueOak-1.0.0": "permissive",
"CC-BY-4.0": "permissive", "CC-BY-3.0": "permissive", "Unicode-DFS-2016": "permissive", "Unicode-3.0": "permissive",
"0BSD": "public-domain", "Unlicense": "public-domain", "CC0-1.0": "public-domain", "WTFPL": "public-domain",
"MPL-2.0": "weak-copyleft", "EPL-1.0": "weak-copyleft", "EPL-2.0": "weak-copyleft", "CDDL-1.0": "weak-copyleft",
"CDDL-1.1": "weak-copyleft", "LGPL-2.1": "weak-copyleft", "LGPL-3.0": "weak-copyleft", "CC-BY-SA-4.0": "weak-copyleft",
"GPL-2.0": "strong-copyleft", "GPL-3.0": "strong-copyleft",
"AGPL-3.0": "network-copyleft", "SSPL-1.0": "network-copyleft",
"BUSL-1.1": "source-available", "Elastic-2.0": "source-available", "Commons-Clause": "source-available",
"CC-BY-NC-4.0": "source-available", "CC-BY-NC-SA-4.0": "source-available", "PolyForm-Noncommercial-1.0.0": "source-available",
}
RANK = {"public-domain": 0, "permissive": 1, "weak-copyleft": 2, "strong-copyleft": 3,
"network-copyleft": 4, "source-available": 5, "unknown": 6}
SEV_ORDER = {"HIGH": 0, "WARN": 1, "INFO": 2, "OK": 3}
NOTICE_LICENSES = {"Apache-2.0"}
def unwrap(text):
"""Supprime les parenthèses qui entourent toute l'expression : '(MIT OR X)' -> 'MIT OR X'."""
t = text.strip()
while t.startswith("(") and t.endswith(")"):
depth = 0
for i, ch in enumerate(t):
depth += ch == "("
depth -= ch == ")"
if depth == 0 and i < len(t) - 1:
return t
t = t[1:-1].strip()
return t
def normalize(token):
t = unwrap(token)
if not t:
return ""
key = t.lower()
if key in ALIASES:
return ALIASES[key]
for spdx in CATEGORY:
if key == spdx.lower() or key.startswith(spdx.lower() + "-") or key == spdx.lower() + "+":
return t if key.startswith(spdx.lower() + "-") else spdx
return t
def category_of(spdx):
if spdx.upper().startswith("LICENSEREF") or spdx.upper().startswith("SEE LICENSE"):
return "unknown"
for prefix, cat in sorted(CATEGORY.items(), key=lambda kv: -len(kv[0])):
if spdx == prefix or spdx.startswith(prefix + "-") or spdx.startswith(prefix + "+"):
return cat
return "unknown"
def classify(expr):
"""Renvoie (expression normalisée, licence choisie, catégorie, exception) pour une expression de licence."""
raw = (expr or "").strip()
if not raw or raw.upper() in ("UNKNOWN", "NONE", "NOASSERTION", "UNLICENSED", "CUSTOM", "N/A"):
return raw or "(manquant)", raw or "(manquant)", "unknown", None
if raw.upper().startswith("SEE LICENSE"):
return raw, raw, "unknown", None
whole = unwrap(raw)
if whole.lower() in ALIASES: # les noms longs tels que "... v2 or later (LGPLv2+)" sont une seule licence
spdx = ALIASES[whole.lower()]
return spdx, spdx, category_of(spdx), None
text = re.sub(r"\s+", " ", whole.replace("/", " OR ").replace(";", " OR "))
exception = None
m = re.search(r"\bWITH\s+([\w.+-]+)", text, re.I)
if m:
exception = m.group(1)
text = text[:m.start()] + text[m.end():]
if re.search(r"\sor\s", text, re.I) and not re.search(r"\sand\s", text, re.I):
parts, mode = re.split(r"\s+or\s+", text, flags=re.I), "or"
elif re.search(r"\sand\s", text, re.I) and not re.search(r"\sor\s", text, re.I):
parts, mode = re.split(r"\s+and\s+", text, flags=re.I), "and"
elif re.search(r"\s(or|and)\s", text, re.I):
parts, mode = re.split(r"\s+(?:or|and)\s+", text, flags=re.I), "and" # mélangé : soyez conservateur
else:
parts, mode = [text], "single"
lics = [normalize(p) for p in parts if normalize(p)]
if not lics:
return raw, raw, "unknown", exception
scored = sorted(lics, key=lambda l: RANK[category_of(l)])
chosen = scored[0] if mode == "or" else scored[-1]
cat = category_of(chosen)
if exception and cat == "strong-copyleft" and re.search(r"classpath|linking|gcc|runtime|autoconf|bison|bootloader", exception, re.I):
cat = "weak-copyleft"
joiner = " OR " if mode == "or" else " AND "
return joiner.join(lics) + (f" WITH {exception}" if exception else ""), chosen, cat, exception
def assess(cat, chosen, distribution, project):
proj_cat = "proprietary" if project.lower() == "proprietary" else category_of(normalize(project))
project_spdx = normalize(project)
if cat == "unknown":
return "HIGH", "La licence est manquante ou non reconnue ; trouvez le texte de licence réel avant de distribuer."
if cat == "source-available":
return "HIGH", "Les conditions source-available ou non commerciales restreignent généralement l'utilisation commerciale ou l'hébergement ; nécessite un examen de licence."
if project_spdx.startswith("GPL-2.0-only") and chosen.startswith("Apache-2.0"):
return "HIGH", "Apache-2.0 est généralement considéré comme incompatible avec les projets GPL-2.0-only."
gpl3_family = re.match(r"(A|L)?GPL-3\.0", chosen)
if project_spdx.startswith("GPL-2.0-only") and gpl3_family:
return "HIGH", f"{chosen} ne peut pas être combiné avec un projet GPL-2.0-only ; le projet aurait besoin de conditions GPL-3.0."
if re.match(r"GPL-3\.0|AGPL-3\.0", project_spdx) and chosen.startswith("GPL-2.0-only"):
return "HIGH", "Le code GPL-2.0-only ne peut pas être re-licencié sous GPL-3.0 ; demandez des conditions 'ou ultérieur' en amont ou remplacez-le."
if proj_cat in ("strong-copyleft", "network-copyleft") and cat in ("strong-copyleft", "network-copyleft"):
if cat == "network-copyleft" and proj_cat != "network-copyleft" and distribution == "saas":
return "WARN", "AGPL/SSPL dans un projet GPL exécuté comme un service : le code source de tout le service doit être offert aux utilisateurs."
return "OK", "Dépendance copyleft dans un projet copyleft ; vérifiez la compatibilité des versions (par exemple GPL-2.0-only vs GPL-3.0)."
if cat == "network-copyleft":
if distribution == "internal":
return "WARN", "Copyleft réseau : acceptable pour une utilisation purement interne, mais l'exposer à des utilisateurs externes déclenche des obligations de code source."
return "HIGH", "Copyleft réseau (AGPL/SSPL) : offrir le service ou le distribuer exige de publier le code source correspondant."
if cat == "strong-copyleft":
if distribution == "distributed":
return "HIGH", "Le code GPL distribué avec votre produit exige que l'œuvre combinée soit publiée sous la GPL."
if distribution == "saas":
return "WARN", "La GPL n'est pas déclenchée par la seule utilisation réseau, mais toute distribution future (applications, sur site, SDK) le serait."
return "INFO", "La GPL pour usage interne uniquement n'a pas d'obligations de distribution ; gardez-la hors de tout ce que vous distribuez."
if cat == "weak-copyleft":
if distribution == "distributed":
return "WARN", "Copyleft faible : distribuez la licence, gardez la bibliothèque remplaçable (liaison dynamique pour LGPL), et publiez les modifications apportées à ses fichiers."
return "INFO", "Copyleft faible : publiez toutes les modifications que vous apportez aux fichiers propres de cette bibliothèque si vous la distribuez plus tard."
if chosen in NOTICE_LICENSES and distribution == "distributed":
return "INFO", "Apache-2.0 : incluez la licence et tout fichier NOTICE dans votre distribution."
return "OK", "Permissive : conservez l'avis de droit d'auteur et le texte de la licence dans vos avis de tiers."
def load(text):
stripped = text.lstrip()
if not stripped:
raise ValueError("l'entrée est vide")
if stripped[0] in "[{":
data = json.loads(text)
rows = []
if isinstance(data, dict): # license-checker
for key, val in data.items():
name, _, ver = key.rpartition("@")
if not name:
name, ver = key, ""
lic = val.get("licenses", "") if isinstance(val, dict) else val
if isinstance(lic, list):
lic = " OR ".join(lic)
rows.append({"name": name, "version": ver, "license": str(lic), "scope": "runtime"})
return rows
for item in data:
if not isinstance(item, dict):
raise ValueError("les éléments de la liste JSON doivent être des objets")
low = {k.lower(): v for k, v in item.items()}
lic = low.get("license", low.get("licenses", ""))
if isinstance(lic, list):
lic = " OR ".join(lic)
rows.append({"name": str(low.get("name", "")), "version": str(low.get("version", "")),
"license": str(lic or ""), "scope": str(low.get("scope", "runtime") or "runtime").lower()})
return rows
reader = csv.DictReader(io.StringIO(text))
fields = [f.strip().lower() for f in (reader.fieldnames or [])]
if "name" not in fields or "license" not in fields:
raise ValueError(f"Le CSV nécessite un en-tête avec au moins les colonnes 'name' et 'license', obtenu : {', '.join(fields) or '(aucun)'}")
rows = []
for r in reader:
r = {(k or "").strip().lower(): (v or "").strip() for k, v in r.items()}
if not r.get("name"):
continue
rows.append({"name": r["name"], "version": r.get("version", ""), "license": r.get("license", ""),
"scope": (r.get("scope") or "runtime").lower()})
return rows
def audit(rows, args):
allow = {normalize(x) for x in args.allow.split(",") if x.strip()} if args.allow else set()
deny = {normalize(x) for x in args.deny.split(",") if x.strip()} if args.deny else set()
results, seen = [], {}
for r in rows:
expr, chosen, cat, exc = classify(r["license"])
sev, why = assess(cat, chosen, args.distribution, args.project_license)
if exc:
why += f" L'exception de licence {exc} s'applique ; lisez sa portée exacte."
if chosen in deny:
sev, why = "HIGH", f"{chosen} est sur votre liste d'interdiction."
elif chosen in allow and sev != "OK":
sev, why = "OK", f"{chosen} est sur votre liste d'autorisation ({why})"
dev = r["scope"] in ("dev", "development", "test", "build")
if dev and sev in ("HIGH", "WARN") and chosen not in deny:
sev, why = "INFO", "Dev uniquement (non distribué) : " + why
results.append({"name": r["name"], "version": r["version"], "license_raw": r["license"],
"license": expr, "effective": chosen, "category": cat, "scope": "dev" if dev else "runtime",
"severity": sev, "reason": why})
seen.setdefault(r["name"].lower(), set()).add(expr)
notes = []
for name, lics in seen.items():
if len(lics) > 1:
notes.append(f"{name} : licences différentes selon les versions ({', '.join(sorted(lics))}) ; vérifiez quelle version vous distribuez.")
results.sort(key=lambda x: (SEV_ORDER[x["severity"]], -RANK[x["category"]], x["name"].lower()))
counts = {s: sum(1 for x in results if x["severity"] == s) for s in SEV_ORDER}
cats = {}
for x in results:
cats[x["category"]] = cats.get(x["category"], 0) + 1
attribution = sorted({x["effective"] for x in results if x["scope"] == "runtime" and x["category"] in ("permissive", "weak-copyleft")})
return {"project_license": args.project_license, "distribution": args.distribution, "packages": len(results),
"severity_counts": counts, "category_counts": cats, "attribution_licenses": attribution,
"notes": notes, "results": results}
def print_text(rep):
print(f"Audit de licence des dépendances : {rep['packages']} paquets, licence du projet {rep['project_license']}, distribution {rep['distribution']}")
c = rep["severity_counts"]
print(f"HIGH {c['HIGH']} WARN {c['WARN']} INFO {c['INFO']} OK {c['OK']}")
print("Catégories : " + ", ".join(f"{k} {v}" for k, v in sorted(rep["category_counts"].items(), key=lambda kv: RANK[kv[0]])))
print()
w = max([len(f"{x['name']}@{x['version']}") for x in rep["results"]] + [7])
print(f"{'SEV':<5} {'PAQUET':<{w}} {'LICENCE':<28} {'CATÉGORIE':<17} PORTÉE")
for x in rep["results"]:
pkg = f"{x['name']}@{x['version']}" if x["version"] else x["name"]
print(f"{x['severity']:<5} {pkg:<{w}} {x['license'][:28]:<28} {x['category']:<17} {x['scope']}")
findings = [x for x in rep["results"] if x["severity"] in ("HIGH", "WARN", "INFO")]
if findings:
print("\nConstatations :")
for x in findings:
print(f"- [{x['severity']}] {x['name']} ({x['license_raw'] or 'aucun champ de licence'}) : {x['reason']}")
if rep["notes"]:
print("\nNotes :")
for n in rep["notes"]:
print(f"- {n}")
if rep["attribution_licenses"]:
print("\nLes avis de tiers doivent inclure les textes de licence pour : " + ", ".join(rep["attribution_licenses"]))
print("\nAide au triage uniquement, pas un avis juridique.")
def main():
p = argparse.ArgumentParser(description="Audite les licences des dépendances.", add_help=True)
p.add_argument("path", help="Fichier CSV ou JSON, ou - pour l'entrée standard")
p.add_argument("--project-license", default="proprietary")
p.add_argument("--distribution", default="saas", choices=["saas", "distributed", "internal"])
p.add_argument("--allow", default="")
p.add_argument("--deny", default="")
p.add_argument("--json", action="store_true")
args = p.parse_args()
try:
text = sys.stdin.read() if args.path == "-" else open(args.path, encoding="utf-8").read()
rows = load(text)
except FileNotFoundError:
print(f"erreur : fichier introuvable : {args.path}", file=sys.stderr)
return 2
except (ValueError, json.JSONDecodeError) as e:
print(f"erreur : {e}", file=sys.stderr)
return 2
if not rows:
print("erreur : aucune dépendance trouvée dans l'entrée", file=sys.stderr)
return 2
rep = audit(rows, args)
if args.json:
print(json.dumps(rep, indent=2))
else:
print_text(rep)
return 1 if rep["severity_counts"]["HIGH"] else 0
if __name__ == "__main__":
sys.exit(main())
Connectez-vous pour voir le prompt complet
Continuer avec:
En vous connectant, vous acceptez nos Conditions et Confidentialité
Utilisation
Ce prompt est conçu pour être utilisé avec coding. Copiez le contenu ci-dessus et collez-le dans votre outil d’IA préféré.
Pour de meilleurs résultats, personnalisez les espaces réservés (indiqués par des crochets ou des majuscules) selon vos besoins.
Discussion
0 commentaires