依存関係ライセンス監査スキル
フリーのプロンプト百科事典 Wikiprompt より
依存関係ライセンス監査スキル 依存関係ライセンスの監査を行うための完全なAIスキルです。ワークフロー、参考資料、テンプレート、例、Pythonスクリプトを含みます。エンジニアリングチームがリリース前にライセンスリスクを特定するのに役立ちます。
プロンプト内容保存
🌐
---
name: dependency-license-audit
description: プロジェクトの依存関係のライセンスを、実際の提供形態(SaaS、配布アプリ/ライブラリ、社内ツール)に基づいて監査します。ライセンス名をSPDXに正規化し、ORおよびAND式を解決し、欠落、AGPL、GPL、ソース利用可能、非互換ライセンスにフラグを立て、優先順位付けされた修正計画と通知リストを作成します。テスト済みのstdlib Python監査スクリプトが含まれます。
---
# 依存関係ライセンス監査
あなたは、リリース前、買収審査、または顧客からのセキュリティ質問票の前に、エンジニアリングチームが依存関係のライセンス問題を発見するのを支援します。生の依存関係リストを、修正すべき項目の短い優先順位付きリストに変換し、それぞれを平易な言葉で説明します。あなたは慎重なエンジニアであり、弁護士ではありません。リスクにフラグを立て、法的訓練を受けた人間が判断しなければならない場合を伝えます。
## このスキルのファイル
- `scripts/audit_licenses.py` - すべての依存関係ライセンスを正規化および分類し、選択された配布モデルに基づいて評価します(Python 3標準ライブラリのみ)
- `references/license-categories.md` - パーミッシブ、弱いコピーレフト、強いコピーレフト、ネットワークコピーレフト、ソース利用可能が実際に何を意味するか、一般的なSPDX ID付きで説明
- `references/remediation-playbook.md` - パッケージの置き換えから通知の追加まで、各タイプの調査結果を修正する方法
- `templates/license-audit-report.md` - レポートのレイアウト
- `examples/example-saas-audit.md` - Node.js SaaSバックエンドの実施済み監査例
## ワークフロー
### 1. プロジェクトの提供形態を理解する
質問するか、推測して仮定として明記します:
- プロジェクトライセンス:プロプライエタリ(クローズドソース)またはオープンソースライセンス(SPDX ID)。
- 配布形態:`saas`(ユーザーはネットワーク経由でのみアクセス)、`distributed`(モバイル/デスクトップアプリ、オンプレミスインストール、デバイス、SDK、公開ライブラリ、顧客に渡すコンテナイメージ)、または`internal`(従業員のみが使用)。
- 会社の方針:常に許可されるライセンスと常にブロックされるライセンス。
同じ依存関係でも、あるモデルでは問題なく、別のモデルではブロッカーになり得るため、このステップを省略しないでください。
### 2. 依存関係インベントリを取得する
手作りのリストよりもツールの出力を優先し、推移的依存関係を含めます:
- Python: `pip-licenses --format=json --with-system`(または`pip-licenses --format=csv`)。
- Node.js: `npx license-checker --json --production`(開発ツールの場合は`--production`なしで再度実行)。
- その他: SBOMまたはパッケージマネージャーからエクスポートされた`name,version,license,scope`を含むCSV。
ビルド、テスト、lintツールは`scope=dev`としてマークします。これらは出荷されないため、その調査結果はINFOに格下げされます。
### 3. 監査を実行する
```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
```
スクリプトは、重大度カウント、正規化されたライセンスとカテゴリを持つすべてのパッケージのテーブル、理由付きの調査結果、バージョン間のライセンス変更、およびサードパーティ通知にテキストが必要なライセンスを出力します。終了コード1は、少なくとも1つのHIGH調査結果があることを意味します。
スクリプトを実行できない場合は、`references/license-categories.md`を使用して各ライセンスを手動で分類し、その旨をレポートに記載します。
### 4. 結論を出す前に検証する
- すべてのHIGHまたはWARN調査結果について、リポジトリまたはパッケージアーカイブ内のパッケージの実際のLICENSEファイルを確認します。メタデータは間違っているか古いことがよくあります。
- 「missing」または「SEE LICENSE IN」エントリについては、参照されているファイルを開き、そのテキストによって分類します。
- デュアルライセンス(OR)のパッケージについては、選択するオプションを記録します。
- バージョン間でライセンスが変更されたパッケージについては、実際にインストールされているバージョンを確認します(ロックファイル)。
### 5. 推奨と報告
`references/remediation-playbook.md`を使用して、調査結果ごとに1つの具体的なアクションを提供します:置き換え、分離、古いパーミッシブバージョンへの固定、商用ライセンスの取得、準拠(ソース公開または通知の追加)、または理由を記録して受け入れ。`templates/license-audit-report.md`を`examples/example-saas-audit.md`のように記入します。
## ルール
- ライセンスが「問題ない」または「違法」であると法的結論として言わないでください。ライセンスが通常何を要求するか、いつ法務顧問に相談すべきかを伝えてください。
- パッケージ名や人気からライセンスを推測しないでください。誰かがライセンステキストを読むまで、不明は不明のままです。
- ユーザーの配布モデルに基づいてリスクを評価し、モデルが変更された場合(例えば、今日はSaaS、来年はオンプレミス)に何が変わるかをメモします。
- レポートは簡潔に保ちます:最初にブロッカー、次に義務(通知、ソース提供)、最後にクリーンなリストを数として。
- 監査をスナップショットとして扱い、依存関係が変更されるたびにCIで実行することを推奨します。
FILE:references/license-categories.md
# ライセンスカテゴリの実践
スクリプトの重大度は、カテゴリとプロジェクトの提供形態によって異なります。このページでは、カテゴリを平易な言葉で説明します。これはエンジニアリングの要約であり、法的助言ではありません。
## パブリックドメインおよび準パブリックドメイン
SPDX: `0BSD`, `Unlicense`, `CC0-1.0`, `MIT-0`, `WTFPL`
- 意味のある義務はありません。通知を保持することは依然として良い習慣です。
## パーミッシブ
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` (caniuse-liteなどのデータ)
- クローズドソース製品での使用、変更、出荷が可能。
- 著作権表示とライセンステキストを、通常はサードパーティ通知ファイルまたは「About / Licenses」画面に保持します。
- Apache-2.0は、NOTICEファイルの保持、そのファイルへの重要な変更の記載も要求し、そのコードに関する特許で訴訟を起こした場合に終了する特許付与を含みます。
- BSD-3-Clauseは、製品を宣伝するために著者の名前を使用することを禁止しています。
## 弱いコピーレフト(ファイルまたはライブラリレベル)
SPDX: `LGPL-2.1-*`, `LGPL-3.0-*`, `MPL-2.0`, `EPL-1.0`, `EPL-2.0`, `CDDL-1.0`, `CDDL-1.1`、リンク例外付きGPL(例:`GPL-2.0-only WITH Classpath-exception-2.0`)
- クローズドソースコードと組み合わせることはできますが、ライブラリ自身のファイルへの変更は、配布時に同じライセンスで共有する必要があります。
- LGPL: ユーザーはライブラリを独自のバージョンに置き換えられる必要があります。動的リンク(共有ライブラリ、別のjar、npmパッケージ)は通常これを容易にします。静的リンクまたは単一バイナリへのバンドルには注意が必要です。
- MPL-2.0: コピーレフトはファイルごとに適用されます。MPLファイルを分離して保持し、それらのファイルへの変更を公開します。
## 強いコピーレフト
SPDX: `GPL-2.0-only`, `GPL-2.0-or-later`, `GPL-3.0-only`, `GPL-3.0-or-later`
- GPLコードを含むまたはリンクするソフトウェアを配布する場合、結合された作品はGPLとそのソースコードの下で提供されなければなりません。
- 自社サーバーでGPLソフトウェアを実行し、ユーザーがネットワーク経由でアクセスできるようにすることは配布ではないため、純粋なSaaS使用は通常許可されます。アプリ、オンプレミスインストーラー、デバイスファームウェア、またはコンテナイメージを顧客に出荷することは配布です。
- バージョンが重要です:`GPL-2.0-only`コードは、GPL-3.0またはApache-2.0コードと1つの作品に組み合わせることはできません。`-or-later`はより多くの余地を与えます。
## ネットワークコピーレフト
SPDX: `AGPL-3.0-only`, `AGPL-3.0-or-later`, `SSPL-1.0`(OSI承認外)
- GPLと同様に、さらに:ユーザーがネットワーク経由で変更バージョンと対話する場合、そのソースコードを提供する必要があります。
- クローズドソースのSaaS製品にとって、これは最も一般的なブロッカーです。変更していなくても、ほとんどの企業で内部レビューが発生します。
- SSPLはさらに進んでいます:サービスとしてソフトウェアを提供することは、サービススタック全体のソースのリリースを要求する場合があります。
## ソース利用可能および非商用
SPDXまたは名称: `BUSL-1.1` (Business Source License), `Elastic-2.0`, Commons Clauseアドオン, `CC-BY-NC-*`, `PolyForm-Noncommercial-1.0.0`
- ソースは見えますが、使用は制限されています:多くの場合、競合するホスト型サービスの禁止、制限を超える本番使用の禁止、または商業使用の全面禁止(変更日まで)。
- 正確な条件のレビューが常に必要です。これらの多くは数年後にオープンライセンスに切り替わるため、古いまたは新しいバージョンが異なる場合があります。
## 不明
- 空のライセンスフィールド、`UNKNOWN`, `NOASSERTION`, `UNLICENSED`, `SEE LICENSE IN <file>`, `LicenseRef-*`、またはスクリプトが認識しない名前。
- 「ライセンスなし」は全著作権所有を意味します:使用する許可がありません。実際の条件を見つけるか、パッケージを置き換えてください。
## 式
- `MIT OR Apache-2.0`: どちらかを選択できます。自分に合う方を選び、選択を記録します。
- `MIT AND BSD-3-Clause`: 両方に従う必要があります。最も厳しい方がカテゴリを決定します。
- `GPL-2.0-only WITH <exception>`: 例外はコピーレフトを狭めます。それを読んでください。
FILE:references/remediation-playbook.md
# 是正プレイブック
調査結果ごとに1つのアクションを選択し、担当者と一緒に書き留めます。リスクを除去する最も安価なアクションを優先します。
## 作業順序
1. 不明および欠落ライセンス(コードを使用する権利がまったくない可能性があります)。
2. 出荷またはホストされたコード内のネットワークコピーレフトおよびソース利用可能パッケージ。
3. 配布製品内の強いコピーレフト、およびオープンソースプロジェクト内のバージョン非互換性。
4. 弱いコピーレフトの義務(リンク、変更の公開)。
5. その他すべての通知。
## アクション
### 実際のライセンスを見つける
- パッケージのリポジトリとアーカイブを開き、LICENSE、COPYING、またはメインソースファイルのヘッダーを読みます。
- メタデータが単に古いだけかどうかを確認します(古いパッケージやフォークでよくあるケース)。
- 本当にライセンスがない場合は、メンテナーにライセンスを追加するよう依頼するか(リクエストにchoosealicense.comへのリンクを含める)、パッケージを置き換えます。
### パッケージを置き換える
- パーミッシブライセンスと同様のAPIを持つ、メンテナンスされた代替品を検索します。
- 工数を時間単位で見積もり、ブロックするリリースとともにバックログに追加します。
- 推移的依存関係の場合、それを引き込んでいる直接依存関係を探します。別のバージョンまたは設定フラグで削除できることがよくあります。
### 古いライセンスのバージョンに固定する
- プロジェクトがパーミッシブライセンスからソース利用可能(またはその逆)に切り替えた場合、古いバージョンは古いライセンスを保持します。
- 固定は短期的な修正です:セキュリティアップデートはありません。適切な置き換えを計画してください。
### 分離する
- コピーレフトツールを、コードにリンクする代わりに、コマンドライン、HTTP、またはキューを介してコードと通信する別のプロセスまたはサービスとして実行します。
- 可能であれば変更せずに保持します。境界を文書化します。
- これはGPLコマンドラインツールを使用する一般的な方法ですが、AGPLおよびSSPLについては法務顧問に確認してもらいます。
### 商用ライセンスを購入する
- 多くのデュアルライセンスプロジェクト(オープンソース+商用)は、コピーレフトの義務を除去するライセンスを販売しています。価格と更新条件をメモします。
### 準拠する
- 弱いコピーレフト:ライブラリを別の置き換え可能なファイルまたはパッケージとして保持し、そのファイルへの変更を公開し、ライセンスを含めます。
- 配布製品内のGPL:製品全体が互換性のある条件でオープンソースにできる場合のみ。これはビジネス上の決定であり、エンジニアリング上の決定ではありません。
- ホスト型サービス内のAGPL:サービスのユーザーに、変更を含む対応するソースを提供します。
### 理由を記録して受け入れる
- 開発専用ツール(リンター、テストランナー、ビルドツール)が出荷されない場合は、通常問題ありません。「開発専用、配布なし」と記録します。
- GPLまたはAGPLツールの社内専用使用は通常問題ありません。誰が使用し、外部ユーザーに公開されていないことを記録します。
## サードパーティ通知
- インベントリから通知ファイルを生成します:パッケージ、バージョン、ライセンス、著作権行、完全なライセンステキスト(個別のライセンステキストごとに1コピー)。
- Apache NOTICEファイルはそのまま含めます。
- アプリ内(「設定 > バージョン情報 > ライセンス」)、インストーラー、コンテナイメージに出荷します。SaaSの場合は、フッターまたはドキュメントからリンクします。
## クリーンに保つ
- ロックファイルが変更されるたびにCIで監査を実行し、HIGHでビルドを失敗させます。
- 法務顧問と合意した許可リスト(例:MIT、BSD、ISC、Apache-2.0)と拒否リスト(例:AGPL-3.0、SSPL-1.0、BUSL-1.1)を維持し、`--allow`および`--deny`で渡します。
FILE:templates/license-audit-report.md
# 依存関係ライセンス監査: <プロジェクト名>
日付: <YYYY-MM-DD>
プロジェクトライセンス: <proprietary | SPDX ID>
配布形態: <saas | distributed | internal> (<1行:ユーザーがソフトウェアを入手する方法>)
インベントリソース: <ツールとコマンド、ロックファイルコミット>
監査対象パッケージ: <n> (<ランタイム n>, <開発 n>)
## 概要
- ブロッカー (HIGH): <n> - <1行で名前を挙げる>
- 義務 (WARN): <n> - <1行>
- メモ (INFO): <n>
- クリーン (OK): <n>
- 判定: <出荷可能 | 修正後に出荷 | ブロック> 理由: <理由>。
## ブロッカー
| パッケージ | バージョン | ライセンス(検出時) | カテゴリ | 私たちにとって重要な理由 | アクション | 担当者 | 期限 |
|---|---|---|---|---|---|---|---|
| | | | | | | | |
## 義務
| パッケージ | ライセンス | 私たちがしなければならないこと | 完了? |
|---|---|---|---|
| | | | |
## 手動で検証済み
- <パッケージ>: メタデータは<x>と表示、LICENSEファイルは<y>と表示。
## 決定と仮定
- デュアルライセンスパッケージと選択したオプション: <パッケージ: オプション>
- 理由を記録して受け入れたリスク: <パッケージ: 理由、承認者>
- 仮定: <例:「管理ダッシュボードは社内専用です」>
## 提供形態を変更した場合の変更点
- <例:「オンプレミス版を出荷する場合、readline-sync (GPL-3.0) がブロッカーになります。」>
## サードパーティ通知
通知ファイルに全文が必要なライセンス: <リスト>
通知ファイルの場所: <パスまたはURL>
## 次回の監査
ロックファイルが変更されるたびにCIで実行。次のリリースまたは日付の前に完全なレビュー。
_このレポートはエンジニアリングトリアージであり、法的助言ではありません。ブロッカーは、例外に依存する前に法務顧問によって確認されなければなりません。_
FILE:examples/example-saas-audit.md
# 例: Node.js SaaSバックエンドのライセンス監査
## リクエスト
「私たちはクローズドソースのB2B SaaSです。エンタープライズ顧客がオープンソースライセンスについてのセキュリティ質問票を送ってき続けており、来年はオンプレミス版を販売するかもしれません。これが私たちの依存関係エクスポートです。心配すべきことはありますか?」
インベントリ (`deps.csv`、ロックファイルからエクスポート、18行):
```
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
```
## スクリプト実行
```
$ 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.
```
終了コード 1 (HIGH調査結果あり)。
## レポート(記入済みテンプレート、短縮版)
# 依存関係ライセンス監査: acme-api
プロジェクトライセンス: proprietary。配布形態: saas(顧客はWebアプリとREST APIを使用。現在、彼らのマシンには何もインストールされていません)。
監査対象パッケージ: 18 (ランタイム 16, 開発 2)。
### 概要
- ブロッカー (HIGH): 5 - 使用可能なライセンスがない2つのパッケージ、ソース利用可能な2つのパッケージ、AGPLの1つのパッケージ。
- 義務 (WARN): 1 - SaaSでは問題ないが、計画中のオンプレミス版をブロックするGPL-3.0パッケージ。
- 判定: 修正後に出荷。ここには珍しいものはなく、各項目に安価な修正があります。
### ブロッカー
| パッケージ | ライセンス(検出時) | 私たちにとって重要な理由 | アクション |
|---|---|---|---|
| chart-kit-pro 1.4.0 | SEE LICENSE IN LICENSE.md | LICENSE.mdは商用ライセンスであり、有料キーでのみ使用を許可します。記録上のキーはありません。 | 購入したチームに確認するか、パーミッシブなチャートライブラリに置き換えます。 |
| fast-csv-lite 0.9.0 | (missing) | ライセンスがないということは、使用する許可がないことを意味します。 | メンテナーにライセンスの追加を依頼。その間、MIT条件のメンテナンスされたCSVパーサーに切り替えます。 |
| redis-om 0.4.3 | BUSL-1.1 | Business Source Licenseは、変更日まで本番使用を制限します。条件はプロジェクトごとに異なります。 | 正確な「Additional Use Grant」を読んでください。私たちの使用を禁止する場合は、置き換えるかライセンスを購入します。 |
| search-client 8.1.0 | Elastic-2.0 | Elastic License 2.0は、ソフトウェアをマネージドサービスとして提供することを禁止しています。同じツリー内のバージョン7.10.2はApache-2.0です。 | ロックファイルを確認:どのモジュールが8.1.0をインポートしていますか?両方をApache-2.0ラインに固定するか、Apacheライセンスのフォークに移動します。 |
| ghostscript-wrapper 2.1.0 | AGPL-3.0-only | API内で顧客のPDFをレンダリングします。AGPLのネットワーク使用は、ユーザーにソースを提供することを要求します。 | PDFレンダリングを別の変更されていないワーカーサービスに移動し、法務顧問に確認してもらうか、商用ライセンスを購入します。 |
### 義務
| パッケージ | ライセンス | 私たちがしなければならないこと |
|---|---|---|
| readline-sync 1.4.10 | GPL-3.0-only | SaaSでは問題ありません。オンプレミス版に同梱するとブロッカーになります。管理CLIスクリプトでのみ使用されるため、サーバーイメージから削除します。 |
| mariadb-connector 3.3.1 | LGPL-2.1-or-later | 通常のnpm依存関係として保持(置き換え可能)。パッチを適用する場合は変更を公開します。 |
### 手動で検証済み
- dompurify: デュアルライセンス `MPL-2.0 OR Apache-2.0`。Apache-2.0を選択します。
- eslint-plugin-gpl-thing: 開発専用、出荷されません。受け入れ。
### オンプレミスを出荷する場合の変更点
- readline-sync (GPL-3.0) はHIGHになり、mariadb-connector (LGPL) は置き換え可能性のチェックが必要になり、Apache-2.0パッケージはインストーラーにNOTICEファイルが必要になります。そのリリースの前に`--distribution distributed`で再実行してください。
### サードパーティ通知
全文が必要なライセンス: Apache-2.0, LGPL-2.1-or-later, MIT, Python-2.0。アプリのフッターから通知ページにリンクします。
_エンジニアリングトリアージであり、法的助言ではありません。_
## これが良い回答である理由
- すべてのパッケージをこの会社の実際の配布モデルに基づいて評価し、オンプレミス計画で何が変わるかを示しています。
- メタデータでは表示できない事実(商用LICENSE.md、重複するsearch-clientバージョン)を確認しています。
- すべてのブロッカーに、具体的で安価なアクションが1つあります。
FILE:scripts/audit_licenses.py
#!/usr/bin/env python3
"""プロジェクトの提供形態に照らしてサードパーティ依存関係ライセンスを監査します。
使用法:
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]
入力(自動検出):
ヘッダー付きCSV: name,version,license[,scope] (scope: runtime | dev; デフォルト runtime)
JSONリスト: [{"name": "...", "version": "...", "license": "...", "scope": "dev"}]
pip-licenses --format=json: [{"Name": "...", "Version": "...", "License": "..."}]
license-checker --json (npm): {"pkg@1.2.3": {"licenses": "MIT"}, ...}
オプション:
--project-license プロジェクトのライセンス: proprietary(デフォルト)またはSPDX ID(MIT、Apache-2.0、GPL-3.0-onlyなど)
--distribution saas(ネットワークサービス、デフォルト)| distributed(アプリ、バイナリ、デバイス、ライブラリ)| internal
--allow / --deny ポリシーで常に許可または常にブロックするカンマ区切りのSPDX ID
--json 機械可読な出力
ライセンス名はSPDX IDに正規化され、ORを含む式は最もパーミッシブなオプションを採用し、ANDは最も制限的なオプションを採用します。各パッケージには、選択された配布形態に応じてカテゴリと重大度(OK、INFO、WARN、HIGH)が割り当てられます。開発専用パッケージは出荷されないため、その調査結果はINFOに格下げされます。
終了コード: 0 HIGH調査結果なし、1 HIGHが少なくとも1つ、2 使用法または入力エラー。
これはエンジニアリングトリアージ支援であり、法的助言ではありません。標準ライブラリのみ。
"""
import argparse
import csv
import io
import json
import re
import sys
ALIASES = { # 小文字の一般的な名前 -> SPDX ID
"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 = { # SPDX IDプレフィックス -> カテゴリ
"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):
"""式全体を包む括弧を削除します: '(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):
"""ライセンス式に対して (正規化された式, 選択されたライセンス, カテゴリ, 例外) を返します。"""
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: # "... v2 or later (LGPLv2+)" のような長い名前は1つのライセンス
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" # 混合: 保守的に
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", "ライセンスが欠落しているか認識できません。出荷前に実際のライセンステキストを見つけてください。"
if cat == "source-available":
return "HIGH", "ソース利用可能または非商用の条件は通常、商用利用またはホスティングを制限します。ライセンスのレビューが必要です。"
if project_spdx.startswith("GPL-2.0-only") and chosen.startswith("Apache-2.0"):
return "HIGH", "Apache-2.0は一般に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} はGPL-2.0-onlyプロジェクトと組み合わせることはできません。プロジェクトはGPL-3.0条件が必要になります。"
if re.match(r"GPL-3\.0|AGPL-3\.0", project_spdx) and chosen.startswith("GPL-2.0-only"):
return "HIGH", "GPL-2.0-onlyコードはGPL-3.0の下で再ライセンスできません。上流に「or later」条件を依頼するか、置き換えてください。"
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", "サービスとして実行されるGPLプロジェクト内のAGPL/SSPL: サービス全体のソースをユーザーに提供する必要があります。"
return "OK", "コピーレフトプロジェクト内のコピーレフト依存関係。バージョンの互換性を確認してください(例:GPL-2.0-only vs GPL-3.0)。"
if cat == "network-copyleft":
if distribution == "internal":
return "WARN", "ネットワークコピーレフト: 純粋に社内利用では問題ありませんが、外部ユーザーに公開するとソース提供義務が発生します。"
return "HIGH", "ネットワークコピーレフト (AGPL/SSPL): サービスを提供または出荷するには、対応するソースの公開が必要です。"
if cat == "strong-copyleft":
if distribution == "distributed":
return "HIGH", "製品とともに配布されるGPLコードは、結合された作品をGPLの下で公開することを要求します。"
if distribution == "saas":
return "WARN", "GPLはネットワーク使用のみでは発動しませんが、将来の配布(アプリ、オンプレミス、SDK)では発動します。"
return "INFO", "社内専用のGPLには配布義務はありません。出荷するものからは除外してください。"
if cat == "weak-copyleft":
if distribution == "distributed":
return "WARN", "弱いコピーレフト: ライセンスを同梱し、ライブラリを置き換え可能に保ち(LGPLの動的リンク)、そのファイルへの変更を公開してください。"
return "INFO", "弱いコピーレフト: 後で配布する場合、このライブラリ自身のファイルに加えた変更を公開してください。"
if chosen in NOTICE_LICENSES and distribution == "distributed":
return "INFO", "Apache-2.0: 配布物にライセンスとNOTICEファイルを含めてください。"
return "OK", "パーミッシブ: サードパーティ通知に著作権表示とライセンステキストを保持してください。"
def load(text):
stripped = text.lstrip()
if not stripped:
raise ValueError("入力が空です")
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("JSONリストの項目はオブジェクトである必要があります")
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には少なくとも'name'と'license'列を含むヘッダーが必要です。取得: {', '.join(fields) or '(なし)'}")
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" ライセンス例外 {exc} が適用されます。その正確な範囲を読んでください。"
if chosen in deny:
sev, why = "HIGH", f"{chosen} は拒否リストにあります。"
elif chosen in allow and sev != "OK":
sev, why = "OK", f"{chosen} は許可リストにあります ({why})"
dev = r["scope"] in ("dev", "development", "test", "build")
if dev and sev in ("HIGH", "WARN") and chosen not in deny:
sev, why = "INFO", "開発専用(出荷されません): " + 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}: バージョン間でライセンスが異なります ({', '.join(sorted(lics))})。出荷するバージョンを確認してください。")
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"依存関係ライセンス監査: {rep['packages']} パッケージ、プロジェクトライセンス {rep['project_license']}、配布形態 {rep['distribution']}")
c = rep["severity_counts"]
print(f"HIGH {c['HIGH']} WARN {c['WARN']} INFO {c['INFO']} OK {c['OK']}")
print("カテゴリ: " + ", ".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} {'PACKAGE':<{w}} {'LICENSE':<28} {'CATEGORY':<17} SCOPE")
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("\n調査結果:")
for x in findings:
print(f"- [{x['severity']}] {x['name']} ({x['license_raw'] or 'ライセンスフィールドなし'}): {x['reason']}")
if rep["notes"]:
print("\nメモ:")
for n in rep["notes"]:
print(f"- {n}")
if rep["attribution_licenses"]:
print("\nサードパーティ通知には以下のライセンステキストを含める必要があります: " + ", ".join(rep["attribution_licenses"]))
print("\nトリアージ支援のみであり、法的助言ではありません。")
def main():
p = argparse.ArgumentParser(description="依存関係ライセンスを監査します。", add_help=True)
p.add_argument("path", help="CSVまたはJSONファイル、または標準入力の場合は-")
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"エラー: ファイルが見つかりません: {args.path}", file=sys.stderr)
return 2
except (ValueError, json.JSONDecodeError) as e:
print(f"エラー: {e}", file=sys.stderr)
return 2
if not rows:
print("エラー: 入力に依存関係が見つかりません", 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())
ログインして完全なプロンプトを表示
次で続行:
ログインすると、次に同意したことになります: 利用規約 と プライバシーポリシー
使い方
このプロンプトは coding 向けに設計されています。上の内容をコピーして、お好みの AI ツールに貼り付けてください。
最良の結果を得るには、プレースホルダー(角括弧や大文字で示された部分)を具体的な要件に置き換えてください。
ノート
0 件のコメント