mirror of
				https://gitea.com/Lydanne/buildx.git
				synced 2025-11-04 10:03:42 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			410 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			410 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package in_toto
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"io/ioutil"
 | 
						|
	"os"
 | 
						|
	"os/exec"
 | 
						|
	"path/filepath"
 | 
						|
	"reflect"
 | 
						|
	"strings"
 | 
						|
	"syscall"
 | 
						|
 | 
						|
	"github.com/shibumi/go-pathspec"
 | 
						|
)
 | 
						|
 | 
						|
// ErrSymCycle signals a detected symlink cycle in our RecordArtifacts() function.
 | 
						|
var ErrSymCycle = errors.New("symlink cycle detected")
 | 
						|
 | 
						|
// ErrUnsupportedHashAlgorithm signals a missing hash mapping in getHashMapping
 | 
						|
var ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm detected")
 | 
						|
 | 
						|
var ErrEmptyCommandArgs = errors.New("the command args are empty")
 | 
						|
 | 
						|
// visitedSymlinks is a hashset that contains all paths that we have visited.
 | 
						|
var visitedSymlinks Set
 | 
						|
 | 
						|
/*
 | 
						|
RecordArtifact reads and hashes the contents of the file at the passed path
 | 
						|
using sha256 and returns a map in the following format:
 | 
						|
 | 
						|
	{
 | 
						|
		"<path>": {
 | 
						|
			"sha256": <hex representation of hash>
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
If reading the file fails, the first return value is nil and the second return
 | 
						|
value is the error.
 | 
						|
NOTE: For cross-platform consistency Windows-style line separators (CRLF) are
 | 
						|
normalized to Unix-style line separators (LF) before hashing file contents.
 | 
						|
*/
 | 
						|
