...

Source file src/github.com/in-toto/in-toto-golang/in_toto/runlib.go

Documentation: github.com/in-toto/in-toto-golang/in_toto

     1  package in_toto
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     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  var ErrEmptyCommandArgs = errors.New("the command args are empty")
    25  
    26  // visitedSymlinks is a hashset that contains all paths that we have visited.
    27  var visitedSymlinks Set
    28  
    29  /*
    30  RecordArtifact reads and hashes the contents of the file at the passed path
    31  using sha256 and returns a map in the following format:
    32  
    33  	{
    34  		"<path>": {
    35  			"sha256": <hex representation of hash>
    36  		}
    37  	}
    38  
    39  If reading the file fails, the first return value is nil and the second return
    40  value is the error.
    41  NOTE: For cross-platform consistency Windows-style line separators (CRLF) are
    42  normalized to Unix-style line separators (LF) before hashing file contents.
    43  */
    44  func RecordArtifact(path string, hashAlgorithms []string, lineNormalization bool) (map[string]interface{}, error) {
    45  	supportedHashMappings := getHashMapping()
    46  	// Read file from passed path
    47  	contents, err := os.ReadFile(path)
    48  	hashedContentsMap := make(map[string]interface{})
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  
    53  	if lineNormalization {
    54  		// "Normalize" file contents. We convert all line separators to '\n'
    55  		// for keeping operating system independence
    56  		contents = bytes.ReplaceAll(contents, []byte("\r\n"), []byte("\n"))
    57  		contents = bytes.ReplaceAll(contents, []byte("\r"), []byte("\n"))
    58  	}
    59  
    60  	// Create a map of all the hashes present in the hash_func list
    61  	for _, element := range hashAlgorithms {
    62  		if _, ok := supportedHashMappings[element]; !ok {
    63  			return nil, fmt.Errorf("%w: %s", ErrUnsupportedHashAlgorithm, element)
    64  		}
    65  		h := supportedHashMappings[element]
    66  		result := fmt.Sprintf("%x", hashToHex(h(), contents))
    67  		hashedContentsMap[element] = result
    68  	}
    69  
    70  	// Return it in a format that is conformant with link metadata artifacts
    71  	return hashedContentsMap, nil
    72  }
    73  
    74  /*
    75  RecordArtifacts is a wrapper around recordArtifacts.
    76  RecordArtifacts initializes a set for storing visited symlinks,
    77  calls recordArtifacts and deletes the set if no longer needed.
    78  recordArtifacts walks through the passed slice of paths, traversing
    79  subdirectories, and calls RecordArtifact for each file. It returns a map in
    80  the following format:
    81  
    82  	{
    83  		"<path>": {
    84  			"sha256": <hex representation of hash>
    85  		},
    86  		"<path>": {
    87  		"sha256": <hex representation of hash>
    88  		},
    89  		...
    90  	}
    91  
    92  If recording an artifact fails the first return value is nil and the second
    93  return value is the error.
    94  */
    95  func RecordArtifacts(paths []string, hashAlgorithms []string, gitignorePatterns []string, lStripPaths []string, lineNormalization bool, followSymlinkDirs bool) (evalArtifacts map[string]interface{}, err error) {
    96  	// Make sure to initialize a fresh hashset for every RecordArtifacts call
    97  	visitedSymlinks = NewSet()
    98  	evalArtifacts, err = recordArtifacts(paths, hashAlgorithms, gitignorePatterns, lStripPaths, lineNormalization, followSymlinkDirs)
    99  	// pass result and error through
   100  	return evalArtifacts, err
   101  }
   102  
   103  /*
   104  recordArtifacts walks through the passed slice of paths, traversing
   105  subdirectories, and calls RecordArtifact for each file. It returns a map in
   106  the following format:
   107  
   108  	{
   109  		"<path>": {
   110  			"sha256": <hex representation of hash>
   111  		},
   112  		"<path>": {
   113  		"sha256": <hex representation of hash>
   114  		},
   115  		...
   116  	}
   117  
   118  If recording an artifact fails the first return value is nil and the second
   119  return value is the error.
   120  */
   121  func recordArtifacts(paths []string, hashAlgorithms []string, gitignorePatterns []string, lStripPaths []string, lineNormalization bool, followSymlinkDirs bool) (map[string]interface{}, error) {
   122  	artifacts := make(map[string]interface{})
   123  	for _, path := range paths {
   124  		err := filepath.Walk(path,
   125  			func(path string, info os.FileInfo, err error) error {
   126  				// Abort if Walk function has a problem,
   127  				// e.g. path does not exist
   128  				if err != nil {
   129  					return err
   130  				}
   131  				// We need to call pathspec.GitIgnore inside of our filepath.Walk, because otherwise
   132  				// we will not catch all paths. Just imagine a path like "." and a pattern like "*.pub".
   133  				// If we would call pathspec outside of the filepath.Walk this would not match.
   134  				ignore, err := pathspec.GitIgnore(gitignorePatterns, path)
   135  				if err != nil {
   136  					return err
   137  				}
   138  				if ignore {
   139  					return nil
   140  				}
   141  				// Don't hash directories
   142  				if info.IsDir() {
   143  					return nil
   144  				}
   145  
   146  				// check for symlink and evaluate the last element in a symlink
   147  				// chain via filepath.EvalSymlinks. We use EvalSymlinks here,
   148  				// because with os.Readlink() we would just read the next
   149  				// element in a possible symlink chain. This would mean more
   150  				// iterations. infoMode()&os.ModeSymlink uses the file
   151  				// type bitmask to check for a symlink.
   152  				if info.Mode()&os.ModeSymlink == os.ModeSymlink {
   153  					// return with error if we detect a symlink cycle
   154  					if ok := visitedSymlinks.Has(path); ok {
   155  						// this error will get passed through
   156  						// to RecordArtifacts()
   157  						return ErrSymCycle
   158  					}
   159  					evalSym, err := filepath.EvalSymlinks(path)
   160  					if err != nil {
   161  						return err
   162  					}
   163  					info, err := os.Stat(evalSym)
   164  					if err != nil {
   165  						return err
   166  					}
   167  					targetIsDir := false
   168  					if info.IsDir() {
   169  						if !followSymlinkDirs {
   170  							// We don't follow symlinked directories
   171  							return nil
   172  						}
   173  						targetIsDir = true
   174  					}
   175  					// add symlink to visitedSymlinks set
   176  					// this way, we know which link we have visited already
   177  					// if we visit a symlink twice, we have detected a symlink cycle
   178  					visitedSymlinks.Add(path)
   179  					// We recursively call recordArtifacts() to follow
   180  					// the new path.
   181  					evalArtifacts, evalErr := recordArtifacts([]string{evalSym}, hashAlgorithms, gitignorePatterns, lStripPaths, lineNormalization, followSymlinkDirs)
   182  					if evalErr != nil {
   183  						return evalErr
   184  					}
   185  					for key, value := range evalArtifacts {
   186  						if targetIsDir {
   187  							symlinkPath := filepath.Join(path, strings.TrimPrefix(key, evalSym))
   188  							artifacts[symlinkPath] = value
   189  						} else {
   190  							artifacts[path] = value
   191  						}
   192  					}
   193  					return nil
   194  				}
   195  				artifact, err := RecordArtifact(path, hashAlgorithms, lineNormalization)
   196  				// Abort if artifact can't be recorded, e.g.
   197  				// due to file permissions
   198  				if err != nil {
   199  					return err
   200  				}
   201  
   202  				for _, strip := range lStripPaths {
   203  					if strings.HasPrefix(path, strip) {
   204  						path = strings.TrimPrefix(path, strip)
   205  						break
   206  					}
   207  				}
   208  				// Check if path is unique
   209  				if _, exists := artifacts[path]; exists {
   210  					return fmt.Errorf("left stripping has resulted in non unique dictionary key: %s", path)
   211  				}
   212  				artifacts[path] = artifact
   213  				return nil
   214  			})
   215  
   216  		if err != nil {
   217  			return nil, err
   218  		}
   219  	}
   220  
   221  	return artifacts, nil
   222  }
   223  
   224  /*
   225  waitErrToExitCode converts an error returned by Cmd.wait() to an exit code.  It
   226  returns -1 if no exit code can be inferred.
   227  */
   228  func waitErrToExitCode(err error) int {
   229  	// If there's no exit code, we return -1
   230  	retVal := -1
   231  
   232  	// See https://stackoverflow.com/questions/10385551/get-exit-code-go
   233  	if err != nil {
   234  		if exiterr, ok := err.(*exec.ExitError); ok {
   235  			// The program has exited with an exit code != 0
   236  			// This works on both Unix and Windows. Although package
   237  			// syscall is generally platform dependent, WaitStatus is
   238  			// defined for both Unix and Windows and in both cases has
   239  			// an ExitStatus() method with the same signature.
   240  			if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
   241  				retVal = status.ExitStatus()
   242  			}
   243  		}
   244  	} else {
   245  		retVal = 0
   246  	}
   247  
   248  	return retVal
   249  }
   250  
   251  /*
   252  RunCommand executes the passed command in a subprocess.  The first element of
   253  cmdArgs is used as executable and the rest as command arguments.  It captures
   254  and returns stdout, stderr and exit code.  The format of the returned map is:
   255  
   256  	{
   257  		"return-value": <exit code>,
   258  		"stdout": "<standard output>",
   259  		"stderr": "<standard error>"
   260  	}
   261  
   262  If the command cannot be executed or no pipes for stdout or stderr can be
   263  created the first return value is nil and the second return value is the error.
   264  NOTE: Since stdout and stderr are captured, they cannot be seen during the
   265  command execution.
   266  */
   267  func RunCommand(cmdArgs []string, runDir string) (map[string]interface{}, error) {
   268  	if len(cmdArgs) == 0 {
   269  		return nil, ErrEmptyCommandArgs
   270  	}
   271  
   272  	cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
   273  
   274  	if runDir != "" {
   275  		cmd.Dir = runDir
   276  	}
   277  
   278  	stderrPipe, err := cmd.StderrPipe()
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  	stdoutPipe, err := cmd.StdoutPipe()
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  
   287  	if err := cmd.Start(); err != nil {
   288  		return nil, err
   289  	}
   290  
   291  	// TODO: duplicate stdout, stderr
   292  	stdout, _ := io.ReadAll(stdoutPipe)
   293  	stderr, _ := io.ReadAll(stderrPipe)
   294  
   295  	retVal := waitErrToExitCode(cmd.Wait())
   296  
   297  	return map[string]interface{}{
   298  		"return-value": float64(retVal),
   299  		"stdout":       string(stdout),
   300  		"stderr":       string(stderr),
   301  	}, nil
   302  }
   303  
   304  /*
   305  InTotoRun executes commands, e.g. for software supply chain steps or
   306  inspections of an in-toto layout, and creates and returns corresponding link
   307  metadata.  Link metadata contains recorded products at the passed productPaths
   308  and materials at the passed materialPaths.  The returned link is wrapped in a
   309  Metablock object.  If command execution or artifact recording fails the first
   310  return value is an empty Metablock and the second return value is the error.
   311  */
   312  func InTotoRun(name string, runDir string, materialPaths []string, productPaths []string, cmdArgs []string, key Key, hashAlgorithms []string, gitignorePatterns []string, lStripPaths []string, lineNormalization bool, followSymlinkDirs bool, useDSSE bool) (Metadata, error) {
   313  	materials, err := RecordArtifacts(materialPaths, hashAlgorithms, gitignorePatterns, lStripPaths, lineNormalization, followSymlinkDirs)
   314  	if err != nil {
   315  		return nil, err
   316  	}
   317  
   318  	// make sure that we only run RunCommand if cmdArgs is not nil or empty
   319  	byProducts := map[string]interface{}{}
   320  	if len(cmdArgs) != 0 {
   321  		byProducts, err = RunCommand(cmdArgs, runDir)
   322  		if err != nil {
   323  			return nil, err
   324  		}
   325  	}
   326  
   327  	products, err := RecordArtifacts(productPaths, hashAlgorithms, gitignorePatterns, lStripPaths, lineNormalization, followSymlinkDirs)
   328  	if err != nil {
   329  		return nil, err
   330  	}
   331  
   332  	link := Link{
   333  		Type:        "link",
   334  		Name:        name,
   335  		Materials:   materials,
   336  		Products:    products,
   337  		ByProducts:  byProducts,
   338  		Command:     cmdArgs,
   339  		Environment: map[string]interface{}{},
   340  	}
   341  
   342  	if useDSSE {
   343  		env := &Envelope{}
   344  		if err := env.SetPayload(link); err != nil {
   345  			return nil, err
   346  		}
   347  
   348  		if !reflect.ValueOf(key).IsZero() {
   349  			if err := env.Sign(key); err != nil {
   350  				return nil, err
   351  			}
   352  		}
   353  
   354  		return env, nil
   355  	}
   356  
   357  	linkMb := &Metablock{Signed: link, Signatures: []Signature{}}
   358  	if !reflect.ValueOf(key).IsZero() {
   359  		if err := linkMb.Sign(key); err != nil {
   360  			return nil, err
   361  		}
   362  	}
   363  
   364  	return linkMb, nil
   365  }
   366  
   367  /*
   368  InTotoRecordStart begins the creation of a link metablock file in two steps,
   369  in order to provide evidence for supply chain steps that cannot be carries out
   370  by a single command.  InTotoRecordStart collects the hashes of the materials
   371  before any commands are run, signs the unfinished link, and returns the link.
   372  */
   373  func InTotoRecordStart(name string, materialPaths []string, key Key, hashAlgorithms, gitignorePatterns []string, lStripPaths []string, lineNormalization bool, followSymlinkDirs bool, useDSSE bool) (Metadata, error) {
   374  	materials, err := RecordArtifacts(materialPaths, hashAlgorithms, gitignorePatterns, lStripPaths, lineNormalization, followSymlinkDirs)
   375  	if err != nil {
   376  		return nil, err
   377  	}
   378  
   379  	link := Link{
   380  		Type:        "link",
   381  		Name:        name,
   382  		Materials:   materials,
   383  		Products:    map[string]interface{}{},
   384  		ByProducts:  map[string]interface{}{},
   385  		Command:     []string{},
   386  		Environment: map[string]interface{}{},
   387  	}
   388  
   389  	if useDSSE {
   390  		env := &Envelope{}
   391  		if err := env.SetPayload(link); err != nil {
   392  			return nil, err
   393  		}
   394  
   395  		if !reflect.ValueOf(key).IsZero() {
   396  			if err := env.Sign(key); err != nil {
   397  				return nil, err
   398  			}
   399  		}
   400  
   401  		return env, nil
   402  	}
   403  
   404  	linkMb := &Metablock{Signed: link, Signatures: []Signature{}}
   405  	linkMb.Signatures = []Signature{}
   406  	if !reflect.ValueOf(key).IsZero() {
   407  		if err := linkMb.Sign(key); err != nil {
   408  			return nil, err
   409  		}
   410  	}
   411  
   412  	return linkMb, nil
   413  }
   414  
   415  /*
   416  InTotoRecordStop ends the creation of a metatadata link file created by
   417  InTotoRecordStart. InTotoRecordStop takes in a signed unfinished link metablock
   418  created by InTotoRecordStart and records the hashes of any products creted by
   419  commands run between InTotoRecordStart and InTotoRecordStop.  The resultant
   420  finished link metablock is then signed by the provided key and returned.
   421  */
   422  func InTotoRecordStop(prelimLinkEnv Metadata, productPaths []string, key Key, hashAlgorithms, gitignorePatterns []string, lStripPaths []string, lineNormalization bool, followSymlinkDirs bool, useDSSE bool) (Metadata, error) {
   423  	if err := prelimLinkEnv.VerifySignature(key); err != nil {
   424  		return nil, err
   425  	}
   426  
   427  	link, ok := prelimLinkEnv.GetPayload().(Link)
   428  	if !ok {
   429  		return nil, errors.New("invalid metadata block")
   430  	}
   431  
   432  	products, err := RecordArtifacts(productPaths, hashAlgorithms, gitignorePatterns, lStripPaths, lineNormalization, followSymlinkDirs)
   433  	if err != nil {
   434  		return nil, err
   435  	}
   436  
   437  	link.Products = products
   438  
   439  	if useDSSE {
   440  		env := &Envelope{}
   441  		if err := env.SetPayload(link); err != nil {
   442  			return nil, err
   443  		}
   444  
   445  		if !reflect.ValueOf(key).IsZero() {
   446  			if err := env.Sign(key); err != nil {
   447  				return nil, err
   448  			}
   449  		}
   450  
   451  		return env, nil
   452  	}
   453  
   454  	linkMb := &Metablock{Signed: link, Signatures: []Signature{}}
   455  	if !reflect.ValueOf(key).IsZero() {
   456  		if err := linkMb.Sign(key); err != nil {
   457  			return linkMb, err
   458  		}
   459  	}
   460  
   461  	return linkMb, nil
   462  }
   463  

View as plain text