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

fix: Fix missing checks on product include/exclude glob for attestation. #66

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
22 changes: 22 additions & 0 deletions CHANGELOG-ATTESTORS.md
Original file line number Diff line number Diff line change
@@ -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.
38 changes: 33 additions & 5 deletions attestation/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ 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"
)

// 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 {
Expand Down Expand Up @@ -57,15 +59,16 @@ 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
}

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
}
}
Expand All @@ -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
}

Expand All @@ -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
}
Expand Down
6 changes: 3 additions & 3 deletions attestation/file/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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)
}
111 changes: 101 additions & 10 deletions attestation/material/material.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkjell I was just thinking if we might need to mention somewhere that the version is bumped. Like a changelog of some sorts? This will break current policies that are in place. Same goes for the product.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added CHANGELOG-ATTESTORS.md.

RunType = attestation.MaterialRunType

defaultIncludeGlob = "*"
defaultExcludeGlob = ""
)

// This is a hacky way to create a compile time error in case the attestor
Expand All @@ -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"`
matglas marked this conversation as resolved.
Show resolved Hide resolved
}

type attestorConfiguration struct {
IncludeGlob string `json:"includeGlob"`
ExcludeGlob string `json:"excludeGlob"`
}

func (a *Attestor) Name() string {
Expand Down Expand Up @@ -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
}
Expand All @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to make this backwards compatible. I think we can check a.Type() for the version and unmarshal into the new struct format.

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
}

Expand Down
55 changes: 44 additions & 11 deletions attestation/product/product.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "*"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}

Expand All @@ -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
Expand Down
Loading