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

redhat-openshift-ecosystem / openshift-preflight / 22185704777

19 Feb 2026 02:24PM UTC coverage: 83.333% (-0.7%) from 84.053%
22185704777

push

github

acornett21
engine: support filter/allowlist for untar

Signed-off-by: Caleb Xu <caxu@redhat.com>

145 of 168 new or added lines in 2 files covered. (86.31%)

127 existing lines in 23 files now uncovered.

5185 of 6222 relevant lines covered (83.33%)

164.92 hits per line

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

80.51
/internal/engine/engine.go
1
package engine
2

3
import (
4
        "bytes"
5
        "context"
6
        "crypto/md5"
7
        "encoding/json"
8
        "fmt"
9
        "io"
10
        "io/fs"
11
        "maps"
12
        "net/http"
13
        "os"
14
        "os/exec"
15
        "path"
16
        "regexp"
17
        "slices"
18
        "strings"
19
        "time"
20

21
        "github.com/go-logr/logr"
22

23
        "github.com/redhat-openshift-ecosystem/openshift-preflight/artifacts"
24
        "github.com/redhat-openshift-ecosystem/openshift-preflight/certification"
25
        "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/check"
26
        "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/image"
27
        "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/log"
28
        "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/openshift"
29
        "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/operatorsdk"
30
        "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/option"
31
        "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/policy"
32
        containerpol "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/policy/container"
33
        operatorpol "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/policy/operator"
34
        "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/pyxis"
35
        "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/rpm"
36
        "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/runtime"
37

38
        "github.com/google/go-containerregistry/pkg/crane"
39
        "github.com/google/go-containerregistry/pkg/name"
40
        "github.com/google/go-containerregistry/pkg/v1/cache"
41
)
42

43
// New creates a new CraneEngine from the passed params
44
func New(ctx context.Context,
45
        checks []check.Check,
46
        kubeconfig []byte,
47
        cfg runtime.Config,
48
) (craneEngine, error) {
1✔
49
        return craneEngine{
1✔
50
                kubeconfig:         kubeconfig,
1✔
51
                dockerConfig:       cfg.DockerConfig,
1✔
52
                image:              cfg.Image,
1✔
53
                checks:             checks,
1✔
54
                isBundle:           cfg.Bundle,
1✔
55
                isScratch:          cfg.Scratch,
1✔
56
                platform:           cfg.Platform,
1✔
57
                insecure:           cfg.Insecure,
1✔
58
                manifestListDigest: cfg.ManifestListDigest,
1✔
59
        }, nil
1✔
60
}
1✔
61

62
// CraneEngine implements a certification.CheckEngine, and leverage crane to interact with
63
// the container registry and target image.
64
type craneEngine struct {
65
        // Kubeconfig is a byte slice containing a valid Kubeconfig to be used by checks.
66
        kubeconfig []byte
67
        // DockerConfig is the credential required to pull the image.
68
        dockerConfig string
69
        // Image is what is being tested, and should contain the
70
        // fully addressable path (including registry, namespaces, etc)
71
        // to the image
72
        image string
73
        // Checks is an array of all checks to be executed against
74
        // the image provided.
75
        checks []check.Check
76
        // Platform is the container platform to use. E.g. amd64.
77
        platform string
78

79
        // IsBundle is an indicator that the asset is a bundle.
80
        isBundle bool
81

82
        // IsScratch is an indicator that the asset is a scratch image
83
        isScratch bool
84

85
        // Insecure controls whether to allow an insecure connection to
86
        // the registry crane connects with.
87
        insecure bool
88

89
        // ManifestListDigest is the sha256 digest for the manifest list
90
        manifestListDigest string
91

92
        imageRef image.ImageReference
93
        results  certification.Results
94
}
95

96
func (c *craneEngine) CranePlatform() string {
6✔
97
        return c.platform
6✔
98
}
6✔
99

100
func (c *craneEngine) CraneDockerConfig() string {
6✔
101
        return c.dockerConfig
6✔
102
}
6✔
103

104
func (c *craneEngine) CraneInsecure() bool {
6✔
105
        return c.insecure
6✔
106
}
6✔
107

108
var _ option.CraneConfig = &craneEngine{}
109

