• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

in-toto / in-toto-golang / 11294296860

11 Oct 2024 02:36PM UTC coverage: 71.191% (-0.2%) from 71.437%
11294296860

Pull #357

github

web-flow
Merge 56379e229 into a4ccdfc1a
Pull Request #357: chore(deps): bump coverallsapp/github-action from 2.3.0 to 2.3.1

2308 of 3242 relevant lines covered (71.19%)

528.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.74
/in_toto/verifylib.go
1
/*
2
Package in_toto implements types and routines to verify a software supply chain
3
according to the in-toto specification.
4
See https://github.com/in-toto/docs/blob/master/in-toto-spec.md
5
*/
6
package in_toto
7

8
import (
9
        "crypto/x509"
10
        "errors"
11
        "fmt"
12
        "io"
13
        "os"
14
        "path"
15
        "path/filepath"
16
        "reflect"
17
        "regexp"
18
        "strings"
19
        "time"
20
)
21

22
// ErrInspectionRunDirIsSymlink gets thrown if the runDir is a symlink
23
var ErrInspectionRunDirIsSymlink = errors.New("runDir is a symlink. This is a security risk")
24

25
var ErrNotLayout = errors.New("verification workflow passed a non-layout")
26

27
/*
28
RunInspections iteratively executes the command in the Run field of all
29
inspections of the passed layout, creating unsigned link metadata that records
30
all files found in the current working directory as materials (before command
31
execution) and products (after command execution).  A map with inspection names
32
as keys and Metablocks containing the generated link metadata as values is
33
returned.  The format is:
34

35
        {
36
                <inspection name> : Metablock,
37
                <inspection name> : Metablock,
38
                ...
39
        }
40

41
If executing the inspection command fails, or if the executed command has a
42
non-zero exit code, the first return value is an empty Metablock map and the
43
second return value is the error.
44
*/
45
func RunInspections(layout Layout, runDir string, lineNormalization bool, useDSSE bool) (map[string]Metadata, error) {
54✔
46
        inspectionMetadata := make(map[string]Metadata)
54✔
47

54✔
48
        for _, inspection := range layout.Inspect {
108✔
49

54✔
50
                paths := []string{"."}
54✔
51
                if runDir != "" {
60✔
52
                        paths = []string{runDir}
6✔
53
                }
6✔
54

55
                linkEnv, err := InTotoRun(inspection.Name, runDir, paths, paths,
54✔
56
                        inspection.Run, Key{}, []string{"sha256"}, nil, nil, lineNormalization, false, useDSSE)
54✔
57

54✔
58
                if err != nil {
60✔
59
                        return nil, err
6✔
60
                }
6✔
61

62
                link, ok := linkEnv.GetPayload().(Link)
48✔
63
                if !ok {
48✔
64
                        return nil, fmt.Errorf("invalid metadata")
×
65
                }
×
66
                retVal := link.ByProducts["return-value"]
48✔
67
                if retVal != float64(0) {
54✔
68
                        return nil, fmt.Errorf("inspection command '%s' of inspection '%s'"+
6✔
69
                                " returned a non-zero value: %d", inspection.Run, inspection.Name,
6✔
70
                                retVal)
6✔
71
                }
6✔
72

73
                // Dump inspection link to cwd using the short link name format
74
                linkName := fmt.Sprintf(LinkNameFormatShort, inspection.Name)
42✔
75
                if err := linkEnv.Dump(linkName); err != nil {
42✔
76
                        fmt.Printf("JSON serialization or writing failed: %s", err)
×
77
                }
×
78

79
                inspectionMetadata[inspection.Name] = linkEnv
42✔
80
        }
81
        return inspectionMetadata, nil
42✔
82
}
83

84
// verifyMatchRule is a helper function to process artifact rules of
85
// type MATCH. See VerifyArtifacts for more details.
86
func verifyMatchRule(ruleData map[string]string,
87
        srcArtifacts map[string]HashObj, srcArtifactQueue Set,
88
        itemsMetadata map[string]Metadata) Set {
174✔
89
        consumed := NewSet()
174✔
90
        // Get destination link metadata
174✔
91
        dstLinkEnv, exists := itemsMetadata[ruleData["dstName"]]
174✔
92
        if !exists {
186✔
93
                // Destination link does not exist, rule can't consume any
12✔
94
                // artifacts
12✔
95
                return consumed
12✔
96
        }
12✔
97

98
        dstLink, ok := dstLinkEnv.GetPayload().(Link)
162✔
99
        if !ok {
162✔
100
                fmt.Printf("invalid metadata")
×
101
                return consumed
×
102
        }
×
103

104
        // Get artifacts from destination link metadata
105
        var dstArtifacts map[string]HashObj
162✔
106
        switch ruleData["dstType"] {
162✔
107
        case "materials":
48✔
108
                dstArtifacts = dstLink.Materials
48✔
109
        case "products":
114✔
110
                dstArtifacts = dstLink.Products
114✔
111
        }
112

113
        // cleanup paths in pattern and artifact maps
114
        if ruleData["pattern"] != "" {
324✔
115
                ruleData["pattern"] = path.Clean(ruleData["pattern"])
162✔
116
        }
162✔
117
        for k := range srcArtifacts {
3,082✔
118
                if path.Clean(k) != k {
2,926✔
119
                        srcArtifacts[path.Clean(k)] = srcArtifacts[k]
6✔
120
                        delete(srcArtifacts, k)
6✔
121
                }
6✔
122
        }
123
        for k := range dstArtifacts {
378✔
124
                if path.Clean(k) != k {
222✔
125
                        dstArtifacts[path.Clean(k)] = dstArtifacts[k]
6✔
126
                        delete(dstArtifacts, k)
6✔
127
                }
6✔
128
        }
129

130
        // Normalize optional source and destination prefixes, i.e. if
131
        // there is a prefix, then add a trailing slash if not there yet
132
        for _, prefix := range []string{"srcPrefix", "dstPrefix"} {
486✔
133
                if ruleData[prefix] != "" {
336✔
134
                        ruleData[prefix] = path.Clean(ruleData[prefix])
12✔
135
                        if !strings.HasSuffix(ruleData[prefix], "/") {
24✔
136
                                ruleData[prefix] += "/"
12✔
137
                        }
12✔
138
                }
139
        }
140
        // Iterate over queue and mark consumed artifacts
141
        for srcPath := range srcArtifactQueue {
3,054✔
142
                // Remove optional source prefix from source artifact path
2,892✔
143
                // Noop if prefix is empty, or artifact does not have it
2,892✔
144
                srcBasePath := strings.TrimPrefix(srcPath, ruleData["srcPrefix"])
2,892✔
145

2,892✔
146
                // Ignore artifacts not matched by rule pattern
2,892✔
147
                matched, err := match(ruleData["pattern"], srcBasePath)
2,892✔
148
                if err != nil || !matched {
5,610✔
149
                        continue
2,718✔
150
                }
151

152
                // Construct corresponding destination artifact path, i.e.
153
                // an optional destination prefix plus the source base path
154
                dstPath := path.Clean(path.Join(ruleData["dstPrefix"], srcBasePath))
174✔
155

174✔
156
                // Try to find the corresponding destination artifact
174✔
157
                dstArtifact, exists := dstArtifacts[dstPath]
174✔
158
                // Ignore artifacts without corresponding destination artifact
174✔
159
                if !exists {
180✔
160
                        continue
6✔
161
                }
162

163
                // Ignore artifact pairs with no matching hashes
164
                if !reflect.DeepEqual(srcArtifacts[srcPath], dstArtifact) {
180✔
165
                        continue
12✔
166
                }
167

168
                // Only if a source and destination artifact pair was found and
169
                // their hashes are equal, will we mark the source artifact as
170
                // successfully consumed, i.e. it will be removed from the queue
171
                consumed.Add(srcPath)
156✔
172
        }
173
        return consumed
162✔
174
}
175

