diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml index d9fcaaac9d..acd15badbf 100644 --- a/.github/workflows/sbom.yaml +++ b/.github/workflows/sbom.yaml @@ -84,18 +84,18 @@ jobs: - name: Install SPDX Tools run: | - wget https://github.com/spdx/tools/releases/download/v2.2.7/spdx-tools-2.2.7.zip - unzip spdx-tools-2.2.7.zip + wget https://github.com/spdx/tools-java/releases/download/v1.0.4/tools-java-1.0.4.zip + unzip tools-java-1.0.4.zip - name: Generate and Validate run: | img=$(go run ./ build ./) - go run ./ deps $img --sbom=spdx > sbom.txt + go run ./ deps $img --sbom=spdx | tee sbom.json - java -jar ./spdx-tools-2.2.7-jar-with-dependencies.jar Verify sbom.txt + java -jar ./tools-java-1.0.4-jar-with-dependencies.jar Verify sbom.json - uses: actions/upload-artifact@v3 if: ${{ always() }} with: - name: sbom.txt - path: sbom.txt + name: sbom.json + path: sbom.json diff --git a/internal/sbom/spdx.go b/internal/sbom/spdx.go index b8a14e9b23..fefc9cbd8b 100644 --- a/internal/sbom/spdx.go +++ b/internal/sbom/spdx.go @@ -16,11 +16,9 @@ package sbom import ( "bytes" - "encoding/base64" - "encoding/hex" + "encoding/json" "fmt" "strings" - "text/template" "time" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -40,82 +38,185 @@ func GenerateSPDX(koVersion string, date time.Time, mod []byte, imgDigest v1.Has return nil, err } + mainPackageID := "SPDXRef-Package-" + strings.ReplaceAll(bi.Main.Path, "/", ".") + + doc := Document{ + Version: Version, + DataLicense: "CC0-1.0", + ID: "SPDXRef-DOCUMENT", + Name: bi.Main.Path, + Namespace: "http://spdx.org/spdxdocs/" + bi.Main.Path, + DocumentDescribes: []string{mainPackageID}, + CreationInfo: CreationInfo{ + Created: date.Format(dateFormat), + Creators: []string{"Tool: ko " + koVersion}, + }, + Packages: make([]Package, 0, 1+len(bi.Deps)), + Relationships: make([]Relationship, 0, 1+len(bi.Deps)), + } + + doc.Relationships = append(doc.Relationships, Relationship{ + Element: "SPDXRef-DOCUMENT", + Type: "DESCRIBES", + Related: mainPackageID, + }) + + doc.Packages = append(doc.Packages, Package{ + Name: bi.Main.Path, + ID: mainPackageID, + // TODO: PackageSupplier: "Organization: " + bs.Main.Path + DownloadLocation: "https://" + bi.Main.Path, + FilesAnalyzed: false, + // TODO: PackageHomePage: "https://" + bi.Main.Path, + LicenseConcluded: NOASSERTION, + LicenseDeclared: NOASSERTION, + CopyrightText: NOASSERTION, + ExternalRefs: []ExternalRef{{ + Category: "PACKAGE_MANAGER", + Type: "purl", + Locator: ociRef(bi.Path, imgDigest), + }}, + }) + + for _, dep := range bi.Deps { + depID := fmt.Sprintf("SPDXRef-Package-%s-%s", + strings.ReplaceAll(dep.Path, "/", "."), + dep.Version) + + doc.Relationships = append(doc.Relationships, Relationship{ + Element: mainPackageID, + Type: "DEPENDS_ON", + Related: depID, + }) + + pkg := Package{ + Name: dep.Path, + ID: depID, + Version: dep.Version, + // TODO: PackageSupplier: "Organization: " + dep.Path + DownloadLocation: fmt.Sprintf("https://proxy.golang.org/%s/@v/%s.zip", dep.Path, dep.Version), + FilesAnalyzed: false, + LicenseConcluded: NOASSERTION, + LicenseDeclared: NOASSERTION, + CopyrightText: NOASSERTION, + ExternalRefs: []ExternalRef{{ + Category: "PACKAGE_MANAGER", + Type: "purl", + Locator: goRef(dep.Path, dep.Version), + }}, + } + + if dep.Sum != "" { + pkg.Checksums = []Checksum{{ + Algorithm: "SHA256", + Value: h1ToSHA256(dep.Sum), + }} + } + + doc.Packages = append(doc.Packages, pkg) + } + var buf bytes.Buffer - if err := tmpl.Execute(&buf, tmplInfo{ - BuildInfo: *bi, - Date: date.Format(dateFormat), - KoVersion: koVersion, - ImgDigest: imgDigest, - }); err != nil { + enc := json.NewEncoder(&buf) + enc.SetIndent("", " ") + if err := enc.Encode(doc); err != nil { return nil, err } return buf.Bytes(), nil } -type tmplInfo struct { - BuildInfo - Date, UUID, KoVersion string - ImgDigest v1.Hash +// Below this is forked from here: +// https://github.com/kubernetes-sigs/bom/blob/main/pkg/spdx/json/v2.2.2/types.go + +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const ( + NOASSERTION = "NOASSERTION" + Version = "SPDX-2.2" +) + +type Document struct { + ID string `json:"SPDXID"` + Name string `json:"name"` + Version string `json:"spdxVersion"` + CreationInfo CreationInfo `json:"creationInfo"` + DataLicense string `json:"dataLicense"` + Namespace string `json:"documentNamespace"` + DocumentDescribes []string `json:"documentDescribes,omitempty"` + Files []File `json:"files,omitempty"` + Packages []Package `json:"packages,omitempty"` + Relationships []Relationship `json:"relationships,omitempty"` +} + +type CreationInfo struct { + Created string `json:"created"` // Date + Creators []string `json:"creators,omitempty"` + LicenseListVersion string `json:"licenseListVersion,omitempty"` } -// TODO: use k8s.io/release/pkg/bom -var tmpl = template.Must(template.New("").Funcs(template.FuncMap{ - "dots": func(s string) string { return strings.ReplaceAll(s, "/", ".") }, - "goRef": func(p, v string) string { return goRef(p, v) }, - "ociRef": func(p string, d v1.Hash) string { return ociRef(p, d) }, - "h1toSHA256": func(s string) (string, error) { - if !strings.HasPrefix(s, "h1:") { - return "", fmt.Errorf("malformed sum prefix: %q", s) - } - b, err := base64.StdEncoding.DecodeString(s[3:]) - if err != nil { - return "", fmt.Errorf("malformed sum: %q: %w", s, err) - } - return hex.EncodeToString(b), nil - }, -}).Parse(`SPDXVersion: SPDX-2.2 -DataLicense: CC0-1.0 -SPDXID: SPDXRef-DOCUMENT -DocumentName: {{ .BuildInfo.Main.Path }} -DocumentNamespace: http://spdx.org/spdxpackages/{{ .BuildInfo.Main.Path }} -Creator: Tool: ko {{ .KoVersion }} -Created: {{ .Date }} - -##### Package representing {{ .BuildInfo.Main.Path }} - -PackageName: {{ .BuildInfo.Main.Path }} -SPDXID: SPDXRef-Package-{{ .BuildInfo.Main.Path | dots }} -PackageSupplier: Organization: {{ .BuildInfo.Main.Path }} -PackageDownloadLocation: https://{{ .BuildInfo.Main.Path }} -FilesAnalyzed: false -PackageHomePage: https://{{ .BuildInfo.Main.Path }} -PackageLicenseConcluded: NOASSERTION -PackageLicenseDeclared: NOASSERTION -PackageCopyrightText: NOASSERTION -PackageLicenseComments: NOASSERTION -PackageComment: NOASSERTION -ExternalRef: PACKAGE-MANAGER purl {{ ociRef .Path .ImgDigest }} - -Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-{{ .BuildInfo.Main.Path | dots }} - -{{ range .Deps }} -Relationship: SPDXRef-Package-{{ $.Main.Path | dots }} DEPENDS_ON SPDXRef-Package-{{ .Path | dots }}-{{ .Version }} - -##### Package representing {{ .Path }} - -PackageName: {{ .Path }} -SPDXID: SPDXRef-Package-{{ .Path | dots }}-{{ .Version }} -PackageVersion: {{ .Version }} -PackageSupplier: Organization: {{ .Path }} -PackageDownloadLocation: https://proxy.golang.org/{{ .Path }}/@v/{{ .Version }}.zip -FilesAnalyzed: false -{{ if .Sum }}PackageChecksum: SHA256: {{ .Sum | h1toSHA256 }} -{{ end }}PackageLicenseConcluded: NOASSERTION -PackageLicenseDeclared: NOASSERTION -PackageCopyrightText: NOASSERTION -PackageLicenseComments: NOASSERTION -PackageComment: NOASSERTION -ExternalRef: PACKAGE-MANAGER purl {{ goRef .Path .Version }} - -{{ end }} -`)) +type Package struct { + ID string `json:"SPDXID"` + Name string `json:"name"` + Version string `json:"versionInfo"` + FilesAnalyzed bool `json:"filesAnalyzed"` + LicenseDeclared string `json:"licenseDeclared"` + LicenseConcluded string `json:"licenseConcluded"` + Description string `json:"description,omitempty"` + DownloadLocation string `json:"downloadLocation"` + Originator string `json:"originator,omitempty"` + SourceInfo string `json:"sourceInfo,omitempty"` + CopyrightText string `json:"copyrightText"` + HasFiles []string `json:"hasFiles,omitempty"` + LicenseInfoFromFiles []string `json:"licenseInfoFromFiles,omitempty"` + Checksums []Checksum `json:"checksums,omitempty"` + ExternalRefs []ExternalRef `json:"externalRefs,omitempty"` + VerificationCode *PackageVerificationCode `json:"packageVerificationCode,omitempty"` +} + +type PackageVerificationCode struct { + Value string `json:"packageVerificationCodeValue"` + ExcludedFiles []string `json:"packageVerificationCodeExcludedFiles,omitempty"` +} + +type File struct { + ID string `json:"SPDXID"` + Name string `json:"fileName"` + CopyrightText string `json:"copyrightText"` + NoticeText string `json:"noticeText,omitempty"` + LicenseConcluded string `json:"licenseConcluded"` + Description string `json:"description,omitempty"` + FileTypes []string `json:"fileTypes,omitempty"` + LicenseInfoInFile []string `json:"licenseInfoInFiles"` // List of licenses + Checksums []Checksum `json:"checksums"` +} + +type Checksum struct { + Algorithm string `json:"algorithm"` + Value string `json:"checksumValue"` +} + +type ExternalRef struct { + Category string `json:"referenceCategory"` + Locator string `json:"referenceLocator"` + Type string `json:"referenceType"` +} + +type Relationship struct { + Element string `json:"spdxElementId"` + Type string `json:"relationshipType"` + Related string `json:"relatedSpdxElement"` +} diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index a78938f428..6da01e0ac7 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -333,7 +333,7 @@ func spdx(version string) sbomber { if err != nil { return nil, "", err } - return b, ctypes.SPDXMediaType, nil + return b, ctypes.SPDXJSONMediaType, nil } }