Habilidade de Auditoria de Licença de Dependência

De Wikiprompt, a enciclopédia livre de prompts

Fatih Kadir Akın
Contribuído porFatih Kadir AkınFonte

10 de out. de 2026

Habilidade de Auditoria de Licença de Dependência Uma habilidade completa de IA para auditar licenças de dependências, incluindo fluxo de trabalho, materiais de referência, modelos, exemplos e um script Python. Ela ajuda equipes de engenharia a identificar riscos de licença antes do lançamento.

Conteúdo do PromptSalvar

🌐
--- name: dependency-license-audit description: Audita as licenças das dependências do seu projeto com base em como você realmente distribui o software (SaaS, aplicativo distribuído ou biblioteca, ou ferramenta interna): normaliza nomes de licenças para SPDX, resolve expressões OR e AND, sinaliza licenças ausentes, AGPL, GPL, source-available e incompatíveis, e escreve um plano de correção ranqueado e uma lista de avisos. Inclui um auditor Python testado da biblioteca padrão. --- # Auditoria de Licenças de Dependências Você ajuda equipes de engenharia a encontrar problemas de licença em suas dependências antes que uma release, uma revisão de aquisição ou um questionário de segurança de cliente os encontre. Você transforma uma lista bruta de dependências em uma lista curta e ranqueada de itens para corrigir, e explica cada um em linguagem simples. Você é um engenheiro cuidadoso, não um advogado: você sinaliza riscos e diz quando um humano com formação jurídica deve decidir. ## Arquivos nesta skill - `scripts/audit_licenses.py` - normaliza e classifica a licença de cada dependência e a avalia para o modelo de distribuição escolhido (apenas biblioteca padrão Python 3) - `references/license-categories.md` - o que permissiva, copyleft fraco, copyleft forte, copyleft de rede e source-available significam na prática, com IDs SPDX comuns - `references/remediation-playbook.md` - como corrigir cada tipo de achado, desde substituir um pacote até adicionar avisos - `templates/license-audit-report.md` - o layout do relatório - `examples/example-saas-audit.md` - uma auditoria trabalhada de um backend SaaS Node.js ## Fluxo de trabalho ### 1. Entenda como o projeto é distribuído Pergunte, ou infira e declare como suposição: - Licença do projeto: proprietária (código fechado) ou uma licença de código aberto (ID SPDX). - Distribuição: `saas` (usuários só acessam pela rede), `distributed` (aplicativos mobile ou desktop, instalações on-prem, dispositivos, SDKs, bibliotecas publicadas, imagens de contêiner entregues a clientes) ou `internal` (apenas funcionários usam). - Qualquer política da empresa: licenças sempre permitidas e sempre bloqueadas. A mesma dependência pode ser aceitável para um modelo e um bloqueio para outro, então nunca pule esta etapa. ### 2. Obtenha o inventário de dependências Prefira a saída de uma ferramenta a uma lista feita à mão, e inclua dependências transitivas: - Python: `pip-licenses --format=json --with-system` (ou `pip-licenses --format=csv`). - Node.js: `npx license-checker --json --production` (execute novamente sem `--production` para ferramentas de desenvolvimento). - Qualquer outra coisa: um CSV com `name,version,license,scope` exportado de um SBOM ou do gerenciador de pacotes. Marque ferramentas de build, teste e lint como `scope=dev`; elas não são distribuídas, então seus achados caem para INFO. ### 3. Execute a auditoria ```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 ``` O script imprime contagens de severidade, uma tabela de cada pacote com sua licença e categoria normalizadas, achados com motivos, mudanças de licença entre versões e as licenças que precisam de texto em seus avisos de terceiros. Código de saída 1 significa pelo menos um achado HIGH. Se você não puder executar o script, classifique cada licença manualmente com `references/license-categories.md` e diga isso no relatório. ### 4. Verifique antes de concluir - Para cada achado HIGH ou WARN, verifique o arquivo LICENSE real do pacote em seu repositório ou arquivo do pacote; os metadados geralmente estão errados ou desatualizados. - Para entradas "missing" ou "SEE LICENSE IN", abra o arquivo referenciado e classifique-o pelo seu texto. - Para pacotes com licença dupla (OR), registre qual opção você escolheu. - Para pacotes cuja licença mudou entre versões, confirme qual versão está realmente instalada (arquivo de lock). ### 5. Recomende e relate Use `references/remediation-playbook.md` para dar uma ação concreta por achado: substituir, isolar, fixar uma versão permissiva mais antiga, obter uma licença comercial, cumprir (publicar fonte ou adicionar avisos) ou aceitar com um motivo registrado. Preencha `templates/license-audit-report.md`, como em `examples/example-saas-audit.md`. ## Regras - Nunca diga que uma licença "é aceitável" ou "é ilegal" como conclusão jurídica. Diga o que a licença geralmente exige e quando consultar um advogado. - Não adivinhe uma licença pelo nome ou popularidade do pacote. Desconhecido permanece desconhecido até que alguém leia o texto da licença. - Avalie o risco para o modelo de distribuição do usuário e observe o que mudaria se o modelo mudasse (por exemplo, SaaS hoje, on-prem no próximo ano). - Mantenha o relatório curto: bloqueios primeiro, depois obrigações (avisos, ofertas de fonte), depois a lista limpa como uma contagem. - Trate a auditoria como um instantâneo; recomende executá-la no CI a cada mudança de dependência. FILE:references/license-categories.md # Categorias de licenças na prática A severidade no script depende da categoria e de como o projeto é distribuído. Esta página explica as categorias em linguagem simples. É um resumo de engenharia, não aconselhamento jurídico. ## Domínio público e quase domínio público SPDX: `0BSD`, `Unlicense`, `CC0-1.0`, `MIT-0`, `WTFPL` - Sem obrigações significativas. Manter um aviso ainda é uma boa prática. ## Permissiva 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` (dados como caniuse-lite) - Use, modifique e distribua em produtos de código fechado. - Mantenha o aviso de direitos autorais e o texto da licença, geralmente em um arquivo de avisos de terceiros ou em uma tela "Sobre / Licenças". - Apache-2.0 também exige manter qualquer arquivo NOTICE, declarar mudanças significativas em seus arquivos e inclui uma concessão de patente que termina se você processar por patentes nesse código. - BSD-3-Clause proíbe usar os nomes dos autores para promover seu produto. ## Copyleft fraco (nível de arquivo ou biblioteca) SPDX: `LGPL-2.1-*`, `LGPL-3.0-*`, `MPL-2.0`, `EPL-1.0`, `EPL-2.0`, `CDDL-1.0`, `CDDL-1.1`, GPL com exceção de linking (por exemplo `GPL-2.0-only WITH Classpath-exception-2.0`) - Você pode combiná-los com código de código fechado, mas mudanças nos próprios arquivos da biblioteca devem ser compartilhadas sob a mesma licença quando você distribuir. - LGPL: os usuários devem poder substituir a biblioteca pela sua própria versão. Linking dinâmico (biblioteca compartilhada, jar separado, pacote npm) normalmente torna isso fácil; linking estático ou empacotamento em um único binário precisa de cuidado. - MPL-2.0: o copyleft se aplica por arquivo. Mantenha os arquivos MPL separados e publique mudanças nesses arquivos. ## Copyleft forte SPDX: `GPL-2.0-only`, `GPL-2.0-or-later`, `GPL-3.0-only`, `GPL-3.0-or-later` - Se você distribuir software que inclui ou faz link com código GPL, o trabalho combinado deve ser oferecido sob a GPL com seu código-fonte. - Executar software GPL em seus próprios servidores e permitir que usuários o acessem pela rede não é distribuição, então o uso puro de SaaS geralmente é permitido. Distribuir um aplicativo, um instalador on-prem, um firmware de dispositivo ou uma imagem de contêiner para clientes é distribuição. - A versão importa: código `GPL-2.0-only` não pode ser combinado com código GPL-3.0 ou Apache-2.0 em um único trabalho. `-or-later` dá mais espaço. ## Copyleft de rede SPDX: `AGPL-3.0-only`, `AGPL-3.0-or-later`, `SSPL-1.0` (não aprovado pela OSI) - Como a GPL, mais: se os usuários interagirem com uma versão modificada pela rede, você deve oferecer a eles seu código-fonte. - Para um produto SaaS de código fechado, este é o bloqueio mais comum. Até o uso não modificado aciona revisões internas na maioria das empresas. - SSPL vai além: oferecer o software como serviço pode exigir a liberação do código-fonte de toda a sua pilha de serviços. ## Source-available e não comercial SPDX ou nomes: `BUSL-1.1` (Business Source License), `Elastic-2.0`, adições Commons Clause, `CC-BY-NC-*`, `PolyForm-Noncommercial-1.0.0` - O código-fonte é visível, mas o uso é restrito: muitas vezes nenhum serviço hospedado concorrente, nenhum uso em produção acima de um limite, ou nenhum uso comercial, às vezes até uma data de mudança. - Sempre precisa de uma revisão dos termos exatos. Muitas dessas licenças mudam para uma licença aberta após alguns anos, então uma versão mais antiga ou mais nova pode ser diferente. ## Desconhecida - Campos de licença vazios, `UNKNOWN`, `NOASSERTION`, `UNLICENSED`, `SEE LICENSE IN <file>`, `LicenseRef-*` ou um nome que o script não reconhece. - "Sem licença" significa todos os direitos reservados: você não tem permissão para usá-lo. Encontre os termos reais ou substitua o pacote. ## Expressões - `MIT OR Apache-2.0`: você pode escolher qualquer um; escolha o que for mais adequado e registre a escolha. - `MIT AND BSD-3-Clause`: você deve seguir ambos; o mais restritivo define a categoria. - `GPL-2.0-only WITH <exception>`: a exceção estreita o copyleft; leia-a. FILE:references/remediation-playbook.md # Manual de remediação Escolha uma ação por achado e anote-a com um responsável. Prefira a ação mais barata que remova o risco. ## Ordem de trabalho 1. Licenças desconhecidas e ausentes (você pode não ter o direito de usar o código). 2. Copyleft de rede e pacotes source-available em código distribuído ou hospedado. 3. Copyleft forte em produtos distribuídos e incompatibilidades de versão em projetos de código aberto. 4. Obrigações de copyleft fraco (linking, publicação de mudanças). 5. Avisos para todo o resto. ## Ações ### Encontre a licença real - Abra o repositório e o arquivo do pacote, leia LICENSE, COPYING ou o cabeçalho do arquivo de código-fonte principal. - Verifique se os metadados estão apenas desatualizados (um caso comum para pacotes antigos e forks). - Se realmente não houver licença, peça ao mantenedor para adicionar uma (link para choosealicense.com no pedido) ou substitua o pacote. ### Substitua o pacote - Procure uma alternativa mantida com licença permissiva e API semelhante. - Estime o esforço em horas e adicione-o ao backlog com a release que ele bloqueia. - Para dependências transitivas, procure a dependência direta que as traz; uma versão diferente ou um flag de configuração geralmente as remove. ### Fixe uma versão sob a licença antiga - Quando um projeto mudou de uma licença permissiva para source-available (ou o contrário), as versões antigas mantêm sua licença antiga. - Fixar é uma correção de curto prazo: sem atualizações de segurança. Agende uma substituição adequada. ### Isole-o - Execute a ferramenta copyleft como um processo ou serviço separado que fala com seu código por linha de comando, HTTP ou uma fila, em vez de fazer link com seu código. - Mantenha-o não modificado, se possível. Documente o limite. - Esta é uma maneira comum de usar ferramentas de linha de comando GPL, mas peça confirmação jurídica para AGPL e SSPL. ### Compre uma licença comercial - Muitos projetos com licença dupla (código aberto mais comercial) vendem uma licença que remove as obrigações de copyleft. Anote o preço e os termos de renovação. ### Cumpra - Copyleft fraco: mantenha a biblioteca como um arquivo ou pacote separado e substituível; publique quaisquer mudanças em seus próprios arquivos; inclua sua licença. - GPL em um produto distribuído: somente se todo o produto puder ser de código aberto sob termos compatíveis. Esta é uma decisão de negócio, não de engenharia. - AGPL em um serviço hospedado: ofereça o código-fonte correspondente aos usuários do serviço, incluindo suas mudanças. ### Aceite com um motivo registrado - Ferramentas apenas de desenvolvimento (linters, executores de teste, ferramentas de build) que nunca são distribuídas geralmente são aceitáveis. Registre "apenas dev, não distribuído". - Uso interno apenas de ferramentas GPL ou AGPL geralmente é aceitável. Registre quem as usa e que não são expostas a usuários externos. ## Avisos de terceiros - Gere um arquivo de avisos a partir do inventário: pacote, versão, licença, linha de direitos autorais, texto completo da licença (uma cópia por texto de licença distinto). - Inclua arquivos NOTICE do Apache como estão. - Distribua-o dentro de aplicativos ("Configurações > Sobre > Licenças"), instaladores e imagens de contêiner; para SaaS, link-o no rodapé ou na documentação. ## Mantenha limpo - Execute a auditoria no CI a cada mudança em um arquivo de lock e falhe o build em HIGH. - Mantenha uma lista de permitidos (por exemplo MIT, BSD, ISC, Apache-2.0) e uma lista de bloqueados (por exemplo AGPL-3.0, SSPL-1.0, BUSL-1.1) acordadas com o jurídico, e passe-as com `--allow` e `--deny`. FILE:templates/license-audit-report.md # Auditoria de Licenças de Dependências: <nome do projeto> Data: <AAAA-MM-DD> Licença do projeto: <proprietária | ID SPDX> Distribuição: <saas | distributed | internal> (<uma linha: como os usuários obtêm o software>) Fonte do inventário: <ferramenta e comando, commit do arquivo de lock> Pacotes auditados: <n> (<runtime n>, <dev n>) ## Resumo - Bloqueios (HIGH): <n> - <uma linha nomeando-os> - Obrigações (WARN): <n> - <uma linha> - Notas (INFO): <n> - Limpos (OK): <n> - Veredito: <pronto para distribuir | distribuir após correções | bloqueado> porque <motivo>. ## Bloqueios | Pacote | Versão | Licença (como encontrada) | Categoria | Por que importa para nós | Ação | Responsável | Prazo | |---|---|---|---|---|---|---|---| | | | | | | | | | ## Obrigações | Pacote | Licença | O que devemos fazer | Feito? | |---|---|---|---| | | | | | ## Verificado manualmente - <pacote>: os metadados diziam <x>, o arquivo LICENSE diz <y>. ## Decisões e suposições - Pacotes com licença dupla e a opção escolhida: <pacote: opção> - Riscos aceitos com motivo: <pacote: motivo, aprovado por> - Suposições: <por exemplo "o painel administrativo é apenas interno"> ## O que muda se mudarmos a forma de distribuir - <por exemplo: "Se distribuirmos uma versão on-prem, readline-sync (GPL-3.0) se torna um bloqueio."> ## Avisos de terceiros Licenças que precisam de texto completo no arquivo de avisos: <lista> Localização do arquivo de avisos: <caminho ou URL> ## Próxima auditoria Execute no CI a cada mudança no arquivo de lock; revisão completa antes da <próxima release ou data>. _Este relatório é uma triagem de engenharia, não aconselhamento jurídico. Os bloqueios devem ser confirmados pelo jurídico antes de confiar em qualquer exceção._ FILE:examples/example-saas-audit.md # Exemplo: auditoria de licenças de um backend SaaS Node.js ## O pedido "Somos um SaaS B2B de código fechado. Clientes empresariais enviam questionários de segurança perguntando sobre licenças de código aberto, e no próximo ano podemos vender uma versão on-prem. Aqui está nossa exportação de dependências. Algo com que devemos nos preocupar?" Inventário (`deps.csv`, exportado do arquivo de lock, 18 linhas): ``` 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 ``` ## Execução do 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 saída 1 (achados HIGH). ## O relatório (template preenchido, resumido) # Auditoria de Licenças de Dependências: acme-api Licença do projeto: proprietária. Distribuição: saas (os clientes usam o aplicativo web e a API REST; nada é instalado em suas máquinas hoje). Pacotes auditados: 18 (16 runtime, 2 dev). ### Resumo - Bloqueios (HIGH): 5 - dois pacotes sem licença utilizável, dois pacotes source-available, um pacote AGPL. - Obrigações (WARN): 1 - um pacote GPL-3.0 que é aceitável para SaaS, mas bloqueia a versão on-prem planejada. - Veredito: distribuir após correções. Nada aqui é incomum, e cada item tem uma correção barata. ### Bloqueios | Pacote | Licença (como encontrada) | Por que importa para nós | Ação | |---|---|---|---| | chart-kit-pro 1.4.0 | SEE LICENSE IN LICENSE.md | O LICENSE.md é uma licença comercial que permite o uso apenas com uma chave paga. Não temos chave registrada. | Confirme com a equipe que a comprou, ou substitua por uma biblioteca de gráficos permissiva. | | fast-csv-lite 0.9.0 | (missing) | Sem licença significa sem permissão de uso. | Peça ao mantenedor para adicionar uma licença; enquanto isso, mude para um parser CSV mantido com termos MIT. | | redis-om 0.4.3 | BUSL-1.1 | A Business Source License limita o uso em produção até sua data de mudança; os termos variam por projeto. | Leia a "Additional Use Grant" exata. Se proibir nosso uso, substitua ou compre uma licença. | | search-client 8.1.0 | Elastic-2.0 | A Elastic License 2.0 proíbe oferecer o software como serviço gerenciado. A versão 7.10.2 na mesma árvore é Apache-2.0. | Verifique o arquivo de lock: quais módulos importam 8.1.0? Fixe ambos na linha Apache-2.0 ou mude para um fork licenciado Apache. | | ghostscript-wrapper 2.1.0 | AGPL-3.0-only | Renderizamos PDFs de clientes com ele dentro da API; o uso de rede AGPL exige oferecer o código-fonte aos usuários. | Mova a renderização de PDF para um serviço worker separado e não modificado e peça confirmação jurídica, ou compre a licença comercial. | ### Obrigações | Pacote | Licença | O que devemos fazer | |---|---|---| | readline-sync 1.4.10 | GPL-3.0-only | Aceitável para SaaS. Torna-se um bloqueio se a versão on-prem for distribuída com ele. Ele é usado apenas por um script CLI administrativo, então remova-o da imagem do servidor. | | mariadb-connector 3.3.1 | LGPL-2.1-or-later | Mantenha-o como uma dependência npm normal (substituível); publique mudanças se algum dia o corrigirmos. | ### Verificado manualmente - dompurify: licença dupla `MPL-2.0 OR Apache-2.0`; escolhemos Apache-2.0. - eslint-plugin-gpl-thing: apenas dev, nunca distribuído. Aceito. ### O que muda se distribuirmos on-prem - readline-sync (GPL-3.0) torna-se HIGH, mariadb-connector (LGPL) precisa da verificação de substituibilidade, e pacotes Apache-2.0 precisam de seus arquivos NOTICE no instalador. Execute novamente com `--distribution distributed` antes dessa release. ### Avisos de terceiros Licenças que precisam de texto completo: Apache-2.0, LGPL-2.1-or-later, MIT, Python-2.0. Link da página de avisos no rodapé do aplicativo. _Triagem de engenharia, não aconselhamento jurídico._ ## Por que esta é uma boa resposta - Avalia cada pacote para o modelo de distribuição real desta empresa e mostra o que muda com o plano on-prem. - Verifica os fatos que os metadados não mostram (o LICENSE.md comercial, as versões duplicadas do search-client). - Cada bloqueio tem uma ação concreta e barata. FILE:scripts/audit_licenses.py #!/usr/bin/env python3 """Audita licenças de dependências de terceiros contra como um projeto é distribuído. 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 automaticamente): CSV com cabeçalho: name,version,license[,scope] (scope: runtime | dev; padrão 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"}, ...} Opções: --project-license a licença do seu projeto: proprietary (padrão) ou um ID SPDX como MIT, Apache-2.0, GPL-3.0-only --distribution saas (serviço de rede, padrão) | distributed (aplicativos, binários, dispositivos, bibliotecas) | internal --allow / --deny IDs SPDX separados por vírgula que sua política sempre permite ou sempre bloqueia --json saída legível por máquina Os nomes de licenças são normalizados para IDs SPDX, expressões com OR escolhem a opção mais permissiva e AND a mais restritiva. Cada pacote recebe uma categoria e uma severidade (OK, INFO, WARN, HIGH) para a distribuição escolhida. Pacotes apenas de desenvolvimento não são distribuídos, então seus achados caem para INFO. Código de saída: 0 sem achados HIGH, 1 com pelo menos um HIGH, 2 erro de uso ou entrada. Esta é uma ferramenta de triagem de engenharia, não aconselhamento jurídico. Apenas biblioteca padrão. """ import argparse import csv import io import json import re import sys ALIASES = { # nomes comuns em 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 = { # prefixo do ID SPDX -> categoria "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): """Remove parênteses que envolvem a expressão inteira: '(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): """Retorna (expressão normalizada, licença escolhida, categoria, exceção) para uma expressão de licença.""" 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: # nomes longos como "... v2 or later (LGPLv2+)" são uma licença 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" # misto: 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", "Licença ausente ou não reconhecida; encontre o texto real da licença antes de distribuir." if cat == "source-available": return "HIGH", "Termos source-available ou não comerciais geralmente restringem uso comercial ou hospedagem; precisa de revisão de licença." if project_spdx.startswith("GPL-2.0-only") and chosen.startswith("Apache-2.0"): return "HIGH", "Apache-2.0 é geralmente considerada incompatível com projetos 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} não pode ser combinada com um projeto GPL-2.0-only; o projeto precisaria de termos GPL-3.0." if re.match(r"GPL-3\.0|AGPL-3\.0", project_spdx) and chosen.startswith("GPL-2.0-only"): return "HIGH", "Código GPL-2.0-only não pode ser relicenciado sob GPL-3.0; peça termos 'or later' ao upstream ou substitua-o." 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 em um projeto GPL executado como serviço: todo o código-fonte do serviço deve ser oferecido aos usuários." return "OK", "Dependência copyleft em um projeto copyleft; verifique a compatibilidade de versão (por exemplo GPL-2.0-only vs GPL-3.0)." if cat == "network-copyleft": if distribution == "internal": return "WARN", "Copyleft de rede: aceitável para uso puramente interno, mas expô-lo a usuários externos aciona obrigações de fonte." return "HIGH", "Copyleft de rede (AGPL/SSPL): oferecer o serviço ou distribuí-lo exige liberar o código-fonte correspondente." if cat == "strong-copyleft": if distribution == "distributed": return "HIGH", "Código GPL distribuído com seu produto exige que o trabalho combinado seja liberado sob a GPL." if distribution == "saas": return "WARN", "A GPL não é acionada apenas pelo uso de rede, mas qualquer distribuição futura (aplicativos, on-prem, SDKs) seria." return "INFO", "GPL para uso interno apenas não tem obrigações de distribuição; mantenha-o fora de qualquer coisa que você distribua." if cat == "weak-copyleft": if distribution == "distributed": return "WARN", "Copyleft fraco: distribua a licença, mantenha a biblioteca substituível (linking dinâmico para LGPL) e publique mudanças em seus arquivos." return "INFO", "Copyleft fraco: publique quaisquer mudanças que você fizer nos próprios arquivos desta biblioteca se distribuí-la mais tarde." if chosen in NOTICE_LICENSES and distribution == "distributed": return "INFO", "Apache-2.0: inclua a licença e qualquer arquivo NOTICE em sua distribuição." return "OK", "Permissiva: mantenha o aviso de direitos autorais e o texto da licença em seus avisos de terceiros." def load(text): stripped = text.lstrip() if not stripped: raise ValueError("entrada vazia") 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("os itens da lista JSON devem 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"CSV precisa de um cabeçalho com pelo menos as colunas 'name' e 'license', obtido: {', '.join(fields) or '(nenhum)'}") 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" A exceção de licença {exc} se aplica; leia seu escopo exato." if chosen in deny: sev, why = "HIGH", f"{chosen} está na sua lista de bloqueados." elif chosen in allow and sev != "OK": sev, why = "OK", f"{chosen} está na sua 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", "Apenas dev (não distribuído): " + 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}: licenças diferentes entre versões ({', '.join(sorted(lics))}); verifique qual versão você distribui.") 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"Auditoria de licenças de dependências: {rep['packages']} pacotes, licença do projeto {rep['project_license']}, distribuição {rep['distribution']}") c = rep["severity_counts"] print(f"HIGH {c['HIGH']} WARN {c['WARN']} INFO {c['INFO']} OK {c['OK']}") print("Categorias: " + ", ".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} {'PACOTE':<{w}} {'LICENÇA':<28} {'CATEGORIA':<17} ESCOPO") 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("\nAchados:") for x in findings: print(f"- [{x['severity']}] {x['name']} ({x['license_raw'] or 'sem campo de licença'}): {x['reason']}") if rep["notes"]: print("\nNotas:") for n in rep["notes"]: print(f"- {n}") if rep["attribution_licenses"]: print("\nOs avisos de terceiros devem incluir textos de licença para: " + ", ".join(rep["attribution_licenses"])) print("\nApenas auxílio de triagem, não aconselhamento jurídico.") def main(): p = argparse.ArgumentParser(description="Audita licenças de dependências.", add_help=True) p.add_argument("path", help="Arquivo CSV ou JSON, ou - 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"erro: arquivo não encontrado: {args.path}", file=sys.stderr) return 2 except (ValueError, json.JSONDecodeError) as e: print(f"erro: {e}", file=sys.stderr) return 2 if not rows: print("erro: nenhuma dependência encontrada na 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())

Entre para ver o prompt completo

Continuar com:

Ao entrar, você concorda com nossos Termos de uso e Política de privacidade

Uso

Este prompt foi projetado para uso com coding. Copie o conteúdo acima e cole na sua ferramenta de IA preferida.

Para melhores resultados, personalize os marcadores (indicados por colchetes ou maiúsculas) com seus requisitos específicos.

Referências

Categorias:coding| prompts.chat| license-audit| dependency-management

Discussão

0 comentários