176
/*
177
VerifyArtifacts iteratively applies the material and product rules of the
178
passed items (step or inspection) to enforce and authorize artifacts (materials
179
or products) reported by the corresponding link and to guarantee that
180
artifacts are linked together across links.  In the beginning all artifacts are
181
placed in a queue according to their type.  If an artifact gets consumed by a
182
rule it is removed from the queue.  An artifact can only be consumed once in
183
the course of processing the set of rules in ExpectedMaterials or
184
ExpectedProducts.
185

186
Rules of type MATCH, ALLOW, CREATE, DELETE, MODIFY and DISALLOW are supported.
187

188
All rules except for DISALLOW consume queued artifacts on success, and
189
leave the queue unchanged on failure.  Hence, it is left to a terminal
190
DISALLOW rule to fail overall verification, if artifacts are left in the queue
191
that should have been consumed by preceding rules.
192
*/
193
func VerifyArtifacts(items []interface{},
194
        itemsMetadata map[string]Metadata) error {
228✔
195
        // Verify artifact rules for each item in the layout
228✔
196
        for _, itemI := range items {
492✔
197
                // The layout item (interface) must be a Link or an Inspection we are only
264✔
198
                // interested in the name and the expected materials and products
264✔
199
                var itemName string
264✔
200
                var expectedMaterials [][]string
264✔
201
                var expectedProducts [][]string
264✔
202

264✔
203
                switch item := itemI.(type) {
264✔
204
                case Step:
162✔
205
                        itemName = item.Name
162✔
206
                        expectedMaterials = item.ExpectedMaterials
162✔
207
                        expectedProducts = item.ExpectedProducts
162✔
208

209
                case Inspection:
96✔
210
                        itemName = item.Name
96✔
211
                        expectedMaterials = item.ExpectedMaterials
96✔
212
                        expectedProducts = item.ExpectedProducts
96✔
213

214
                default: // Something wrong
6✔
215
                        return fmt.Errorf("VerifyArtifacts received an item of invalid type,"+
6✔
216
                                " elements of passed slice 'items' must be one of 'Step' or"+
6✔
217
                                " 'Inspection', got: '%s'", reflect.TypeOf(item))
6✔
218
                }
219

220
                // Use the item's name to extract the corresponding link
221
                srcLinkEnv, exists := itemsMetadata[itemName]
258✔
222
                if !exists {
270✔
223
                        return fmt.Errorf("VerifyArtifacts could not find metadata"+
12✔
224
                                " for item '%s', got: '%s'", itemName, itemsMetadata)
12✔
225
                }
12✔
226

227
                // Create shortcuts to materials and products (including hashes) reported
228
                // by the item's link, required to verify "match" rules
229
                link, ok := srcLinkEnv.GetPayload().(Link)
246✔
230
                if !ok {
246✔
231
                        return fmt.Errorf("invalid metadata")
×
232
                }
×
233
                materials := link.Materials
246✔
234
                products := link.Products
246✔
235

246✔
236
                // All other rules only require the material or product paths (without
246✔
237
                // hashes). We extract them from the corresponding maps and store them as
246✔
238
                // sets for convenience in further processing
246✔
239
                materialPaths := NewSet()
246✔
240
                for _, p := range artifactsDictKeyStrings(materials) {
1,764✔
241
                        materialPaths.Add(path.Clean(p))
1,518✔
242
                }
1,518✔
243
                productPaths := NewSet()
246✔
244
                for _, p := range artifactsDictKeyStrings(products) {
1,782✔
245
                        productPaths.Add(path.Clean(p))
1,536✔
246
                }
1,536✔
247

248
                // For `create`, `delete` and `modify` rules we prepare sets of artifacts
249
                // (without hashes) that were created, deleted or modified in the current
250
                // step or inspection
251
                created := productPaths.Difference(materialPaths)
246✔
252
                deleted := materialPaths.Difference(productPaths)
246✔
253
                remained := materialPaths.Intersection(productPaths)
246✔
254
                modified := NewSet()
246✔
255
                for name := range remained {
1,650✔
256
                        if !reflect.DeepEqual(materials[name], products[name]) {
1,416✔
257
                                modified.Add(name)
12✔
258
                        }
12✔
259
                }
260

261
                // For each item we have to run rule verification, once per artifact type.
262
                // Here we prepare the corresponding data for each round.
263
                verificationDataList := []map[string]interface{}{
246✔
264
                        {
246✔
265
                                "srcType":       "materials",
246✔
266
                                "rules":         expectedMaterials,
246✔
267
                                "artifacts":     materials,
246✔
268
                                "artifactPaths": materialPaths,
246✔
269
                        },
246✔
270
                        {
246✔
271
                                "srcType":       "products",
246✔
272
                                "rules":         expectedProducts,
246✔
273
                                "artifacts":     products,
246✔
274
                                "artifactPaths": productPaths,
246✔
275
                        },
246✔
276
                }
246✔
277
                // TODO: Add logging library (see in-toto/in-toto-golang#4)
246✔
278
                // fmt.Printf("Verifying %s '%s' ", reflect.TypeOf(itemI), itemName)
246✔
279

246✔
280
                // Process all material rules using the corresponding materials and all
246✔
281
                // product rules using the corresponding products
246✔
282
                for _, verificationData := range verificationDataList {
684✔
283
                        // TODO: Add logging library (see in-toto/in-toto-golang#4)
438✔
284
                        // fmt.Printf("%s...\n", verificationData["srcType"])
438✔
285

438✔
286
                        rules, ok := verificationData["rules"].([][]string)
438✔
287
                        if !ok {
438✔
288
                                return fmt.Errorf(`rules must be of type [][]string`)
×
289
                        }
×
290
                        artifacts, ok := verificationData["artifacts"].(map[string]HashObj)
438✔
291
                        if !ok {
438✔
292
                                return fmt.Errorf(`artifacts must be of type map[string]HashObj`)
×
293
                        }
×
294
                        // Use artifacts (without hashes) as base queue. Each rule only operates
295
                        // on artifacts in that queue.  If a rule consumes an artifact (i.e. can
296
                        // be applied successfully), the artifact is removed from the queue. By
297
                        // applying a DISALLOW rule eventually, verification may return an error,
298
                        // if the rule matches any artifacts in the queue that should have been
299
                        // consumed earlier.
300
                        queue, ok := verificationData["artifactPaths"].(Set)
438✔
301
                        if !ok {
438✔
302
                                return fmt.Errorf(`queue must be of type Set`)
×
303
                        }
×
304
                        // TODO: Add logging library (see in-toto/in-toto-golang#4)
305
                        // fmt.Printf("Initial state\nMaterials: %s\nProducts: %s\nQueue: %s\n\n",
306
                        //         materialPaths.Slice(), productPaths.Slice(), queue.Slice())
307

308
                        // Verify rules sequentially
309
                        for _, rule := range rules {
1,002✔
310
                                // Parse rule and error out if it is malformed
564✔
311
                                // NOTE: the rule format should have been validated before
564✔
312
                                ruleData, err := UnpackRule(rule)
564✔
313
                                if err != nil {
588✔
314
                                        return err
24✔
315
                                }
24✔
316

317
                                // Apply rule pattern to filter queued artifacts that are up for rule
318
                                // specific consumption
319
                                filtered := queue.Filter(path.Clean(ruleData["pattern"]))
540✔
320

540✔
321
                                var consumed Set
540✔
322
                                switch ruleData["type"] {
540✔
323
                                case "match":
126✔
324
                                        // Note: here we need to perform more elaborate filtering
126✔
325
                                        consumed = verifyMatchRule(ruleData, artifacts, queue, itemsMetadata)
126✔
326

327
                                case "allow":
126✔
328
                                        // Consumes all filtered artifacts
126✔
329
                                        consumed = filtered
126✔
330

331
                                case "create":
18✔
332
                                        // Consumes filtered artifacts that were created
18✔
333
                                        consumed = filtered.Intersection(created)
18✔
334

335
                                case "delete":
6✔
336
                                        // Consumes filtered artifacts that were deleted
6✔
337
                                        consumed = filtered.Intersection(deleted)
6✔
338

339
                                case "modify":
18✔
340
                                        // Consumes filtered artifacts that were modified
18✔
341
                                        consumed = filtered.Intersection(modified)
18✔
342

343
                                case "disallow":
216✔
344
                                        // Does not consume but errors out if artifacts were filtered
216✔
345
                                        if len(filtered) > 0 {
270✔
346
                                                return fmt.Errorf("artifact verification failed for %s '%s',"+
54✔
347
                                                        " %s %s disallowed by rule %s",
54✔
348
                                                        reflect.TypeOf(itemI).Name(), itemName,
54✔
349
                                                        verificationData["srcType"], filtered.Slice(), rule)
54✔
350
                                        }
54✔
351
                                case "require":
30✔
352
                                        // REQUIRE is somewhat of a weird animal that does not use
30✔
353
                                        // patterns bur rather single filenames (for now).
30✔
354
                                        if !queue.Has(ruleData["pattern"]) {
54✔
355
                                                return fmt.Errorf("artifact verification failed for %s in REQUIRE '%s',"+
24✔
356
                                                        " because %s is not in %s", verificationData["srcType"],
24✔
357
                                                        ruleData["pattern"], ruleData["pattern"], queue.Slice())
24✔
358
                                        }
24✔
359
                                }
360
                                // Update queue by removing consumed artifacts
361
                                queue = queue.Difference(consumed)
462✔
362
                                // TODO: Add logging library (see in-toto/in-toto-golang#4)
363
                                // fmt.Printf("Rule: %s\nQueue: %s\n\n", rule, queue.Slice())
364
                        }
365
                }
366
        }
367
        return nil
108✔
368
}
369

