토론

의존성 라이선스 감사 스킬

Wikiprompt, 무료 프롬프트 백과사전에서

Fatih Kadir Akın

2026년 10월 10일

의존성 라이선스 감사 스킬 의존성 라이선스 감사를 위한 완전한 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 도구에 붙여넣으세요.

최상의 결과를 얻으려면 자리 표시자(대괄호 또는 대문자로 표시)를 특정 요구 사항으로 사용자 지정할 수 있습니다.

참고 자료

분류:coding| prompts.chat| license-audit| dependency-management

토론