110
func (c *craneEngine) ExecuteChecks(ctx context.Context) error {
6✔
111
        logger := logr.FromContextOrDiscard(ctx)
6✔
112
        logger.Info("target image", "image", c.image)
6✔
113

6✔
114
        // pull the image and save to fs
6✔
115
        logger.V(log.DBG).Info("pulling image from target registry")
6✔
116
        options := option.GenerateCraneOptions(ctx, c)
6✔
117
        img, err := crane.Pull(c.image, options...)
6✔
118
        if err != nil {
7✔
119
                return fmt.Errorf("failed to pull remote container: %v", err)
1✔
120
        }
1✔
121

122
        // create tmpdir to receive extracted fs
123
        tmpdir, err := os.MkdirTemp(os.TempDir(), "preflight-*")
5✔
124
        if err != nil {
5✔
125
                return fmt.Errorf("failed to create temporary directory: %v", err)
×
126
        }
×
127
        logger.V(log.DBG).Info("created temporary directory", "path", tmpdir)
5✔
128
        defer func() {
10✔
129
                if err := os.RemoveAll(tmpdir); err != nil {
5✔
130
                        logger.Error(err, "unable to clean up tmpdir", "tempDir", tmpdir)
×
131
                }
×
132
        }()
133

134
        imageTarPath := path.Join(tmpdir, "cache")
5✔
135
        if err := os.Mkdir(imageTarPath, 0o755); err != nil {
5✔
136
                return fmt.Errorf("failed to create cache directory: %s: %v", imageTarPath, err)
×
137
        }
×
138

139
        img = cache.Image(img, cache.NewFilesystemCache(imageTarPath))
5✔
140

5✔
141
        containerFSPath := path.Join(tmpdir, "fs")
5✔
142
        if err := os.Mkdir(containerFSPath, 0o755); err != nil {
5✔
143
                return fmt.Errorf("failed to create container expansion directory: %s: %v", containerFSPath, err)
×
144
        }
×
145

146
        requiredFilePatternsCount := 0
5✔
147
        for _, check := range c.checks {
40✔
148
                requiredFilePatternsCount += len(check.RequiredFilePatterns())
35✔
149
        }
35✔
150

151
        requiredFilePatterns := make([]string, 0, requiredFilePatternsCount)
5✔
152
        for _, check := range c.checks {
40✔
153
                requiredFilePatterns = append(requiredFilePatterns, check.RequiredFilePatterns()...)
35✔
154
        }
35✔
155
        for i, pattern := range requiredFilePatterns {
5✔
NEW
156
                requiredFilePatterns[i] = strings.TrimLeft(pattern, "/")
×
NEW
157
        }
×
158

159
        slices.Sort(requiredFilePatterns)
5✔
160
        requiredFilePatterns = slices.Compact(requiredFilePatterns)
5✔
161

5✔
162
        if err := untar(ctx, containerFSPath, img, requiredFilePatterns); err != nil {
6✔
163
                return err
1✔
164
        }
1✔
165

166
        reference, err := name.ParseReference(c.image)
4✔
167
        if err != nil {
4✔
168
                return fmt.Errorf("image uri could not be parsed: %v", err)
×
169
        }
×
170

171
        // store the image internals in the engine image reference to pass to validations.
172
        c.imageRef = image.ImageReference{
4✔
173
                ImageURI:           c.image,
4✔
174
                ImageFSPath:        containerFSPath,
4✔
175
                ImageInfo:          img,
4✔
176
                ImageRegistry:      reference.Context().RegistryStr(),
4✔
177
                ImageRepository:    reference.Context().RepositoryStr(),
4✔
178
                ImageTagOrSha:      reference.Identifier(),
4✔
179
                ManifestListDigest: c.manifestListDigest,
4✔
180
        }
4✔
181

4✔
182
        if err := writeCertImage(ctx, c.imageRef); err != nil {
4✔
183
                return fmt.Errorf("could not write cert image: %v", err)
×
184
        }
×
185

186
        if !c.isScratch {
8✔
187
                if err := writeRPMManifest(ctx, containerFSPath); err != nil {
4✔
188
                        return fmt.Errorf("could not write rpm manifest: %v", err)
×
189
                }
×
190
        }
191

192
        if c.isBundle {
7✔
193
                // Record test cluster version
3✔
194
                version, err := openshift.GetOpenshiftClusterVersion(ctx, c.kubeconfig)
3✔
195
                if err != nil {
6✔
196
                        logger.Error(err, "could not determine test cluster version")
3✔
197
                }
3✔
198
                c.results.TestedOn = version
3✔
199
        } else {
1✔
200
                logger.V(log.DBG).Info("Container checks do not require a cluster. skipping cluster version check.")
1✔
201
                c.results.TestedOn = runtime.UnknownOpenshiftClusterVersion()
1✔
202
        }
1✔
203

204
        // execute checks
205
        logger.V(log.DBG).Info("executing checks")
4✔
206
        for _, executedCheck := range c.checks {
32✔
207
                logger := logger.WithValues("check", executedCheck.Name())
28✔
208
                ctx := logr.NewContext(ctx, logger)
28✔
209
                c.results.TestedImage = c.image
28✔
210

28✔
211
                logger.V(log.DBG).Info("running check")
28✔
212
                if executedCheck.Metadata().Level == check.LevelOptional || executedCheck.Metadata().Level == check.LevelWarn {
44✔
213
                        logger.Info(fmt.Sprintf("Check %s is not currently being enforced.", executedCheck.Name()))
16✔
214
                }
16✔
215

216
                // run the validation
217
                checkStartTime := time.Now()
28✔
218
                checkPassed, err := executedCheck.Validate(ctx, c.imageRef)
28✔
219
                checkElapsedTime := time.Since(checkStartTime)
28✔
220

28✔
221
                if err != nil {
36✔
222
                        logger.WithValues("result", "ERROR", "err", err.Error()).Info("check completed")
8✔
223
                        result := certification.Result{Check: executedCheck, ElapsedTime: checkElapsedTime}
8✔
224
                        c.results.Errors = appendUnlessOptional(c.results.Errors, *result.WithError(err))
8✔
225
                        continue
8✔
226
                }
227

228
                if !checkPassed {
28✔
229
                        // if a test doesn't pass but is of level warn include it in warning results, instead of failed results
8✔
230
                        if executedCheck.Metadata().Level == check.LevelWarn {
12✔
231
                                logger.WithValues("result", "WARNING").Info("check completed")
4✔
232
                                c.results.Warned = appendUnlessOptional(c.results.Warned, certification.Result{Check: executedCheck, ElapsedTime: checkElapsedTime})
4✔
233
                                continue
4✔
234
                        }
235
                        logger.WithValues("result", "FAILED").Info("check completed")
4✔
236
                        c.results.Failed = appendUnlessOptional(c.results.Failed, certification.Result{Check: executedCheck, ElapsedTime: checkElapsedTime})
4✔
237
                        continue
4✔
238
                }
239

240
                logger.WithValues("result", "PASSED").Info("check completed")
12✔
241
                c.results.Passed = appendUnlessOptional(c.results.Passed, certification.Result{Check: executedCheck, ElapsedTime: checkElapsedTime})
12✔
242
        }
243

244
        if len(c.results.Errors) > 0 || len(c.results.Failed) > 0 {
8✔
245
                c.results.PassedOverall = false
4✔
246
        } else {
4✔
247
                c.results.PassedOverall = true
×
248
        }
×
249

250
        if c.isBundle { // for operators:
7✔
251
                // hash the contents of the bundle.
3✔
252
                md5sum, err := generateBundleHash(ctx, c.imageRef.ImageFSPath)
3✔
253
                if err != nil {
3✔
254
                        logger.Error(err, "could not generate bundle hash")
×
255
                }
×
256
                c.results.CertificationHash = md5sum
3✔
257
        } else { // for containers:
1✔
258
                // Inform the user about the sha/tag binding.
1✔
259

1✔
260
                // By this point, we should have already resolved the digest so
1✔
261
                // we don't handle this error, but fail safe and don't log a potentially
1✔
262
                // incorrect line message to the user.
1✔
263
                if resolvedDigest, err := c.imageRef.ImageInfo.Digest(); err == nil {
2✔
264
                        msg, warn := tagDigestBindingInfo(c.imageRef.ImageTagOrSha, resolvedDigest.String())
1✔
265
                        if warn {
1✔
266
                                logger.Info(fmt.Sprintf("Warning: %s", msg))
×
267
                        } else {
1✔
268
                                logger.Info(msg)
1✔
269
                        }
1✔
270
                }
271
        }
272

273
        return nil
4✔
274
}
275