370
/*
371
ReduceStepsMetadata merges for each step of the passed Layout all the passed
372
per-functionary links into a single link, asserting that the reported Materials
373
and Products are equal across links for a given step.  This function may be
374
used at a time during the overall verification, where link threshold's have
375
been verified and subsequent verification only needs one exemplary link per
376
step.  The function returns a map with one Metablock (link) per step:
377

378
        {
379
                <step name> : Metablock,
380
                <step name> : Metablock,
381
                ...
382
        }
383

384
If links corresponding to the same step report different Materials or different
385
Products, the first return value is an empty Metablock map and the second
386
return value is the error.
387
*/
388
func ReduceStepsMetadata(layout Layout,
389
        stepsMetadata map[string]map[string]Metadata) (map[string]Metadata,
390
        error) {
72✔
391
        stepsMetadataReduced := make(map[string]Metadata)
72✔
392

72✔
393
        for _, step := range layout.Steps {
186✔
394
                linksPerStep, ok := stepsMetadata[step.Name]
114✔
395
                // We should never get here, layout verification must fail earlier
114✔
396
                if !ok || len(linksPerStep) < 1 {
120✔
397
                        panic("Could not reduce metadata for step '" + step.Name +
6✔
398
                                "', no link metadata found.")
6✔
399
                }
400

401
                // Get the first link (could be any link) for the current step, which will
402
                // serve as reference link for below comparisons
403
                var referenceKeyID string
108✔
404
                var referenceLinkEnv Metadata
108✔
405
                for keyID, linkEnv := range linksPerStep {
216✔
406
                        referenceLinkEnv = linkEnv
108✔
407
                        referenceKeyID = keyID
108✔
408
                        break
108✔
409
                }
410

411
                // Only one link, nothing to reduce, take the reference link
412
                if len(linksPerStep) == 1 {
186✔
413
                        stepsMetadataReduced[step.Name] = referenceLinkEnv
78✔
414

78✔
415
                        // Multiple links, reduce but first check
78✔
416
                } else {
108✔
417
                        // Artifact maps must be equal for each type among all links
30✔
418
                        // TODO: What should we do if there are more links, than the
30✔
419
                        // threshold requires, but not all of them are equal? Right now we would
30✔
420
                        // also error.
30✔
421
                        for keyID, linkEnv := range linksPerStep {
85✔
422
                                link, ok := linkEnv.GetPayload().(Link)
55✔
423
                                if !ok {
55✔
424
                                        return nil, fmt.Errorf("invalid metadata")
×
425
                                }
×
426
                                refLink, ok := referenceLinkEnv.GetPayload().(Link)
55✔
427
                                if !ok {
55✔
428
                                        return nil, fmt.Errorf("invalid metadata")
×
429
                                }
×
430
                                if !reflect.DeepEqual(link.Materials, refLink.Materials) ||
55✔
431
                                        !reflect.DeepEqual(link.Products, refLink.Products) {
79✔
432
                                        return nil, fmt.Errorf("link '%s' and '%s' have different"+
24✔
433
                                                " artifacts",
24✔
434
                                                fmt.Sprintf(LinkNameFormat, step.Name, referenceKeyID),
24✔
435
                                                fmt.Sprintf(LinkNameFormat, step.Name, keyID))
24✔
436
                                }
24✔
437
                        }
438
                        // We haven't errored out, so we can reduce (i.e take the reference link)
439
                        stepsMetadataReduced[step.Name] = referenceLinkEnv
6✔
440
                }
441
        }
442
        return stepsMetadataReduced, nil
42✔
443
}
444

