diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1478ee5f2..cb91e6ea6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -132,6 +132,7 @@ jobs: copilot-plugin: ${{ steps.filter.outputs.copilot-plugin }} operator-chart: ${{ steps.filter.outputs.operator-chart }} desktop: ${{ steps.filter.outputs.desktop }} + licenses: ${{ steps.filter.outputs.licenses }} calico-cni: ${{ steps.filter.outputs.calico-cni }} steps: - name: πŸ“„ Checkout @@ -258,6 +259,19 @@ jobs: - 'go.mod' - 'go.sum' - '.github/workflows/ci.yaml' + licenses: + # THIRD_PARTY_LICENSES tracks BOTH module graphs; anything that + # changes either graph (or the generator itself) can drift the + # inventory, so regenerate-and-self-heal on those paths (#5716). + - 'go.mod' + - 'go.sum' + - 'desktop/go.mod' + - 'desktop/go.sum' + - 'scripts/gen-third-party-licenses/**' + - 'THIRD_PARTY_LICENSES' + # The verify-licenses job's own behaviour lives in this file + # (same pattern as the go filter above). + - '.github/workflows/ci.yaml' calico-cni: # The Calico installer derives the tigera-operator chart version # from the calico/node image tag in this Dockerfile (see @@ -499,12 +513,12 @@ jobs: name: πŸ“€ Auto-Commit Generated Changes runs-on: ubuntu-latest timeout-minutes: 15 - needs: [changes, generate, verify-desktop-tidy] + needs: [changes, generate, verify-desktop-tidy, verify-licenses] if: >- always() && github.event_name != 'merge_group' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) - && (needs.generate.result == 'success' || needs.verify-desktop-tidy.result == 'success') + && (needs.generate.result == 'success' || needs.verify-desktop-tidy.result == 'success' || needs.verify-licenses.result == 'success') permissions: contents: write pull-requests: write @@ -1289,6 +1303,70 @@ jobs: exit 1 fi + verify-licenses: + name: πŸ“œ Verify Third-Party License Inventory + runs-on: ubuntu-latest + timeout-minutes: 20 + needs: [changes] + if: github.event_name != 'merge_group' && needs.changes.outputs.licenses == 'true' + permissions: + contents: read + steps: + - name: πŸ“„ Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + + - name: βš™οΈ Setup Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version-file: go.mod + + - name: πŸ“¦ Install go-licenses + run: go install github.com/google/go-licenses/v2@v2.0.1 + + # THIRD_PARTY_LICENSES is generated from BOTH Go module graphs (root + + # desktop/) by scripts/gen-third-party-licenses, which emits a + # deterministic document (no timestamp), so any diff after regeneration + # is real inventory drift. Self-heal the same way generated files are + # synced: regenerate, capture the diff as a patch artifact, and let the + # πŸ“€ Auto-Commit Generated Changes job apply it to the PR branch (#5716). + # Fork PRs β€” which that job cannot push to β€” hard-fail with guidance. + # The generator also fails here when a NEW dependency has no license + # file bundled and no manual verification recorded in + # scripts/gen-third-party-licenses/verified_unknown.go. + - name: πŸ“œ Regenerate license inventory + run: make licenses + + - name: πŸ“€ Upload patch + run: | + git add -N . + git diff > /tmp/licenses.patch + if [ -s /tmp/licenses.patch ]; then + echo "License inventory drift detected; patch will be auto-committed on same-repo PRs." + else + echo "License inventory already up to date." + fi + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: licenses-patch + path: /tmp/licenses.patch + if-no-files-found: ignore + retention-days: 1 + + - name: ❌ Fail if license inventory is out of date (fork PR) + if: >- + github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name != github.repository + shell: bash + run: | + if [ -s /tmp/licenses.patch ]; then + echo "::error::THIRD_PARTY_LICENSES is out of date. Please run 'make licenses' locally and commit the result." + exit 1 + fi + validate-calico-chart: name: πŸ” Validate Calico Chart Version runs-on: ubuntu-latest @@ -1366,6 +1444,7 @@ jobs: operator-chart-lint, operator-chart-e2e, verify-desktop-tidy, + verify-licenses, validate-calico-chart, ] if: ${{ always() }} @@ -1393,4 +1472,5 @@ jobs: ${{ needs.operator-chart-lint.result }} ${{ needs.operator-chart-e2e.result }} ${{ needs.verify-desktop-tidy.result }} + ${{ needs.verify-licenses.result }} ${{ needs.validate-calico-chart.result }} diff --git a/AGENTS.md b/AGENTS.md index efdac396e..0b017838b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -376,7 +376,10 @@ dependency order and is THE regeneration command. The artifacts, for reference: `docs/src/content/docs/configuration/declarative-configuration.mdx` (`go generate ./docs/...`), `pkg/svc/chat/docs_generated.go` (`go generate ./pkg/svc/chat/...`, after docs), `mocks.go` files (`mockery`), and `web/ui/src/generated/ksail-config.ts` (`npm --prefix web/ui run gen:types`, -after schemas). See also `.github/instructions/`. +after schemas). `THIRD_PARTY_LICENSES` is generated separately by `make licenses` +(scripts/gen-third-party-licenses; deterministic output, CI self-heals drift on module-graph +changes β€” a new dependency without a bundled license file must be manually verified and recorded +in `scripts/gen-third-party-licenses/verified_unknown.go`). See also `.github/instructions/`. **Shared machine / autonomous worktrees:** only create/inspect/delete clusters you created; build throwaway binaries to `/tmp` (not `./ksail`) to avoid polluting the worktree. Maintainers building locally should still use the standard `make build` (`go build -o diff --git a/Makefile b/Makefile index f8597951f..3089876b0 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ SHELL := /bin/bash DESKTOP_DIR := desktop VERSION ?= $(shell git describe --tags --always 2>/dev/null | sed 's/^v//' || echo dev) -.PHONY: help ui build test desktop desktop-app generate +.PHONY: help ui build test desktop desktop-app generate licenses help: ## Show available targets. @grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ @@ -25,6 +25,9 @@ desktop-app: ui ## Build the macOS KSail.app bundle (macOS only); output: ./KSai test: ## Run the Go unit tests. go test ./... +licenses: ## Regenerate THIRD_PARTY_LICENSES from both Go module graphs (root + desktop/). Requires go-licenses (go install github.com/google/go-licenses/v2@v2.0.1). + go run ./scripts/gen-third-party-licenses + generate: ## Regenerate ALL generated artifacts (JSON schema, CRD/deepcopy, reference docs, chat docs, mocks, web UI types). Ordering matters: schema before web UI types, docs before chat docs. go generate ./schemas/... ./pkg/apis/... go generate ./docs/... diff --git a/THIRD_PARTY_LICENSES b/THIRD_PARTY_LICENSES index c219850d4..7a0595398 100644 --- a/THIRD_PARTY_LICENSES +++ b/THIRD_PARTY_LICENSES @@ -10,55 +10,36 @@ KSail itself is licensed under the PolyForm Shield License GENERATION PROVENANCE ------------------------------------------------------------ -Generated by: go-licenses (github.com/google/go-licenses) -Scope: All direct and transitive Go module dependencies -Command: go-licenses csv ./... | go-licenses save ./... -Includes indirect dependencies: Yes -Last regenerated: 2025-05-13 - -To regenerate, run: - go install github.com/google/go-licenses@latest - go-licenses csv ./... 2>/dev/null > licenses.csv - go-licenses save ./... --save_path=./licenses \ - --ignore=github.com/devantler-tech/ksail 2>/dev/null -Then consolidate with the generation script. +Generated by: scripts/gen-third-party-licenses (go-licenses, +github.com/google/go-licenses/v2) +Scope: all direct and transitive Go module dependencies of +both Go modules (repo root + desktop/) +Regenerate with: make licenses +Regeneration history: see git log for this file (no timestamp +is embedded so an unchanged module graph reproduces the file +byte-identically). LICENSE SUMMARY ------------------------------------------------------------ - 0BSD: 1 module(s) - Apache-2.0: 361 module(s) - BSD-2-Clause: 19 module(s) - BSD-2-Clause-FreeBSD: 1 module(s) - BSD-3-Clause: 106 module(s) + Apache-2.0: 360 module(s) + BSD-0-Clause: 1 module(s) + BSD-2-Clause: 22 module(s) + BSD-3-Clause: 109 module(s) CC0-1.0: 1 module(s) - ISC: 2 module(s) - MIT: 283 module(s) - MPL-2.0: 40 module(s) - Unknown (verified, see notes): 13 module(s) + ISC: 3 module(s) + MIT: 291 module(s) + MPL-2.0: 42 module(s) + Unknown (verified, see notes): 25 module(s) Unlicense: 3 module(s) - Total: 830 module(s) + XZ: 1 module(s) + Total: 858 module(s) -============================================================ -License: 0BSD -============================================================ - -Modules: - - github.com/mikelolasagasti/xz - -License text: +LICENSE ELECTIONS AND CLASSIFICATION NOTES ------------------------------------------------------------ -Copyright (C) 2015-2017 Michael Cross - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. + - github.com/apparentlymart/go-textseg/v15/textseg + LICENSE is the MIT text; pinned (go-licenses classification alternates MIT/Unicode-DFS-2016) + - github.com/spdx/tools-golang + dual-licensed Apache-2.0 OR GPL-2.0-or-later (LICENSE.code); Apache-2.0 elected ============================================================ License: Apache-2.0 @@ -127,7 +108,6 @@ Modules: - github.com/aws/aws-sdk-go-v2/feature/s3/manager - github.com/aws/aws-sdk-go-v2/internal/configsources - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 - - github.com/aws/aws-sdk-go-v2/internal/ini - github.com/aws/aws-sdk-go-v2/internal/v4a - github.com/aws/aws-sdk-go-v2/service/ecr - github.com/aws/aws-sdk-go-v2/service/ecrpublic @@ -149,6 +129,7 @@ Modules: - github.com/briandowns/spinner - github.com/chrismellard/docker-credential-acr-env/pkg - github.com/cilium/cilium + - github.com/cilium/hive - github.com/cncf/xds/go - github.com/containerd/console - github.com/containerd/containerd @@ -162,18 +143,17 @@ Modules: - github.com/containerd/log - github.com/containerd/platforms - github.com/containerd/plugin - - github.com/containerd/stargz-snapshotter/estargz - github.com/containerd/ttrpc - github.com/containerd/typeurl/v2 - github.com/containernetworking/cni + - github.com/containernetworking/plugins/pkg - github.com/containers/libtrust - github.com/containers/ocicrypt - github.com/containers/storage + - github.com/coreos/go-iptables/iptables - github.com/coreos/go-oidc/v3/oidc - github.com/coreos/go-semver/semver - github.com/coreos/go-systemd/v22 - - github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer - - github.com/deitch/magic/pkg/magic - github.com/derailed/k9s - github.com/derailed/tcell/v2 - github.com/dimchansky/utfbom @@ -204,12 +184,14 @@ Modules: - github.com/go-jose/go-jose/v4 - github.com/go-logr/logr - github.com/go-logr/stdr + - github.com/go-logr/zapr - github.com/go-openapi/analysis - github.com/go-openapi/errors - github.com/go-openapi/jsonpointer - github.com/go-openapi/jsonreference - github.com/go-openapi/loads - github.com/go-openapi/runtime + - github.com/go-openapi/runtime/server-middleware - github.com/go-openapi/spec - github.com/go-openapi/strfmt - github.com/go-openapi/swag @@ -239,12 +221,8 @@ Modules: - github.com/googleapis/enterprise-certificate-proxy/client - github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus - github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors - - github.com/in-toto/attestation/go - - github.com/in-toto/in-toto-golang/in_toto - - github.com/inconshreveable/go-update - github.com/jmespath/go-jmespath - github.com/jonboulle/clockwork - - github.com/klauspost/compress - github.com/knqyf263/go-apk-version - github.com/kubernetes-csi/external-snapshotter/client/v8 - github.com/kubescape/backend/pkg @@ -293,12 +271,10 @@ Modules: - github.com/opencontainers/go-digest - github.com/opencontainers/image-spec - github.com/opencontainers/runtime-spec/specs-go - - github.com/opencontainers/selinux/go-selinux - github.com/openvex/go-vex/pkg - github.com/pb33f/ordered-map/v2 - github.com/pborman/indent - github.com/pdfcpu/pdfcpu - - github.com/pelletier/go-toml - github.com/petermattis/goid - github.com/pjbgf/sha1cd - github.com/project-copacetic/copacetic/pkg @@ -310,6 +286,7 @@ Modules: - github.com/quay/claircore/toolkit/types/cpe - github.com/quay/zlog - github.com/rakyll/hey/requester + - github.com/rancher/k3k/pkg/apis/k3k.io - github.com/rancher/wharfie/pkg/registries - github.com/santhosh-tekuri/jsonschema/v6 - github.com/sasha-s/go-deadlock @@ -322,7 +299,7 @@ Modules: - github.com/sigstore/protobuf-specs/gen/pb-go - github.com/sigstore/rekor-tiles/v2 - github.com/sigstore/rekor/pkg - - github.com/sigstore/sigstore-go/pkg + - github.com/sigstore/sigstore-go - github.com/sigstore/sigstore/pkg - github.com/sigstore/timestamp-authority/v2/pkg/verification - github.com/skeema/knownhosts @@ -339,6 +316,8 @@ Modules: - github.com/transparency-dev/formats/log - github.com/transparency-dev/merkle - github.com/vbatts/go-mtree/pkg/govis + - github.com/vishvananda/netlink + - github.com/vishvananda/netns - github.com/wagoodman/go-presenter - github.com/xanzy/ssh-agent - github.com/xeipuuv/gojsonpointer @@ -350,6 +329,7 @@ Modules: - go.etcd.io/etcd/api/v3 - go.etcd.io/etcd/client/pkg/v3 - go.etcd.io/etcd/client/v3 + - go.etcd.io/etcd/etcdutl/v3/snapshot - go.etcd.io/etcd/pkg/v3 - go.etcd.io/etcd/server/v3 - go.etcd.io/raft/v3 @@ -405,7 +385,6 @@ Modules: - k8s.io/kube-openapi/pkg/validation/spec - k8s.io/kube-openapi/pkg/validation/strfmt - k8s.io/kube-proxy/config/v1alpha1 - - k8s.io/kube-scheduler/config/v1 - k8s.io/kubectl/pkg - k8s.io/kubelet/config/v1beta1 - k8s.io/kubernetes/cmd/kubeadm/app @@ -417,19 +396,21 @@ Modules: - sigs.k8s.io/apiserver-network-proxy/konnectivity-client - sigs.k8s.io/cli-utils/pkg/object - sigs.k8s.io/controller-runtime + - sigs.k8s.io/gateway-api/apis/v1 - sigs.k8s.io/json - sigs.k8s.io/kind/pkg + - sigs.k8s.io/knftables - sigs.k8s.io/kustomize/api - sigs.k8s.io/kustomize/kyaml - sigs.k8s.io/kwok - sigs.k8s.io/randfill - sigs.k8s.io/release-utils/version - sigs.k8s.io/structured-merge-diff/v6 - - sigs.k8s.io/yaml License text: ------------------------------------------------------------ -Apache License + + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -631,6 +612,28 @@ Apache License See the License for the specific language governing permissions and limitations under the License. +============================================================ +License: BSD-0-Clause +============================================================ + +Modules: + - github.com/mikelolasagasti/xz + +License text: +------------------------------------------------------------ +Copyright (C) 2015-2017 Michael Cross + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + ============================================================ License: BSD-2-Clause ============================================================ @@ -639,15 +642,18 @@ Modules: - github.com/cli/safeexec - github.com/digitorus/timestamp - github.com/emirpasic/gods + - github.com/godbus/dbus/v5 - github.com/gorilla/websocket - github.com/huaweicloud/huaweicloud-sdk-go-v3 - github.com/karrick/godirwalk - github.com/magiconair/properties + - github.com/moby/sys/capability - github.com/nwaples/rardecode/v2 - github.com/pkg/browser - github.com/pkg/errors - github.com/pkg/profile - github.com/pkg/xattr + - github.com/rcrowley/go-metrics - github.com/russross/blackfriday/v2 - github.com/syndtr/goleveldb/leveldb - github.com/tonistiigi/dchapes-mode @@ -684,45 +690,6 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -============================================================ -License: BSD-2-Clause-FreeBSD -============================================================ - -Modules: - - github.com/rcrowley/go-metrics - -License text: ------------------------------------------------------------- -Copyright 2012 Richard Crowley. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - 1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - -THIS SOFTWARE IS PROVIDED BY RICHARD CROWLEY ``AS IS'' AND ANY EXPRESS -OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL RICHARD CROWLEY OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF -THE POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and documentation -are those of the authors and should not be interpreted as representing -official policies, either expressed or implied, of Richard Crowley. - ============================================================ License: BSD-3-Clause ============================================================ @@ -748,6 +715,7 @@ Modules: - github.com/bodgit/sevenzip - github.com/bodgit/windows - github.com/chai2010/gettext-go + - github.com/cilium/hive/script - github.com/cloudflare/circl - github.com/dsnet/compress - github.com/evanphx/json-patch @@ -770,6 +738,7 @@ Modules: - github.com/google/licensecheck - github.com/google/uuid - github.com/googleapis/gax-go/v2 + - github.com/gopacket/gopacket - github.com/gorilla/css/scanner - github.com/gorilla/mux - github.com/grpc-ecosystem/grpc-gateway/v2 @@ -777,7 +746,6 @@ Modules: - github.com/hhrutter/lzw - github.com/hhrutter/tiff - github.com/ianlancetaylor/demangle - - github.com/imdario/mergo - github.com/inconshreveable/go-update/internal/osext - github.com/insomniacslk/dhcp - github.com/kastenhq/goversion/version @@ -831,6 +799,8 @@ Modules: - k8s.io/client-go/third_party/forked/golang/template - k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json - k8s.io/utils/internal/third_party/forked/golang + - modernc.org/libc + - modernc.org/mathutil - modernc.org/memory - modernc.org/sqlite/lib - mvdan.cc/sh/v3 @@ -918,7 +888,7 @@ and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); -iii. publicity and privacy rights pertaining to a person's image or +iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject @@ -935,9 +905,9 @@ including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. -2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. +2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. -3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. +3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. @@ -953,7 +923,7 @@ discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any -person's Copyright and Related Rights in the Work. Further, Affirmer +person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. @@ -966,16 +936,15 @@ License: ISC ============================================================ Modules: + - github.com/coder/websocket - github.com/davecgh/go-spew/spew - github.com/go-restruct/restruct License text: ------------------------------------------------------------ -ISC License - -Copyright (c) 2012-2016 Dave Collins +Copyright (c) 2025 Coder -Permission to use, copy, modify, and/or distribute this software for any +Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. @@ -1013,8 +982,7 @@ Modules: - github.com/Masterminds/semver/v3 - github.com/Masterminds/sprig/v3 - github.com/Masterminds/squirrel - - github.com/ProtonMail/go-mime - - github.com/ProtonMail/gopenpgp/v2 + - github.com/ProtonMail/gopenpgp/v3 - github.com/VividCortex/ewma - github.com/a8m/envsubst/parse - github.com/acarl005/stripansi @@ -1022,6 +990,7 @@ Modules: - github.com/agnivade/levenshtein - github.com/alecthomas/chroma/v2 - github.com/alecthomas/participle/v2/lexer + - github.com/alexflint/go-filemutex - github.com/anchore/go-homedir - github.com/anchore/go-lzo - github.com/anchore/go-rpmdb/pkg @@ -1090,6 +1059,7 @@ Modules: - github.com/fatih/color - github.com/felixge/fgprof - github.com/felixge/httpsnoop + - github.com/florianl/go-tc - github.com/francoispqt/gojay - github.com/fvbommel/sortorder - github.com/fxamacker/cbor/v2 @@ -1137,6 +1107,7 @@ Modules: - github.com/jmoiron/sqlx - github.com/johnfercher/go-tree/node - github.com/johnfercher/maroto/v2 + - github.com/josharian/native - github.com/jsimonetti/rtnetlink/v2 - github.com/json-iterator/go - github.com/jung-kurt/gofpdf @@ -1148,6 +1119,7 @@ Modules: - github.com/k3d-io/k3d/v5 - github.com/kballard/go-shellquote - github.com/kevinburke/ssh_config + - github.com/klauspost/compress - github.com/klauspost/compress/zstd/internal/xxhash - github.com/klauspost/cpuid/v2 - github.com/klauspost/pgzip @@ -1172,13 +1144,16 @@ Modules: - github.com/mattn/go-isatty - github.com/mattn/go-runewidth - github.com/mdlayher/ethtool + - github.com/mdlayher/genetlink - github.com/mdlayher/netlink + - github.com/mdlayher/socket - github.com/mgutz/ansi - github.com/mholt/archives - github.com/mikefarah/yq/v4/pkg/yqlib - github.com/mitchellh/colorstring - github.com/mitchellh/copystructure - github.com/mitchellh/go-homedir + - github.com/mitchellh/go-testing-interface - github.com/mitchellh/go-wordwrap - github.com/mitchellh/hashstructure/v2 - github.com/mitchellh/mapstructure @@ -1190,7 +1165,6 @@ Modules: - github.com/muesli/cancelreader - github.com/muesli/reflow - github.com/muesli/termenv - - github.com/ncruces/go-strftime - github.com/neticdk/go-stdlib/diff - github.com/nxadm/tail - github.com/nxadm/tail/ratelimiter @@ -1203,6 +1177,7 @@ Modules: - github.com/otiai10/copy - github.com/package-url/packageurl-go - github.com/pandatix/go-cvss + - github.com/pelletier/go-toml - github.com/pelletier/go-toml/v2 - github.com/peterbourgon/diskv - github.com/pin/tftp/v3 @@ -1217,8 +1192,6 @@ Modules: - github.com/sagikazarmark/locafero - github.com/sahilm/fuzzy - github.com/saintfish/chardet - - github.com/samber/do/v2 - - github.com/samber/go-type-to-string - github.com/samber/lo - github.com/schollz/progressbar/v3 - github.com/secure-systems-lab/go-securesystemslib @@ -1254,18 +1227,22 @@ Modules: - github.com/vifraa/gopom - github.com/wagoodman/go-partybus - github.com/wagoodman/go-progress + - github.com/wailsapp/wails/v3 - github.com/wzshiming/ctc - - github.com/wzshiming/getch - github.com/wzshiming/httpseek - github.com/wzshiming/winseq - github.com/x448/float16 + - github.com/xiang90/probing - github.com/xlab/treeprint - github.com/xo/terminfo + - github.com/youmark/pkcs8 - github.com/yuin/goldmark - github.com/yuin/goldmark-emoji + - github.com/zalando/go-keyring - github.com/zclconf/go-cty/cty - go.etcd.io/bbolt - go.uber.org/atomic + - go.uber.org/dig - go.uber.org/multierr - go.uber.org/zap - go.yaml.in/yaml/v3 @@ -1274,7 +1251,7 @@ Modules: - gorm.io/gorm - k8s.io/client-go/third_party/forked/httpcache - k8s.io/kube-openapi/pkg/internal/third_party/govalidator - - modernc.org/libc + - sigs.k8s.io/yaml License text: ------------------------------------------------------------ @@ -1314,9 +1291,11 @@ Modules: - github.com/hashicorp/errwrap - github.com/hashicorp/go-cleanhttp - github.com/hashicorp/go-getter + - github.com/hashicorp/go-getter/v2 - github.com/hashicorp/go-multierror - github.com/hashicorp/go-retryablehttp - github.com/hashicorp/go-rootcerts + - github.com/hashicorp/go-safetemp - github.com/hashicorp/go-secure-stdlib/parseutil - github.com/hashicorp/go-secure-stdlib/strutil - github.com/hashicorp/go-sockaddr @@ -1709,12 +1688,41 @@ License: Unknown (license file not bundled in Go module) The modules below are reported as "Unknown" by go-licenses because their Go module archives do not include a LICENSE file. Each module's actual license has been manually verified -against its source repository: +against its source repository (see +scripts/gen-third-party-licenses/verified_unknown.go): - github.com/alibabacloud-go/cr-20160607/client Verified: Apache-2.0 (https://github.com/alibabacloud-go/cr-20160607) + - github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer + Verified: Apache-2.0 (https://github.com/cyberphone/json-canonicalization) + - github.com/deitch/magic/pkg/magic + Verified: Apache-2.0 (https://github.com/deitch/magic) + - github.com/deitch/magic/pkg/magic/internal + Verified: Apache-2.0 (https://github.com/deitch/magic) + - github.com/deitch/magic/pkg/magic/parser + Verified: Apache-2.0 (https://github.com/deitch/magic) + - github.com/in-toto/attestation/go/predicates/provenance/v02 + Verified: Apache-2.0 (https://github.com/in-toto/attestation) + - github.com/in-toto/attestation/go/predicates/provenance/v1 + Verified: Apache-2.0 (https://github.com/in-toto/attestation) + - github.com/in-toto/attestation/go/v1 + Verified: Apache-2.0 (https://github.com/in-toto/attestation) + - github.com/in-toto/in-toto-golang/in_toto + Verified: Apache-2.0 (https://github.com/in-toto/in-toto-golang) + - github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common + Verified: Apache-2.0 (https://github.com/in-toto/in-toto-golang) + - github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.1 + Verified: Apache-2.0 (https://github.com/in-toto/in-toto-golang) + - github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2 + Verified: Apache-2.0 (https://github.com/in-toto/in-toto-golang) + - github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1 + Verified: Apache-2.0 (https://github.com/in-toto/in-toto-golang) + - github.com/inconshreveable/go-update + Verified: Apache-2.0 (https://github.com/inconshreveable/go-update) - github.com/loft-sh/admin-apis/pkg/licenseapi Verified: Apache-2.0 (https://github.com/loft-sh/admin-apis) + - github.com/loft-sh/external-types/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1 + Verified: None published β€” risk-accepted; upstream license request filed at https://github.com/loft-sh/vcluster/issues/4039 (https://github.com/loft-sh/external-types) - github.com/segmentio/asm/ascii Verified: MIT (https://github.com/segmentio/asm) - github.com/segmentio/asm/base64 @@ -1733,10 +1741,6 @@ against its source repository: Verified: MIT (https://github.com/segmentio/asm) - github.com/segmentio/asm/keyset Verified: MIT (https://github.com/segmentio/asm) - - github.com/xi2/xz - Verified: Public domain (https://github.com/xi2/xz) - - modernc.org/mathutil - Verified: BSD-3-Clause (https://gitlab.com/cznic/mathutil) License texts are not reproduced here because the module archives do not include them. See the linked repositories @@ -1778,3 +1782,30 @@ OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to +============================================================ +License: XZ +============================================================ + +Modules: + - github.com/xi2/xz + +License text: +------------------------------------------------------------ +Licensing of github.com/xi2/xz +============================== + + This Go package is a modified version of + + XZ Embedded + + The contents of the testdata directory are modified versions of + the test files from + + XZ Utils + + All the files in this package have been written by Michael Cross, + Lasse Collin and/or Igor PavLov. All these files have been put + into the public domain. You can do whatever you want with these + files. + + This software is provided "as is", without any warranty. diff --git a/scripts/gen-third-party-licenses/format.go b/scripts/gen-third-party-licenses/format.go new file mode 100644 index 000000000..6a7b1c28e --- /dev/null +++ b/scripts/gen-third-party-licenses/format.go @@ -0,0 +1,179 @@ +package main + +import ( + "fmt" + "sort" + "strings" +) + +const rule = "============================================================" + +const subRule = "------------------------------------------------------------" + +// applyOverrides reclassifies modules pinned in classificationOverrides to +// their elected license. It returns the adjusted deps (sorted order +// preserved). +func applyOverrides(deps []dependency) []dependency { + overrides := classificationOverrides() + out := make([]dependency, len(deps)) + + for index, dep := range deps { + entry, ok := overrides[dep.module] + if ok { + dep.license = entry.elected + } + + out[index] = dep + } + + return out +} + +// render produces the full THIRD_PARTY_LICENSES document. deps must be sorted +// by module and already override-adjusted; texts maps license type β†’ +// representative license text. +func render(deps []dependency, texts map[string]string) string { + groups := map[string][]string{} + for _, dep := range deps { + groups[dep.license] = append(groups[dep.license], dep.module) + } + + var builder strings.Builder + + writeHeader(&builder, groups, len(deps)) + writeElections(&builder) + + for _, license := range sortedLicenses(groups) { + if license == unknownLicense { + writeUnknownSection(&builder, groups[license]) + + continue + } + + writeLicenseSection(&builder, license, groups[license], texts[license]) + } + + return builder.String() +} + +func writeHeader(builder *strings.Builder, groups map[string][]string, total int) { + fmt.Fprintf(builder, `THIRD-PARTY SOFTWARE NOTICES AND INFORMATION +%s + +KSail incorporates third-party software components. The +following notices identify the components used, along with +their respective licenses. + +KSail itself is licensed under the PolyForm Shield License +1.0.0. See LICENSE in the project root. + +GENERATION PROVENANCE +%s +Generated by: scripts/gen-third-party-licenses (go-licenses, +github.com/google/go-licenses/v2) +Scope: all direct and transitive Go module dependencies of +both Go modules (repo root + desktop/) +Regenerate with: make licenses +Regeneration history: see git log for this file (no timestamp +is embedded so an unchanged module graph reproduces the file +byte-identically). + +LICENSE SUMMARY +%s +`, rule, subRule, subRule) + + for _, license := range sortedLicenses(groups) { + name := license + if license == unknownLicense { + name = "Unknown (verified, see notes)" + } + + fmt.Fprintf(builder, " %s: %d module(s)\n", name, len(groups[license])) + } + + fmt.Fprintf(builder, " Total: %d module(s)\n", total) +} + +// writeElections documents modules with a pinned/elected classification and +// the reason for each. +func writeElections(builder *strings.Builder) { + overrides := classificationOverrides() + if len(overrides) == 0 { + return + } + + fmt.Fprintf(builder, "\nLICENSE ELECTIONS AND CLASSIFICATION NOTES\n%s\n", subRule) + + modules := make([]string, 0, len(overrides)) + for module := range overrides { + modules = append(modules, module) + } + + sort.Strings(modules) + + for _, module := range modules { + fmt.Fprintf(builder, " - %s\n %s\n", module, overrides[module].note) + } +} + +func writeLicenseSection(builder *strings.Builder, license string, modules []string, text string) { + fmt.Fprintf(builder, "\n%s\nLicense: %s\n%s\n\nModules:\n", rule, license, rule) + + for _, module := range modules { + fmt.Fprintf(builder, " - %s\n", module) + } + + fmt.Fprintf(builder, "\nLicense text:\n%s\n%s\n", subRule, text) +} + +// writeUnknownSection lists modules whose archives bundle no license file, +// each with its manual verification note from verified_unknown.go. +func writeUnknownSection(builder *strings.Builder, modules []string) { + verified := verifiedUnknown() + + fmt.Fprintf(builder, ` +%s +License: Unknown (license file not bundled in Go module) +%s + +The modules below are reported as "Unknown" by go-licenses +because their Go module archives do not include a LICENSE +file. Each module's actual license has been manually verified +against its source repository (see +scripts/gen-third-party-licenses/verified_unknown.go): + +`, rule, rule) + + for _, module := range modules { + entry, ok := verified[module] + if !ok { + // checkUnknowns guarantees every Unknown module is verified before + // render runs; surface an invariant violation instead of emitting + // an empty "Verified: ()" line that reads as vetted. + fmt.Fprintf(builder, + " - %s\n Verified: UNVERIFIED β€” generator invariant violated\n", + module) + + continue + } + + fmt.Fprintf(builder, " - %s\n Verified: %s (%s)\n", module, entry.license, entry.url) + } + + fmt.Fprint(builder, ` +License texts are not reproduced here because the module +archives do not include them. See the linked repositories +for authoritative license terms. +`) +} + +func sortedLicenses(groups map[string][]string) []string { + licenses := make([]string, 0, len(groups)) + for license := range groups { + licenses = append(licenses, license) + } + + sort.Strings(licenses) + + return licenses +} diff --git a/scripts/gen-third-party-licenses/main.go b/scripts/gen-third-party-licenses/main.go new file mode 100644 index 000000000..f3b42beb7 --- /dev/null +++ b/scripts/gen-third-party-licenses/main.go @@ -0,0 +1,314 @@ +// Command gen-third-party-licenses regenerates the repo-root THIRD_PARTY_LICENSES +// inventory from the live module graphs of both Go modules (root + desktop/). +// +// It shells out to go-licenses (github.com/google/go-licenses/v2 β€” the same tool +// and version CI's license check installs) for the per-module license +// classification, merges the two module graphs, and emits one consolidated, +// deterministic document: a summary, then one section per license type with the +// module list and a representative license text read from the module cache. +// +// Modules go-licenses reports as "Unknown" (no license file in the module +// archive) must be manually verified and recorded in verified_unknown.go β€” +// the generator fails on any unverified Unknown module so the inventory can +// never silently carry an unreviewed dependency. +// +// Run via `make licenses` (or `go run ./scripts/gen-third-party-licenses`). +// The output contains no timestamp so a re-run with an unchanged module graph +// is byte-identical (CI relies on that for drift detection); regeneration +// history lives in git. +package main + +import ( + "bytes" + "context" + "encoding/csv" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "time" +) + +const ownModulePrefix = "github.com/devantler-tech/ksail" + +// unknownLicense is the license type go-licenses reports when a module archive +// bundles no license file. +const unknownLicense = "Unknown" + +// licenseFileMode is the permission mode for the generated inventory (git +// tracks only the executable bit, so owner-only write is fine). +const licenseFileMode = 0o600 + +var errUnverifiedUnknown = errors.New( + "modules with no bundled license file and no manual verification " + + "(verify each module's license against its source repository and add it to " + + "scripts/gen-third-party-licenses/verified_unknown.go)") + +var errNoLicenseFile = errors.New("no module license file found") + +// generationTimeout bounds the whole run (go-licenses walks both module +// graphs and reads the module cache; a wedged subprocess must not hang a +// local `make licenses` forever β€” CI has its own job-level timeout). +const generationTimeout = 15 * time.Minute + +type dependency struct { + // module is the import path exactly as go-licenses csv emits it β€” for + // modules whose license lives at a sub-package level this is a PACKAGE + // import path (e.g. github.com/segmentio/asm/ascii), not the module root. + module string + license string +} + +func main() { + err := run() + if err != nil { + fmt.Fprintf(os.Stderr, "gen-third-party-licenses: %v\n", err) + os.Exit(1) + } +} + +func run() error { + ctx, cancel := context.WithTimeout(context.Background(), generationTimeout) + defer cancel() + + repoRoot, err := findRepoRoot(ctx) + if err != nil { + return err + } + + moduleDirs := []string{repoRoot, filepath.Join(repoRoot, "desktop")} + + deps, err := collectDependencies(ctx, moduleDirs) + if err != nil { + return err + } + + deps = applyOverrides(deps) + + err = checkUnknowns(deps) + if err != nil { + return err + } + + texts, err := collectLicenseTexts(ctx, moduleDirs, deps) + if err != nil { + return err + } + + outPath := filepath.Join(repoRoot, "THIRD_PARTY_LICENSES") + + err = os.WriteFile(outPath, []byte(render(deps, texts)), licenseFileMode) + if err != nil { + return fmt.Errorf("writing %s: %w", outPath, err) + } + + return nil +} + +func findRepoRoot(ctx context.Context) (string, error) { + out, err := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel").Output() + if err != nil { + return "", fmt.Errorf("locating repo root: %w", err) + } + + return strings.TrimSpace(string(out)), nil +} + +// collectDependencies runs `go-licenses csv ./...` in every module dir and +// merges the results into one moduleβ†’license inventory, skipping the repo's +// own modules. +func collectDependencies(ctx context.Context, moduleDirs []string) ([]dependency, error) { + byModule := map[string]string{} + + for _, dir := range moduleDirs { + cmd := exec.CommandContext(ctx, "go-licenses", "csv", "./...") + cmd.Dir = dir + + var stdout, stderr bytes.Buffer + + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + return nil, fmt.Errorf("go-licenses csv in %s: %w\n%s", dir, err, stderr.String()) + } + + err = mergeCSV(byModule, &stdout) + if err != nil { + return nil, fmt.Errorf("parsing go-licenses csv output from %s: %w", dir, err) + } + } + + deps := make([]dependency, 0, len(byModule)) + for module, license := range byModule { + deps = append(deps, dependency{module: module, license: license}) + } + + sort.Slice(deps, func(left, right int) bool { return deps[left].module < deps[right].module }) + + return deps, nil +} + +// mergeCSV folds one go-licenses CSV stream (module,url,license) into the +// accumulated inventory. First writer wins so the root module's classification +// takes precedence over desktop's for shared dependencies. +func mergeCSV(byModule map[string]string, reader io.Reader) error { + csvReader := csv.NewReader(reader) + csvReader.FieldsPerRecord = 3 + + for { + record, err := csvReader.Read() + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + return fmt.Errorf("reading csv record: %w", err) + } + + module, license := record[0], record[2] + if strings.HasPrefix(module, ownModulePrefix) { + continue + } + + _, seen := byModule[module] + if !seen { + byModule[module] = license + } + } + + return nil +} + +// checkUnknowns fails when a module classified Unknown has no manually +// verified entry in verified_unknown.go. +func checkUnknowns(deps []dependency) error { + verified := verifiedUnknown() + + var unverified []string + + for _, dep := range deps { + if dep.license != unknownLicense { + continue + } + + _, ok := verified[dep.module] + if !ok { + unverified = append(unverified, dep.module) + } + } + + if len(unverified) == 0 { + return nil + } + + return fmt.Errorf("%w:\n %s", errUnverifiedUnknown, strings.Join(unverified, "\n ")) +} + +// collectLicenseTexts returns one representative license text per license +// type: the license file of the alphabetically-first module of that type +// (deterministic across runs), read from the module cache directory `go list` +// resolves for the package. +func collectLicenseTexts( + ctx context.Context, moduleDirs []string, deps []dependency, +) (map[string]string, error) { + texts := map[string]string{} + + for _, dep := range representativeModules(deps) { + text, err := readModuleLicense(ctx, moduleDirs, dep.module) + if err != nil { + return nil, fmt.Errorf("license text for %s (%s): %w", dep.module, dep.license, err) + } + + texts[dep.license] = text + } + + return texts, nil +} + +// representativeModules picks the alphabetically-first module per license type +// (excluding Unknown, whose section carries verification notes instead of a +// text). deps must already be sorted by module. +func representativeModules(deps []dependency) []dependency { + seen := map[string]bool{} + + var reps []dependency + + for _, dep := range deps { + if dep.license == unknownLicense || seen[dep.license] { + continue + } + + seen[dep.license] = true + + reps = append(reps, dep) + } + + return reps +} + +// readModuleLicense resolves the package's module directory (module cache) +// via `go list` in any of the module dirs and reads the license file found +// closest to the package: the package's own directory first, then each parent +// up to the module root (go-licenses classifies at the same granularity). +func readModuleLicense(ctx context.Context, moduleDirs []string, pkg string) (string, error) { + for _, dir := range moduleDirs { + // #nosec G204 -- pkg comes from go-licenses csv output over this repo's + // own module graph, not from user input. + cmd := exec.CommandContext( + ctx, "go", "list", "-f", "{{if .Module}}{{.Module.Dir}}|{{.Dir}}{{end}}", pkg) + cmd.Dir = dir + + out, err := cmd.Output() + if err != nil { + continue + } + + moduleDir, pkgDir, ok := strings.Cut(strings.TrimSpace(string(out)), "|") + if !ok || moduleDir == "" { + continue + } + + for candidate := pkgDir; strings.HasPrefix(candidate, moduleDir); candidate = filepath.Dir(candidate) { + text, found := readLicenseFile(candidate) + if found { + return text, nil + } + } + } + + return "", fmt.Errorf("%w: package %s", errNoLicenseFile, pkg) +} + +// readLicenseFile returns the content of the alphabetically-first regular +// file in dir whose name looks like a license file. +func readLicenseFile(dir string) (string, bool) { + entries, err := os.ReadDir(dir) + if err != nil { + return "", false + } + + for _, entry := range entries { + name := strings.ToUpper(entry.Name()) + if entry.IsDir() || + (!strings.Contains(name, "LICEN") && !strings.HasPrefix(name, "COPYING")) { + continue + } + + // #nosec G304 -- dir is a directory inside the local Go module cache, + // resolved via `go list`; not user input. + data, err := os.ReadFile(filepath.Join(dir, entry.Name())) + if err != nil { + continue + } + + return strings.TrimRight(string(data), "\n"), true + } + + return "", false +} diff --git a/scripts/gen-third-party-licenses/main_test.go b/scripts/gen-third-party-licenses/main_test.go new file mode 100644 index 000000000..c07940686 --- /dev/null +++ b/scripts/gen-third-party-licenses/main_test.go @@ -0,0 +1,143 @@ +package main + +import ( + "strings" + "testing" +) + +func TestMergeCSVSkipsOwnModulesAndDedupes(t *testing.T) { + t.Parallel() + + byModule := map[string]string{} + + root := "github.com/foo/bar,https://example.com,MIT\n" + + "github.com/devantler-tech/ksail/v7/pkg/cli,https://example.com,Unknown\n" + + "github.com/baz/qux,https://example.com,Apache-2.0\n" + + err := mergeCSV(byModule, strings.NewReader(root)) + if err != nil { + t.Fatalf("mergeCSV(root): %v", err) + } + + desktop := "github.com/foo/bar,https://example.com,Unknown\n" + + "github.com/devantler-tech/ksail/desktop,https://example.com,Unknown\n" + + "github.com/desktop/only,https://example.com,MIT\n" + + err = mergeCSV(byModule, strings.NewReader(desktop)) + if err != nil { + t.Fatalf("mergeCSV(desktop): %v", err) + } + + want := map[string]string{ + "github.com/foo/bar": "MIT", // root's classification wins + "github.com/baz/qux": "Apache-2.0", + "github.com/desktop/only": "MIT", + } + if len(byModule) != len(want) { + t.Fatalf("got %d modules, want %d: %v", len(byModule), len(want), byModule) + } + + for module, license := range want { + if byModule[module] != license { + t.Errorf("byModule[%q] = %q, want %q", module, byModule[module], license) + } + } +} + +func TestCheckUnknownsFailsOnUnverifiedModule(t *testing.T) { + t.Parallel() + + deps := []dependency{ + {module: "github.com/not/verified", license: "Unknown"}, + {module: "github.com/segmentio/asm/ascii", license: "Unknown"}, // verified + {module: "github.com/fine/mit", license: "MIT"}, + } + + err := checkUnknowns(deps) + if err == nil { + t.Fatal("checkUnknowns() = nil, want error for unverified module") + } + + if !strings.Contains(err.Error(), "github.com/not/verified") { + t.Errorf("error %q does not name the unverified module", err) + } + + if strings.Contains(err.Error(), "segmentio") { + t.Errorf("error %q names an already-verified module", err) + } +} + +func TestApplyOverridesElectsDualLicense(t *testing.T) { + t.Parallel() + + deps := applyOverrides([]dependency{ + {module: "github.com/spdx/tools-golang", license: "GPL-2.0"}, + {module: "github.com/foo/bar", license: "MIT"}, + }) + + if deps[0].license != "Apache-2.0" { + t.Errorf("spdx/tools-golang license = %q, want elected Apache-2.0", deps[0].license) + } + + if deps[1].license != "MIT" { + t.Errorf("unrelated module license = %q, want MIT untouched", deps[1].license) + } +} + +func TestRepresentativeModulesPicksFirstPerLicense(t *testing.T) { + t.Parallel() + + reps := representativeModules([]dependency{ + {module: "github.com/a/first", license: "MIT"}, + {module: "github.com/b/second", license: "MIT"}, + {module: "github.com/c/apache", license: "Apache-2.0"}, + {module: "github.com/d/unknown", license: "Unknown"}, + }) + + if len(reps) != 2 { + t.Fatalf("got %d representatives, want 2: %v", len(reps), reps) + } + + if reps[0].module != "github.com/a/first" || reps[1].module != "github.com/c/apache" { + t.Errorf("representatives = %v, want first module per non-Unknown license", reps) + } +} + +func TestRenderShapeAndDeterminism(t *testing.T) { + t.Parallel() + + deps := applyOverrides([]dependency{ + {module: "github.com/a/mit", license: "MIT"}, + {module: "github.com/segmentio/asm/ascii", license: "Unknown"}, + {module: "github.com/spdx/tools-golang", license: "GPL-2.0"}, + }) + texts := map[string]string{"MIT": "MIT LICENSE TEXT", "Apache-2.0": "APACHE TEXT"} + + doc := render(deps, texts) + + for _, want := range []string{ + "LICENSE SUMMARY", + " Apache-2.0: 1 module(s)", + " MIT: 1 module(s)", + " Unknown (verified, see notes): 1 module(s)", + " Total: 3 module(s)", + "LICENSE ELECTIONS AND CLASSIFICATION NOTES", + "Apache-2.0 elected", + "License: MIT", + "MIT LICENSE TEXT", + "License: Unknown (license file not bundled in Go module)", + "Verified: MIT (https://github.com/segmentio/asm)", + } { + if !strings.Contains(doc, want) { + t.Errorf("rendered document missing %q", want) + } + } + + if strings.Contains(doc, "License: GPL-2.0") { + t.Error("dual-licensed module rendered under GPL-2.0 instead of its election") + } + + if doc != render(deps, texts) { + t.Error("render is not deterministic for identical input") + } +} diff --git a/scripts/gen-third-party-licenses/verified_unknown.go b/scripts/gen-third-party-licenses/verified_unknown.go new file mode 100644 index 000000000..4ab2a2bcd --- /dev/null +++ b/scripts/gen-third-party-licenses/verified_unknown.go @@ -0,0 +1,127 @@ +package main + +const ( + apache2 = "Apache-2.0" + mit = "MIT" + + urlAlibabaCR = "https://github.com/alibabacloud-go/cr-20160607" + urlDeitchMagic = "https://github.com/deitch/magic" + urlAttestation = "https://github.com/in-toto/attestation" + urlInToto = "https://github.com/in-toto/in-toto-golang" + urlSegmentio = "https://github.com/segmentio/asm" + + modJSONCanonicalizer = "github.com/cyberphone/json-canonicalization" + + "/go/src/webpki.org/jsoncanonicalizer" + modExternalTypes = "github.com/loft-sh/external-types" + + "/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" +) + +// verification records a manual license verification for a module whose Go +// module archive bundles no license file (go-licenses reports it "Unknown"). +// Each entry MUST be verified against the module's source repository before +// being added; the generator fails on any Unknown module missing here. +type verification struct { + // license is the verified SPDX identifier, or a short explanation when the + // project publishes no license at all. + license string + // url is the source repository the verification was performed against. + url string +} + +// verifiedUnknown maps go-licenses "Unknown" modules to their manually +// verified licenses. Most entries are false negatives: the repository ships a +// LICENSE at its root that go-licenses cannot discover at the sub-package +// level (the same set CI's `go-licenses check --ignore` flags document). +// Sibling packages of one repository share a single verification via +// verifyAll β€” the verification was performed once against that repository. +func verifiedUnknown() map[string]verification { + out := map[string]verification{ + "github.com/alibabacloud-go/cr-20160607/client": {license: apache2, url: urlAlibabaCR}, + modJSONCanonicalizer: { + license: apache2, + url: "https://github.com/cyberphone/json-canonicalization", + }, + "github.com/inconshreveable/go-update": { + license: apache2, url: "https://github.com/inconshreveable/go-update", + }, + "github.com/loft-sh/admin-apis/pkg/licenseapi": { + license: apache2, url: "https://github.com/loft-sh/admin-apis", + }, + modExternalTypes: { + license: "None published β€” risk-accepted; upstream license request filed at " + + "https://github.com/loft-sh/vcluster/issues/4039", + url: "https://github.com/loft-sh/external-types", + }, + } + + verifyAll(out, verification{license: apache2, url: urlDeitchMagic}, + "github.com/deitch/magic/pkg/magic", + "github.com/deitch/magic/pkg/magic/internal", + "github.com/deitch/magic/pkg/magic/parser", + ) + verifyAll(out, verification{license: apache2, url: urlAttestation}, + "github.com/in-toto/attestation/go/predicates/provenance/v02", + "github.com/in-toto/attestation/go/predicates/provenance/v1", + "github.com/in-toto/attestation/go/v1", + ) + verifyAll(out, verification{license: apache2, url: urlInToto}, + "github.com/in-toto/in-toto-golang/in_toto", + "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common", + "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.1", + "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2", + "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1", + ) + verifyAll(out, verification{license: mit, url: urlSegmentio}, + "github.com/segmentio/asm/ascii", + "github.com/segmentio/asm/base64", + "github.com/segmentio/asm/cpu", + "github.com/segmentio/asm/cpu/arm", + "github.com/segmentio/asm/cpu/arm64", + "github.com/segmentio/asm/cpu/cpuid", + "github.com/segmentio/asm/cpu/x86", + "github.com/segmentio/asm/internal/unsafebytes", + "github.com/segmentio/asm/keyset", + ) + + return out +} + +// verifyAll records one repository-level verification for every listed +// package import path. +func verifyAll(dst map[string]verification, entry verification, pkgs ...string) { + for _, pkg := range pkgs { + dst[pkg] = entry + } +} + +// override pins a module whose go-licenses classification is wrong, ambiguous, +// or non-deterministic, recording the manually verified (or elected) +// classification and the reason. +type override struct { + elected string + note string +} + +// classificationOverrides pins modules whose go-licenses classification is +// wrong, ambiguous, or non-deterministic: dual-licensed modules (recording +// the license this project elects) and modules the classifier flaps on +// run-to-run (which would break the byte-identical-output guarantee CI's +// drift check relies on). +func classificationOverrides() map[string]override { + return map[string]override{ + // LICENSE.code: "Apache-2.0 OR GPL-2.0-or-later"; Apache-2.0 elected. + "github.com/spdx/tools-golang": { + elected: apache2, + note: "dual-licensed Apache-2.0 OR GPL-2.0-or-later (LICENSE.code); " + + "Apache-2.0 elected", + }, + // LICENSE is the plain MIT text (verified 2026-07-04), but go-licenses + // classifies it as MIT on one run and Unicode-DFS-2016 on another β€” + // pinned so regeneration stays deterministic. + "github.com/apparentlymart/go-textseg/v15/textseg": { + elected: mit, + note: "LICENSE is the MIT text; pinned (go-licenses classification " + + "alternates MIT/Unicode-DFS-2016)", + }, + } +}