Skip to content

Commit

Permalink
Merge pull request #3258 from jedevc/sbom-filelist
Browse files Browse the repository at this point in the history
Supplement generated SBOMs with layer information post-build
  • Loading branch information
tonistiigi authored Nov 23, 2022
2 parents b3be4ec + 733b5b1 commit 1bc934e
Show file tree
Hide file tree
Showing 49 changed files with 4,346 additions and 126 deletions.
90 changes: 90 additions & 0 deletions cache/filelist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package cache

import (
"archive/tar"
"context"
"encoding/json"
"fmt"
"io"
"path"
"sort"

cdcompression "github.com/containerd/containerd/archive/compression"
"github.com/moby/buildkit/session"
)

const keyFileList = "filelist"

// FileList returns an ordered list of files present in the cache record that were
// changed compared to the parent. The paths of the files are in same format as they
// are in the tar stream (AUFS whiteout format). If the reference does not have a
// a blob associated with it, the list is empty.
func (sr *immutableRef) FileList(ctx context.Context, s session.Group) ([]string, error) {
res, err := g.Do(ctx, fmt.Sprintf("filelist-%s", sr.ID()), func(ctx context.Context) (interface{}, error) {
dt, err := sr.GetExternal(keyFileList)
if err == nil && dt != nil {
var files []string
if err := json.Unmarshal(dt, &files); err != nil {
return nil, err
}
return files, nil
}

if sr.getBlob() == "" {
return nil, nil
}

// lazy blobs need to be pulled first
if err := sr.Extract(ctx, s); err != nil {
return nil, err
}

desc, err := sr.ociDesc(ctx, sr.descHandlers, false)
if err != nil {
return nil, err
}

ra, err := sr.cm.ContentStore.ReaderAt(ctx, desc)
if err != nil {
return nil, err
}

r, err := cdcompression.DecompressStream(io.NewSectionReader(ra, 0, ra.Size()))
if err != nil {
return nil, err
}
defer r.Close()

var files []string

rdr := tar.NewReader(r)
for {
hdr, err := rdr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
name := path.Clean(hdr.Name)
files = append(files, name)
}
sort.Strings(files)

dt, err = json.Marshal(files)
if err != nil {
return nil, err
}
if err := sr.SetExternal(keyFileList, dt); err != nil {
return nil, err
}
return files, nil
})
if err != nil {
return nil, err
}
if res == nil {
return nil, nil
}
return res.([]string), nil
}
1 change: 1 addition & 0 deletions cache/refs.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type ImmutableRef interface {
Extract(ctx context.Context, s session.Group) error // +progress
GetRemotes(ctx context.Context, createIfNeeded bool, cfg config.RefConfig, all bool, s session.Group) ([]*solver.Remote, error)
LayerChain() RefList
FileList(ctx context.Context, s session.Group) ([]string, error)
}

