Skip to content

Commit

Permalink
Merge pull request #1218 from xnox/external-purls
Browse files Browse the repository at this point in the history
sbom: include external refs for fetched sourcecode in SPDX
  • Loading branch information
xnox authored May 22, 2024
2 parents c31490a + 2ff511f commit e162d67
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 2 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,36 @@ jobs:
docker run --rm -v $(pwd)/sbom.json:/sbom.json --entrypoint "sh" cgr.dev/chainguard/wolfi-base -c "apk add spdx-tools-java && tools-java Verify /sbom.json"
done
- name: Verify SBOM External Refs (git-checkout)
if: matrix.example == 'git-checkout.yaml'
run: |
set -euxo pipefail
tar -Oxf packages/x86_64/git-checkout*.apk var/lib/db/sbom > git-checkout.sbom.json
# Verify APK ref
grep '"pkg:apk/unknown/git-checkout@v0.0.1-r0?arch=x86_64"' git-checkout.sbom.json
# Verify github tag ref
grep '"pkg:github/puerco/hello.git@v0.0.1"' sbom.json git-checkout.sbom.json
# Verify github sha ref
grep '"pkg:github/puerco/hello.git@a73c4feb284dc6ed1e5758740f717f99dcd4c9d7"' git-checkout.sbom.json
# Verify generic git ref
grep '"pkg:generic/hello@v0.0.1?vcs_url=git%2Bhttps%3A%2F%2Fgitlab.com%2Fxnox%2Fhello.git%40a73c4feb284dc6ed1e5758740f717f99dcd4c9d7"' git-checkout.sbom.json
# Verify ConfigFile ref
grep '"pkg:github/chainguard-dev/melange@${{github.sha}}#examples/git-checkout.yaml"' git-checkout.sbom.json
- name: Verify SBOM External Refs (gnu-hello)
if: matrix.example == 'gnu-hello.yaml'
run: |
set -euxo pipefail
tar -Oxf packages/x86_64/hello-2*.apk var/lib/db/sbom > hello.sbom.json
# Verify generic fetch ref
grep '"pkg:generic/hello@2.12?checksum=sha256%3Acf04af86dc085268c5f4470fbae49b18afbc221b78096aab842d934a76bad0ab\\u0026download_url=https%3A%2F%2Fftp.gnu.org%2Fgnu%2Fhello%2Fhello-2.12.tar.gz"' hello.sbom.json
- name: Check packages can be installed with apk
run: |
set -euxo pipefail
Expand Down
18 changes: 17 additions & 1 deletion .github/workflows/wolfi-presubmit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,23 @@ jobs:
- run: |
make SHELL="/bin/bash" MELANGE="sudo melange" package/${{ matrix.package }}
- run: |
- name: "Retrieve Wolfi advisory data"
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
repository: "wolfi-dev/advisories"
path: "data/wolfi-advisories"

# this need to point to main to always get the latest action
- uses: wolfi-dev/actions/install-wolfictl@main # main

- name: Test installable and Scan for CVEs
run: |
for f in packages/x86_64/${{ matrix.package }}-*.apk; do
docker run --rm -v $(pwd):/work cgr.dev/chainguard/wolfi-base apk add --allow-untrusted /work/$f
wolfictl scan \
--advisories-repo-dir 'data/wolfi-advisories' \
--advisory-filter 'resolved' \
--require-zero \
$f \
2> /dev/null # The error message renders strangely on GitHub Actions, and the important information is already being sent to stdout.
done
7 changes: 7 additions & 0 deletions examples/git-checkout.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,10 @@ pipeline:
destination: tag-unpeeled
tag: v0.0.1
expected-commit: fed9b28e2973bee65bcc503c6ab6522e8bfdd3d1