276
func appendUnlessOptional(results []certification.Result, result certification.Result) []certification.Result {
28✔
277
        if result.Check.Metadata().Level == "optional" {
36✔
278
                return results
8✔
279
        }
8✔
280
        return append(results, result)
20✔
281
}
282

283
// tagDigestBindingInfo emits a log line describing tag and digest binding semantics.
284
// The providedIdentifer is the tag or digest of the image as the user gave it at the commandline.
285
// resolvedDigest
286
func tagDigestBindingInfo(providedIdentifier string, resolvedDigest string) (msg string, warn bool) {
3✔
287
        if strings.HasPrefix(providedIdentifier, "sha256:") {
4✔
288
                return "You've provided an image by digest. " +
1✔
289
                                "When submitting this image to Red Hat for certification, " +
1✔
290
                                "no tag will be associated with this image. " +
1✔
291
                                "If you would like to associate a tag with this image, " +
1✔
292
                                "please rerun this tool replacing your image reference with a tag.",
1✔
293
                        true
1✔
294
        }
1✔
295

296
        return fmt.Sprintf(
2✔
297
                `This image's tag %s will be paired with digest %s `+
2✔
298
                        `once this image has been published in accordance `+
2✔
299
                        `with Red Hat Certification policy. `+
2✔
300
                        `You may then add or remove any supplemental tags `+
2✔
301
                        `through your Red Hat Connect portal as you see fit.`,
2✔
302
                providedIdentifier, resolvedDigest,
2✔
303
        ), false
2✔
304
}
305