type MutableRef interface {
Expand Down
143 changes: 143 additions & 0 deletions exporter/attestation/make.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package attestation

import (
"context"
"encoding/json"
"os"

"github.com/containerd/continuity/fs"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/moby/buildkit/cache"
gatewaypb "github.com/moby/buildkit/frontend/gateway/pb"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/snapshot"
"github.com/moby/buildkit/solver/result"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)

// ReadAll reads the content of an attestation.
func ReadAll(ctx context.Context, s session.Group, refs map[string]cache.ImmutableRef, att result.Attestation) ([]byte, error) {
var content []byte
if att.ContentFunc != nil {
data, err := att.ContentFunc()
if err != nil {
return nil, err
}
content = data
} else {
if refs == nil {
return nil, errors.Errorf("no refs map provided to lookup attestation keys")
}
ref, ok := refs[att.Ref]
if !ok {
return nil, errors.Errorf("key %s not found in refs map", att.Ref)
}
mount, err := ref.Mount(ctx, true, s)
if err != nil {
return nil, err
}
lm := snapshot.LocalMounter(mount)
src, err := lm.Mount()
if err != nil {
return nil, err
}
defer lm.Unmount()

p, err := fs.RootPath(src, att.Path)
if err != nil {
return nil, err
}
content, err = os.ReadFile(p)
if err != nil {
return nil, errors.Wrap(err, "cannot read in-toto attestation")
}
}
if len(content) == 0 {
content = nil
}
return content, nil
}

// MakeInTotoStatements iterates over all provided result attestations and
// generates intoto attestation statements.
func MakeInTotoStatements(ctx context.Context, s session.Group, refs map[string]cache.ImmutableRef, attestations []result.Attestation, defaultSubjects []intoto.Subject) ([]intoto.Statement, error) {
eg, ctx := errgroup.WithContext(ctx)
statements := make([]intoto.Statement, len(attestations))

for i, att := range attestations {
i, att := i, att
eg.Go(func() error {
content, err := ReadAll(ctx, s, refs, att)
if err != nil {
return err
}

switch att.Kind {
case gatewaypb.AttestationKindInToto:
stmt, err := makeInTotoStatement(ctx, content, att, defaultSubjects)
if err != nil {
return err
}
statements[i] = *stmt
case gatewaypb.AttestationKindBundle:
return errors.New("bundle attestation kind must be un-bundled first")
}
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, err
}
return statements, nil
}

func makeInTotoStatement(ctx context.Context, content []byte, attestation result.Attestation, defaultSubjects []intoto.Subject) (*intoto.Statement, error) {
if len(attestation.InToto.Subjects) == 0 {
attestation.InToto.Subjects = []result.InTotoSubject{{
Kind: gatewaypb.InTotoSubjectKindSelf,
}}
}
subjects := []intoto.Subject{}
for _, subject := range attestation.InToto.Subjects {
subjectName := "_"
if subject.Name != "" {
subjectName = subject.Name
}

switch subject.Kind {
case gatewaypb.InTotoSubjectKindSelf:
for _, defaultSubject := range defaultSubjects {
subjectNames := []string{}
subjectNames = append(subjectNames, defaultSubject.Name)
if subjectName != "_" {
subjectNames = append(subjectNames, subjectName)
}

for _, name := range subjectNames {
subjects = append(subjects, intoto.Subject{
Name: name,
Digest: defaultSubject.Digest,
})
}
}
case gatewaypb.InTotoSubjectKindRaw:
subjects = append(subjects, intoto.Subject{
Name: subjectName,
Digest: result.ToDigestMap(subject.Digest...),
})
default:
return nil, errors.Errorf("unknown attestation subject type %T", subject)
}
}

stmt := intoto.Statement{
StatementHeader: intoto.StatementHeader{
Type: intoto.StatementInTotoV01,
PredicateType: attestation.InToto.PredicateType,
Subject: subjects,
},
Predicate: json.RawMessage(content),
}
return &stmt, nil
}
117 changes: 0 additions & 117 deletions exporter/attestation/generate.go → exporter/attestation/unbundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,123 +17,6 @@ import (
"golang.org/x/sync/errgroup"
)

// Generate iterates over all provided result attestations and generates intoto
// attestation statements.
func Generate(ctx context.Context, s session.Group, refs map[string]cache.ImmutableRef, attestations []result.Attestation, defaultSubjects []intoto.Subject) ([]intoto.Statement, error) {
eg, ctx := errgroup.WithContext(ctx)
statements := make([]intoto.Statement, len(attestations))

for i, att := range attestations {
i, att := i, att
eg.Go(func() error {
var content []byte
if att.ContentFunc != nil {
var err error
content, err = att.ContentFunc()
if err != nil {
return err
}
} else {
if refs == nil {
return errors.Errorf("no refs map provided to lookup attestation keys")
}
ref, ok := refs[att.Ref]
if !ok {
return errors.Errorf("key %s not found in refs map", att.Ref)
}
mount, err := ref.Mount(ctx, true, s)
if err != nil {
return err
}
lm := snapshot.LocalMounter(mount)
src, err := lm.Mount()
if err != nil {
return err
}
defer lm.Unmount()

p, err := fs.RootPath(src, att.Path)
if err != nil {
return err
}
content, err = os.ReadFile(p)
if err != nil {
return errors.Wrap(err, "cannot read in-toto attestation")
}
}
if len(content) == 0 {
content = nil
}

switch att.Kind {
case gatewaypb.AttestationKindInToto:
stmt, err := generateInToto(ctx, content, att, defaultSubjects)
if err != nil {
return err
}
statements[i] = *stmt
case gatewaypb.AttestationKindBundle:
return errors.New("bundle attestation kind must be un-bundled first")
}
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, err
}
return statements, nil
}

func generateInToto(ctx context.Context, content []byte, attestation result.Attestation, defaultSubjects []intoto.Subject) (*intoto.Statement, error) {
if len(attestation.InToto.Subjects) == 0 {
attestation.InToto.Subjects = []result.InTotoSubject{{
Kind: gatewaypb.InTotoSubjectKindSelf,
}}
}
subjects := []intoto.Subject{}
for _, subject := range attestation.InToto.Subjects {
subjectName := "_"
if subject.Name != "" {
subjectName = subject.Name
}

switch subject.Kind {
case gatewaypb.InTotoSubjectKindSelf:
for _, defaultSubject := range defaultSubjects {
subjectNames := []string{}
subjectNames = append(subjectNames, defaultSubject.Name)
if subjectName != "_" {
subjectNames = append(subjectNames, subjectName)
}

for _, name := range subjectNames {
subjects = append(subjects, intoto.Subject{
Name: name,
Digest: defaultSubject.Digest,
})
}
}
case gatewaypb.InTotoSubjectKindRaw:
subjects = append(subjects, intoto.Subject{
Name: subjectName,
Digest: result.ToDigestMap(subject.Digest...),
})
default:
return nil, errors.Errorf("unknown attestation subject type %T", subject)
}
}

stmt := intoto.Statement{
StatementHeader: intoto.StatementHeader{
Type: intoto.StatementInTotoV01,
PredicateType: attestation.InToto.PredicateType,
Subject: subjects,
},
Predicate: json.RawMessage(content),
}
return &stmt, nil
}

// Unbundle iterates over all provided result attestations and un-bundles any
// bundled attestations by loading them from the provided refs map.
func Unbundle(ctx context.Context, s session.Group, refs map[string]cache.ImmutableRef, bundled []result.Attestation) ([]result.Attestation, error) {
Expand Down
Loading

0 comments on commit 1bc934e

Please sign in to comment.