Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkg/build/build_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ func TestBuild_BuildPackage(t *testing.T) {
name: "7zip-two-fetches",
expectedVersion: "2301-r3",
},
{
name: "git-checkout-cherry-pick",
expectedVersion: "1.2-r1",
},
{
name: "bogus-version",
expectedVersion: "1.0.0_b6",
Expand Down
34 changes: 34 additions & 0 deletions pkg/build/testdata/build_configs/git-checkout-cherry-pick.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package:
name: git-checkout-cherry-pick
version: 1.2
epoch: 1
description: Test package for cherry-pick SBOM generation
copyright:
- license: Apache-2.0
checks:
disabled:
- empty

environment:
contents:
keyring:
- https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
repositories:
- https://packages.wolfi.dev/os
packages:
- busybox

pipeline:
- uses: git-checkout
with:
repository: https://github.com/chainguard-dev/hello-world-golang.git
tag: v${{package.version}}
expected-commit: bade10b96289e0c108ef6f7ea26be293c4946299
cherry-picks: |
main/618bb31108414cb031a29e6ca521e1192079c1af: Update README and add license
main/3918c15bbe67b08ccf7a3027b1fba3f1518cf328: Update clone url in readme

- runs: |
# Create a simple test file to make the package non-empty
mkdir -p ${{targets.destdir}}/usr/share/hello
cp README.md ${{targets.destdir}}/usr/share/hello/ || echo "README created from cherry-picks" > ${{targets.destdir}}/usr/share/hello/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
{
"SPDXID": "SPDXRef-DOCUMENT",
"name": "apk-git-checkout-cherry-pick-1.2-r1",
"spdxVersion": "SPDX-2.3",
"creationInfo": {
"created": "0001-01-01T00:00:00Z",
"creators": [
"Tool: melange (devel)",
"Organization: Chainguard, Inc"
],
"licenseListVersion": "3.22"
},
"dataLicense": "CC0-1.0",
"documentNamespace": "https://spdx.org/spdxdocs/chainguard/melange/db5631710be34672aba80a8b7f2c4f4d",
"documentDescribes": [
"SPDXRef-Package-git-checkout-cherry-pick-1.2-r1"
],
"packages": [
{
"SPDXID": "SPDXRef-OperatingSystem",
"name": "wolfi",
"versionInfo": "20230201",
"filesAnalyzed": false,
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "NOASSERTION",
"description": "Operating System",
"downloadLocation": "NOASSERTION",
"originator": "Organization: Wolfi",
"supplier": "Organization: Wolfi",
"primaryPackagePurpose": "OPERATING-SYSTEM"
},
{
"SPDXID": "SPDXRef-Package-git-checkout-cherry-pick-1.2-r1",
"name": "git-checkout-cherry-pick",
"versionInfo": "1.2-r1",
"filesAnalyzed": false,
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "Apache-2.0",
"downloadLocation": "NOASSERTION",
"originator": "Organization: Wolfi",
"supplier": "Organization: Wolfi",
"copyrightText": "NOASSERTION",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:apk/wolfi/git-checkout-cherry-pick@1.2-r1?arch=x86_64\u0026distro=wolfi",
"referenceType": "purl"
}
]
},
{
"SPDXID": "SPDXRef-Package-testdata-buildC95configs-git-checkout-cherry-pick.yaml-c0ffee",
"name": "testdata/build_configs/git-checkout-cherry-pick.yaml",
"versionInfo": "c0ffee",
"filesAnalyzed": false,
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "NOASSERTION",
"downloadLocation": "NOASSERTION",
"originator": "Organization: Wolfi",
"supplier": "Organization: Wolfi",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:github/wolfi-dev/os@c0ffee#testdata/build_configs/git-checkout-cherry-pick.yaml",
"referenceType": "purl"
}
]
},
{
"SPDXID": "SPDXRef-Package-github.com-chainguard-dev-hello-world-golang.git-v1.2-bade10b96289e0c108ef6f7ea26be293c4946299-0",
"name": "hello-world-golang",
"versionInfo": "v1.2",
"filesAnalyzed": false,
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "Apache-2.0",
"downloadLocation": "https://github.com/chainguard-dev/hello-world-golang/archive/bade10b96289e0c108ef6f7ea26be293c4946299.tar.gz",
"originator": "Organization: Chainguard-Dev",
"supplier": "Organization: Chainguard-Dev",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:github/chainguard-dev/hello-world-golang@v1.2",
"referenceType": "purl"
},
{
"referenceCategory": "OTHER",
"referenceLocator": "main/618bb31108414cb031a29e6ca521e1192079c1af",
"referenceType": "cherry-pick"
},
{
"referenceCategory": "OTHER",
"referenceLocator": "main/3918c15bbe67b08ccf7a3027b1fba3f1518cf328",
"referenceType": "cherry-pick"
}
]
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-Package-git-checkout-cherry-pick-1.2-r1",
"relationshipType": "DESCRIBED_BY",
"relatedSpdxElement": "SPDXRef-Package-testdata-buildC95configs-git-checkout-cherry-pick.yaml-c0ffee"
},
{
"spdxElementId": "SPDXRef-Package-git-checkout-cherry-pick-1.2-r1",
"relationshipType": "GENERATED_FROM",
"relatedSpdxElement": "SPDXRef-Package-github.com-chainguard-dev-hello-world-golang.git-v1.2-bade10b96289e0c108ef6f7ea26be293c4946299-0"
}
]
}
69 changes: 60 additions & 9 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"time"