306
func generateBundleHash(ctx context.Context, bundlePath string) (string, error) {
3✔
307
        logger := logr.FromContextOrDiscard(ctx)
3✔
308
        files := make(map[string]string)
3✔
309
        fileSystem := os.DirFS(bundlePath)
3✔
310

3✔
311
        hashBuffer := bytes.Buffer{}
3✔
312

3✔
313
        _ = fs.WalkDir(fileSystem, ".", func(path string, d fs.DirEntry, err error) error {
6✔
314
                if err != nil {
3✔
315
                        return fmt.Errorf("could not read bundle directory: %s: %w", path, err)
×
316
                }
×
317
                if d.Name() == "Dockerfile" {
3✔
318
                        return nil
×
319
                }
×
320
                if d.IsDir() {
6✔
321
                        return nil
3✔
322
                }
3✔
UNCOV
323
                filebytes, err := fs.ReadFile(fileSystem, path)
×
UNCOV
324
                if err != nil {
×
UNCOV
325
                        return fmt.Errorf("could not read file: %s: %w", path, err)
×
UNCOV
326
                }
×
UNCOV
327
                md5sum := fmt.Sprintf("%x", md5.Sum(filebytes))
×
UNCOV
328
                files[md5sum] = fmt.Sprintf("./%s", path)
×
UNCOV
329
                return nil
×
330
        })
331

332
        keys := slices.Collect(maps.Keys(files))
3✔
333
        slices.Sort(keys)
3✔
334

3✔
335
        for _, k := range keys {
3✔
UNCOV
336
                hashBuffer.WriteString(fmt.Sprintf("%s  %s\n", k, files[k]))
×
UNCOV
337
        }
×
338

339
        artifactsWriter := artifacts.WriterFromContext(ctx)
3✔
340
        if artifactsWriter != nil {
6✔
341
                _, err := artifactsWriter.WriteFile("hashes.txt", &hashBuffer)
3✔
342
                if err != nil {
3✔
343
                        return "", fmt.Errorf("could not write hash file to artifacts dir: %w", err)
×
344
                }
×
345
        }
346

347
        sum := fmt.Sprintf("%x", md5.Sum(hashBuffer.Bytes()))
3✔
348

3✔
349
        logger.V(log.DBG).Info("md5 sum", "md5sum", sum)
3✔
350

3✔
351
        return sum, nil
3✔
352
}
353

354
// Results will return the results of check execution.
355
func (c *craneEngine) Results(ctx context.Context) certification.Results {
×
356
        return c.results
×
357
}
×
358