- uses: git-checkout
with:
repository: https://gitlab.com/xnox/hello.git
destination: gitlab
tag: v0.0.1
expected-commit: a73c4feb284dc6ed1e5758740f717f99dcd4c9d7
75 changes: 75 additions & 0 deletions pkg/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ import (
"cloud.google.com/go/storage"
"github.com/chainguard-dev/clog"
apkofs "github.com/chainguard-dev/go-apk/pkg/fs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/storage/filesystem"
purl "github.com/package-url/packageurl-go"
"github.com/yookoala/realpath"
"github.com/zealic/xignore"
"go.opentelemetry.io/otel"
Expand Down Expand Up @@ -473,6 +476,62 @@ func (b *Build) IsBuildLess() bool {
return len(b.Configuration.Pipeline) == 0
}

// ConfigFileExternalRef calculates ExternalRef for the melange config
// file itself.
func (b *Build) ConfigFileExternalRef() (*purl.PackageURL, error) {
// configFile must exist
configpath, err := filepath.Abs(b.ConfigFile)
if err != nil {
return nil, err
}
// If not a git repository, skip
opt := &git.PlainOpenOptions{DetectDotGit: true}
r, err := git.PlainOpenWithOptions(configpath, opt)
if err != nil {
return nil, nil
}
// If no remote origin, skip (local git repo)
remote, err := r.Remote("origin")
if err != nil {
return nil, nil
}
repository := remote.Config().URLs[0]
// Only supports github-actions style https github checkouts
if !strings.HasPrefix(repository, "https://github.com/") {
return nil, nil
}
namespace, name, _ := strings.Cut(strings.TrimPrefix(repository, "https://github.com/"), "/")

// Head must exist
ref, err := r.Head()
if err != nil {
return nil, err
}
version := ref.Hash()

// Try to get configfile as subpath in the repository
s, ok := r.Storer.(*filesystem.Storage)
if !ok {
return nil, errors.New("Repository storage is not filesystem.Storage")
}
base := filepath.Dir(s.Filesystem().Root())
subpath, err := filepath.Rel(base, configpath)
if err != nil {
return nil, err
}
newpurl := &purl.PackageURL{
Type: "github",
Namespace: namespace,
Name: name,
Version: version.String(),
Subpath: subpath,
}
if err := newpurl.Normalize(); err != nil {
return nil, err
}
return newpurl, nil
}

func (b *Build) PopulateCache(ctx context.Context) error {
log := clog.FromContext(ctx)
ctx, span := otel.Tracer("melange").Start(ctx, "PopulateCache")
Expand Down Expand Up @@ -646,6 +705,8 @@ func (b *Build) BuildPackage(ctx context.Context) error {
b.GuestDir = guestDir
}

var externalRefs []purl.PackageURL

log.Infof("evaluating pipelines for package requirements")
for _, p := range b.Configuration.Pipeline {
// fine to pass nil for config, since not running in container.
Expand All @@ -654,6 +715,7 @@ func (b *Build) BuildPackage(ctx context.Context) error {
if err := pctx.ApplyNeeds(ctx, &pb); err != nil {
return fmt.Errorf("unable to apply pipeline requirements: %w", err)
}
externalRefs = append(externalRefs, pctx.ExternalRefs...)
}

for _, spkg := range b.Configuration.Subpackages {
Expand All @@ -665,10 +727,21 @@ func (b *Build) BuildPackage(ctx context.Context) error {
if err := pctx.ApplyNeeds(ctx, &pb); err != nil {
return fmt.Errorf("unable to apply pipeline requirements: %w", err)
}
externalRefs = append(externalRefs, pctx.ExternalRefs...)
}
}
pb.Subpackage = nil

configFileRef, err := b.ConfigFileExternalRef()
if err != nil {
return fmt.Errorf("failed to create ExternalRef for configfile: %w", err)
}

// In SPDX v3 there is dedicate field for this
// https://spdx.github.io/spdx-spec/v3.0/model/Build/Properties/configSourceUri/
log.Infof("adding external ref %s for ConfigFile", configFileRef)
externalRefs = append(externalRefs, *configFileRef)

if b.EmptyWorkspace {
log.Infof("empty workspace requested")
} else {
Expand Down Expand Up @@ -845,6 +918,7 @@ func (b *Build) BuildPackage(ctx context.Context) error {
PackageVersion: fmt.Sprintf("%s-r%d", b.Configuration.Package.Version, b.Configuration.Package.Epoch),
License: b.Configuration.Package.LicenseExpression(),
LicensingInfos: licensinginfos,
ExternalRefs: externalRefs,
Copyright: b.Configuration.Package.FullCopyright(),
Namespace: namespace,
Arch: b.Arch.ToAPK(),
Expand All @@ -860,6 +934,7 @@ func (b *Build) BuildPackage(ctx context.Context) error {
PackageVersion: fmt.Sprintf("%s-r%d", b.Configuration.Package.Version, b.Configuration.Package.Epoch),
License: b.Configuration.Package.LicenseExpression(),
LicensingInfos: licensinginfos,
ExternalRefs: externalRefs,
Copyright: b.Configuration.Package.FullCopyright(),
Namespace: namespace,
Arch: b.Arch.ToAPK(),
Expand Down
87 changes: 87 additions & 0 deletions pkg/build/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"gopkg.in/yaml.v3"

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

"chainguard.dev/melange/pkg/cond"
"chainguard.dev/melange/pkg/config"
Expand All @@ -46,6 +47,7 @@ type PipelineContext struct {
// Ordered list of pipeline directories to search for pipelines
PipelineDirs []string
steps int
ExternalRefs []purl.PackageURL
}

func NewPipelineContext(p *config.Pipeline, environment *apko_types.ImageConfiguration, config *container.Config, pipelineDirs []string) *PipelineContext {
Expand Down Expand Up @@ -513,6 +515,15 @@ func (pctx *PipelineContext) ApplyNeeds(ctx context.Context, pb *PipelineBuild)
return err
}

externalRefs, err := pctx.computeExternalRefs(spctx)
if err != nil {
return err
}
if externalRefs != nil {
log.Infof(" adding external refs %s for pipeline %q", externalRefs, pctx.Identity())
pctx.ExternalRefs = append(pctx.ExternalRefs, externalRefs...)
}

if err := spctx.ApplyNeeds(ctx, pb); err != nil {
return err
}
Expand All @@ -531,6 +542,82 @@ func (pctx *PipelineContext) ApplyNeeds(ctx context.Context, pb *PipelineBuild)
return nil
}

// computeExternalRefs generates PURLs for subpipelines
func (pctx *PipelineContext) computeExternalRefs(spctx *PipelineContext) ([]purl.PackageURL, error) {
var purls []purl.PackageURL
var newpurl purl.PackageURL

switch pctx.Pipeline.Uses {
case "fetch":
args := make(map[string]string)
args["download_url"] = spctx.Pipeline.With["${{inputs.uri}}"]
if len(spctx.Pipeline.With["${{inputs.expected-sha256}}"]) > 0 {
args["checksum"] = "sha256:" + spctx.Pipeline.With["${{inputs.expected-sha256}}"]
}
if len(spctx.Pipeline.With["${{inputs.expected-sha512}}"]) > 0 {
args["checksum"] = "sha512:" + spctx.Pipeline.With["${{inputs.expected-sha512}}"]
}
newpurl = purl.PackageURL{
Type: "generic",
Name: spctx.Pipeline.With["${{inputs.purl-name}}"],
Version: spctx.Pipeline.With["${{inputs.purl-version}}"],
Qualifiers: purl.QualifiersFromMap(args),
}
if err := newpurl.Normalize(); err != nil {
return nil, err
}
purls = append(purls, newpurl)

case "git-checkout":
repository := spctx.Pipeline.With["${{inputs.repository}}"]
if strings.HasPrefix(repository, "https://github.com/") {
namespace, name, _ := strings.Cut(strings.TrimPrefix(repository, "https://github.com/"), "/")
versions := []string{
spctx.Pipeline.With["${{inputs.tag}}"],
spctx.Pipeline.With["${{inputs.expected-commit}}"],
}
for _, version := range versions {
if version != "" {
newpurl = purl.PackageURL{
Type: "github",
Namespace: namespace,
Name: name,
Version: version,
}
if err := newpurl.Normalize(); err != nil {
return nil, err
}
purls = append(purls, newpurl)
}
}
} else {
// Create nice looking package name, last component of uri, without .git
name := strings.TrimSuffix(filepath.Base(repository), ".git")
// Encode vcs_url with git+ prefix and @commit suffix
vcsUrl := "git+" + repository
if len(spctx.Pipeline.With["${{inputs.expected-commit}}"]) > 0 {
vcsUrl = vcsUrl + "@" + spctx.Pipeline.With["${{inputs.expected-commit}}"]
}
// Use tag as version
version := ""
if len(spctx.Pipeline.With["${{inputs.tag}}"]) > 0 {
version = spctx.Pipeline.With["${{inputs.tag}}"]
}
newpurl = purl.PackageURL{
Type: "generic",
Name: name,
Version: version,
Qualifiers: purl.QualifiersFromMap(map[string]string{"vcs_url": vcsUrl}),
}
if err := newpurl.Normalize(); err != nil {
return nil, err
}
purls = append(purls, newpurl)
}
}
return purls, nil
}

// pipelineStepWorkDir returns the workdir for the current pipeline step.
func (pctx *PipelineContext) pipelineStepWorkDir() (string, error) {
if pctx.Pipeline.WorkDir == "" {
Expand Down
10 changes: 10 additions & 0 deletions pkg/build/pipelines/fetch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ inputs:
description: |
The expected SHA512 of the downloaded artifact.
purl-name:
description: |
package-URL (PURL) name for use in SPDX SBOM External References
default: ${{package.name}}

purl-version:
description: |
package-URL (PURL) version for use in SPDX SBOM External References
default: ${{package.version}}

uri:
description: |
The URI to fetch as an artifact.
Expand Down
7 changes: 6 additions & 1 deletion pkg/sbom/bom.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
// data) designed to be transcoded to specific formats.
package sbom

import "fmt"
import (
"fmt"

purl "github.com/package-url/packageurl-go"
)

type bom struct {
Packages []pkg
Expand All @@ -42,6 +46,7 @@ type pkg struct {
Arch string
Checksums map[string]string
Relationships []relationship
ExternalRefs []purl.PackageURL
}

func (p *pkg) ID() string {
Expand Down
2 changes: 2 additions & 0 deletions pkg/sbom/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"time"

"github.com/chainguard-dev/clog"
purl "github.com/package-url/packageurl-go"
"go.opentelemetry.io/otel"
)

Expand All @@ -33,6 +34,7 @@ type Spec struct {
PackageVersion string
License string // Full SPDX license expression
LicensingInfos map[string]string
ExternalRefs []purl.PackageURL
Copyright string
Namespace string
Arch string
Expand Down
8 changes: 8 additions & 0 deletions pkg/sbom/implementation.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ func generateAPKPackage(spec *Spec) (pkg, error) {
Relationships: []relationship{},
LicenseDeclared: spdx.NOASSERTION,
LicenseConcluded: spdx.NOASSERTION, // remove when omitted upstream
ExternalRefs: spec.ExternalRefs,
Copyright: spec.Copyright,
Namespace: spec.Namespace,
Arch: spec.Arch,
Expand Down Expand Up @@ -149,6 +150,13 @@ func addPackage(doc *spdx.Document, p *pkg) {
Type: "purl",
})
}
for _, purl := range p.ExternalRefs {
spdxPkg.ExternalRefs = append(spdxPkg.ExternalRefs, spdx.ExternalRef{
Category: "PACKAGE_MANAGER",
Locator: purl.ToString(),
Type: "purl",
})
}

doc.Packages = append(doc.Packages, spdxPkg)

Expand Down

0 comments on commit e162d67

Please sign in to comment.