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
19 var ErrSymCycle = errors.New("symlink cycle detected")
20
21
22 var ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm detected")
23
24 var ErrEmptyCommandArgs = errors.New("the command args are empty")
25
26
27 var visitedSymlinks Set
28
29
44 func RecordArtifact(path string, hashAlgorithms []string, lineNormalization bool) (map[string]interface{}, error) {
45 supportedHashMappings := getHashMapping()
46
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
55
56 contents = bytes.ReplaceAll(contents, []byte("\r\n"), []byte("\n"))
57 contents = bytes.ReplaceAll(contents, []byte("\r"), []byte("\n"))
58 }
59
60
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
71 return hashedContentsMap, nil
72 }
73
74
95 func RecordArtifacts(paths []string, hashAlgorithms []string, gitignorePatterns []string, lStripPaths []string, lineNormalization bool, followSymlinkDirs bool) (evalArtifacts map[string]interface{}, err error) {
96
97 visitedSymlinks = NewSet()
98 evalArtifacts, err = recordArtifacts(paths, hashAlgorithms, gitignorePatterns, lStripPaths, lineNormalization, followSymlinkDirs)
99
100 return evalArtifacts, err
101 }
102
103
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
127
128 if err != nil {
129 return err
130 }
131
132
133
134 ignore, err := pathspec.GitIgnore(gitignorePatterns, path)
135 if err != nil {
136 return err
137 }
138 if ignore {
139 return nil
140 }
141
142 if info.IsDir() {
143 return nil
144 }
145
146
147
148
149
150
151
152 if info.Mode()&os.ModeSymlink == os.ModeSymlink {
153
154 if ok := visitedSymlinks.Has(path); ok {
155
156
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
171 return nil
172 }
173 targetIsDir = true
174 }
175
176
177
178 visitedSymlinks.Add(path)
179
180
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
197
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
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
228 func waitErrToExitCode(err error) int {
229
230 retVal := -1
231
232
233 if err != nil {
234 if exiterr, ok := err.(*exec.ExitError); ok {
235
236
237
238
239
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
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
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
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
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
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
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