445
/*
446
VerifyStepCommandAlignment (soft) verifies that for each step of the passed
447
layout the command executed, as per the passed link, matches the expected
448
command, as per the layout.  Soft verification means that, in case a command
449
does not align, a warning is issued.
450
*/
451
func VerifyStepCommandAlignment(layout Layout,
452
        stepsMetadata map[string]map[string]Metadata) {
48✔
453
        for _, step := range layout.Steps {
138✔
454
                linksPerStep, ok := stepsMetadata[step.Name]
90✔
455
                // We should never get here, layout verification must fail earlier
90✔
456
                if !ok || len(linksPerStep) < 1 {
96✔
457
                        panic("Could not verify command alignment for step '" + step.Name +
6✔
458
                                "', no link metadata found.")
6✔
459
                }
460

461
                for signerKeyID, linkEnv := range linksPerStep {
168✔
462
                        link, ok := linkEnv.GetPayload().(Link)
84✔
463
                        if !ok {
84✔
464
                                fmt.Printf("invalid metadata")
×
465
                                return
×
466
                        }
×
467
                        expectedCommandS := strings.Join(step.ExpectedCommand, " ")
84✔
468
                        executedCommandS := strings.Join(link.Command, " ")
84✔
469

84✔
470
                        if expectedCommandS != executedCommandS {
90✔
471
                                linkName := fmt.Sprintf(LinkNameFormat, step.Name, signerKeyID)
6✔
472
                                fmt.Printf("WARNING: Expected command for step '%s' (%s) and command"+
6✔
473
                                        " reported by '%s' (%s) differ.\n",
6✔
474
                                        step.Name, expectedCommandS, linkName, executedCommandS)
6✔
475
                        }
6✔
476
                }
477
        }
478
}
479

480
/*
481
LoadLayoutCertificates loads the root and intermediate CAs from the layout if in the layout.
482
This will be used to check signatures that were used to sign links but not configured
483
in the PubKeys section of the step.  No configured CAs means we don't want to allow this.
484
Returned CertPools will be empty in this case.
485
*/
486
func LoadLayoutCertificates(layout Layout, intermediatePems [][]byte) (*x509.CertPool, *x509.CertPool, error) {
66✔
487
        rootPool := x509.NewCertPool()
66✔
488
        for _, certPem := range layout.RootCas {
108✔
489
                ok := rootPool.AppendCertsFromPEM([]byte(certPem.KeyVal.Certificate))
42✔
490
                if !ok {
48✔
491
                        return nil, nil, fmt.Errorf("failed to load root certificates for layout")
6✔
492
                }
6✔
493
        }
494

495
        intermediatePool := x509.NewCertPool()
60✔
496
        for _, intermediatePem := range layout.IntermediateCas {
96✔
497
                ok := intermediatePool.AppendCertsFromPEM([]byte(intermediatePem.KeyVal.Certificate))
36✔
498
                if !ok {
42✔
499
                        return nil, nil, fmt.Errorf("failed to load intermediate certificates for layout")
6✔
500
                }
6✔
501
        }
502

503
        for _, intermediatePem := range intermediatePems {
66✔
504
                ok := intermediatePool.AppendCertsFromPEM(intermediatePem)
12✔
505
                if !ok {
18✔
506
                        return nil, nil, fmt.Errorf("failed to load provided intermediate certificates")
6✔
507
                }
6✔
508
        }
509

510
        return rootPool, intermediatePool, nil
48✔
511
}
512

