github.com/boxboat/in-toto-golang@v0.0.3-0.20210303203820-2fa16ecbe6f6/in_toto/runlib.go (about)

     1  package in_toto
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"reflect"
    12  	"strings"
    13  	"syscall"
    14  
    15  	"github.com/shibumi/go-pathspec"
    16  )
    17  
    18  // ErrSymCycle signals a detected symlink cycle in our RecordArtifacts() function.
    19  var ErrSymCycle = errors.New("symlink cycle detected")
    20  
    21  // ErrUnsupportedHashAlgorithm signals a missing hash mapping in getHashMapping
    22  var ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm detected")
    23  
    24  // visitedSymlinks is a hashset that contains all paths that we have visited.
    25  var visitedSymlinks Set
    26  
    27  /*
    28  RecordArtifact reads and hashes the contents of the file at the passed path
    29  using sha256 and returns a map in the following format:
    30  
    31  	{
    32  		"<path>": {
    33  			"sha256": <hex representation of hash>
    34  		}
    35  	}
    36  
    37  If reading the file fails, the first return value is nil and the second return
    38  value is the error.
    39  NOTE: For cross-platform consistency Windows-style line separators (CRLF) are
    40  normalized to Unix-style line separators (LF) before hashing file contents.
    41  */
    42  func RecordArtifact(path string, hashAlgorithms []string) (map[string]interface{}, error) {
    43  	supportedHashMappings := getHashMapping()
    44  	// Read file from passed path
    45  	contents, err := ioutil.ReadFile(path)
    46  	hashedContentsMap := make(map[string]interface{})
    47  	if err != nil {
    48  		return nil, err
    49  	}
    50  	// "Normalize" file contents. We convert all line separators to '\n'
    51  	// for keeping operating system independence
    52  	contents = bytes.ReplaceAll(contents, []byte("\r\n"), []byte("\n"))
    53  
    54  	// Create a map of all the hashes present in the hash_func list
    55  	for _, element := range hashAlgorithms {
    56  		if _, ok := supportedHashMappings[element]; !ok {
    57  			return nil, fmt.Errorf("%w: %s", ErrUnsupportedHashAlgorithm, element)
    58  		}
    59  		h := supportedHashMappings[element]
    60  		result := fmt.Sprintf("%x", hashToHex(h(), contents))
    61  		hashedContentsMap[element] = result
    62  	}
    63  
    64  	// Return it in a format that is conformant with link metadata artifacts
    65  	return hashedContentsMap, nil
    66  }
    67  
    68  /*
    69  RecordArtifacts is a wrapper around recordArtifacts.
    70  RecordArtifacts initializes a set for storing visited symlinks,
    71  calls recordArtifacts and deletes the set if no longer needed.
    72  recordArtifacts walks through the passed slice of paths, traversing
    73  subdirectories, and calls RecordArtifact for each file. It returns a map in
    74  the following format:
    75  
    76  	{
    77  		"<path>": {
    78  			"sha256": <hex representation of hash>
    79  		},
    80  		"<path>": {
    81  		"sha256": <hex representation of hash>
    82  		},
    83  		...
    84  	}
    85  
    86  If recording an artifact fails the first return value is nil and the second
    87  return value is the error.
    88  */
    89  func RecordArtifacts(paths []string, hashAlgorithms []string, gitignorePatterns []string, lStripPaths []string) (evalArtifacts map[string]interface{}, err error) {
    90  	// Make sure to initialize a fresh hashset for every RecordArtifacts call
    91  	visitedSymlinks = NewSet()
    92  	evalArtifacts, err = recordArtifacts(paths, hashAlgorithms, gitignorePatterns, lStripPaths)
    93  	// pass result and error through
    94  	return evalArtifacts, err
    95  }
    96  
    97  /*
    98  recordArtifacts walks through the passed slice of paths, traversing
    99  subdirectories, and calls RecordArtifact for each file. It returns a map in
   100  the following format:
   101  
   102  	{
   103  		"<path>": {
   104  			"sha256": <hex representation of hash>
   105  		},
   106  		"<path>": {
   107  		"sha256": <hex representation of hash>
   108  		},
   109  		...
   110  	}
   111  
   112  If recording an artifact fails the first return value is nil and the second
   113  return value is the error.
   114  */
   115  func recordArtifacts(paths []string, hashAlgorithms []string, gitignorePatterns []string, lStripPaths []string) (map[string]interface{}, error) {
   116  	artifacts := make(map[string]interface{})
   117  	for _, path := range paths {
   118  		err := filepath.Walk(path,
   119  			func(path string, info os.FileInfo, err error) error {
   120  				// Abort if Walk function has a problem,
   121  				// e.g. path does not exist
   122  				if err != nil {
   123  					return err
   124  				}
   125  				// We need to call pathspec.GitIgnore inside of our filepath.Walk, because otherwise
   126  				// we will not catch all paths. Just imagine a path like "." and a pattern like "*.pub".
   127  				// If we would call pathspec outside of the filepath.Walk this would not match.
   128  				ignore, err := pathspec.GitIgnore(gitignorePatterns, path)
   129  				if err != nil {
   130  					return err
   131  				}
   132  				if ignore {
   133  					return nil
   134  				}
   135  				// Don't hash directories
   136  				if info.IsDir() {
   137  					return nil
   138  				}
   139  
   140  				// check for symlink and evaluate the last element in a symlink
   141  				// chain via filepath.EvalSymlinks. We use EvalSymlinks here,
   142  				// because with os.Readlink() we would just read the next
   143  				// element in a possible symlink chain. This would mean more
   144  				// iterations. infoMode()&os.ModeSymlink uses the file
   145  				// type bitmask to check for a symlink.
   146  				if info.Mode()&os.ModeSymlink == os.ModeSymlink {
   147  					// return with error if we detect a symlink cycle
   148  					if ok := visitedSymlinks.Has(path); ok {
   149  						// this error will get passed through
   150  						// to RecordArtifacts()
   151  						return ErrSymCycle
   152  					}
   153  					evalSym, err := filepath.EvalSymlinks(path)
   154  					if err != nil {
   155  						return err
   156  					}
   157  					// add symlink to visitedSymlinks set
   158  					// this way, we know which link we have visited already
   159  					// if we visit a symlink twice, we have detected a symlink cycle
   160  					visitedSymlinks.Add(path)
   161  					// We recursively call RecordArtifacts() to follow
   162  					// the new path.
   163  					evalArtifacts, evalErr := recordArtifacts([]string{evalSym}, hashAlgorithms, gitignorePatterns, lStripPaths)
   164  					if evalErr != nil {
   165  						return evalErr
   166  					}
   167  					for key, value := range evalArtifacts {
   168  						artifacts[key] = value
   169  					}
   170  					return nil
   171  				}
   172  				artifact, err := RecordArtifact(path, hashAlgorithms)
   173  				// Abort if artifact can't be recorded, e.g.
   174  				// due to file permissions
   175  				if err != nil {
   176  					return err
   177  				}
   178  
   179  				for _, strip := range lStripPaths {
   180  					if strings.HasPrefix(path, strip) {
   181  						path = strings.TrimPrefix(path, strip)
   182  						break
   183  					}
   184  				}
   185  
   186  				artifacts[path] = artifact
   187  				return nil
   188  			})
   189  
   190  		if err != nil {
   191  			return nil, err
   192  		}
   193  	}
   194  
   195  	return artifacts, nil
   196  }
   197  
   198  /*
   199  waitErrToExitCode converts an error returned by Cmd.wait() to an exit code.  It
   200  returns -1 if no exit code can be inferred.
   201  */
   202  func waitErrToExitCode(err error) int {
   203  	// If there's no exit code, we return -1
   204  	retVal := -1
   205  
   206  	// See https://stackoverflow.com/questions/10385551/get-exit-code-go
   207  	if err != nil {
   208  		if exiterr, ok := err.(*exec.ExitError); ok {
   209  			// The program has exited with an exit code != 0
   210  			// This works on both Unix and Windows. Although package
   211  			// syscall is generally platform dependent, WaitStatus is
   212  			// defined for both Unix and Windows and in both cases has
   213  			// an ExitStatus() method with the same signature.
   214  			if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
   215  				retVal = status.ExitStatus()
   216  			}
   217  		}
   218  	} else {
   219  		retVal = 0
   220  	}
   221  
   222  	return retVal
   223  }
   224  
   225  /*
   226  RunCommand executes the passed command in a subprocess.  The first element of
   227  cmdArgs is used as executable and the rest as command arguments.  It captures
   228  and returns stdout, stderr and exit code.  The format of the returned map is:
   229  
   230  	{
   231  		"return-value": <exit code>,
   232  		"stdout": "<standard output>",
   233  		"stderr": "<standard error>"
   234  	}
   235  
   236  If the command cannot be executed or no pipes for stdout or stderr can be
   237  created the first return value is nil and the second return value is the error.
   238  NOTE: Since stdout and stderr are captured, they cannot be seen during the
   239  command execution.
   240  */
   241  func RunCommand(cmdArgs []string) (map[string]interface{}, error) {
   242  	cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
   243  
   244  	stderrPipe, err := cmd.StderrPipe()
   245  	if err != nil {
   246  		return nil, err
   247  	}
   248  	stdoutPipe, err := cmd.StdoutPipe()
   249  	if err != nil {
   250  		return nil, err
   251  	}
   252  
   253  	if err := cmd.Start(); err != nil {
   254  		return nil, err
   255  	}
   256  
   257  	// TODO: duplicate stdout, stderr
   258  	stdout, _ := ioutil.ReadAll(stdoutPipe)
   259  	stderr, _ := ioutil.ReadAll(stderrPipe)
   260  
   261  	retVal := waitErrToExitCode(cmd.Wait())
   262  
   263  	return map[string]interface{}{
   264  		"return-value": float64(retVal),
   265  		"stdout":       string(stdout),
   266  		"stderr":       string(stderr),
   267  	}, nil
   268  }
   269  
   270  /*
   271  InTotoRun executes commands, e.g. for software supply chain steps or
   272  inspections of an in-toto layout, and creates and returns corresponding link
   273  metadata.  Link metadata contains recorded products at the passed productPaths
   274  and materials at the passed materialPaths.  The returned link is wrapped in a
   275  Metablock object.  If command execution or artifact recording fails the first
   276  return value is an empty Metablock and the second return value is the error.
   277  */
   278  func InTotoRun(name string, materialPaths []string, productPaths []string,
   279  	cmdArgs []string, key Key, hashAlgorithms []string, gitignorePatterns []string,
   280  	lStripPaths []string) (Metablock, error) {
   281  	var linkMb Metablock
   282  
   283  	materials, err := RecordArtifacts(materialPaths, hashAlgorithms, gitignorePatterns, lStripPaths)
   284  	if err != nil {
   285  		fmt.Println(err)
   286  		return linkMb, err
   287  	}
   288  
   289  	byProducts, err := RunCommand(cmdArgs)
   290  	if err != nil {
   291  		fmt.Println(err)
   292  		return linkMb, err
   293  	}
   294  
   295  	products, err := RecordArtifacts(productPaths, hashAlgorithms, gitignorePatterns, lStripPaths)
   296  	if err != nil {
   297  		fmt.Println(err)
   298  		return linkMb, err
   299  	}
   300  
   301  	linkMb.Signed = Link{
   302  		Type:        "link",
   303  		Name:        name,
   304  		Materials:   materials,
   305  		Products:    products,
   306  		ByProducts:  byProducts,
   307  		Command:     cmdArgs,
   308  		Environment: map[string]interface{}{},
   309  	}
   310  
   311  	linkMb.Signatures = []Signature{}
   312  	// We use a new feature from Go1.13 here, to check the key struct.
   313  	// IsZero() will return True, if the key hasn't been initialized
   314  
   315  	// with other values than the default ones.
   316  	if !reflect.ValueOf(key).IsZero() {
   317  		if err := linkMb.Sign(key); err != nil {
   318  			fmt.Println(err)
   319  			return linkMb, err
   320  		}
   321  	}
   322  
   323  	return linkMb, nil
   324  }
   325  
   326  func InTotoRecordStart(name string, materialPaths []string, key Key, hashAlgorithms, gitignorePatterns []string, lStripPaths []string) (Metablock, error) {
   327  	var linkMb Metablock
   328  	materials, err := RecordArtifacts(materialPaths, hashAlgorithms, gitignorePatterns, lStripPaths)
   329  	if err != nil {
   330  		return linkMb, err
   331  	}
   332  
   333  	linkMb.Signed = Link{
   334  		Type:        "link",
   335  		Name:        name,
   336  		Materials:   materials,
   337  		Products:    map[string]interface{}{},
   338  		ByProducts:  map[string]interface{}{},
   339  		Command:     []string{},
   340  		Environment: map[string]interface{}{},
   341  	}
   342  
   343  	if !reflect.ValueOf(key).IsZero() {
   344  		if err := linkMb.Sign(key); err != nil {
   345  			return linkMb, err
   346  		}
   347  	}
   348  
   349  	return linkMb, nil
   350  }
   351  
   352  func InTotoRecordStop(prelimLinkMb Metablock, productPaths []string, key Key, hashAlgorithms, gitignorePatterns []string, lStripPaths []string) (Metablock, error) {
   353  	var linkMb Metablock
   354  	if err := prelimLinkMb.VerifySignature(key); err != nil {
   355  		return linkMb, err
   356  	}
   357  
   358  	link, ok := prelimLinkMb.Signed.(Link)
   359  	if !ok {
   360  		return linkMb, errors.New("Invalid metadata block")
   361  	}
   362  
   363  	products, err := RecordArtifacts(productPaths, hashAlgorithms, gitignorePatterns, lStripPaths)
   364  	if err != nil {
   365  		return linkMb, err
   366  	}
   367  
   368  	link.Products = products
   369  	linkMb.Signed = link
   370  
   371  	if !reflect.ValueOf(key).IsZero() {
   372  		if err := linkMb.Sign(key); err != nil {
   373  			return linkMb, err
   374  		}
   375  	}
   376  
   377  	return linkMb, nil
   378  }