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 }