513
/*
514
VerifyLinkSignatureThesholds verifies that for each step of the passed layout,
515
there are at least Threshold links, validly signed by different authorized
516
functionaries.  The returned map of link metadata per steps contains only
517
links with valid signatures from distinct functionaries and has the format:
518

519
        {
520
                <step name> : {
521
                <key id>: Metablock,
522
                <key id>: Metablock,
523
                ...
524
                },
525
                <step name> : {
526
                <key id>: Metablock,
527
                <key id>: Metablock,
528
                ...
529
                }
530
                ...
531
        }
532

533
If for any step of the layout there are not enough links available, the first
534
return value is an empty map of Metablock maps and the second return value is
535
the error.
536
*/
537
func VerifyLinkSignatureThesholds(layout Layout,
538
        stepsMetadata map[string]map[string]Metadata, rootCertPool, intermediateCertPool *x509.CertPool) (
539
        map[string]map[string]Metadata, error) {
84✔
540
        // This will stores links with valid signature from an authorized functionary
84✔
541
        // for all steps
84✔
542
        stepsMetadataVerified := make(map[string]map[string]Metadata)
84✔
543

84✔
544
        // Try to find enough (>= threshold) links each with a valid signature from
84✔
545
        // distinct authorized functionaries for each step
84✔
546
        for _, step := range layout.Steps {
210✔
547
                var stepErr error
126✔
548

126✔
549
                // This will store links with valid signature from an authorized
126✔
550
                // functionary for the given step
126✔
551
                linksPerStepVerified := make(map[string]Metadata)
126✔
552

126✔
553
                // Check if there are any links at all for the given step
126✔
554
                linksPerStep, ok := stepsMetadata[step.Name]
126✔
555
                if !ok || len(linksPerStep) < 1 {
138✔
556
                        stepErr = fmt.Errorf("no links found")
12✔
557
                }
12✔
558

559
                // For each link corresponding to a step, check that the signer key was
560
                // authorized, the layout contains a verification key and the signature
561
                // verification passes.  Only good links are stored, to verify thresholds
562
                // below.
563
                isAuthorizedSignature := false
126✔
564
                for signerKeyID, linkEnv := range linksPerStep {
270✔
565
                        for _, authorizedKeyID := range step.PubKeys {
336✔
566
                                if signerKeyID == authorizedKeyID {
336✔
567
                                        if verifierKey, ok := layout.Keys[authorizedKeyID]; ok {
282✔
568
                                                if err := linkEnv.VerifySignature(verifierKey); err == nil {
264✔
569
                                                        linksPerStepVerified[signerKeyID] = linkEnv
126✔
570
                                                        isAuthorizedSignature = true
126✔
571
                                                        break
126✔
572
                                                }
573
                                        }
574
                                }
575
                        }
576

577
                        // If the signer's key wasn't in our step's pubkeys array, check the cert pool to
578
                        // see if the key is known to us.
579
                        if !isAuthorizedSignature {
144✔
580
                                sig, err := linkEnv.GetSignatureForKeyID(signerKeyID)
×
581
                                if err != nil {
×
582
                                        stepErr = err
×
583
                                        continue
×
584
                                }
585

586
                                cert, err := sig.GetCertificate()
×
587
                                if err != nil {
×
588
                                        stepErr = err
×
589
                                        continue
×
590
                                }
591

592
                                // test certificate against the step's constraints to make sure it's a valid functionary
593
                                err = step.CheckCertConstraints(cert, layout.RootCAIDs(), rootCertPool, intermediateCertPool)
×
594
                                if err != nil {
×
595
                                        stepErr = err
×
596
                                        continue
×
597
                                }
598

599
                                err = linkEnv.VerifySignature(cert)
×
600
                                if err != nil {
×
601
                                        stepErr = err
×
602
                                        continue
×
603
                                }
604

605
                                linksPerStepVerified[signerKeyID] = linkEnv
×
606
                        }
607
                }
608

609
                // Store all good links for a step
610
                stepsMetadataVerified[step.Name] = linksPerStepVerified
126✔
611

126✔
612
                if len(linksPerStepVerified) < step.Threshold {
156✔
613
                        linksPerStep := stepsMetadata[step.Name]
30✔
614
                        return nil, fmt.Errorf("step '%s' requires '%d' link metadata file(s)."+
30✔
615
                                " '%d' out of '%d' available link(s) have a valid signature from an"+
30✔
616
                                " authorized signer: %v", step.Name, step.Threshold,
30✔
617
                                len(linksPerStepVerified), len(linksPerStep), stepErr)
30✔
618
                }
30✔
619
        }
620
        return stepsMetadataVerified, nil
54✔
621
}
622

623
/*
624
LoadLinksForLayout loads for every Step of the passed Layout a Metablock
625
containing the corresponding Link.  A base path to a directory that contains
626
the links may be passed using linkDir.  Link file names are constructed,
627
using LinkNameFormat together with the corresponding step name and authorized
628
functionary key ids.  A map of link metadata is returned and has the following
629
format:
630

631
        {
632
                <step name> : {
633
                        <key id>: Metablock,
634
                        <key id>: Metablock,
635
                        ...
636
                },
637
                <step name> : {
638
                <key id>: Metablock,
639
                <key id>: Metablock,
640
                ...
641
                }
642
                ...
643
        }
644

645
If a link cannot be loaded at a constructed link name or is invalid, it is
646
ignored. Only a preliminary threshold check is performed, that is, if there
647
aren't at least Threshold links for any given step, the first return value
648
is an empty map of Metablock maps and the second return value is the error.
649
*/
650
func LoadLinksForLayout(layout Layout, linkDir string) (map[string]map[string]Metadata, error) {
54✔
651
        stepsMetadata := make(map[string]map[string]Metadata)
54✔
652

54✔
653
        for _, step := range layout.Steps {
150✔
654
                linksPerStep := make(map[string]Metadata)
96✔
655
                // Since we can verify against certificates belonging to a CA, we need to
96✔
656
                // load any possible links
96✔
657
                linkFiles, err := filepath.Glob(path.Join(linkDir, fmt.Sprintf(LinkGlobFormat, step.Name)))
96✔
658
                if err != nil {
96✔
659
                        return nil, err
×
660
                }
×
661

662
                for _, linkPath := range linkFiles {
204✔
663
                        linkEnv, err := LoadMetadata(linkPath)
108✔
664
                        if err != nil {
108✔
665
                                continue
×
666
                        }
667

668
                        // To get the full key from the metadata's signatures, we have to check
669
                        // for one with the same short id...
670
                        signerShortKeyID := strings.TrimSuffix(strings.TrimPrefix(filepath.Base(linkPath), step.Name+"."), ".link")
108✔
671
                        for _, sig := range linkEnv.Sigs() {
216✔
672
                                if strings.HasPrefix(sig.KeyID, signerShortKeyID) {
216✔
673
                                        linksPerStep[sig.KeyID] = linkEnv
108✔
674
                                        break
108✔
675
                                }
676
                        }
677
                }
678

679
                if len(linksPerStep) < step.Threshold {
102✔
680
                        return nil, fmt.Errorf("step '%s' requires '%d' link metadata file(s),"+
6✔
681
                                " found '%d'", step.Name, step.Threshold, len(linksPerStep))
6✔
682
                }
6✔
683

684
                stepsMetadata[step.Name] = linksPerStep
90✔
685
        }
686

687
        return stepsMetadata, nil
48✔
688
}
689

690
/*
691
VerifyLayoutExpiration verifies that the passed Layout has not expired.  It
692
returns an error if the (zulu) date in the Expires field is in the past.
693
*/
694
func VerifyLayoutExpiration(layout Layout) error {
54✔
695
        expires, err := time.Parse(ISO8601DateSchema, layout.Expires)
54✔
696
        if err != nil {
60✔
697
                return err
6✔
698
        }
6✔
699
        // Uses timezone of expires, i.e. UTC
700
        if time.Until(expires) < 0 {
54✔
701
                return fmt.Errorf("layout has expired on '%s'", expires)
6✔
702
        }
6✔
703
        return nil
42✔
704
}
705