359
// writeCertImage takes imageRef and writes it to disk as JSON representing a pyxis.CertImage
360
// struct. The file is written at path certification.DefaultCertImageFilename.
361
//
362
//nolint:unparam // ctx is unused. Keep for future use.
363
func writeCertImage(ctx context.Context, imageRef image.ImageReference) error {
4✔
364
        logger := logr.FromContextOrDiscard(ctx)
4✔
365

4✔
366
        config, err := imageRef.ImageInfo.ConfigFile()
4✔
367
        if err != nil {
4✔
368
                return fmt.Errorf("failed to get image config file: %w", err)
×
369
        }
×
370

371
        manifest, err := imageRef.ImageInfo.Manifest()
4✔
372
        if err != nil {
4✔
373
                return fmt.Errorf("failed to get image manifest: %w", err)
×
374
        }
×
375

376
        digest, err := imageRef.ImageInfo.Digest()
4✔
377
        if err != nil {
4✔
378
                return fmt.Errorf("failed to get image digest: %w", err)
×
379
        }
×
380

381
        rawConfig, err := imageRef.ImageInfo.RawConfigFile()
4✔
382
        if err != nil {
4✔
383
                return fmt.Errorf("failed to image raw config file: %w", err)
×
384
        }
×
385

386
        size, err := imageRef.ImageInfo.Size()
4✔
387
        if err != nil {
4✔
388
                return fmt.Errorf("failed to get image size: %w", err)
×
389
        }
×
390

391
        labels := convertLabels(config.Config.Labels)
4✔
392
        layerSizes := make([]pyxis.Layer, 0, len(config.RootFS.DiffIDs))
4✔
393
        for _, diffid := range config.RootFS.DiffIDs {
16✔
394
                layer, err := imageRef.ImageInfo.LayerByDiffID(diffid)
12✔
395
                if err != nil {
12✔
396
                        return fmt.Errorf("could not get layer by diff id: %w", err)
×
397
                }
×
398

399
                written, err := func() (int64, error) {
24✔
400
                        uncompressed, err := layer.Uncompressed()
12✔
401
                        if err != nil {
12✔
402
                                return 0, fmt.Errorf("could not get uncompressed layer: %w", err)
×
403
                        }
×
404
                        defer uncompressed.Close()
12✔
405

12✔
406
                        written, err := io.Copy(io.Discard, uncompressed)
12✔
407
                        if err != nil {
12✔
408
                                return written, fmt.Errorf("could not copy from layer: %w", err)
×
409
                        }
×
410

411
                        return written, nil
12✔
412
                }()
413
                if err != nil {
12✔
414
                        return err
×
415
                }
×
416

417
                pyxisLayer := pyxis.Layer{
12✔
418
                        LayerID: diffid.String(),
12✔
419
                        Size:    written,
12✔
420
                }
12✔
421
                layerSizes = append(layerSizes, pyxisLayer)
12✔
422
        }
423

424
        manifestLayers := make([]string, 0, len(manifest.Layers))
4✔
425

4✔
426
        // CertImage expects the layers to be stored in the order from base to top.
4✔
427
        // Index 0 is the base layer, and the last index is the top layer.
4✔
428
        for _, layer := range slices.Backward(manifest.Layers) {
16✔
429
                manifestLayers = append(manifestLayers, layer.Digest.String())
12✔
430
        }
12✔
431

432
        sumLayersSizeBytes := sumLayerSizeBytes(layerSizes)
4✔
433

4✔
434
        addedDate := time.Now().UTC().Format(time.RFC3339)
4✔
435

4✔
436
        tags := make([]pyxis.Tag, 0, 1)
4✔
437
        tags = append(tags, pyxis.Tag{
4✔
438
                AddedDate: addedDate,
4✔
439
                Name:      imageRef.ImageTagOrSha,
4✔
440
        })
4✔
441

4✔
442
        repositories := make([]pyxis.Repository, 0, 1)
4✔
443
        repositories = append(repositories, pyxis.Repository{
4✔
444
                PushDate:           addedDate,
4✔
445
                Registry:           imageRef.ImageRegistry,
4✔
446
                Repository:         imageRef.ImageRepository,
4✔
447
                Tags:               tags,
4✔
448
                ManifestListDigest: imageRef.ManifestListDigest,
4✔
449
        })
4✔
450

4✔
451
        certImage := pyxis.CertImage{
4✔
452
                DockerImageDigest: digest.String(),
4✔
453
                DockerImageID:     manifest.Config.Digest.String(),
4✔
454
                ImageID:           digest.String(),
4✔
455
                Architecture:      config.Architecture,
4✔
456
                ParsedData: &pyxis.ParsedData{
4✔
457
                        Architecture:           config.Architecture,
4✔
458
                        Command:                strings.Join(config.Config.Cmd, " "),
4✔
459
                        Created:                config.Created.String(),
4✔
460
                        ImageID:                digest.String(),
4✔
461
                        Labels:                 labels,
4✔
462
                        Layers:                 manifestLayers,
4✔
463
                        OS:                     config.OS,
4✔
464
                        Size:                   size,
4✔
465
                        UncompressedLayerSizes: layerSizes,
4✔
466
                },
4✔
467
                RawConfig:         string(rawConfig),
4✔
468
                Repositories:      repositories,
4✔
469
                SumLayerSizeBytes: sumLayersSizeBytes,
4✔
470
                // This is an assumption that the DiffIDs are in order from base up.
4✔
471
                // Need more evidence that this is always the case.
4✔
472
                UncompressedTopLayerID: config.RootFS.DiffIDs[0].String(),
4✔
473
        }
4✔
474

4✔
475
        // calling MarshalIndent so the json file written to disk is human-readable when opened
4✔
476
        certImageJSON, err := json.MarshalIndent(certImage, "", "    ")
4✔
477
        if err != nil {
4✔
478
                return fmt.Errorf("could not marshal cert image: %w", err)
×
479
        }
×
480

481
        artifactWriter := artifacts.WriterFromContext(ctx)
4✔
482
        if artifactWriter != nil {
8✔
483
                fileName, err := artifactWriter.WriteFile(check.DefaultCertImageFilename, bytes.NewReader(certImageJSON))
4✔
484
                if err != nil {
4✔
485
                        return fmt.Errorf("failed to save file to artifacts directory: %w", err)
×
486
                }
×
487

488
                logger.V(log.TRC).Info("image config written to disk", "filename", fileName)
4✔
489
        }
490

491
        return nil
4✔
492
}
493

494
func getBgName(srcrpm string) string {
3✔
495
        parts := strings.Split(srcrpm, "-")
3✔
496
        return strings.Join(parts[0:len(parts)-2], "-")
3✔
497
}
3✔
498