func RecordArtifact(path string, hashAlgorithms []string, lineNormalization bool) (map[string]interface{}, error) {
 | 
						|
	supportedHashMappings := getHashMapping()
 | 
						|
	// Read file from passed path
 | 
						|
	contents, err := ioutil.ReadFile(path)
 | 
						|
	hashedContentsMap := make(map[string]interface{})
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	if lineNormalization {
 | 
						|
		// "Normalize" file contents. We convert all line separators to '\n'
 | 
						|
		// for keeping operating system independence
 | 
						|
		contents = bytes.ReplaceAll(contents, []byte("\r\n"), []byte("\n"))
 | 
						|
		contents = bytes.ReplaceAll(contents, []byte("\r"), []byte("\n"))
 | 
						|
	}
 | 
						|
 | 
						|
	// Create a map of all the hashes present in the hash_func list
 | 
						|
	for _, element := range hashAlgorithms {
 | 
						|
		if _, ok := supportedHashMappings[element]; !ok {
 | 
						|
			return nil, fmt.Errorf("%w: %s", ErrUnsupportedHashAlgorithm, element)
 | 
						|
		}
 | 
						|
		h := supportedHashMappings[element]
 | 
						|
		result := fmt.Sprintf("%x", hashToHex(h(), contents))
 | 
						|
		hashedContentsMap[element] = result
 | 
						|
	}
 | 
						|
 | 
						|
	// Return it in a format that is conformant with link metadata artifacts
 | 
						|
	return hashedContentsMap, nil
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
RecordArtifacts is a wrapper around recordArtifacts.
 | 
						|
RecordArtifacts initializes a set for storing visited symlinks,
 | 
						|
calls recordArtifacts and deletes the set if no longer needed.
 | 
						|
recordArtifacts walks through the passed slice of paths, traversing
 | 
						|
subdirectories, and calls RecordArtifact for each file. It returns a map in
 | 
						|
the following format:
 | 
						|
 | 
						|
	{
 | 
						|
		"<path>": {
 | 
						|
			"sha256": <hex representation of hash>
 | 
						|
		},
 | 
						|
		"<path>": {
 | 
						|
		"sha256": <hex representation of hash>
 | 
						|
		},
 | 
						|
		...
 | 
						|
	}
 | 
						|
 | 
						|
If recording an artifact fails the first return value is nil and the second
 | 
						|
return value is the error.
 | 
						|
*/
 | 
						|
func RecordArtifacts(paths []string, hashAlgorithms []string, gitignorePatterns []string, lStripPaths []string, lineNormalization bool) (evalArtifacts map[string]interface{}, err error) {
 | 
						|
	// Make sure to initialize a fresh hashset for every RecordArtifacts call
 | 
						|
	visitedSymlinks = NewSet()
 | 
						|
	evalArtifacts, err = recordArtifacts(paths, hashAlgorithms, gitignorePatterns, lStripPaths, lineNormalization)
 | 
						|
	// pass result and error through
 | 
						|
	return evalArtifacts, err
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
recordArtifacts walks through the passed slice of paths, traversing
 | 
						|
subdirectories, and calls RecordArtifact for each file. It returns a map in
 | 
						|
the following format:
 | 
						|
 | 
						|
	{
 | 
						|
		"<path>": {
 | 
						|
			"sha256": <hex representation of hash>
 | 
						|
		},
 | 
						|
		"<path>": {
 | 
						|
		"sha256": <hex representation of hash>
 | 
						|
		},
 | 
						|
		...
 | 
						|
	}
 | 
						|
 | 
						|
If recording an artifact fails the first return value is nil and the second
 | 
						|
return value is the error.
 | 
						|
*/
 | 
						|
func recordArtifacts(paths []string, hashAlgorithms []string, gitignorePatterns []string, lStripPaths []string, lineNormalization bool) (map[string]interface{}, error) {
 | 
						|
	artifacts := make(map[string]interface{})
 | 
						|
	for _, path := range paths {
 | 
						|
		err := filepath.Walk(path,
 | 
						|
			func(path string, info os.FileInfo, err error) error {
 | 
						|
				// Abort if Walk function has a problem,
 | 
						|
				// e.g. path does not exist
 | 
						|
				if err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
				// We need to call pathspec.GitIgnore inside of our filepath.Walk, because otherwise
 | 
						|
				// we will not catch all paths. Just imagine a path like "." and a pattern like "*.pub".
 | 
						|
				// If we would call pathspec outside of the filepath.Walk this would not match.
 | 
						|
				ignore, err := pathspec.GitIgnore(gitignorePatterns, path)
 | 
						|
				if err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
				if ignore {
 | 
						|
					return nil
 | 
						|
				}
 | 
						|
				// Don't hash directories
 | 
						|
				if info.IsDir() {
 | 
						|
					return nil
 | 
						|
				}
 | 
						|
 | 
						|
				// check for symlink and evaluate the last element in a symlink
 | 
						|
				// chain via filepath.EvalSymlinks. We use EvalSymlinks here,
 | 
						|
				// because with os.Readlink() we would just read the next
 | 
						|
				// element in a possible symlink chain. This would mean more
 | 
						|
				// iterations. infoMode()&os.ModeSymlink uses the file
 | 
						|
				// type bitmask to check for a symlink.
 | 
						|
				if info.Mode()&os.ModeSymlink == os.ModeSymlink {
 | 
						|
					// return with error if we detect a symlink cycle
 | 
						|
					if ok := visitedSymlinks.Has(path); ok {
 | 
						|
						// this error will get passed through
 | 
						|
						// to RecordArtifacts()
 | 
						|
						return ErrSymCycle
 | 
						|
					}
 | 
						|
					evalSym, err := filepath.EvalSymlinks(path)
 | 
						|
					if err != nil {
 | 
						|
						return err
 | 
						|
					}
 | 
						|
					// add symlink to visitedSymlinks set
 | 
						|
					// this way, we know which link we have visited already
 | 
						|
					// if we visit a symlink twice, we have detected a symlink cycle
 | 
						|
					visitedSymlinks.Add(path)
 | 
						|
					// We recursively call RecordArtifacts() to follow
 | 
						|
					// the new path.
 | 
						|
					evalArtifacts, evalErr := recordArtifacts([]string{evalSym}, hashAlgorithms, gitignorePatterns, lStripPaths, lineNormalization)
 | 
						|
					if evalErr != nil {
 | 
						|
						return evalErr
 | 
						|
					}
 | 
						|
					for key, value := range evalArtifacts {
 | 
						|
						artifacts[key] = value
 | 
						|
					}
 | 
						|
					return nil
 | 
						|
				}
 | 
						|
				artifact, err := RecordArtifact(path, hashAlgorithms, lineNormalization)
 | 
						|
				// Abort if artifact can't be recorded, e.g.
 | 
						|
				// due to file permissions
 | 
						|
				if err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
 | 
						|
				for _, strip := range lStripPaths {
 | 
						|
					if strings.HasPrefix(path, strip) {
 | 
						|
						path = strings.TrimPrefix(path, strip)
 | 
						|
						break
 | 
						|
					}
 | 
						|
				}
 | 
						|
				// Check if path is unique
 | 
						|
				_, existingPath := artifacts[path]
 | 
						|
				if existingPath {
 | 
						|
					return fmt.Errorf("left stripping has resulted in non unique dictionary key: %s", path)
 | 
						|
				}
 | 
						|
				artifacts[path] = artifact
 | 
						|
				return nil
 | 
						|
			})
 | 
						|
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return artifacts, nil
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
waitErrToExitCode converts an error returned by Cmd.wait() to an exit code.  It
 | 
						|
returns -1 if no exit code can be inferred.
 | 
						|
*/
 | 
						|
func waitErrToExitCode(err error) int {
 | 
						|
	// If there's no exit code, we return -1
 | 
						|
	retVal := -1
 | 
						|
 | 
						|
	// See https://stackoverflow.com/questions/10385551/get-exit-code-go
 | 
						|
	if err != nil {
 | 
						|
		if exiterr, ok := err.(*exec.ExitError); ok {
 | 
						|
			// The program has exited with an exit code != 0
 | 
						|
			// This works on both Unix and Windows. Although package
 | 
						|
			// syscall is generally platform dependent, WaitStatus is
 | 
						|
			// defined for both Unix and Windows and in both cases has
 | 
						|
			// an ExitStatus() method with the same signature.
 | 
						|
			if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
 | 
						|
				retVal = status.ExitStatus()
 | 
						|
			}
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		retVal = 0
 | 
						|
	}
 | 
						|
 | 
						|
	return retVal
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
RunCommand executes the passed command in a subprocess.  The first element of
 | 
						|
cmdArgs is used as executable and the rest as command arguments.  It captures
 | 
						|
and returns stdout, stderr and exit code.  The format of the returned map is:
 | 
						|
 | 
						|
	{
 | 
						|
		"return-value": <exit code>,
 | 
						|
		"stdout": "<standard output>",
 | 
						|
		"stderr": "<standard error>"
 | 
						|
	}
 | 
						|
 | 
						|
If the command cannot be executed or no pipes for stdout or stderr can be
 | 
						|
created the first return value is nil and the second return value is the error.
 | 
						|
NOTE: Since stdout and stderr are captured, they cannot be seen during the
 | 
						|
command execution.
 | 
						|
*/
 | 
						|
func RunCommand(cmdArgs []string, runDir string) (map[string]interface{}, error) {
 | 
						|
	if len(cmdArgs) == 0 {
 | 
						|
		return nil, ErrEmptyCommandArgs
 | 
						|
	}
 | 
						|
 | 
						|
	cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
 | 
						|
 | 
						|
	if runDir != "" {
 | 
						|
		cmd.Dir = runDir
 | 
						|
	}
 | 
						|
 | 
						|
	stderrPipe, err := cmd.StderrPipe()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	stdoutPipe, err := cmd.StdoutPipe()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	if err := cmd.Start(); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	// TODO: duplicate stdout, stderr
 | 
						|
	stdout, _ := ioutil.ReadAll(stdoutPipe)
 | 
						|
	stderr, _ := ioutil.ReadAll(stderrPipe)
 | 
						|
 | 
						|
	retVal := waitErrToExitCode(cmd.Wait())
 | 
						|
 | 
						|
	return map[string]interface{}{
 | 
						|
		"return-value": float64(retVal),
 | 
						|
		"stdout":       string(stdout),
 | 
						|
		"stderr":       string(stderr),
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
InTotoRun executes commands, e.g. for software supply chain steps or
 | 
						|
inspections of an in-toto layout, and creates and returns corresponding link
 | 
						|
metadata.  Link metadata contains recorded products at the passed productPaths
 | 
						|
and materials at the passed materialPaths.  The returned link is wrapped in a
 | 
						|
Metablock object.  If command execution or artifact recording fails the first
 | 
						|
return value is an empty Metablock and the second return value is the error.
 | 
						|
*/
 | 
						|
func InTotoRun(name string, runDir string, materialPaths []string, productPaths []string,
 | 
						|
	cmdArgs []string, key Key, hashAlgorithms []string, gitignorePatterns []string,
 | 
						|
	lStripPaths []string, lineNormalization bool) (Metablock, error) {
 | 
						|
	var linkMb Metablock
 | 
						|
 | 
						|
	materials, err := RecordArtifacts(materialPaths, hashAlgorithms, gitignorePatterns, lStripPaths, lineNormalization)
 | 
						|
	if err != nil {
 | 
						|
		return linkMb, err
 | 
						|
	}
 | 
						|
 | 
						|
	// make sure that we only run RunCommand if cmdArgs is not nil or empty
 | 
						|
	byProducts := map[string]interface{}{}
 | 
						|
	if len(cmdArgs) != 0 {
 | 
						|
		byProducts, err = RunCommand(cmdArgs, runDir)
 | 
						|
		if err != nil {
 | 
						|
			return linkMb, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	products, err := RecordArtifacts(productPaths, hashAlgorithms, gitignorePatterns, lStripPaths, lineNormalization)
 | 
						|
	if err != nil {
 | 
						|
		return linkMb, err
 | 
						|
	}
 | 
						|
 | 
						|
	linkMb.Signed = Link{
 | 
						|
		Type:        "link",
 | 
						|
		Name:        name,
 | 
						|
		Materials:   materials,
 | 
						|
		Products:    products,
 | 
						|
		ByProducts:  byProducts,
 | 
						|
		Command:     cmdArgs,
 | 
						|
		Environment: map[string]interface{}{},
 | 
						|
	}
 | 
						|
 | 
						|
	linkMb.Signatures = []Signature{}
 | 
						|
	// We use a new feature from Go1.13 here, to check the key struct.
 | 
						|
	// IsZero() will return True, if the key hasn't been initialized
 | 
						|
 | 
						|
	// with other values than the default ones.
 | 
						|
	if !reflect.ValueOf(key).IsZero() {
 | 
						|
		if err := linkMb.Sign(key); err != nil {
 | 
						|
			return linkMb, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return linkMb, nil
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
InTotoRecordStart begins the creation of a link metablock file in two steps,
 | 
						|
in order to provide evidence for supply chain steps that cannot be carries out
 | 
						|
by a single command.  InTotoRecordStart collects the hashes of the materials
 | 
						|
before any commands are run, signs the unfinished link, and returns the link.
 | 
						|
*/
 | 
						|
func InTotoRecordStart(name string, materialPaths []string, key Key, hashAlgorithms, gitignorePatterns []string, lStripPaths []string, lineNormalization bool) (Metablock, error) {
 | 
						|
	var linkMb Metablock
 | 
						|
	materials, err := RecordArtifacts(materialPaths, hashAlgorithms, gitignorePatterns, lStripPaths, lineNormalization)
 | 
						|
	if err != nil {
 | 
						|
		return linkMb, err
 | 
						|
	}
 | 
						|
 | 
						|
	linkMb.Signed = Link{
 | 
						|
		Type:        "link",
 | 
						|
		Name:        name,
 | 
						|
		Materials:   materials,
 | 
						|
		Products:    map[string]interface{}{},
 | 
						|
		ByProducts:  map[string]interface{}{},
 | 
						|
		Command:     []string{},
 | 
						|
		Environment: map[string]interface{}{},
 | 
						|
	}
 | 
						|
 | 
						|
	if !reflect.ValueOf(key).IsZero() {
 | 
						|
		if err := linkMb.Sign(key); err != nil {
 | 
						|
			return linkMb, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return linkMb, nil
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
InTotoRecordStop ends the creation of a metatadata link file created by
 | 
						|
InTotoRecordStart. InTotoRecordStop takes in a signed unfinished link metablock
 | 
						|
created by InTotoRecordStart and records the hashes of any products creted by
 | 
						|
commands run between InTotoRecordStart and InTotoRecordStop.  The resultant
 | 
						|
finished link metablock is then signed by the provided key and returned.
 | 
						|
*/
 | 
						|
func InTotoRecordStop(prelimLinkMb Metablock, productPaths []string, key Key, hashAlgorithms, gitignorePatterns []string, lStripPaths []string, lineNormalization bool) (Metablock, error) {
 | 
						|
	var linkMb Metablock
 | 
						|
	if err := prelimLinkMb.VerifySignature(key); err != nil {
 | 
						|
		return linkMb, err
 | 
						|
	}
 | 
						|
 | 
						|
	link, ok := prelimLinkMb.Signed.(Link)
 | 
						|
	if !ok {
 | 
						|
		return linkMb, errors.New("invalid metadata block")
 | 
						|
	}
 | 
						|
 | 
						|
	products, err := RecordArtifacts(productPaths, hashAlgorithms, gitignorePatterns, lStripPaths, lineNormalization)
 | 
						|
	if err != nil {
 | 
						|
		return linkMb, err
 | 
						|
	}
 | 
						|
 | 
						|
	link.Products = products
 | 
						|
	linkMb.Signed = link
 | 
						|
 | 
						|
	if !reflect.ValueOf(key).IsZero() {
 | 
						|
		if err := linkMb.Sign(key); err != nil {
 | 
						|
			return linkMb, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return linkMb, nil
 | 
						|
}
 |