706
/*
707
VerifyLayoutSignatures verifies for each key in the passed key map the
708
corresponding signature of the Layout in the passed Metablock's Signed field.
709
Signatures and keys are associated by key id.  If the key map is empty, or the
710
Metablock's Signature field does not have a signature for one or more of the
711
passed keys, or a matching signature is invalid, an error is returned.
712
*/
713
func VerifyLayoutSignatures(layoutEnv Metadata,
714
        layoutKeys map[string]Key) error {
54✔
715
        if len(layoutKeys) < 1 {
60✔
716
                return fmt.Errorf("layout verification requires at least one key")
6✔
717
        }
6✔
718

719
        for _, key := range layoutKeys {
96✔
720
                if err := layoutEnv.VerifySignature(key); err != nil {
54✔
721
                        return err
6✔
722
                }
6✔
723
        }
724
        return nil
42✔
725
}
726

727
/*
728
GetSummaryLink merges the materials of the first step (as mentioned in the
729
layout) and the products of the last step and returns a new link. This link
730
reports the materials and products and summarizes the overall software supply
731
chain.
732
NOTE: The assumption is that the steps mentioned in the layout are to be
733
performed sequentially. So, the first step mentioned in the layout denotes what
734
comes into the supply chain and the last step denotes what goes out.
735
*/
736
func GetSummaryLink(layout Layout, stepsMetadataReduced map[string]Metadata,
737
        stepName string, useDSSE bool) (Metadata, error) {
42✔
738
        var summaryLink Link
42✔
739
        if len(layout.Steps) > 0 {
84✔
740
                firstStepLink := stepsMetadataReduced[layout.Steps[0].Name]
42✔
741
                lastStepLink := stepsMetadataReduced[layout.Steps[len(layout.Steps)-1].Name]
42✔
742

42✔
743
                firstStepPayloadLink, ok := firstStepLink.GetPayload().(Link)
42✔
744
                if !ok {
42✔
745
                        return nil, fmt.Errorf("invalid metadata")
×
746
                }
×
747
                summaryLink.Materials = firstStepPayloadLink.Materials
42✔
748
                summaryLink.Name = stepName
42✔
749
                summaryLink.Type = firstStepPayloadLink.Type
42✔
750

42✔
751
                lastStepPayloadLink, ok := lastStepLink.GetPayload().(Link)
42✔
752
                if !ok {
42✔
753
                        return nil, fmt.Errorf("invalid metadata")
×
754
                }
×
755
                summaryLink.Products = lastStepPayloadLink.Products
42✔
756
                summaryLink.ByProducts = lastStepPayloadLink.ByProducts
42✔
757
                // Using the last command of the sublayout as the command
42✔
758
                // of the summary link can be misleading. Is it necessary to
42✔
759
                // include all the commands executed as part of sublayout?
42✔
760
                summaryLink.Command = lastStepPayloadLink.Command
42✔
761
        }
762

763
        if useDSSE {
54✔
764
                env := &Envelope{}
12✔
765
                if err := env.SetPayload(summaryLink); err != nil {
12✔
766
                        return nil, err
×
767
                }
×
768

769
                return env, nil
12✔
770
        }
771

772
        return &Metablock{Signed: summaryLink}, nil
30✔
773
}
774

775
/*
776
VerifySublayouts checks if any step in the supply chain is a sublayout, and if
777
so, recursively resolves it and replaces it with a summary link summarizing the
778
steps carried out in the sublayout.
779
*/
780
func VerifySublayouts(layout Layout,
781
        stepsMetadataVerified map[string]map[string]Metadata,
782
        superLayoutLinkPath string, intermediatePems [][]byte, lineNormalization bool) (map[string]map[string]Metadata, error) {
42✔
783
        for stepName, linkData := range stepsMetadataVerified {
126✔
784
                for keyID, metadata := range linkData {
168✔
785
                        if _, ok := metadata.GetPayload().(Layout); ok {
90✔
786
                                layoutKeys := make(map[string]Key)
6✔
787
                                layoutKeys[keyID] = layout.Keys[keyID]
6✔
788

6✔
789
                                sublayoutLinkDir := fmt.Sprintf(SublayoutLinkDirFormat,
6✔
790
                                        stepName, keyID)
6✔
791
                                sublayoutLinkPath := filepath.Join(superLayoutLinkPath,
6✔
792
                                        sublayoutLinkDir)
6✔
793
                                summaryLink, err := InTotoVerify(metadata, layoutKeys,
6✔
794
                                        sublayoutLinkPath, stepName, make(map[string]string), intermediatePems, lineNormalization)
6✔
795
                                if err != nil {
6✔
796
                                        return nil, err
×
797
                                }
×
798
                                linkData[keyID] = summaryLink
6✔
799
                        }
800

801
                }
802
        }
803
        return stepsMetadataVerified, nil
42✔
804
}
805

806
// TODO: find a better way than two helper functions for the replacer op
807

808
func substituteParamatersInSlice(replacer *strings.Replacer, slice []string) []string {
36✔
809
        newSlice := make([]string, 0)
36✔
810
        for _, item := range slice {
156✔
811
                newSlice = append(newSlice, replacer.Replace(item))
120✔
812
        }
120✔
813
        return newSlice
36✔
814
}
815

816
func substituteParametersInSliceOfSlices(replacer *strings.Replacer,
817
        slice [][]string) [][]string {
24✔
818
        newSlice := make([][]string, 0)
24✔
819
        for _, item := range slice {
48✔
820
                newSlice = append(newSlice, substituteParamatersInSlice(replacer,
24✔
821
                        item))
24✔
822
        }
24✔
823
        return newSlice
24✔
824
}
825