499
func writeRPMManifest(ctx context.Context, containerFSPath string) error {
4✔
500
        logger := logr.FromContextOrDiscard(ctx)
4✔
501
        pkgList, err := rpm.GetPackageList(ctx, containerFSPath)
4✔
502
        if err != nil {
8✔
503
                logger.Error(err, "could not get rpm list, continuing without it")
4✔
504
        }
4✔
505

506
        // covert rpm struct to pxyis struct
507
        rpms := make([]pyxis.RPM, 0, len(pkgList))
4✔
508
        rpmSuffixRegexp, err := regexp.Compile("(-[0-9].*)")
4✔
509
        if err != nil {
4✔
510
                return fmt.Errorf("error while compiling regexp: %w", err)
×
511
        }
×
512
        pgpKeyIdRegexp, err := regexp.Compile(".*, Key ID (.*)")
4✔
513
        if err != nil {
4✔
514
                return fmt.Errorf("error while compiling regexp: %w", err)
×
515
        }
×
516
        for _, packageInfo := range pkgList {
4✔
517
                var bgName, endChop, srpmNevra, pgpKeyID string
×
518

×
519
                // accounting for the fact that not all packages have a source rpm
×
520
                if len(packageInfo.SourceRpm) > 0 {
×
521
                        bgName = getBgName(packageInfo.SourceRpm)
×
522
                        endChop = strings.TrimPrefix(strings.TrimSuffix(rpmSuffixRegexp.FindString(packageInfo.SourceRpm), ".rpm"), "-")
×
523

×
524
                        srpmNevra = fmt.Sprintf("%s-%d:%s", bgName, packageInfo.Epoch, endChop)
×
525
                }
×
526

527
                if len(packageInfo.PGP) > 0 {
×
528
                        matches := pgpKeyIdRegexp.FindStringSubmatch(packageInfo.PGP)
×
529
                        if matches != nil {
×
530
                                pgpKeyID = matches[1]
×
531
                        } else {
×
532
                                logger.V(log.DBG).Info("string did not match the format required", "pgp", packageInfo.PGP)
×
533
                                pgpKeyID = ""
×
534
                        }
×
535
                }
536

537
                pyxisRPM := pyxis.RPM{
×
538
                        Architecture: packageInfo.Arch,
×
539
                        Gpg:          pgpKeyID,
×
540
                        Name:         packageInfo.Name,
×
541
                        Nvra:         fmt.Sprintf("%s-%s-%s.%s", packageInfo.Name, packageInfo.Version, packageInfo.Release, packageInfo.Arch),
×
542
                        Release:      packageInfo.Release,
×
543
                        SrpmName:     bgName,
×
544
                        SrpmNevra:    srpmNevra,
×
545
                        Summary:      packageInfo.Summary,
×
546
                        Version:      packageInfo.Version,
×
547
                }
×
548

×
549
                rpms = append(rpms, pyxisRPM)
×
550
        }
551

552
        rpmManifest := pyxis.RPMManifest{
4✔
553
                RPMS: rpms,
4✔
554
        }
4✔
555

4✔
556
        // calling MarshalIndent so the json file written to disk is human-readable when opened
4✔
557
        rpmManifestJSON, err := json.MarshalIndent(rpmManifest, "", "    ")
4✔
558
        if err != nil {
4✔
559
                return fmt.Errorf("could not marshal rpm manifest: %w", err)
×
560
        }
×
561

562
        if artifactWriter := artifacts.WriterFromContext(ctx); artifactWriter != nil {
8✔
563
                fileName, err := artifactWriter.WriteFile(check.DefaultRPMManifestFilename, bytes.NewReader(rpmManifestJSON))
4✔
564
                if err != nil {
4✔
565
                        return fmt.Errorf("failed to save file to artifacts directory: %w", err)
×
566
                }
×
567

568
                logger.V(log.TRC).Info("rpm manifest written to disk", "filename", fileName)
4✔
569
        }
570

571
        return nil
4✔
572
}
573

574
func sumLayerSizeBytes(layers []pyxis.Layer) int64 {
4✔
575
        var sum int64
4✔
576
        for _, layer := range layers {
16✔
577
                sum += layer.Size
12✔
578
        }
12✔
579

580
        return sum
4✔
581
}
582

583
func convertLabels(imageLabels map[string]string) []pyxis.Label {
4✔
584
        pyxisLabels := make([]pyxis.Label, 0, len(imageLabels))
4✔
585
        for key, value := range imageLabels {
4✔
586
                label := pyxis.Label{
×
587
                        Name:  key,
×
588
                        Value: value,
×
589
                }
×
590

×
591
                pyxisLabels = append(pyxisLabels, label)
×
592
        }
×
593

594
        return pyxisLabels
4✔
595
}
596

597
// OperatorCheckConfig contains configuration relevant to an individual check's execution.
598
type OperatorCheckConfig struct {
599
        ScorecardImage, ScorecardWaitTime, ScorecardNamespace, ScorecardServiceAccount string
600
        IndexImage, DockerConfig, Channel                                              string
601
        Kubeconfig                                                                     []byte
602
        CSVTimeout                                                                     time.Duration
603
        SubscriptionTimeout                                                            time.Duration
604
}
605

