Habilidad de Auditoría de Licencias de Dependencias
De Wikiprompt, la enciclopedia libre de prompts
Habilidad de Auditoría de Licencias de Dependencias Una habilidad completa de IA para auditar licencias de dependencias, incluyendo flujo de trabajo, materiales de referencia, plantillas, ejemplos y un script de Python. Ayuda a los equipos de ingeniería a identificar riesgos de licencias antes del lanzamiento.
Contenido del PromptGuardar
🌐
---
name: dependency-license-audit
description: Audita las licencias de las dependencias de tu proyecto según cómo las distribuyes realmente (SaaS, aplicación distribuida o librería, o herramienta interna): normaliza los nombres de licencia a SPDX, resuelve expresiones OR y AND, marca las licencias faltantes, AGPL, GPL, de código disponible e incompatibles, y escribe un plan de corrección clasificado y una lista de avisos. Incluye un auditor de Python de la biblioteca estándar probado.
---
# Auditoría de Licencias de Dependencias
Ayudas a los equipos de ingeniería a encontrar problemas de licencia en sus dependencias antes de que un lanzamiento, una revisión de adquisición o un cuestionario de seguridad de un cliente los encuentre. Conviertes una lista de dependencias en bruto en una lista corta y clasificada de cosas a corregir, y explicas cada una en lenguaje sencillo. Eres un ingeniero cuidadoso, no un abogado: señalas el riesgo y dices cuándo una persona con formación legal debe decidir.
## Archivos en esta habilidad
- `scripts/audit_licenses.py` - normaliza y clasifica cada licencia de dependencia y la califica para el modelo de distribución elegido (solo biblioteca estándar de Python 3)
- `references/license-categories.md` - qué significan en la práctica permisivo, copyleft débil, copyleft fuerte, copyleft de red y código disponible, con IDs SPDX comunes
- `references/remediation-playbook.md` - cómo corregir cada tipo de hallazgo, desde reemplazar un paquete hasta añadir avisos
- `templates/license-audit-report.md` - el diseño del informe
- `examples/example-saas-audit.md` - una auditoría trabajada de un backend SaaS de Node.js
## Flujo de trabajo
### 1. Comprender cómo se distribuye el proyecto
Pregunta, o infiere y declara como suposición:
- Licencia del proyecto: propietaria (código cerrado) o una licencia de código abierto (ID SPDX).
- Distribución: `saas` (los usuarios solo lo alcanzan a través de una red), `distributed` (aplicaciones móviles o de escritorio, instalaciones on-prem, dispositivos, SDKs, librerías publicadas, imágenes de contenedor entregadas a clientes), o `internal` (solo lo usan empleados).
- Cualquier política de la empresa: licencias siempre permitidas y siempre bloqueadas.
La misma dependencia puede estar bien para un modelo y ser un bloqueador para otro, así que nunca te saltes este paso.
### 2. Obtener el inventario de dependencias
Prefiere la salida de una herramienta sobre una lista hecha a mano, e incluye dependencias transitivas:
- Python: `pip-licenses --format=json --with-system` (o `pip-licenses --format=csv`).
- Node.js: `npx license-checker --json --production` (ejecuta de nuevo sin `--production` para herramientas de desarrollo).
- Cualquier otra cosa: un CSV con `name,version,license,scope` exportado desde un SBOM o el gestor de paquetes.
Marca las herramientas de compilación, prueba y lint como `scope=dev`; no se distribuyen, por lo que sus hallazgos bajan a INFO.
### 3. Ejecutar la auditoría
```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
```
El script imprime recuentos de severidad, una tabla de cada paquete con su licencia y categoría normalizadas, hallazgos con razones, cambios de licencia de versión a versión, y las licencias que necesitan texto en tus avisos de terceros. El código de salida 1 significa al menos un hallazgo HIGH.
Si no puedes ejecutar el script, clasifica cada licencia a mano con `references/license-categories.md` y dilo en el informe.
### 4. Verificar antes de concluir
- Para cada hallazgo HIGH o WARN, comprueba el archivo LICENSE real del paquete en su repositorio o archivo de paquete; los metadatos a menudo son incorrectos o están desactualizados.
- Para entradas "missing" o "SEE LICENSE IN", abre el archivo referenciado y clasifícalo por su texto.
- Para paquetes con doble licencia (OR), registra qué opción eliges.
- Para paquetes cuya licencia cambió entre versiones, confirma qué versión está realmente instalada (archivo de bloqueo).
### 5. Recomendar e informar
Usa `references/remediation-playbook.md` para dar una acción concreta por hallazgo: reemplazar, aislar, fijar una versión permisiva anterior, obtener una licencia comercial, cumplir (publicar el código fuente o añadir avisos), o aceptar con una razón registrada. Completa `templates/license-audit-report.md`, como en `examples/example-saas-audit.md`.
## Reglas
- Nunca digas que una licencia "está bien" o "es ilegal" como conclusión legal. Di lo que la licencia suele requerir y cuándo consultar al asesor legal.
- No adivines una licencia por el nombre del paquete o su popularidad. Lo desconocido permanece desconocido hasta que alguien lea el texto de la licencia.
- Califica el riesgo para el modelo de distribución del usuario, y anota qué cambiaría si el modelo cambia (por ejemplo, SaaS hoy, on-prem el año que viene).
- Mantén el informe breve: bloqueadores primero, luego obligaciones (avisos, ofertas de código fuente), luego la lista limpia como un recuento.
- Trata la auditoría como una instantánea; recomienda ejecutarla en CI en cada cambio de dependencia.
FILE:references/license-categories.md
# Categorías de licencias en la práctica
La severidad en el script depende de la categoría y de cómo se distribuye el proyecto. Esta página explica las categorías en lenguaje sencillo. Es un resumen de ingeniería, no asesoramiento legal.
## Dominio público y casi dominio público
SPDX: `0BSD`, `Unlicense`, `CC0-1.0`, `MIT-0`, `WTFPL`
- Sin obligaciones significativas. Mantener un aviso sigue siendo una buena práctica.
## Permisivo
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` (datos como caniuse-lite)
- Usar, modificar y distribuir en productos de código cerrado.
- Mantener el aviso de copyright y el texto de la licencia, generalmente en un archivo de avisos de terceros o en una pantalla "Acerca de / Licencias".
- Apache-2.0 también requiere mantener cualquier archivo NOTICE, declarar cambios significativos en sus archivos, e incluye una concesión de patentes que termina si demandas por patentes en ese código.
- BSD-3-Clause prohíbe usar los nombres de los autores para promocionar tu producto.
## Copyleft débil (a nivel de archivo o librería)
SPDX: `LGPL-2.1-*`, `LGPL-3.0-*`, `MPL-2.0`, `EPL-1.0`, `EPL-2.0`, `CDDL-1.0`, `CDDL-1.1`, GPL con una excepción de enlace (por ejemplo `GPL-2.0-only WITH Classpath-exception-2.0`)
- Puedes combinarlos con código de código cerrado, pero los cambios a los archivos propios de la librería deben compartirse bajo la misma licencia cuando distribuyas.
- LGPL: los usuarios deben poder reemplazar la librería con su propia versión. El enlace dinámico (librería compartida, jar separado, paquete npm) normalmente facilita esto; el enlace estático o el empaquetado en un solo binario requiere cuidado.
- MPL-2.0: el copyleft se aplica por archivo. Mantén los archivos MPL separados y publica los cambios a esos archivos.
## Copyleft fuerte
SPDX: `GPL-2.0-only`, `GPL-2.0-or-later`, `GPL-3.0-only`, `GPL-3.0-or-later`
- Si distribuyes software que incluye o enlaza código GPL, la obra combinada debe ofrecerse bajo la GPL con su código fuente.
- Ejecutar software GPL en tus propios servidores y permitir que los usuarios lo alcancen a través de una red no es distribución, por lo que el uso puro de SaaS suele estar permitido. Enviar una aplicación, un instalador on-prem, un firmware de dispositivo o una imagen de contenedor a clientes es distribución.
- La versión importa: el código `GPL-2.0-only` no puede combinarse con código GPL-3.0 o Apache-2.0 en una sola obra. `-or-later` da más margen.
## Copyleft de red
SPDX: `AGPL-3.0-only`, `AGPL-3.0-or-later`, `SSPL-1.0` (no aprobada por OSI)
- Como la GPL, más: si los usuarios interactúan con una versión modificada a través de una red, debes ofrecerles su código fuente.
- Para un producto SaaS de código cerrado, este es el bloqueador más común. Incluso el uso sin modificar desencadena revisiones internas en la mayoría de las empresas.
- SSPL va más allá: ofrecer el software como servicio puede requerir publicar el código fuente de toda tu pila de servicios.
## Código disponible y no comercial
SPDX o nombres: `BUSL-1.1` (Business Source License), `Elastic-2.0`, add-ons de Commons Clause, `CC-BY-NC-*`, `PolyForm-Noncommercial-1.0.0`
- El código fuente es visible, pero el uso está restringido: a menudo sin servicio alojado competidor, sin uso en producción por encima de un límite, o sin uso comercial en absoluto, a veces hasta una fecha de cambio.
- Siempre necesita una revisión de los términos exactos. Muchas de estas cambian a una licencia abierta después de unos años, por lo que una versión anterior o posterior puede diferir.
## Desconocido
- Campos de licencia vacíos, `UNKNOWN`, `NOASSERTION`, `UNLICENSED`, `SEE LICENSE IN <archivo>`, `LicenseRef-*`, o un nombre que el script no reconoce.
- "Sin licencia" significa todos los derechos reservados: no tienes permiso para usarlo. Encuentra los términos reales o reemplaza el paquete.
## Expresiones
- `MIT OR Apache-2.0`: puedes elegir cualquiera; elige el que te convenga y registra la elección.
- `MIT AND BSD-3-Clause`: debes cumplir ambas; la más estricta establece la categoría.
- `GPL-2.0-only WITH <excepción>`: la excepción reduce el copyleft; léela.
FILE:references/remediation-playbook.md
# Libro de jugadas de remediación
Elige una acción por hallazgo y anótala con un responsable. Prefiere la acción más barata que elimine el riesgo.
## Orden de trabajo
1. Licencias desconocidas y faltantes (puede que no tengas derecho a usar el código en absoluto).
2. Paquetes de copyleft de red y código disponible en código distribuido o alojado.
3. Copyleft fuerte en productos distribuidos, e incompatibilidades de versión en proyectos de código abierto.
4. Obligaciones de copyleft débil (enlace, publicación de cambios).
5. Avisos para todo lo demás.
## Acciones
### Encontrar la licencia real
- Abre el repositorio y el archivo del paquete, lee LICENSE, COPYING, o el encabezado del archivo fuente principal.
- Comprueba si los metadatos solo están desactualizados (un caso común para paquetes antiguos y forks).
- Si realmente no hay licencia, pide al mantenedor que añada una (enlaza a choosealicense.com en la solicitud) o reemplaza el paquete.
### Reemplazar el paquete
- Busca una alternativa mantenida con una licencia permisiva y una API similar.
- Estima el esfuerzo en horas y añádelo al backlog con el lanzamiento que bloquea.
- Para dependencias transitivas, busca la dependencia directa que lo introduce; una versión diferente o un flag de configuración a menudo lo elimina.
### Fijar una versión bajo la licencia anterior
- Cuando un proyecto cambió de una licencia permisiva a código disponible (o viceversa), las versiones antiguas mantienen su licencia anterior.
- Fijar es una solución a corto plazo: sin actualizaciones de seguridad. Programa un reemplazo adecuado.
### Aislarlo
- Ejecuta la herramienta de copyleft como un proceso o servicio separado que hable con tu código a través de una línea de comandos, HTTP o una cola, en lugar de enlazarlo en tu código.
- Mantenlo sin modificar si es posible. Documenta el límite.
- Esta es una forma común de usar herramientas de línea de comandos GPL, pero haz que el asesor legal lo confirme para AGPL y SSPL.
### Comprar una licencia comercial
- Muchos proyectos con doble licencia (código abierto más comercial) venden una licencia que elimina las obligaciones de copyleft. Anota el precio y los términos de renovación.
### Cumplir
- Copyleft débil: mantén la librería como un archivo o paquete separado y reemplazable; publica cualquier cambio en sus propios archivos; incluye su licencia.
- GPL en un producto distribuido: solo si todo el producto puede ser de código abierto bajo términos compatibles. Esta es una decisión empresarial, no de ingeniería.
- AGPL en un servicio alojado: ofrece el código fuente correspondiente a los usuarios del servicio, incluidos tus cambios.
### Aceptar con una razón registrada
- Herramientas solo de desarrollo (linters, ejecutores de pruebas, herramientas de compilación) que nunca se distribuyen suelen estar bien. Registra "solo dev, no distribuido".
- El uso solo interno de herramientas GPL o AGPL suele estar bien. Registra quién las usa y que no están expuestas a usuarios externos.
## Avisos de terceros
- Genera un archivo de avisos desde el inventario: paquete, versión, licencia, línea de copyright, texto completo de la licencia (una copia por texto de licencia distinto).
- Incluye los archivos NOTICE de Apache tal como están.
- Envíalo dentro de las aplicaciones ("Configuración > Acerca de > Licencias"), instaladores e imágenes de contenedor; para SaaS, enlázalo desde el pie de página o la documentación.
## Mantenerlo limpio
- Ejecuta la auditoría en CI en cada cambio a un archivo de bloqueo y falla la compilación en HIGH.
- Mantén una lista de permitidos (por ejemplo MIT, BSD, ISC, Apache-2.0) y una lista de denegados (por ejemplo AGPL-3.0, SSPL-1.0, BUSL-1.1) acordadas con el asesor legal, y pásalas con `--allow` y `--deny`.
FILE:templates/license-audit-report.md
# Auditoría de Licencias de Dependencias: <nombre del proyecto>
Fecha: <AAAA-MM-DD>
Licencia del proyecto: <propietaria | ID SPDX>
Distribución: <saas | distributed | internal> (<una línea: cómo obtienen los usuarios el software>)
Fuente del inventario: <herramienta y comando, commit del archivo de bloqueo>
Paquetes auditados: <n> (<n en tiempo de ejecución>, <n dev>)
## Resumen
- Bloqueadores (HIGH): <n> - <una línea nombrándolos>
- Obligaciones (WARN): <n> - <una línea>
- Notas (INFO): <n>
- Limpio (OK): <n>
- Veredicto: <listo para enviar | enviar después de correcciones | bloqueado> porque <razón>.
## Bloqueadores
| Paquete | Versión | Licencia (como se encontró) | Categoría | Por qué importa para nosotros | Acción | Responsable | Fecha límite |
|---|---|---|---|---|---|---|---|
| | | | | | | | |
## Obligaciones
| Paquete | Licencia | Qué debemos hacer | ¿Hecho? |
|---|---|---|---|
| | | | |
## Verificado a mano
- <paquete>: los metadatos decían <x>, el archivo LICENSE dice <y>.
## Decisiones y suposiciones
- Paquetes con doble licencia y la opción elegida: <paquete: opción>
- Riesgos aceptados con razón: <paquete: razón, aprobado por>
- Suposiciones: <por ejemplo "el panel de administración es solo interno">
## Qué cambia si cambiamos cómo distribuimos
- <por ejemplo: "Si enviamos una versión on-prem, readline-sync (GPL-3.0) se convierte en un bloqueador.">
## Avisos de terceros
Licencias que necesitan texto completo en el archivo de avisos: <lista>
Ubicación del archivo de avisos: <ruta o URL>
## Próxima auditoría
Ejecutar en CI en cada cambio al archivo de bloqueo; revisión completa antes del <próximo lanzamiento o fecha>.
_Este informe es un triaje de ingeniería, no asesoramiento legal. Los bloqueadores deben ser confirmados por el asesor legal antes de confiar en cualquier excepción._
FILE:examples/example-saas-audit.md
# Ejemplo: auditoría de licencias de un backend SaaS de Node.js
## La solicitud
"Somos un SaaS B2B de código cerrado. Los clientes empresariales envían cuestionarios de seguridad preguntando sobre licencias de código abierto, y el año que viene podemos vender una versión on-prem. Aquí está nuestra exportación de dependencias. ¿Hay algo que debamos revisar?"
Inventario (`deps.csv`, exportado desde el archivo de bloqueo, 18 filas):
```
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
```
## Ejecución del script
```
$ python3 scripts/audit_licenses.py deps.csv --project-license proprietary --distribution saas
Dependency license audit: 18 packages, project license proprietary, 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 (missing) 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: different licenses across versions (Apache-2.0, Elastic-2.0); check which version you ship.
```
Código de salida 1 (hallazgos HIGH).
## El informe (plantilla completada, abreviada)
# Auditoría de Licencias de Dependencias: acme-api
Licencia del proyecto: propietaria. Distribución: saas (los clientes usan la aplicación web y la API REST; nada se instala en sus máquinas hoy).
Paquetes auditados: 18 (16 en tiempo de ejecución, 2 dev).
### Resumen
- Bloqueadores (HIGH): 5 - dos paquetes sin licencia utilizable, dos paquetes de código disponible, un paquete AGPL.
- Obligaciones (WARN): 1 - un paquete GPL-3.0 que está bien para SaaS pero bloquea la versión on-prem planificada.
- Veredicto: enviar después de correcciones. Nada aquí es inusual, y cada elemento tiene una corrección barata.
### Bloqueadores
| Paquete | Licencia (como se encontró) | Por qué importa para nosotros | Acción |
|---|---|---|---|
| chart-kit-pro 1.4.0 | SEE LICENSE IN LICENSE.md | El LICENSE.md es una licencia comercial que permite el uso solo con una clave pagada. No tenemos clave registrada. | Confirma con el equipo que la compró, o reemplaza con una librería de gráficos permisiva. |
| fast-csv-lite 0.9.0 | (missing) | Sin licencia significa sin permiso de uso. | Pide al mantenedor que añada una licencia; mientras tanto, cambia a un analizador CSV mantenido con términos MIT. |
| redis-om 0.4.3 | BUSL-1.1 | La Business Source License limita el uso en producción hasta su fecha de cambio; los términos varían por proyecto. | Lee la "Additional Use Grant" exacta. Si prohíbe nuestro uso, reemplaza o compra una licencia. |
| search-client 8.1.0 | Elastic-2.0 | La Elastic License 2.0 prohíbe ofrecer el software como servicio gestionado. La versión 7.10.2 en el mismo árbol es Apache-2.0. | Comprueba el archivo de bloqueo: ¿qué módulos importan 8.1.0? Fija ambos a la línea Apache-2.0 o muévete a un fork con licencia Apache. |
| ghostscript-wrapper 2.1.0 | AGPL-3.0-only | Renderizamos PDFs de clientes con él dentro de la API; el uso de red AGPL requiere ofrecer el código fuente a los usuarios. | Mueve el renderizado de PDF a un servicio de trabajo separado y sin modificar y haz que el asesor legal lo confirme, o compra la licencia comercial. |
### Obligaciones
| Paquete | Licencia | Qué debemos hacer |
|---|---|---|
| readline-sync 1.4.10 | GPL-3.0-only | Bien para SaaS. Se convierte en un bloqueador si la versión on-prem se envía con él. Solo lo usa un script CLI de administración, así que elimínalo de la imagen del servidor. |
| mariadb-connector 3.3.1 | LGPL-2.1-or-later | Mantenlo como una dependencia npm normal (reemplazable); publica los cambios si alguna vez lo parcheamos. |
### Verificado a mano
- dompurify: doble licencia `MPL-2.0 OR Apache-2.0`; elegimos Apache-2.0.
- eslint-plugin-gpl-thing: solo dev, nunca se envía. Aceptado.
### Qué cambia si enviamos on-prem
- readline-sync (GPL-3.0) se convierte en HIGH, mariadb-connector (LGPL) necesita la verificación de reemplazabilidad, y los paquetes Apache-2.0 necesitan sus archivos NOTICE en el instalador. Vuelve a ejecutar con `--distribution distributed` antes de ese lanzamiento.
### Avisos de terceros
Licencias que necesitan texto completo: Apache-2.0, LGPL-2.1-or-later, MIT, Python-2.0. Enlaza la página de avisos desde el pie de página de la aplicación.
_Triaje de ingeniería, no asesoramiento legal._
## Por qué esta es una buena respuesta
- Califica cada paquete para el modelo de distribución real de esta empresa, y muestra qué cambia con el plan on-prem.
- Comprueba los hechos que los metadatos no pueden mostrar (el LICENSE.md comercial, las versiones duplicadas de search-client).
- Cada bloqueador tiene una acción concreta y barata.
FILE:scripts/audit_licenses.py
#!/usr/bin/env python3
"""Audita las licencias de dependencias de terceros contra cómo se distribuye un proyecto.
Uso:
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]
Entrada (detectada automáticamente):
CSV con un encabezado: name,version,license[,scope] (scope: runtime | dev; por defecto runtime)
Lista JSON: [{"name": "...", "version": "...", "license": "...", "scope": "dev"}]
pip-licenses --format=json: [{"Name": "...", "Version": "...", "License": "..."}]
license-checker --json (npm): {"pkg@1.2.3": {"licenses": "MIT"}, ...}
Opciones:
--project-license la licencia de tu proyecto: proprietary (por defecto) o un ID SPDX como MIT, Apache-2.0, GPL-3.0-only
--distribution saas (servicio de red, por defecto) | distributed (aplicaciones, binarios, dispositivos, librerías) | internal
--allow / --deny IDs SPDX separados por comas que tu política siempre permite o siempre bloquea
--json salida legible por máquina
Los nombres de licencia se normalizan a IDs SPDX, las expresiones con OR toman la opción más permisiva y AND la más restrictiva. Cada paquete recibe una categoría y una severidad (OK, INFO, WARN, HIGH) para la distribución elegida.
Los paquetes solo dev no se distribuyen, por lo que sus hallazgos bajan a INFO.
Código de salida: 0 sin hallazgos HIGH, 1 al menos un HIGH, 2 error de uso o entrada.
Esto es una ayuda de triaje de ingeniería, no asesoramiento legal. Solo biblioteca estándar.
"""
import argparse
import csv
import io
import json
import re
import sys
ALIASES = { # nombres comunes en minúsculas -> ID 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 = { # prefijo de ID SPDX -> categoría
"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):
"""Elimina los paréntesis que envuelven toda la expresión: '(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):
"""Devuelve (expresión normalizada, licencia elegida, categoría, excepción) para una expresión de licencia."""
raw = (expr or "").strip()
if not raw or raw.upper() in ("UNKNOWN", "NONE", "NOASSERTION", "UNLICENSED", "CUSTOM", "N/A"):
return raw or "(missing)", raw or "(missing)", "unknown", None
if raw.upper().startswith("SEE LICENSE"):
return raw, raw, "unknown", None
whole = unwrap(raw)
if whole.lower() in ALIASES: # nombres largos como "... v2 or later (LGPLv2+)" son una licencia
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" # mixto: ser conservador
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 licencia falta o no se reconoce; encuentra el texto de la licencia real antes de distribuir."
if cat == "source-available":
return "HIGH", "Los términos de código disponible o no comercial suelen restringir el uso comercial o el alojamiento; necesita una revisión de licencia."
if project_spdx.startswith("GPL-2.0-only") and chosen.startswith("Apache-2.0"):
return "HIGH", "Apache-2.0 generalmente se considera incompatible con proyectos 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} no puede combinarse con un proyecto GPL-2.0-only; el proyecto necesitaría términos GPL-3.0."
if re.match(r"GPL-3\.0|AGPL-3\.0", project_spdx) and chosen.startswith("GPL-2.0-only"):
return "HIGH", "El código GPL-2.0-only no puede relicenciarse bajo GPL-3.0; pide términos 'or later' al upstream o reemplázalo."
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 en un proyecto GPL ejecutado como servicio: todo el código fuente del servicio debe ofrecerse a los usuarios."
return "OK", "Dependencia copyleft en un proyecto copyleft; comprueba la compatibilidad de versiones (por ejemplo GPL-2.0-only vs GPL-3.0)."
if cat == "network-copyleft":
if distribution == "internal":
return "WARN", "Copyleft de red: bien para uso puramente interno, pero exponerlo a usuarios externos desencadena obligaciones de código fuente."
return "HIGH", "Copyleft de red (AGPL/SSPL): ofrecer el servicio o distribuirlo requiere publicar el código fuente correspondiente."
if cat == "strong-copyleft":
if distribution == "distributed":
return "HIGH", "El código GPL distribuido con tu producto requiere que la obra combinada se publique bajo la GPL."
if distribution == "saas":
return "WARN", "La GPL no se activa solo por el uso de red, pero cualquier distribución futura (aplicaciones, on-prem, SDKs) lo haría."
return "INFO", "La GPL para uso solo interno no tiene obligaciones de distribución; mantenla fuera de cualquier cosa que distribuyas."
if cat == "weak-copyleft":
if distribution == "distributed":
return "WARN", "Copyleft débil: distribuye la licencia, mantén la librería reemplazable (enlace dinámico para LGPL) y publica los cambios a sus archivos."
return "INFO", "Copyleft débil: publica cualquier cambio que hagas a los archivos propios de esta librería si la distribuyes más tarde."
if chosen in NOTICE_LICENSES and distribution == "distributed":
return "INFO", "Apache-2.0: incluye la licencia y cualquier archivo NOTICE en tu distribución."
return "OK", "Permisivo: mantén el aviso de copyright y el texto de la licencia en tus avisos de terceros."
def load(text):
stripped = text.lstrip()
if not stripped:
raise ValueError("la entrada está vacía")
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("los elementos de la lista JSON deben ser objetos")
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"El CSV necesita un encabezado con al menos las columnas 'name' y 'license', se obtuvo: {', '.join(fields) or '(ninguno)'}")
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" Se aplica la excepción de licencia {exc}; lee su alcance exacto."
if chosen in deny:
sev, why = "HIGH", f"{chosen} está en tu lista de denegados."
elif chosen in allow and sev != "OK":
sev, why = "OK", f"{chosen} está en tu lista de permitidos ({why})"
dev = r["scope"] in ("dev", "development", "test", "build")
if dev and sev in ("HIGH", "WARN") and chosen not in deny:
sev, why = "INFO", "Solo dev (no distribuido): " + 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}: diferentes licencias entre versiones ({', '.join(sorted(lics))}); comprueba qué versión distribuyes.")
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"Auditoría de licencias de dependencias: {rep['packages']} paquetes, licencia del proyecto {rep['project_license']}, distribución {rep['distribution']}")
c = rep["severity_counts"]
print(f"HIGH {c['HIGH']} WARN {c['WARN']} INFO {c['INFO']} OK {c['OK']}")
print("Categorías: " + ", ".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} {'PAQUETE':<{w}} {'LICENCIA':<28} {'CATEGORÍA':<17} ÁMBITO")
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("\nHallazgos:")
for x in findings:
print(f"- [{x['severity']}] {x['name']} ({x['license_raw'] or 'sin campo de licencia'}): {x['reason']}")
if rep["notes"]:
print("\nNotas:")
for n in rep["notes"]:
print(f"- {n}")
if rep["attribution_licenses"]:
print("\nLos avisos de terceros deben incluir textos de licencia para: " + ", ".join(rep["attribution_licenses"]))
print("\nSolo ayuda de triaje, no asesoramiento legal.")
def main():
p = argparse.ArgumentParser(description="Audita licencias de dependencias.", add_help=True)
p.add_argument("path", help="Archivo CSV o JSON, o - para stdin")
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"error: archivo no encontrado: {args.path}", file=sys.stderr)
return 2
except (ValueError, json.JSONDecodeError) as e:
print(f"error: {e}", file=sys.stderr)
return 2
if not rows:
print("error: no se encontraron dependencias en la entrada", 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())
Iniciá sesión para ver el prompt completo
Continuar con:
Al iniciar sesión, aceptás nuestros Términos de uso y Política de privacidad
Uso
Este prompt está diseñado para usarse con coding. Copiá el contenido de arriba y pegalo en tu herramienta de IA preferida.
Para mejores resultados, personalizá los marcadores (indicados con corchetes o mayúsculas) con tus requisitos específicos.
Discusión
0 comentarios