826
/*
827
SubstituteParameters performs parameter substitution in steps and inspections
828
in the following fields:
829
- Expected Materials and Expected Products of both
830
- Run of inspections
831
- Expected Command of steps
832
The substitution marker is '{}' and the keyword within the braces is replaced
833
by a value found in the substitution map passed, parameterDictionary. The
834
layout with parameters substituted is returned to the calling function.
835
*/
836
func SubstituteParameters(layout Layout,
837
        parameterDictionary map[string]string) (Layout, error) {
48✔
838

48✔
839
        if len(parameterDictionary) == 0 {
84✔
840
                return layout, nil
36✔
841
        }
36✔
842

843
        parameters := make([]string, 0)
12✔
844

12✔
845
        re := regexp.MustCompile("^[a-zA-Z0-9_-]+$")
12✔
846

12✔
847
        for parameter, value := range parameterDictionary {
48✔
848
                parameterFormatCheck := re.MatchString(parameter)
36✔
849
                if !parameterFormatCheck {
42✔
850
                        return layout, fmt.Errorf("invalid format for parameter")
6✔
851
                }
6✔
852

853
                parameters = append(parameters, "{"+parameter+"}")
30✔
854
                parameters = append(parameters, value)
30✔
855
        }
856

857
        replacer := strings.NewReplacer(parameters...)
6✔
858

6✔
859
        for i := range layout.Steps {
12✔
860
                layout.Steps[i].ExpectedMaterials = substituteParametersInSliceOfSlices(
6✔
861
                        replacer, layout.Steps[i].ExpectedMaterials)
6✔
862
                layout.Steps[i].ExpectedProducts = substituteParametersInSliceOfSlices(
6✔
863
                        replacer, layout.Steps[i].ExpectedProducts)
6✔
864
                layout.Steps[i].ExpectedCommand = substituteParamatersInSlice(replacer,
6✔
865
                        layout.Steps[i].ExpectedCommand)
6✔
866
        }
6✔
867

868
        for i := range layout.Inspect {
12✔
869
                layout.Inspect[i].ExpectedMaterials =
6✔
870
                        substituteParametersInSliceOfSlices(replacer,
6✔
871
                                layout.Inspect[i].ExpectedMaterials)
6✔
872
                layout.Inspect[i].ExpectedProducts =
6✔
873
                        substituteParametersInSliceOfSlices(replacer,
6✔
874
                                layout.Inspect[i].ExpectedProducts)
6✔
875
                layout.Inspect[i].Run = substituteParamatersInSlice(replacer,
6✔
876
                        layout.Inspect[i].Run)
6✔
877
        }
6✔
878

879
        return layout, nil
6✔
880
}
881

882
/*
883
InTotoVerify can be used to verify an entire software supply chain according to
884
the in-toto specification.  It requires the metadata of the root layout, a map
885
that contains public keys to verify the root layout signatures, a path to a
886
directory from where it can load link metadata files, which are treated as
887
signed evidence for the steps defined in the layout, a step name, and a
888
paramater dictionary used for parameter substitution. The step name only
889
matters for sublayouts, where it's important to associate the summary of that
890
step with a unique name. The verification routine is as follows:
891

892
1. Verify layout signature(s) using passed key(s)
893
2. Verify layout expiration date
894
3. Substitute parameters in layout
895
4. Load link metadata files for steps of layout
896
5. Verify signatures and signature thresholds for steps of layout
897
6. Verify sublayouts recursively
898
7. Verify command alignment for steps of layout (only warns)
899
8. Verify artifact rules for steps of layout
900
9. Execute inspection commands (generates link metadata for each inspection)
901
10. Verify artifact rules for inspections of layout
902

903
InTotoVerify returns a summary link wrapped in a Metablock object and an error
904
value. If any of the verification routines fail, verification is aborted and
905
error is returned. In such an instance, the first value remains an empty
906
Metablock object.
907

908
NOTE: Artifact rules of type "create", "modify"
909
and "delete" are currently not supported.
910
*/
911
func InTotoVerify(layoutEnv Metadata, layoutKeys map[string]Key,
912
        linkDir string, stepName string, parameterDictionary map[string]string, intermediatePems [][]byte, lineNormalization bool) (
913
        Metadata, error) {
30✔
914

30✔
915
        // Verify root signatures
30✔
916
        if err := VerifyLayoutSignatures(layoutEnv, layoutKeys); err != nil {
30✔
917
                return nil, err
×
918
        }
×
919

920
        useDSSE := false
30✔
921
        if _, ok := layoutEnv.(*Envelope); ok {
42✔
922
                useDSSE = true
12✔
923
        }
12✔
924

925
        // Extract the layout from its Metadata container (for further processing)
926
        layout, ok := layoutEnv.GetPayload().(Layout)
30✔
927
        if !ok {
30✔
928
                return nil, ErrNotLayout
×
929
        }
×
930

931
        // Verify layout expiration
932
        if err := VerifyLayoutExpiration(layout); err != nil {
30✔
933
                return nil, err
×
934
        }
×
935

936
        // Substitute parameters in layout
937
        layout, err := SubstituteParameters(layout, parameterDictionary)
30✔
938
        if err != nil {
30✔
939
                return nil, err
×
940
        }
×
941

942
        rootCertPool, intermediateCertPool, err := LoadLayoutCertificates(layout, intermediatePems)
30✔
943
        if err != nil {
30✔
944
                return nil, err
×
945
        }
×
946

947
        // Load links for layout
948
        stepsMetadata, err := LoadLinksForLayout(layout, linkDir)
30✔
949
        if err != nil {
30✔
950
                return nil, err
×
951
        }
×
952

953
        // Verify link signatures
954
        stepsMetadataVerified, err := VerifyLinkSignatureThesholds(layout,
30✔
955
                stepsMetadata, rootCertPool, intermediateCertPool)
30✔
956
        if err != nil {
30✔
957
                return nil, err
×
958
        }
×
959

960
        // Verify and resolve sublayouts
961
        stepsSublayoutVerified, err := VerifySublayouts(layout,
30✔
962
                stepsMetadataVerified, linkDir, intermediatePems, lineNormalization)
30✔
963
        if err != nil {
30✔
964
                return nil, err
×
965
        }
×
966

967
        // Verify command alignment (WARNING only)
968
        VerifyStepCommandAlignment(layout, stepsSublayoutVerified)
30✔
969

30✔
970
        // Given that signature thresholds have been checked above and the rest of
30✔
971
        // the relevant link properties, i.e. materials and products, have to be
30✔
972
        // exactly equal, we can reduce the map of steps metadata. However, we error
30✔
973
        // if the relevant properties are not equal among links of a step.
30✔
974
        stepsMetadataReduced, err := ReduceStepsMetadata(layout,
30✔
975
                stepsSublayoutVerified)
30✔
976
        if err != nil {
30✔
977
                return nil, err
×
978
        }
×
979

980
        // Verify artifact rules
981
        if err = VerifyArtifacts(layout.stepsAsInterfaceSlice(),
30✔
982
                stepsMetadataReduced); err != nil {
30✔
983
                return nil, err
×
984
        }
×
985

986
        inspectionMetadata, err := RunInspections(layout, "", lineNormalization, useDSSE)
30✔
987
        if err != nil {
30✔
988
                return nil, err
×
989
        }
×
990

991
        // Add steps metadata to inspection metadata, because inspection artifact
992
        // rules may also refer to artifacts reported by step links
993
        for k, v := range stepsMetadataReduced {
96✔
994
                inspectionMetadata[k] = v
66✔
995
        }
66✔
996

997
        if err = VerifyArtifacts(layout.inspectAsInterfaceSlice(),
30✔
998
                inspectionMetadata); err != nil {
30✔
999
                return nil, err
×
1000
        }
×
1001

1002
        summaryLink, err := GetSummaryLink(layout, stepsMetadataReduced, stepName, useDSSE)
30✔
1003
        if err != nil {
30✔
1004
                return nil, err
×
1005
        }
×
1006

1007
        return summaryLink, nil
30✔
1008
}
1009