606
// InitializeOperatorChecks returns opeartor checks for policy p give cfg.
607
func InitializeOperatorChecks(ctx context.Context, p policy.Policy, cfg OperatorCheckConfig) ([]check.Check, error) {
4✔
608
        switch p {
4✔
609
        case policy.PolicyOperator:
3✔
610
                return []check.Check{
3✔
611
                        operatorpol.NewScorecardBasicSpecCheck(operatorsdk.New(cfg.ScorecardImage, exec.Command), cfg.ScorecardNamespace, cfg.ScorecardServiceAccount, cfg.Kubeconfig, cfg.ScorecardWaitTime),
3✔
612
                        operatorpol.NewScorecardOlmSuiteCheck(operatorsdk.New(cfg.ScorecardImage, exec.Command), cfg.ScorecardNamespace, cfg.ScorecardServiceAccount, cfg.Kubeconfig, cfg.ScorecardWaitTime),
3✔
613
                        operatorpol.NewDeployableByOlmCheck(cfg.IndexImage, cfg.DockerConfig, cfg.Channel, operatorpol.WithCSVTimeout(cfg.CSVTimeout), operatorpol.WithSubscriptionTimeout(cfg.SubscriptionTimeout)),
3✔
614
                        operatorpol.NewValidateOperatorBundleCheck(),
3✔
615
                        operatorpol.NewCertifiedImagesCheck(pyxis.NewPyxisClient(
3✔
616
                                check.DefaultPyxisHost,
3✔
617
                                "",
3✔
618
                                "",
3✔
619
                                &http.Client{Timeout: 60 * time.Second}),
3✔
620
                        ),
3✔
621
                        operatorpol.NewSecurityContextConstraintsCheck(),
3✔
622
                        &operatorpol.RelatedImagesCheck{},
3✔
623
                        operatorpol.FollowsRestrictedNetworkEnablementGuidelines{},
3✔
624
                        operatorpol.RequiredAnnotations{},
3✔
625
                }, nil
3✔
626
        }
627

628
        return nil, fmt.Errorf("provided operator policy %s is unknown", p)
1✔
629
}
630

631
// ContainerCheckConfig contains configuration relevant to an individual check's execution.
632
type ContainerCheckConfig struct {
633
        DockerConfig, PyxisAPIToken, CertificationProjectID, PyxisHost string
634
}
635

636
// InitializeContainerChecks returns the appropriate checks for policy p given cfg.
637
func InitializeContainerChecks(ctx context.Context, p policy.Policy, cfg ContainerCheckConfig) ([]check.Check, error) {
16✔
638
        switch p {
16✔
639
        case policy.PolicyContainer:
3✔
640
                return []check.Check{
3✔
641
                        &containerpol.HasLicenseCheck{},
3✔
642
                        containerpol.NewHasUniqueTagCheck(cfg.DockerConfig),
3✔
643
                        &containerpol.MaxLayersCheck{},
3✔
644
                        &containerpol.HasNoProhibitedPackagesCheck{},
3✔
645
                        &containerpol.HasRequiredLabelsCheck{},
3✔
646
                        &containerpol.HasNoProhibitedLabelsCheck{},
3✔
647
                        &containerpol.RunAsNonRootCheck{},
3✔
648
                        &containerpol.HasModifiedFilesCheck{},
3✔
649
                        containerpol.NewBasedOnUbiCheck(pyxis.NewPyxisClient(
3✔
650
                                cfg.PyxisHost,
3✔
651
                                cfg.PyxisAPIToken,
3✔
652
                                cfg.CertificationProjectID,
3✔
653
                                &http.Client{Timeout: 60 * time.Second})),
3✔
654
                        &containerpol.HasProhibitedContainerName{},
3✔
655
                }, nil
3✔
656
        case policy.PolicyRoot:
3✔
657
                return []check.Check{
3✔
658
                        &containerpol.HasLicenseCheck{},
3✔
659
                        containerpol.NewHasUniqueTagCheck(cfg.DockerConfig),
3✔
660
                        &containerpol.MaxLayersCheck{},
3✔
661
                        &containerpol.HasNoProhibitedPackagesCheck{},
3✔
662
                        &containerpol.HasRequiredLabelsCheck{},
3✔
663
                        &containerpol.HasNoProhibitedLabelsCheck{},
3✔
664
                        &containerpol.HasModifiedFilesCheck{},
3✔
665
                        containerpol.NewBasedOnUbiCheck(pyxis.NewPyxisClient(
3✔
666
                                cfg.PyxisHost,
3✔
667
                                cfg.PyxisAPIToken,
3✔
668
                                cfg.CertificationProjectID,
3✔
669
                                &http.Client{Timeout: 60 * time.Second})),
3✔
670
                        &containerpol.HasProhibitedContainerName{},
3✔
671
                }, nil
3✔
672
        case policy.PolicyScratchNonRoot:
3✔
673
                return []check.Check{
3✔
674
                        &containerpol.HasLicenseCheck{},
3✔
675
                        containerpol.NewHasUniqueTagCheck(cfg.DockerConfig),
3✔
676
                        &containerpol.MaxLayersCheck{},
3✔
677
                        &containerpol.HasRequiredLabelsCheck{},
3✔
678
                        &containerpol.HasNoProhibitedLabelsCheck{},
3✔
679
                        &containerpol.RunAsNonRootCheck{},
3✔
680
                        &containerpol.HasProhibitedContainerName{},
3✔
681
                }, nil
3✔
682
        case policy.PolicyScratchRoot:
3✔
683
                return []check.Check{
3✔
684
                        &containerpol.HasLicenseCheck{},
3✔
685
                        containerpol.NewHasUniqueTagCheck(cfg.DockerConfig),
3✔
686
                        &containerpol.MaxLayersCheck{},
3✔
687
                        &containerpol.HasRequiredLabelsCheck{},
3✔
688
                        &containerpol.HasNoProhibitedLabelsCheck{},
3✔
689
                        &containerpol.HasProhibitedContainerName{},
3✔
690
                }, nil
3✔
691
        case policy.PolicyKonflux:
3✔
692
                return []check.Check{
3✔
693
                        &containerpol.HasLicenseCheck{},
3✔
694
                        containerpol.NewHasUniqueTagCheck(cfg.DockerConfig),
3✔
695
                        &containerpol.MaxLayersCheck{},
3✔
696
                        &containerpol.HasNoProhibitedPackagesCheck{},
3✔
697
                        &containerpol.HasRequiredLabelsCheck{},
3✔
698
                        &containerpol.RunAsNonRootCheck{},
3✔
699
                        &containerpol.HasModifiedFilesCheck{},
3✔
700
                        containerpol.NewBasedOnUbiCheck(pyxis.NewPyxisClient(
3✔
701
                                cfg.PyxisHost,
3✔
702
                                cfg.PyxisAPIToken,
3✔
703
                                cfg.CertificationProjectID,
3✔
704
                                &http.Client{Timeout: 60 * time.Second})),
3✔
705
                }, nil
3✔
706
        }
707

708
        return nil, fmt.Errorf("provided container policy %s is unknown", p)
1✔
709
}
710

