diff --git a/CHANGELOG-ATTESTORS.md b/CHANGELOG-ATTESTORS.md new file mode 100644 index 00000000..5495c052 --- /dev/null +++ b/CHANGELOG-ATTESTORS.md @@ -0,0 +1,22 @@ +# Attestor Changelog + +## Product attestor + +### `v0.2` + +Type: https://witness.dev/attestations/product +Version: `v0.2` + +- Attestor configuration has been added as `configuration`. +- Products has been put into its own `products` field. + + +## Material attestator + +### `v0.2` + +Type: https://witness.dev/attestations/product +Version: `v0.2` + +- Attestor configuration has been added as `configuration`. +- Material has been put into its own `materials` field. \ No newline at end of file diff --git a/attestation/file/file.go b/attestation/file/file.go index 14065d6f..4d2869dd 100644 --- a/attestation/file/file.go +++ b/attestation/file/file.go @@ -19,6 +19,8 @@ import ( "os" "path/filepath" + "github.com/gobwas/glob" + "github.com/gobwas/glob/match" "github.com/in-toto/go-witness/cryptoutil" "github.com/in-toto/go-witness/log" ) @@ -26,7 +28,7 @@ import ( // recordArtifacts will walk basePath and record the digests of each file with each of the functions in hashes. // If file already exists in baseArtifacts and the two artifacts are equal the artifact will not be in the // returned map of artifacts. -func RecordArtifacts(basePath string, baseArtifacts map[string]cryptoutil.DigestSet, hashes []cryptoutil.DigestValue, visitedSymlinks map[string]struct{}, processWasTraced bool, openedFiles map[string]bool) (map[string]cryptoutil.DigestSet, error) { +func RecordArtifacts(basePath string, baseArtifacts map[string]cryptoutil.DigestSet, hashes []cryptoutil.DigestValue, visitedSymlinks map[string]struct{}, processWasTraced bool, openedFiles map[string]bool, includeGlob glob.Glob, excludeGlob glob.Glob) (map[string]cryptoutil.DigestSet, error) { artifacts := make(map[string]cryptoutil.DigestSet) err := filepath.Walk(basePath, func(path string, info fs.FileInfo, err error) error { if err != nil { @@ -57,7 +59,7 @@ func RecordArtifacts(basePath string, baseArtifacts map[string]cryptoutil.Digest } visitedSymlinks[linkedPath] = struct{}{} - symlinkedArtifacts, err := RecordArtifacts(linkedPath, baseArtifacts, hashes, visitedSymlinks, processWasTraced, openedFiles) + symlinkedArtifacts, err := RecordArtifacts(linkedPath, baseArtifacts, hashes, visitedSymlinks, processWasTraced, openedFiles, includeGlob, excludeGlob) if err != nil { return err } @@ -65,7 +67,8 @@ func RecordArtifacts(basePath string, baseArtifacts map[string]cryptoutil.Digest for artifactPath, artifact := range symlinkedArtifacts { // all artifacts in the symlink should be recorded relative to our basepath joinedPath := filepath.Join(relPath, artifactPath) - if shouldRecord(joinedPath, artifact, baseArtifacts, processWasTraced, openedFiles) { + + if shouldRecord(joinedPath, artifact, baseArtifacts, processWasTraced, openedFiles, includeGlob, excludeGlob) { artifacts[filepath.Join(relPath, artifactPath)] = artifact } } @@ -78,7 +81,7 @@ func RecordArtifacts(basePath string, baseArtifacts map[string]cryptoutil.Digest return err } - if shouldRecord(relPath, artifact, baseArtifacts, processWasTraced, openedFiles) { + if shouldRecord(relPath, artifact, baseArtifacts, processWasTraced, openedFiles, includeGlob, excludeGlob) { artifacts[relPath] = artifact } @@ -92,10 +95,35 @@ func RecordArtifacts(basePath string, baseArtifacts map[string]cryptoutil.Digest // if the process was traced and the artifact was not one of the opened files, return false // if the artifact is already in baseArtifacts, check if it's changed // if it is not equal to the existing artifact, return true, otherwise return false -func shouldRecord(path string, artifact cryptoutil.DigestSet, baseArtifacts map[string]cryptoutil.DigestSet, processWasTraced bool, openedFiles map[string]bool) bool { +func shouldRecord(path string, artifact cryptoutil.DigestSet, baseArtifacts map[string]cryptoutil.DigestSet, processWasTraced bool, openedFiles map[string]bool, includeGlob glob.Glob, excludeGlob glob.Glob) bool { + superInclude := false + if _, ok := includeGlob.(match.Super); ok { + superInclude = true + } + + excludeGlobNothing := false + if _, ok := excludeGlob.(match.Nothing); ok { + excludeGlobNothing = true + } + + includePath := true + if excludeGlob != nil && excludeGlob.Match(path) { + includePath = false + } + if !(superInclude && !includePath) && includeGlob != nil && includeGlob.Match(path) { + includePath = true + } else if excludeGlobNothing { + includePath = false + } + + if !includePath { + return false + } + if _, ok := openedFiles[path]; !ok && processWasTraced { return false } + if previous, ok := baseArtifacts[path]; ok && artifact.Equal(previous) { return false } diff --git a/attestation/file/file_test.go b/attestation/file/file_test.go index 5379a487..7140117f 100644 --- a/attestation/file/file_test.go +++ b/attestation/file/file_test.go @@ -38,13 +38,13 @@ func TestBrokenSymlink(t *testing.T) { symTestDir := filepath.Join(dir, "symTestDir") require.NoError(t, os.Symlink(testDir, symTestDir)) - _, err := RecordArtifacts(dir, map[string]cryptoutil.DigestSet{}, []cryptoutil.DigestValue{{Hash: crypto.SHA256}}, map[string]struct{}{}, false, map[string]bool{}) + _, err := RecordArtifacts(dir, map[string]cryptoutil.DigestSet{}, []cryptoutil.DigestValue{{Hash: crypto.SHA256}}, map[string]struct{}{}, false, map[string]bool{}, nil, nil) require.NoError(t, err) // remove the symlinks and make sure we don't get an error back require.NoError(t, os.RemoveAll(testDir)) require.NoError(t, os.RemoveAll(testFile)) - _, err = RecordArtifacts(dir, map[string]cryptoutil.DigestSet{}, []cryptoutil.DigestValue{{Hash: crypto.SHA256}}, map[string]struct{}{}, false, map[string]bool{}) + _, err = RecordArtifacts(dir, map[string]cryptoutil.DigestSet{}, []cryptoutil.DigestValue{{Hash: crypto.SHA256}}, map[string]struct{}{}, false, map[string]bool{}, nil, nil) require.NoError(t, err) } @@ -58,6 +58,6 @@ func TestSymlinkCycle(t *testing.T) { require.NoError(t, os.Symlink(dir, symTestDir)) // if a symlink cycle weren't properly handled this would be an infinite loop - _, err := RecordArtifacts(dir, map[string]cryptoutil.DigestSet{}, []cryptoutil.DigestValue{{Hash: crypto.SHA256}}, map[string]struct{}{}, false, map[string]bool{}) + _, err := RecordArtifacts(dir, map[string]cryptoutil.DigestSet{}, []cryptoutil.DigestValue{{Hash: crypto.SHA256}}, map[string]struct{}{}, false, map[string]bool{}, nil, nil) require.NoError(t, err) } diff --git a/attestation/material/material.go b/attestation/material/material.go index 6b99a4e3..c72f25e1 100644 --- a/attestation/material/material.go +++ b/attestation/material/material.go @@ -16,17 +16,23 @@ package material import ( "encoding/json" + "fmt" + "github.com/gobwas/glob" "github.com/in-toto/go-witness/attestation" "github.com/in-toto/go-witness/attestation/file" "github.com/in-toto/go-witness/cryptoutil" + "github.com/in-toto/go-witness/registry" "github.com/invopop/jsonschema" ) const ( Name = "material" - Type = "https://witness.dev/attestations/material/v0.1" + Type = "https://witness.dev/attestations/material/v0.2" RunType = attestation.MaterialRunType + + defaultIncludeGlob = "*" + defaultExcludeGlob = "" ) // This is a hacky way to create a compile time error in case the attestor @@ -49,15 +55,68 @@ type MaterialAttestor interface { } func init() { - attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { - return New() - }) + attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { return New() }, + registry.StringConfigOption( + "include-glob", + "Pattern to use when recording materials. Files that match this pattern will be included as materials in the material attestation.", + defaultIncludeGlob, + func(a attestation.Attestor, includeGlob string) (attestation.Attestor, error) { + prodAttestor, ok := a.(*Attestor) + if !ok { + return a, fmt.Errorf("unexpected attestor type: %T is not a material attestor", a) + } + + WithIncludeGlob(includeGlob)(prodAttestor) + return prodAttestor, nil + }, + ), + registry.StringConfigOption( + "exclude-glob", + "Pattern to use when recording materials. Files that match this pattern will be excluded as materials on the material attestation.", + defaultExcludeGlob, + func(a attestation.Attestor, excludeGlob string) (attestation.Attestor, error) { + prodAttestor, ok := a.(*Attestor) + if !ok { + return a, fmt.Errorf("unexpected attestor type: %T is not a product attestor", a) + } + + WithExcludeGlob(excludeGlob)(prodAttestor) + return prodAttestor, nil + }, + ), + ) } type Option func(*Attestor) +func WithIncludeGlob(glob string) Option { + return func(a *Attestor) { + a.includeGlob = glob + } +} + +func WithExcludeGlob(glob string) Option { + return func(a *Attestor) { + a.excludeGlob = glob + } +} + type Attestor struct { - materials map[string]cryptoutil.DigestSet + materials map[string]cryptoutil.DigestSet + includeGlob string + compiledIncludeGlob glob.Glob + excludeGlob string + compiledExcludeGlob glob.Glob +} + +type attestorJson struct { + Materials map[string]cryptoutil.DigestSet `json:"materials"` + Configuration attestorConfiguration `json:"configuration"` +} + +type attestorConfiguration struct { + IncludeGlob string `json:"includeGlob"` + ExcludeGlob string `json:"excludeGlob"` } func (a *Attestor) Name() string { @@ -90,7 +149,19 @@ func (a *Attestor) Schema() *jsonschema.Schema { } func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { - materials, err := file.RecordArtifacts(ctx.WorkingDir(), nil, ctx.Hashes(), map[string]struct{}{}, false, map[string]bool{}) + compiledIncludeGlob, err := glob.Compile(a.includeGlob) + if err != nil { + return err + } + a.compiledIncludeGlob = compiledIncludeGlob + + compiledExcludeGlob, err := glob.Compile(a.excludeGlob) + if err != nil { + return err + } + a.compiledExcludeGlob = compiledExcludeGlob + + materials, err := file.RecordArtifacts(ctx.WorkingDir(), nil, ctx.Hashes(), map[string]struct{}{}, false, map[string]bool{}, compiledIncludeGlob, compiledExcludeGlob) if err != nil { return err } @@ -100,16 +171,36 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { } func (a *Attestor) MarshalJSON() ([]byte, error) { - return json.Marshal(a.materials) + output := attestorJson{ + Materials: a.materials, + } + + if a.includeGlob != "" || a.excludeGlob != "" { + config := attestorConfiguration{} + + if a.includeGlob != "" { + config.IncludeGlob = a.includeGlob + } + if a.excludeGlob != "" { + config.ExcludeGlob = a.excludeGlob + } + } + + return json.Marshal(output) } func (a *Attestor) UnmarshalJSON(data []byte) error { - mats := make(map[string]cryptoutil.DigestSet) - if err := json.Unmarshal(data, &mats); err != nil { + attestation := attestorJson{ + Materials: make(map[string]cryptoutil.DigestSet), + } + + if err := json.Unmarshal(data, &attestation); err != nil { return err } - a.materials = mats + a.materials = attestation.Materials + a.includeGlob = attestation.Configuration.IncludeGlob + a.excludeGlob = attestation.Configuration.ExcludeGlob return nil } diff --git a/attestation/product/product.go b/attestation/product/product.go index 8c9d6c34..465c003f 100644 --- a/attestation/product/product.go +++ b/attestation/product/product.go @@ -32,7 +32,7 @@ import ( const ( ProductName = "product" - ProductType = "https://witness.dev/attestations/product/v0.1" + ProductType = "https://witness.dev/attestations/product/v0.2" ProductRunType = attestation.ProductRunType defaultIncludeGlob = "*" @@ -117,6 +117,16 @@ type Attestor struct { compiledExcludeGlob glob.Glob } +type attestorJson struct { + Products map[string]attestation.Product `json:"products"` + Configuration *attestorConfiguration `json:"configuration,omitempty"` +} + +type attestorConfiguration struct { + IncludeGlob string `json:"includeGlob"` + ExcludeGlob string `json:"excludeGlob"` +} + func fromDigestMap(workingDir string, digestMap map[string]cryptoutil.DigestSet) map[string]attestation.Product { products := make(map[string]attestation.Product) for fileName, digestSet := range digestMap { @@ -199,7 +209,7 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { } } - products, err := file.RecordArtifacts(ctx.WorkingDir(), a.baseArtifacts, ctx.Hashes(), map[string]struct{}{}, processWasTraced, openedFileSet) + products, err := file.RecordArtifacts(ctx.WorkingDir(), a.baseArtifacts, ctx.Hashes(), map[string]struct{}{}, processWasTraced, openedFileSet, compiledIncludeGlob, compiledExcludeGlob) if err != nil { return err } @@ -209,16 +219,36 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { } func (a *Attestor) MarshalJSON() ([]byte, error) { - return json.Marshal(a.products) + output := attestorJson{ + Products: a.products, + } + + if a.includeGlob != "" || a.excludeGlob != "" { + config := attestorConfiguration{} + + if a.includeGlob != "" { + config.IncludeGlob = a.includeGlob + } + if a.excludeGlob != "" { + config.ExcludeGlob = a.excludeGlob + } + output.Configuration = &config + } + + return json.Marshal(output) } func (a *Attestor) UnmarshalJSON(data []byte) error { - prods := make(map[string]attestation.Product) - if err := json.Unmarshal(data, &prods); err != nil { + attestation := attestorJson{ + Products: make(map[string]attestation.Product), + } + if err := json.Unmarshal(data, &attestation); err != nil { return err } - a.products = prods + a.products = attestation.Products + a.includeGlob = attestation.Configuration.IncludeGlob + a.excludeGlob = attestation.Configuration.ExcludeGlob return nil } @@ -229,15 +259,18 @@ func (a *Attestor) Products() map[string]attestation.Product { func (a *Attestor) Subjects() map[string]cryptoutil.DigestSet { subjects := make(map[string]cryptoutil.DigestSet) for productName, product := range a.products { + + includeSubject := true if a.compiledExcludeGlob != nil && a.compiledExcludeGlob.Match(productName) { - continue + includeSubject = false } - - if a.compiledIncludeGlob != nil && !a.compiledIncludeGlob.Match(productName) { - continue + if a.compiledIncludeGlob != nil && a.compiledIncludeGlob.Match(productName) { + includeSubject = true } - subjects[fmt.Sprintf("file:%v", productName)] = product.Digest + if includeSubject { + subjects[fmt.Sprintf("file:%v", productName)] = product.Digest + } } return subjects