1010
/*
1011
InTotoVerifyWithDirectory provides the same functionality as InTotoVerify, but
1012
adds the possibility to select a local directory from where the inspections are run.
1013
*/
1014
func InTotoVerifyWithDirectory(layoutEnv Metadata, layoutKeys map[string]Key,
1015
        linkDir string, runDir string, stepName string, parameterDictionary map[string]string, intermediatePems [][]byte, lineNormalization bool) (
1016
        Metadata, error) {
6✔
1017

6✔
1018
        // runDir sanity checks
6✔
1019
        // check if path exists
6✔
1020
        info, err := os.Stat(runDir)
6✔
1021
        if err != nil {
6✔
1022
                return nil, err
×
1023
        }
×
1024

1025
        // check if runDir is a symlink
1026
        if info.Mode()&os.ModeSymlink == os.ModeSymlink {
6✔
1027
                return nil, ErrInspectionRunDirIsSymlink
×
1028
        }
×
1029

1030
        // check if runDir is writable and a directory
1031
        err = isWritable(runDir)
6✔
1032
        if err != nil {
6✔
1033
                return nil, err
×
1034
        }
×
1035

1036
        // check if runDir is empty (we do not want to overwrite files)
1037
        // We abuse File.Readdirnames for this action.
1038
        f, err := os.Open(runDir)
6✔
1039
        if err != nil {
6✔
1040
                return nil, err
×
1041
        }
×
1042
        defer f.Close()
6✔
1043
        // We use Readdirnames(1) for performance reasons, one child node
6✔
1044
        // is enough to proof that the directory is not empty
6✔
1045
        _, err = f.Readdirnames(1)
6✔
1046
        // if io.EOF gets returned as error the directory is empty
6✔
1047
        if err == io.EOF {
6✔
1048
                return nil, err
×
1049
        }
×
1050
        err = f.Close()
6✔
1051
        if err != nil {
6✔
1052
                return nil, err
×
1053
        }
×
1054

1055
        // Verify root signatures
1056
        if err := VerifyLayoutSignatures(layoutEnv, layoutKeys); err != nil {
6✔
1057
                return nil, err
×
1058
        }
×
1059

1060
        useDSSE := false
6✔
1061
        if _, ok := layoutEnv.(*Envelope); ok {
6✔
1062
                useDSSE = true
×
1063
        }
×
1064

1065
        // Extract the layout from its Metadata container (for further processing)
1066
        layout, ok := layoutEnv.GetPayload().(Layout)
6✔
1067
        if !ok {
6✔
1068
                return nil, ErrNotLayout
×
1069
        }
×
1070

1071
        // Verify layout expiration
1072
        if err := VerifyLayoutExpiration(layout); err != nil {
6✔
1073
                return nil, err
×
1074
        }
×
1075

1076
        // Substitute parameters in layout
1077
        layout, err = SubstituteParameters(layout, parameterDictionary)
6✔
1078
        if err != nil {
6✔
1079
                return nil, err
×
1080
        }
×
1081

1082
        rootCertPool, intermediateCertPool, err := LoadLayoutCertificates(layout, intermediatePems)
6✔
1083
        if err != nil {
6✔
1084
                return nil, err
×
1085
        }
×
1086

1087
        // Load links for layout
1088
        stepsMetadata, err := LoadLinksForLayout(layout, linkDir)
6✔
1089
        if err != nil {
6✔
1090
                return nil, err
×
1091
        }
×
1092

1093
        // Verify link signatures
1094
        stepsMetadataVerified, err := VerifyLinkSignatureThesholds(layout,
6✔
1095
                stepsMetadata, rootCertPool, intermediateCertPool)
6✔
1096
        if err != nil {
6✔
1097
                return nil, err
×
1098
        }
×
1099

1100
        // Verify and resolve sublayouts
1101
        stepsSublayoutVerified, err := VerifySublayouts(layout,
6✔
1102
                stepsMetadataVerified, linkDir, intermediatePems, lineNormalization)
6✔
1103
        if err != nil {
6✔
1104
                return nil, err
×
1105
        }
×
1106

1107
        // Verify command alignment (WARNING only)
1108
        VerifyStepCommandAlignment(layout, stepsSublayoutVerified)
6✔
1109

6✔
1110
        // Given that signature thresholds have been checked above and the rest of
6✔
1111
        // the relevant link properties, i.e. materials and products, have to be
6✔
1112
        // exactly equal, we can reduce the map of steps metadata. However, we error
6✔
1113
        // if the relevant properties are not equal among links of a step.
6✔
1114
        stepsMetadataReduced, err := ReduceStepsMetadata(layout,
6✔
1115
                stepsSublayoutVerified)
6✔
1116
        if err != nil {
6✔
1117
                return nil, err
×
1118
        }
×
1119

1120
        // Verify artifact rules
1121
        if err = VerifyArtifacts(layout.stepsAsInterfaceSlice(),
6✔
1122
                stepsMetadataReduced); err != nil {
6✔
1123
                return nil, err
×
1124
        }
×
1125

1126
        inspectionMetadata, err := RunInspections(layout, runDir, lineNormalization, useDSSE)
6✔
1127
        if err != nil {
6✔
1128
                return nil, err
×
1129
        }
×
1130

1131
        // Add steps metadata to inspection metadata, because inspection artifact
1132
        // rules may also refer to artifacts reported by step links
1133
        for k, v := range stepsMetadataReduced {
18✔
1134
                inspectionMetadata[k] = v
12✔
1135
        }
12✔
1136

1137
        if err = VerifyArtifacts(layout.inspectAsInterfaceSlice(),
6✔
1138
                inspectionMetadata); err != nil {
6✔
1139
                return nil, err
×
1140
        }
×
1141

1142
        summaryLink, err := GetSummaryLink(layout, stepsMetadataReduced, stepName, useDSSE)
6✔
1143
        if err != nil {
6✔
1144
                return nil, err
×
1145
        }
×
1146

1147
        return summaryLink, nil
6✔
1148
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc