Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sbom support #3954

Merged
merged 1 commit into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
72 changes: 72 additions & 0 deletions docs/sbom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Software Bill-of-Materials

LinuxKit bootable images are composed of existing OCI images.
OCI images, when built, often are scanned to create a
software bill-of-materials (SBoM). The buildkit builder
system itself contains the [ability to integrate SBoM scanning and generation into the build process](https://docs.docker.com/build/attestations/sbom/).

When LinuxKit composes an operating system image using `linuxkit build`,
it will, by default, combine the SBoMs of all the OCI images used to create
the final image.

It looks for SBoMs in the following locations:

* [image attestation storage](https://docs.docker.com/build/attestations/attestation-storage/)

Future support for [OCI Image-Spec v1.1 Artifacts](https://github.com/opencontainers/image-spec)
is under consideration, and will be reviewed when it is generally available.

When building packages with `linuxkit pkg build`, it also has the ability to generate an SBoM for the
package, which later can be consumed by `linuxkit build`.

## Consuming SBoM From Packages

When `linuxkit build` is run, it does the following for dealing with SBoMs:

1. For each OCI image that it processes:
1. check if the image contains an SBoM attestation; it not, skip this step.
1. Retrieve the SBoM attestation.
1. After generating the root filesystem, combine all of the individual SBoMs into a single unified SBoM.
1. Save the output single SBoM into the root of the image as `sbom.spdx.json`.

Currently, only SPDX json format is supported.

### SBoM Scanner and Output Format

By default, linuxkit combines the SBoMs into a file with output format SPDX json,
and the file saved to the filename `sbom.spdx.json`.

In addition, in order to assist with reproducible builds, the creation date/time of the SBoM is
a fixed date/time set by linuxkit, rather than the current date/time. Note, however, that even
with a fixed date/time, reproducible builds depends on reproducible SBoMs on the underlying container images.
This is not always the case, as the unique IDs for each package and file might be deterministic, but it might not.

This can be overridden by using the CLI flags:

* `--no-sbom`: do not find and consolidate the SBoMs
* `--sbom-output <filename>`: the filename to save the output to in the image.
* `--sbom-current-time true|false`: whether or not to use the current time for the SBoM creation date/time (default `false`)

### Disable SBoM for Images

To disable SBoM generation when running `linuxkit build`, use the CLI flag `--sbom false`.

## Generating SBoM For Packages

When `linuxkit pkg build` is run, by default it enables generating an SBoM using the
[SBoM generating capabilities of buildkit](https://www.docker.com/blog/generate-sboms-with-buildkit/).
This means that it inherits all of those capabilities as well, and saves the SBoM in the same location,
as an attestation on the image.

### SBoM Scanner

By default, buildkit runs [syft](http://hub.docker.com/r/anchore/syft) with output format SPDX json,
specifically via its integration image [buildkit-syft-scanner](docker.io/docker/buildkit-syft-scanner).
You can select a different image to run a scanner, provided it complies with the
[buildkit SBoM protocol](https://github.com/moby/buildkit/blob/master/docs/attestations/sbom-protocol.md),
by passing the CLI flag `--sbom-scanner <image>`.

### Disable SBoM for Packages

To disable SBoM generation when running `linuxkit pkg build`, use the CLI flag `--sbom-scanner=false`.

42 changes: 29 additions & 13 deletions src/cmd/linuxkit/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import (
"github.com/spf13/cobra"
)

const defaultNameForStdin = "moby"
const (
defaultNameForStdin = "moby"
defaultSbomFilename = "sbom.spdx.json"
)

type formatList []string

Expand All @@ -37,17 +40,20 @@ func (f *formatList) Type() string {
func buildCmd() *cobra.Command {

var (
name string
dir string
outputFile string
sizeString string
pull bool
docker bool
decompressKernel bool
arch string
cacheDir flagOverEnvVarOverDefaultString
buildFormats formatList
outputTypes = moby.OutputTypes()
name string
dir string
outputFile string
sizeString string
pull bool
docker bool
decompressKernel bool
arch string
cacheDir flagOverEnvVarOverDefaultString
buildFormats formatList
outputTypes = moby.OutputTypes()
noSbom bool
sbomOutputFilename string
sbomCurrentTime bool
)
cmd := &cobra.Command{
Use: "build",
Expand Down Expand Up @@ -192,7 +198,14 @@ The generated image can be in one of multiple formats which can be run on variou
if moby.Streamable(buildFormats[0]) {
tp = buildFormats[0]
}
err = moby.Build(m, w, moby.BuildOpts{Pull: pull, BuilderType: tp, DecompressKernel: decompressKernel, CacheDir: cacheDir.String(), DockerCache: docker, Arch: arch})
var sbomGenerator *moby.SbomGenerator
if !noSbom {
sbomGenerator, err = moby.NewSbomGenerator(sbomOutputFilename, sbomCurrentTime)
if err != nil {
return fmt.Errorf("error creating sbom generator: %v", err)
}
}
err = moby.Build(m, w, moby.BuildOpts{Pull: pull, BuilderType: tp, DecompressKernel: decompressKernel, CacheDir: cacheDir.String(), DockerCache: docker, Arch: arch, SbomGenerator: sbomGenerator})
if err != nil {
return fmt.Errorf("%v", err)
}
Expand Down Expand Up @@ -224,6 +237,9 @@ The generated image can be in one of multiple formats which can be run on variou
cmd.Flags().VarP(&buildFormats, "format", "f", "Formats to create [ "+strings.Join(outputTypes, " ")+" ]")
cacheDir = flagOverEnvVarOverDefaultString{def: defaultLinuxkitCache(), envVar: envVarCacheDir}
cmd.Flags().Var(&cacheDir, "cache", fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir))
cmd.Flags().BoolVar(&noSbom, "no-sbom", false, "suppress consolidation of sboms on input container images to a single sbom and saving in the output filesystem")
cmd.Flags().BoolVar(&sbomCurrentTime, "sbom-current-time", false, "whether to use the current time as the build time in the sbom; this will make the build non-reproducible (default false)")
cmd.Flags().StringVar(&sbomOutputFilename, "sbom-output", defaultSbomFilename, "filename to save the output to in the root filesystem")

return cmd
}
30 changes: 30 additions & 0 deletions src/cmd/linuxkit/cache/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ func matchPlatformsOSArch(platforms ...v1.Platform) match.Matcher {
}
}

// matchAllAnnotations returns a matcher that matches all annotations
func matchAllAnnotations(annotations map[string]string) match.Matcher {
return func(desc v1.Descriptor) bool {
if desc.Annotations == nil {
return false
}
if len(annotations) == 0 {
return true
}
for key, value := range annotations {
if aValue, ok := desc.Annotations[key]; !ok || aValue != value {
return false
}
}
return true
}
}

func (p *Provider) findImage(imageName, architecture string) (v1.Image, error) {
root, err := p.FindRoot(imageName)
if err != nil {
Expand All @@ -50,6 +68,18 @@ func (p *Provider) findImage(imageName, architecture string) (v1.Image, error) {
return nil, fmt.Errorf("no image found for %s", imageName)
}

func (p *Provider) findIndex(imageName string) (v1.ImageIndex, error) {
root, err := p.FindRoot(imageName)
if err != nil {
return nil, err
}
ii, err := root.ImageIndex()
if err != nil {
return nil, fmt.Errorf("no image index found for %s", imageName)
}
return ii, nil
}

// FindDescriptor get the first descriptor pointed to by the image reference, whether tagged or digested
func (p *Provider) FindDescriptor(ref *reference.Spec) (*v1.Descriptor, error) {
index, err := p.cache.ImageIndex()
Expand Down
110 changes: 110 additions & 0 deletions src/cmd/linuxkit/cache/source.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
package cache

import (
"bytes"
"encoding/json"
"fmt"
"io"

"github.com/containerd/containerd/reference"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/match"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/tarball"
intoto "github.com/in-toto/in-toto-golang/in_toto"
lktspec "github.com/linuxkit/linuxkit/src/cmd/linuxkit/spec"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
)

const (
annotationDockerReferenceType = "vnd.docker.reference.type"
annotationAttestationManifest = "attestation-manifest"
annotationDockerReferenceDigest = "vnd.docker.reference.digest"
annotationInTotoPredicateType = "in-toto.io/predicate-type"
annotationSPDXDoc = "https://spdx.dev/Document"
inTotoJsonMediaType = "application/vnd.in-toto+json"
)

// ImageSource a source for an image in the OCI distribution cache.
// Implements a spec.ImageSource.
type ImageSource struct {
Expand All @@ -23,6 +36,11 @@ type ImageSource struct {
descriptor *v1.Descriptor
}

type spdxStatement struct {
intoto.StatementHeader
Predicate json.RawMessage `json:"predicate"`
}

// NewSource return an ImageSource for a specific ref and architecture in the given
// cache directory.
func (p *Provider) NewSource(ref *reference.Spec, architecture string, descriptor *v1.Descriptor) lktspec.ImageSource {
Expand Down Expand Up @@ -101,3 +119,95 @@ func (c ImageSource) V1TarReader(overrideName string) (io.ReadCloser, error) {
func (c ImageSource) Descriptor() *v1.Descriptor {
return c.descriptor
}

// SBoM return the sbom for the image
func (c ImageSource) SBoMs() ([]io.ReadCloser, error) {
index, err := c.provider.findIndex(c.ref.String())
// if it is not an index, we actually do not care much
if err != nil {
return nil, nil
}

// get the digest of the manifest that represents our targeted architecture
descs, err := partial.FindManifests(index, matchPlatformsOSArch(v1.Platform{OS: "linux", Architecture: c.architecture}))
if err != nil {
return nil, err
}
if len(descs) < 1 {
return nil, fmt.Errorf("no manifest found for %s arch %s", c.ref.String(), c.architecture)
}
if len(descs) > 1 {
return nil, fmt.Errorf("multiple manifests found for %s arch %s", c.ref.String(), c.architecture)
}
// get the digest of the manifest that represents our targeted architecture
desc := descs[0]

annotations := map[string]string{
annotationDockerReferenceType: annotationAttestationManifest,
annotationDockerReferenceDigest: desc.Digest.String(),
}
descs, err = partial.FindManifests(index, matchAllAnnotations(annotations))
if err != nil {
return nil, err
}
if len(descs) > 1 {
return nil, fmt.Errorf("multiple manifests found for %s arch %s", c.ref.String(), c.architecture)
}
if len(descs) < 1 {
return nil, nil
}

// get the layers for the first descriptor
images, err := partial.FindImages(index, match.Digests(descs[0].Digest))
if err != nil {
return nil, err
}
if len(images) < 1 {
return nil, fmt.Errorf("no attestation image found for %s arch %s, even though the manifest exists", c.ref.String(), c.architecture)
}
if len(images) > 1 {
return nil, fmt.Errorf("multiple attestation images found for %s arch %s", c.ref.String(), c.architecture)
}
image := images[0]
manifest, err := image.Manifest()
if err != nil {
return nil, err
}
layers, err := image.Layers()
if err != nil {
return nil, err
}
if len(manifest.Layers) != len(layers) {
return nil, fmt.Errorf("manifest layers and image layers do not match for the attestation for %s arch %s", c.ref.String(), c.architecture)
}
var readers []io.ReadCloser
for i, layer := range manifest.Layers {
annotations := layer.Annotations
if annotations[annotationInTotoPredicateType] != annotationSPDXDoc || layer.MediaType != inTotoJsonMediaType {
continue
}
// get the actual blob of the layer
layer, err := layers[i].Compressed()
if err != nil {
return nil, err
}
// read the layer, we want just the predicate, stripping off the header
var buf bytes.Buffer
if _, err := io.Copy(&buf, layer); err != nil {
return nil, err
}
layer.Close()
var stmt spdxStatement
if err := json.Unmarshal(buf.Bytes(), &stmt); err != nil {
return nil, err
}
if stmt.PredicateType != annotationSPDXDoc {
return nil, fmt.Errorf("unexpected predicate type %s", stmt.PredicateType)
}
sbom := stmt.Predicate

readers = append(readers, io.NopCloser(bytes.NewReader(sbom)))
}
// get the content of the single descriptor
return readers, nil
}