apko_types "chainguard.dev/apko/pkg/build/types"
"chainguard.dev/apko/pkg/sbom/generator/spdx"
purl "github.com/package-url/packageurl-go"

"chainguard.dev/melange/pkg/sbom"
Expand Down Expand Up @@ -543,10 +544,52 @@ func SHA256(text string) string {
return hex.EncodeToString(algorithm.Sum(nil))
}

// parseCherryPicksToExternalRefs parses cherry-pick entries and converts them
// to SPDX ExternalRef entries with category "OTHER" and type "cherry-pick".
// The format expected is: [branch/]commit-id: comment
// Each cherry-pick is represented as an ExternalRef with the locator being the
// branch/commit format
func parseCherryPicksToExternalRefs(cherryPicks string) []spdx.ExternalRef {
lines := strings.Split(cherryPicks, "\n")
refs := make([]spdx.ExternalRef, 0, len(lines))
for _, line := range lines {
line = strings.Split(line, "#")[0]
line = strings.TrimSpace(line)
if line == "" {
continue
}

parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
// Invalid format, skip this line
continue
}

locator := strings.TrimSpace(parts[0])
comment := strings.TrimSpace(parts[1])
if locator == "" || comment == "" {
continue
}

// Create an ExternalRef entry
// Note: The comment is currently not included as apko's ExternalRef
// struct doesn't have a Comment field yet. Once that's added, we should
// include it here.
refs = append(refs, spdx.ExternalRef{
Category: "OTHER",
Type: "cherry-pick",
Locator: locator,
// Comment: comment, // TODO: Uncomment once apko supports this field
})
}

return refs
}

// getGitSBOMPackage creates an SBOM package for Git based repositories.
// Returns nil package and nil error if the repository is not from a supported platform or
// if neither a tag of expectedCommit is not provided
func getGitSBOMPackage(repo, tag, expectedCommit string, idComponents []string, licenseDeclared, hint, supplier string) (*sbom.Package, error) {
func getGitSBOMPackage(repo, tag, expectedCommit, cherryPicks string, idComponents []string, licenseDeclared, hint, supplier string) (*sbom.Package, error) {
var repoType, namespace, name, ref string
var downloadLocation string

Expand Down Expand Up @@ -636,14 +679,21 @@ func getGitSBOMPackage(repo, tag, expectedCommit string, idComponents []string,
return nil, err
}

// Parse cherry-picks and create ExternalRefs for them
var externalRefs []spdx.ExternalRef
if cherryPicks != "" {
externalRefs = parseCherryPicksToExternalRefs(cherryPicks)
}

return &sbom.Package{
IDComponents: idComponents,
Name: name,
Version: v,
LicenseDeclared: licenseDeclared,
Namespace: namespace,
PURL: pu,
DownloadLocation: downloadLocation,
IDComponents: idComponents,
Name: name,
Version: v,
LicenseDeclared: licenseDeclared,
Namespace: namespace,
PURL: pu,
DownloadLocation: downloadLocation,
AdditionalExternalRefs: externalRefs,
}, nil
}

Expand Down Expand Up @@ -717,6 +767,7 @@ func (p Pipeline) SBOMPackageForUpstreamSource(licenseDeclared, supplier string,
tag := with["tag"]
expectedCommit := with["expected-commit"]
hint := with["type-hint"]
cherryPicks := with["cherry-picks"]

// We'll use all available data to ensure our SBOM's package ID is unique, even
// when the same repo is git-checked out multiple times.
Expand All @@ -735,7 +786,7 @@ func (p Pipeline) SBOMPackageForUpstreamSource(licenseDeclared, supplier string,
idComponents = append(idComponents, uniqueID)
}

gitPackage, err := getGitSBOMPackage(repo, tag, expectedCommit, idComponents, licenseDeclared, hint, supplier)
gitPackage, err := getGitSBOMPackage(repo, tag, expectedCommit, cherryPicks, idComponents, licenseDeclared, hint, supplier)
if err != nil {
return nil, err
} else if gitPackage != nil {
Expand Down
87 changes: 86 additions & 1 deletion pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"testing"

"chainguard.dev/apko/pkg/sbom/generator/spdx"
"github.com/chainguard-dev/clog/slogtest"
purl "github.com/package-url/packageurl-go"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -875,7 +876,7 @@ func TestGetGitSBOMPackage(t *testing.T) {

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
pkg, err := getGitSBOMPackage(tc.repo, tc.tag, tc.expectedCommit, tc.idComponents, tc.licenseDeclared, tc.typeHint, tc.supplier)
pkg, err := getGitSBOMPackage(tc.repo, tc.tag, tc.expectedCommit, "", tc.idComponents, tc.licenseDeclared, tc.typeHint, tc.supplier)
if tc.expectError {
require.Error(t, err)
return
Expand All @@ -902,6 +903,90 @@ func TestGetGitSBOMPackage(t *testing.T) {
}
}

func TestParseCherryPicksToExternalRefs(t *testing.T) {
testCases := []struct {
name string
input string
expected int // number of expected refs
checkRef func(*testing.T, []spdx.ExternalRef)
}{
{
name: "empty string",
input: "",
expected: 0,
},
{
name: "single cherry-pick",
input: "3.10/c62c9e518b784fe44432a3f4fc265fb95b651906: CVE-2024-4032",
expected: 1,
checkRef: func(t *testing.T, refs []spdx.ExternalRef) {
require.Equal(t, "OTHER", refs[0].Category)
require.Equal(t, "cherry-pick", refs[0].Type)
require.Equal(t, "3.10/c62c9e518b784fe44432a3f4fc265fb95b651906", refs[0].Locator)
},
},
{
name: "multiple cherry-picks",
input: `3.10/c62c9e518b784fe44432a3f4fc265fb95b651906: CVE-2024-4032
main/582b4d7d62f1c512568649ce8b6db085a3d85a9f: Security fix`,
expected: 2,
checkRef: func(t *testing.T, refs []spdx.ExternalRef) {
require.Equal(t, "OTHER", refs[0].Category)
require.Equal(t, "cherry-pick", refs[0].Type)
require.Equal(t, "3.10/c62c9e518b784fe44432a3f4fc265fb95b651906", refs[0].Locator)

require.Equal(t, "OTHER", refs[1].Category)
require.Equal(t, "cherry-pick", refs[1].Type)
require.Equal(t, "main/582b4d7d62f1c512568649ce8b6db085a3d85a9f", refs[1].Locator)
},
},
{
name: "with comments after hash",
input: "3.10/c62c9e518b784fe44432a3f4fc265fb95b651906: CVE-2024-4032 # this is a comment",
expected: 1,
checkRef: func(t *testing.T, refs []spdx.ExternalRef) {
require.Equal(t, "3.10/c62c9e518b784fe44432a3f4fc265fb95b651906", refs[0].Locator)
},
},
{
name: "with empty lines",
input: `3.10/c62c9e518b784fe44432a3f4fc265fb95b651906: CVE-2024-4032

main/582b4d7d62f1c512568649ce8b6db085a3d85a9f: Security fix`,
expected: 2,
},
{
name: "commit without branch prefix",
input: "c62c9e518b784fe44432a3f4fc265fb95b651906: CVE-2024-4032",
expected: 1,
checkRef: func(t *testing.T, refs []spdx.ExternalRef) {
require.Equal(t, "c62c9e518b784fe44432a3f4fc265fb95b651906", refs[0].Locator)
},
},
{
name: "invalid format - missing colon",
input: "3.10/c62c9e518b784fe44432a3f4fc265fb95b651906",
expected: 0,
},
{
name: "invalid format - missing comment",
input: "3.10/c62c9e518b784fe44432a3f4fc265fb95b651906:",
expected: 0,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
refs := parseCherryPicksToExternalRefs(tc.input)
require.Len(t, refs, tc.expected)

if tc.checkRef != nil {
tc.checkRef(t, refs)
}
})
}
}

func TestSetCap(t *testing.T) {
tests := []struct {
setcap []Capability
Expand Down
9 changes: 9 additions & 0 deletions pkg/sbom/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ type Package struct {
// source locations; Leaving this empty will result in NOASSERTION being
// used as its value.
DownloadLocation string

// Additional ExternalRefs beyond the PURL. This is used to store cherry-pick
// commits and other additional references that should be included in the SBOM.
// These will be appended to the ExternalRefs in the SPDX package alongside
// the PURL reference.
AdditionalExternalRefs []spdx.ExternalRef
}

// ToSPDX returns the Package converted to its SPDX representation.
Expand Down Expand Up @@ -179,6 +185,9 @@ func (p Package) getExternalRefs() []spdx.ExternalRef {
})
}

// Append any additional external references (e.g., cherry-picks)
result = append(result, p.AdditionalExternalRefs...)

return result
}

Expand Down
Loading