의존성 라이선스 감사 스킬
Wikiprompt, 무료 프롬프트 백과사전에서
의존성 라이선스 감사 스킬 의존성 라이선스 감사를 위한 완전한 AI 스킬로, 워크플로우, 참고 자료, 템플릿, 예시, 그리고 Python 스크립트를 포함합니다. 이 스킬은 엔지니어링 팀이 릴리스 전에 라이선스 위험을 식별하는 데 도움을 줍니다.
프롬프트 내용저장
🌐
---
name: dependency-license-audit
description: 프로젝트의 실제 배포 방식(SaaS, 배포형 앱 또는 라이브러리, 내부 도구)에 따라 의존성 라이선스를 감사합니다. 라이선스 이름을 SPDX로 정규화하고, OR 및 AND 표현식을 해석하며, 누락, AGPL, GPL, 소스-가능(source-available) 및 비호환 라이선스를 식별하고, 순위가 매겨진 수정 계획과 고지 사항 목록을 작성합니다. 테스트된 표준 라이브러리 Python 감사 도구를 포함합니다.
---
# 의존성 라이선스 감사
릴리스 전, 인수 검토 전, 또는 고객 보안 설문지가 문제를 발견하기 전에 엔지니어링 팀이 의존성에서 라이선스 문제를 찾도록 돕습니다. 원시 의존성 목록을 수정해야 할 짧고 순위가 매겨진 항목 목록으로 바꾸고, 각 항목을 평이한 언어로 설명합니다. 신중한 엔지니어이지 변호사가 아닙니다. 위험을 식별하고 법적 교육을 받은 사람이 결정해야 할 때를 알려줍니다.
## 이 스킬의 파일
- `scripts/audit_licenses.py` - 모든 의존성 라이선스를 정규화하고 분류하며 선택된 배포 모델에 따라 평가합니다 (Python 3 표준 라이브러리만 사용)
- `references/license-categories.md` - 허용적(permissive), 약한 카피레프트(weak copyleft), 강한 카피레프트(strong copyleft), 네트워크 카피레프트(network copyleft), 소스-가능(source-available)이 실제로 의미하는 바와 일반적인 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.
빌드, 테스트, 린트 도구를 `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은 HIGH 발견 사항이 하나 이상 있음을 의미합니다.
스크립트를 실행할 수 없으면 `references/license-categories.md`로 각 라이선스를 수동으로 분류하고 보고서에 그렇게 했다고 명시합니다.
### 4. 결론 내리기 전에 검증
- 모든 HIGH 또는 WARN 발견 사항에 대해 패키지의 저장소 또는 패키지 아카이브에서 실제 LICENSE 파일을 확인합니다. 메타데이터는 종종 잘못되었거나 오래되었습니다.
- "누락" 또는 "SEE LICENSE IN" 항목의 경우 참조된 파일을 열고 텍스트로 분류합니다.
- 이중 라이선스 패키지(OR)의 경우 선택한 옵션을 기록합니다.
- 버전 간 라이선스가 변경된 패키지의 경우 실제로 설치된 버전(잠금 파일)을 확인합니다.
### 5. 권장 및 보고
`references/remediation-playbook.md`를 사용하여 발견 사항별로 하나의 구체적인 조치를 제공합니다: 교체, 격리, 이전 허용 버전 고정, 상용 라이선스 구매, 준수(소스 공개 또는 고지 사항 추가), 또는 기록된 이유와 함께 수용. `examples/example-saas-audit.md`와 같이 `templates/license-audit-report.md`를 작성합니다.
## 규칙
- 라이선스가 "괜찮다" 또는 "불법이다"라고 법적 결론으로 말하지 마십시오. 라이선스가 일반적으로 요구하는 사항과 법률 자문을 받아야 할 때를 말하십시오.
- 패키지 이름이나 인기로 라이선스를 추측하지 마십시오. 누군가 라이선스 텍스트를 읽을 때까지 알 수 없음은 알 수 없음으로 유지됩니다.
- 사용자의 배포 모델에 대한 위험을 평가하고, 모델이 변경되면(예: 오늘 SaaS, 내년 온프레미스) 무엇이 바뀔지 기록합니다.
- 보고서를 간결하게 유지합니다: 차단 요인 먼저, 그 다음 의무 사항(고지 사항, 소스 제공), 그 다음 깨끗한 목록은 개수로.
- 감사를 스냅샷으로 취급합니다. 모든 의존성 변경 시 CI에서 실행할 것을 권장합니다.
FILE:references/license-categories.md
# 실제 라이선스 카테고리
스크립트의 심각도는 카테고리와 프로젝트 배포 방식에 따라 달라집니다. 이 페이지는 카테고리를 평이한 언어로 설명합니다. 엔지니어링 요약이지 법적 조언이 아닙니다.
## 퍼블릭 도메인 및 퍼블릭 도메인에 가까운 경우
SPDX: `0BSD`, `Unlicense`, `CC0-1.0`, `MIT-0`, `WTFPL`
- 의미 있는 의무 없음. 고지 사항을 유지하는 것은 여전히 좋은 관행입니다.
## 허용적(Permissive)
SPDX: `MIT`, `ISC`, `BSD-2-Clause`, `BSD-3-Clause`, `Apache-2.0`, `Zlib`, `BSL-1.0` (Boost), `PSF-2.0`, `Python-2.0`, `CC-BY-4.0` (caniuse-lite와 같은 데이터)
- 폐쇄 소스 제품에서 사용, 수정, 배포 가능.
- 저작권 고지와 라이선스 텍스트를 유지, 일반적으로 타사 고지 파일 또는 "정보 / 라이선스" 화면에 포함.
- 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 코드와 결합할 수 없습니다. `-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. 알 수 없고 누락된 라이선스(코드를 사용할 권리가 전혀 없을 수 있음).
2. 배포되거나 호스팅된 코드의 네트워크 카피레프트 및 소스-가능 패키지.
3. 배포 제품의 강한 카피레프트 및 오픈 소스 프로젝트의 버전 비호환성.
4. 약한 카피레프트 의무(링킹, 변경 사항 공개).
5. 기타 모든 것에 대한 고지 사항.
## 조치
### 실제 라이선스 찾기
- 패키지의 저장소와 아카이브를 열고 LICENSE, COPYING 또는 주요 소스 파일의 헤더를 읽으십시오.
- 메타데이터가 단지 오래된 것인지 확인하십시오(오래된 패키지와 포크의 일반적인 경우).
- 실제로 라이선스가 없으면 유지 관리자에게 추가를 요청하십시오(요청에 choosealicense.com 링크) 또는 패키지를 교체하십시오.
### 패키지 교체
- 허용적 라이선스와 유사한 API를 가진 유지 관리되는 대안을 검색하십시오.
- 노력을 시간으로 추정하고 차단하는 릴리스와 함께 백로그에 추가하십시오.
- 전이적 의존성의 경우 이를 끌어오는 직접 의존성을 찾으십시오. 다른 버전이나 구성 플래그로 종종 제거됩니다.
### 이전 라이선스 버전 고정
- 프로젝트가 허용적 라이선스에서 소스-가능으로 전환한 경우(또는 그 반대), 이전 버전은 이전 라이선스를 유지합니다.
- 고정은 단기 수정입니다: 보안 업데이트 없음. 적절한 교체를 계획하십시오.
### 격리
- 카피레프트 도구를 코드에 링크하는 대신 명령줄, HTTP 또는 큐를 통해 코드와 통신하는 별도 프로세스 또는 서비스로 실행하십시오.
- 가능하면 수정하지 않은 상태로 유지하십시오. 경계를 문서화하십시오.
- GPL 명령줄 도구를 사용하는 일반적인 방법이지만 AGPL 및 SSPL에 대해서는 법률 자문을 받으십시오.
### 상용 라이선스 구매
- 많은 이중 라이선스 프로젝트(오픈 소스 + 상용)가 카피레프트 의무를 제거하는 라이선스를 판매합니다. 가격과 갱신 조건을 기록하십시오.
### 준수
- 약한 카피레프트: 라이브러리를 별도의 교체 가능한 파일 또는 패키지로 유지하고, 자체 파일에 대한 변경 사항을 공개하고, 라이선스를 포함하십시오.
- 배포 제품의 GPL: 전체 제품이 호환 조건으로 오픈 소스가 될 수 있는 경우에만. 이것은 엔지니어링 결정이 아닌 비즈니스 결정입니다.
- 호스팅 서비스의 AGPL: 서비스 사용자에게 해당 소스를 제공하고 변경 사항을 포함하십시오.
### 기록된 이유와 함께 수용
- 배포되지 않는 개발 전용 도구(린터, 테스트 실행기, 빌드 도구)는 일반적으로 괜찮습니다. "개발 전용, 배포 안 함"을 기록하십시오.
- GPL 또는 AGPL 도구의 내부 전용 사용은 일반적으로 괜찮습니다. 사용자와 외부 사용자에게 노출되지 않음을 기록하십시오.
## 타사 고지 사항
- 인벤토리에서 고지 사항 파일 생성: 패키지, 버전, 라이선스, 저작권 줄, 전체 라이선스 텍스트(고유 라이선스 텍스트당 하나의 사본).
- 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> (<한 줄: 사용자가 소프트웨어를 얻는 방법>)
인벤토리 출처: <도구 및 명령, 잠금 파일 커밋>
감사된 패키지: <n> (<런타임 n>, <개발 n>)
## 요약
- 차단 요인 (HIGH): <n> - <한 줄로 이름 지정>
- 의무 사항 (WARN): <n> - <한 줄>
- 참고 사항 (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
프로젝트 라이선스: 독점. 배포 방식: saas (고객은 웹 앱과 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는 변경 날짜까지 프로덕션 사용을 제한하며 조건은 프로젝트마다 다릅니다. | 정확한 "추가 사용 허여"를 읽으십시오. 우리 사용을 금지하면 교체하거나 라이선스를 구매하십시오. |
| 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 버전)을 확인합니다.
- 모든 차단 요인에 하나의 구체적이고 저렴한 조치가 있습니다.
FILE:scripts/audit_licenses.py
#!/usr/bin/env python3
"""Audit third-party dependency licenses against how a project is shipped.
Usage:
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]
Input (detected automatically):
CSV with a header: name,version,license[,scope] (scope: runtime | dev; default runtime)
JSON list: [{"name": "...", "version": "...", "license": "...", "scope": "dev"}]
pip-licenses --format=json: [{"Name": "...", "Version": "...", "License": "..."}]
license-checker --json (npm): {"pkg@1.2.3": {"licenses": "MIT"}, ...}
Options:
--project-license your project's license: proprietary (default) or an SPDX id such as MIT, Apache-2.0, GPL-3.0-only
--distribution saas (network service, default) | distributed (apps, binaries, devices, libraries) | internal
--allow / --deny comma-separated SPDX ids that your policy always allows or always blocks
--json machine-readable output
License names are normalized to SPDX ids, expressions with OR take the most
permissive option and AND takes the most restrictive one. Each package gets a
category and a severity (OK, INFO, WARN, HIGH) for the chosen distribution.
Dev-only packages are not shipped, so their findings drop to INFO.
Exit code: 0 no HIGH findings, 1 at least one HIGH, 2 usage or input error.
This is an engineering triage aid, not legal advice. Standard library only.
"""
import argparse
import csv
import io
import json
import re
import sys
ALIASES = { # lowercased common names -> 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 prefix -> category
"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 parentheses that wrap the whole expression: '(MIT OR X)' -> 'MIT OR X'."""
t = text.strip()
while t.startswith("(") and t.endswith(")"):
depth = 0
for i, ch in enumerate(t):
depth += ch == "("
depth -= ch == ")"
if depth == 0 and i < len(t) - 1:
return t
t = t[1:-1].strip()
return t
def normalize(token):
t = unwrap(token)
if not t:
return ""
key = t.lower()
if key in ALIASES:
return ALIASES[key]
for spdx in CATEGORY:
if key == spdx.lower() or key.startswith(spdx.lower() + "-") or key == spdx.lower() + "+":
return t if key.startswith(spdx.lower() + "-") else spdx
return t
def category_of(spdx):
if spdx.upper().startswith("LICENSEREF") or spdx.upper().startswith("SEE LICENSE"):
return "unknown"
for prefix, cat in sorted(CATEGORY.items(), key=lambda kv: -len(kv[0])):
if spdx == prefix or spdx.startswith(prefix + "-") or spdx.startswith(prefix + "+"):
return cat
return "unknown"
def classify(expr):
"""Return (normalized expression, chosen license, category, exception) for a license expression."""
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: # long names such as "... v2 or later (LGPLv2+)" are one license
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" # mixed: be conservative
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", "License is missing or not recognized; find the real license text before shipping."
if cat == "source-available":
return "HIGH", "Source-available or non-commercial terms usually restrict commercial use or hosting; needs a license review."
if project_spdx.startswith("GPL-2.0-only") and chosen.startswith("Apache-2.0"):
return "HIGH", "Apache-2.0 is generally considered incompatible with GPL-2.0-only projects."
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} cannot be combined with a GPL-2.0-only project; the project would need GPL-3.0 terms."
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 code cannot be relicensed under GPL-3.0; ask upstream for 'or later' terms or replace it."
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 in a GPL project run as a service: the whole service source must be offered to users."
return "OK", "Copyleft dependency in a copyleft project; check version compatibility (for example GPL-2.0-only vs GPL-3.0)."
if cat == "network-copyleft":
if distribution == "internal":
return "WARN", "Network copyleft: fine for purely internal use, but exposing it to outside users triggers source obligations."
return "HIGH", "Network copyleft (AGPL/SSPL): offering the service or shipping it requires releasing the corresponding source."
if cat == "strong-copyleft":
if distribution == "distributed":
return "HIGH", "GPL code distributed with your product requires the combined work to be released under the GPL."
if distribution == "saas":
return "WARN", "GPL is not triggered by network use alone, but any future distribution (apps, on-prem, SDKs) would be."
return "INFO", "GPL for internal use only has no distribution obligations; keep it out of anything you ship."
if cat == "weak-copyleft":
if distribution == "distributed":
return "WARN", "Weak copyleft: ship the license, keep the library replaceable (dynamic linking for LGPL), and publish changes to its files."
return "INFO", "Weak copyleft: publish any changes you make to this library's own files if you distribute it later."
if chosen in NOTICE_LICENSES and distribution == "distributed":
return "INFO", "Apache-2.0: include the license and any NOTICE file in your distribution."
return "OK", "Permissive: keep the copyright notice and license text in your third-party notices."
def load(text):
stripped = text.lstrip()
if not stripped:
raise ValueError("input is empty")
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 list items must be objects")
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 needs a header with at least 'name' and 'license' columns, got: {', '.join(fields) or '(none)'}")
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" License exception {exc} applies; read its exact scope."
if chosen in deny:
sev, why = "HIGH", f"{chosen} is on your deny list."
elif chosen in allow and sev != "OK":
sev, why = "OK", f"{chosen} is on your allow list ({why})"
dev = r["scope"] in ("dev", "development", "test", "build")
if dev and sev in ("HIGH", "WARN") and chosen not in deny:
sev, why = "INFO", "Dev-only (not shipped): " + 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}: different licenses across versions ({', '.join(sorted(lics))}); check which version you ship.")
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"Dependency license audit: {rep['packages']} packages, project license {rep['project_license']}, distribution {rep['distribution']}")
c = rep["severity_counts"]
print(f"HIGH {c['HIGH']} WARN {c['WARN']} INFO {c['INFO']} OK {c['OK']}")
print("Categories: " + ", ".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("\nFindings:")
for x in findings:
print(f"- [{x['severity']}] {x['name']} ({x['license_raw'] or 'no license field'}): {x['reason']}")
if rep["notes"]:
print("\nNotes:")
for n in rep["notes"]:
print(f"- {n}")
if rep["attribution_licenses"]:
print("\nThird-party notices must include license texts for: " + ", ".join(rep["attribution_licenses"]))
print("\nTriage aid only, not legal advice.")
def main():
p = argparse.ArgumentParser(description="Audit dependency licenses.", add_help=True)
p.add_argument("path", help="CSV or JSON file, or - for 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: file not found: {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 dependencies found in the input", 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())
전체 프롬프트를 보려면 로그인하세요
Continue with:
By logging in, you agree to our Terms of Use and Privacy Policy
사용법
이 프롬프트는 coding와 함께 사용하도록 설계되었습니다. 위의 프롬프트 내용을 복사하여 원하는 AI 도구에 붙여넣으세요.
최상의 결과를 얻으려면 자리 표시자(대괄호 또는 대문자로 표시)를 특정 요구 사항으로 사용자 지정할 수 있습니다.
토론
댓글 0개