711
// makeCheckList returns a list of check names.
712
func makeCheckList(checks []check.Check) []string {
12✔
713
        checkNames := make([]string, len(checks))
12✔
714

12✔
715
        for i, check := range checks {
110✔
716
                checkNames[i] = check.Name()
98✔
717
        }
98✔
718

719
        return checkNames
12✔
720
}
721

722
// checkNamesFor produces a slice of names for checks in the requested policy.
723
func checkNamesFor(ctx context.Context, p policy.Policy) []string {
13✔
724
        var c []check.Check
13✔
725
        switch p {
13✔
726
        case policy.PolicyContainer, policy.PolicyRoot, policy.PolicyScratchNonRoot, policy.PolicyScratchRoot, policy.PolicyKonflux:
10✔
727
                c, _ = InitializeContainerChecks(ctx, p, ContainerCheckConfig{})
10✔
728
        case policy.PolicyOperator:
2✔
729
                c, _ = InitializeOperatorChecks(ctx, p, OperatorCheckConfig{})
2✔
730
        default:
1✔
731
                return []string{}
1✔
732
        }
733

734
        return makeCheckList(c)
12✔
735
}
736

737
// OperatorPolicy returns the names of checks in the operator policy.
738
func OperatorPolicy(ctx context.Context) []string {
2✔
739
        return checkNamesFor(ctx, policy.PolicyOperator)
2✔
740
}
2✔
741

742
// ContainerPolicy returns the names of checks in the container policy.
743
func ContainerPolicy(ctx context.Context) []string {
2✔
744
        return checkNamesFor(ctx, policy.PolicyContainer)
2✔
745
}
2✔
746

747
// ScratchNonRootContainerPolicy returns the names of checks in the
748
// container policy with scratch exception.
749
func ScratchNonRootContainerPolicy(ctx context.Context) []string {
2✔
750
        return checkNamesFor(ctx, policy.PolicyScratchNonRoot)
2✔
751
}
2✔
752

753
// ScratchRootContainerPolicy returns the names of checks in the
754
// container policy with scratch and root exception.
755
func ScratchRootContainerPolicy(ctx context.Context) []string {
2✔
756
        return checkNamesFor(ctx, policy.PolicyScratchRoot)
2✔
757
}
2✔
758

759
// RootExceptionContainerPolicy returns the names of checks in the
760
// container policy with root exception.
761
func RootExceptionContainerPolicy(ctx context.Context) []string {
2✔
762
        return checkNamesFor(ctx, policy.PolicyRoot)
2✔
763
}
2✔
764

765
// KonfluxContainerPolicy returns the names of checks to be used in
766
// a konflux pipeline
767
func KonfluxContainerPolicy(ctx context.Context) []string {
2✔
768
        return checkNamesFor(ctx, policy.PolicyKonflux)
2✔
769
}
2✔
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

© 2026 Coveralls, Inc