Fix the attest command (#2337)

* fix attest command

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add notification on how to access the attestation

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix integration test

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2023-11-21 13:29:58 -05:00 committed by GitHub
parent ebeb768f59
commit 4712246897
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 509 additions and 166 deletions

View file

@ -2,6 +2,7 @@ package commands
import (
"fmt"
"io"
"os"
"os/exec"
"strings"
@ -11,15 +12,14 @@ import (
"github.com/wagoodman/go-progress"
"github.com/anchore/clio"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/event/monitor"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/spdxjson"
"github.com/anchore/syft/syft/format/spdxtagvalue"
@ -33,6 +33,7 @@ const (
`
attestSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp
attestHelp = attestExample + attestSchemeHelp
cosignBinName = "cosign"
)
type attestOptions struct {
@ -46,24 +47,7 @@ type attestOptions struct {
func Attest(app clio.Application) *cobra.Command {
id := app.ID()
opts := &attestOptions{
UpdateCheck: options.DefaultUpdateCheck(),
Output: options.Output{
AllowMultipleOutputs: false,
AllowableOptions: []string{
string(syftjson.ID),
string(cyclonedxjson.ID),
string(spdxjson.ID),
string(spdxtagvalue.ID),
},
Outputs: []string{syftjson.ID.String()},
OutputFile: options.OutputFile{ // nolint:staticcheck
Enabled: false, // explicitly not allowed
},
Format: options.DefaultFormat(),
},
Catalog: options.DefaultCatalog(),
}
opts := defaultAttestOptions()
// template format explicitly not allowed
opts.Format.Template.Enabled = false
@ -82,83 +66,96 @@ func Attest(app clio.Application) *cobra.Command {
restoreStdout := ui.CaptureStdoutToTraceLog()
defer restoreStdout()
return runAttest(id, opts, args[0])
return runAttest(id, &opts, args[0])
},
}, opts)
}, &opts)
}
func defaultAttestOptions() attestOptions {
return attestOptions{
Output: defaultAttestOutputOptions(),
UpdateCheck: options.DefaultUpdateCheck(),
Catalog: options.DefaultCatalog(),
}
}
func defaultAttestOutputOptions() options.Output {
return options.Output{
AllowMultipleOutputs: false,
AllowToFile: false,
AllowableOptions: []string{
string(syftjson.ID),
string(cyclonedxjson.ID),
string(spdxjson.ID),
string(spdxtagvalue.ID),
},
Outputs: []string{syftjson.ID.String()},
OutputFile: options.OutputFile{ // nolint:staticcheck
Enabled: false, // explicitly not allowed
},
Format: options.DefaultFormat(),
}
}
//nolint:funlen
func runAttest(id clio.Identification, opts *attestOptions, userInput string) error {
_, err := exec.LookPath("cosign")
if err != nil {
// when cosign is not installed the error will be rendered like so:
// 2023/06/30 08:31:52 error during command execution: 'syft attest' requires cosign to be installed: exec: "cosign": executable file not found in $PATH
return fmt.Errorf("'syft attest' requires cosign to be installed: %w", err)
}
s, err := buildSBOM(id, &opts.Catalog, userInput)
if err != nil {
return fmt.Errorf("unable to build SBOM: %w", err)
// TODO: what other validation here besides binary name?
if !commandExists(cosignBinName) {
return fmt.Errorf("'syft attest' requires cosign to be installed, however it does not appear to be on PATH")
}
// this is the file that will contain the SBOM being attested
f, err := os.CreateTemp("", "syft-attest-")
if err != nil {
return fmt.Errorf("unable to create temp file: %w", err)
}
defer os.Remove(f.Name())
writer, err := opts.SBOMWriter()
s, err := generateSBOMForAttestation(id, &opts.Catalog, userInput)
if err != nil {
return fmt.Errorf("unable to create SBOM writer: %w", err)
return fmt.Errorf("unable to build SBOM: %w", err)
}
if err := writer.Write(*s); err != nil {
return fmt.Errorf("unable to write SBOM to temp file: %w", err)
if err = writeSBOMToFormattedFile(s, f, opts); err != nil {
return fmt.Errorf("unable to write SBOM to file: %w", err)
}
// TODO: what other validation here besides binary name?
cmd := "cosign"
if !commandExists(cmd) {
return fmt.Errorf("unable to find cosign in PATH; make sure you have it installed")
if err = createAttestation(f.Name(), opts, userInput); err != nil {
return err
}
outputNames := opts.OutputNameSet()
var outputName string
switch outputNames.Size() {
case 0:
return fmt.Errorf("no output format specified")
case 1:
outputName = outputNames.List()[0]
default:
return fmt.Errorf("multiple output formats specified: %s", strings.Join(outputNames.List(), ", "))
bus.Notify("Attestation has been created, please check your registry for the output or use the cosign command:")
bus.Notify(fmt.Sprintf("cosign download attestation %s", userInput))
return nil
}
func writeSBOMToFormattedFile(s *sbom.SBOM, sbomFile io.Writer, opts *attestOptions) error {
if sbomFile == nil {
return fmt.Errorf("no output file provided")
}
// Select Cosign predicate type based on defined output type
// As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go
var predicateType string
switch strings.ToLower(outputName) {
case "cyclonedx-json":
predicateType = "cyclonedx"
case "spdx-tag-value", "spdx-tv":
predicateType = "spdx"
case "spdx-json", "json":
predicateType = "spdxjson"
default:
predicateType = "custom"
encs, err := opts.Format.Encoders()
if err != nil {
return fmt.Errorf("unable to create encoders: %w", err)
}
args := []string{"attest", userInput, "--predicate", f.Name(), "--type", predicateType}
if opts.Attest.Key != "" {
args = append(args, "--key", opts.Attest.Key.String())
encoders := format.NewEncoderCollection(encs...)
encoder := encoders.GetByString(opts.Outputs[0])
if encoder == nil {
return fmt.Errorf("unable to find encoder for %q", opts.Outputs[0])
}
execCmd := exec.Command(cmd, args...)
execCmd.Env = os.Environ()
if opts.Attest.Key != "" {
execCmd.Env = append(execCmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", opts.Attest.Password))
} else {
// no key provided, use cosign's keyless mode
execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1")
if err = encoder.Encode(sbomFile, *s); err != nil {
return fmt.Errorf("unable to encode SBOM: %w", err)
}
return nil
}
func createAttestation(sbomFilepath string, opts *attestOptions, userInput string) error {
execCmd, err := attestCommand(sbomFilepath, opts, userInput)
if err != nil {
return fmt.Errorf("unable to craft attest command: %w", err)
}
log.WithFields("cmd", strings.Join(execCmd.Args, " ")).Trace("creating attestation")
@ -201,59 +198,67 @@ func runAttest(id clio.Identification, opts *attestOptions, userInput string) er
}
mon.SetCompleted()
return nil
}
func buildSBOM(id clio.Identification, opts *options.Catalog, userInput string) (*sbom.SBOM, error) {
cfg := source.DetectConfig{
DefaultImageSource: opts.DefaultImagePullSource,
func attestCommand(sbomFilepath string, opts *attestOptions, userInput string) (*exec.Cmd, error) {
outputNames := opts.OutputNameSet()
var outputName string
switch outputNames.Size() {
case 0:
return nil, fmt.Errorf("no output format specified")
case 1:
outputName = outputNames.List()[0]
default:
return nil, fmt.Errorf("multiple output formats specified: %s", strings.Join(outputNames.List(), ", "))
}
detection, err := source.Detect(userInput, cfg)
args := []string{"attest", userInput, "--predicate", sbomFilepath, "--type", predicateType(outputName), "-y"}
if opts.Attest.Key != "" {
args = append(args, "--key", opts.Attest.Key.String())
}
execCmd := exec.Command(cosignBinName, args...)
execCmd.Env = os.Environ()
if opts.Attest.Key != "" {
execCmd.Env = append(execCmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", opts.Attest.Password))
} else {
// no key provided, use cosign's keyless mode
execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1")
}
return execCmd, nil
}
func predicateType(outputName string) string {
// Select Cosign predicate type based on defined output type
// As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go
switch strings.ToLower(outputName) {
case "cyclonedx-json":
return "cyclonedx"
case "spdx-tag-value", "spdx-tv":
return "spdx"
case "spdx-json", "json":
return "spdxjson"
default:
return "custom"
}
}
func generateSBOMForAttestation(id clio.Identification, opts *options.Catalog, userInput string) (*sbom.SBOM, error) {
src, err := getSource(opts, userInput, onlyContainerImages)
if err != nil {
return nil, fmt.Errorf("could not deteremine source: %w", err)
return nil, err
}
if detection.IsContainerImage() {
return nil, fmt.Errorf("attestations are only supported for oci images at this time")
}
var platform *image.Platform
if opts.Platform != "" {
platform, err = image.NewPlatform(opts.Platform)
if err != nil {
return nil, fmt.Errorf("invalid platform: %w", err)
defer func() {
if src != nil {
if err := src.Close(); err != nil {
log.Tracef("unable to close source: %+v", err)
}
}
}
hashers, err := file.Hashers(opts.Source.File.Digests...)
if err != nil {
return nil, fmt.Errorf("invalid hash: %w", err)
}
src, err := detection.NewSource(
source.DetectionSourceConfig{
Alias: source.Alias{
Name: opts.Source.Name,
Version: opts.Source.Version,
},
RegistryOptions: opts.Registry.ToOptions(),
Platform: platform,
Exclude: source.ExcludeConfig{
Paths: opts.Exclusions,
},
DigestAlgorithms: hashers,
BasePath: opts.BasePath,
},
)
if src != nil {
defer src.Close()
}
if err != nil {
return nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
}
}()
s, err := generateSBOM(id, src, opts)
if err != nil {
@ -267,6 +272,13 @@ func buildSBOM(id clio.Identification, opts *options.Catalog, userInput string)
return s, nil
}
func onlyContainerImages(d *source.Detection) error {
if !d.IsContainerImage() {
return fmt.Errorf("attestations are only supported for oci images at this time")
}
return nil
}
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil

View file

@ -0,0 +1,268 @@
package commands
import (
"bytes"
"fmt"
"os/exec"
"regexp"
"strings"
"testing"
"github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/clio"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func Test_writeSBOMToFormattedFile(t *testing.T) {
type args struct {
s *sbom.SBOM
opts *attestOptions
}
tests := []struct {
name string
args args
wantSbomFile string
wantErr bool
}{
{
name: "go case",
args: args{
opts: &attestOptions{
Output: func() options.Output {
def := defaultAttestOutputOptions()
def.Outputs = []string{"syft-json"}
return def
}(),
},
s: &sbom.SBOM{
Artifacts: sbom.Artifacts{},
Relationships: nil,
Source: source.Description{
ID: "source-id",
Name: "source-name",
Version: "source-version",
},
Descriptor: sbom.Descriptor{
Name: "syft-test",
Version: "non-version",
},
},
},
wantSbomFile: `{
"artifacts": [],
"artifactRelationships": [],
"source": {
"id": "source-id",
"name": "source-name",
"version": "source-version",
"type": "",
"metadata": null
},
"distro": {},
"descriptor": {
"name": "syft-test",
"version": "non-version"
},
"schema": {}
}`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sbomFile := &bytes.Buffer{}
err := writeSBOMToFormattedFile(tt.args.s, sbomFile, tt.args.opts)
if (err != nil) != tt.wantErr {
t.Errorf("writeSBOMToFormattedFile() error = %v, wantErr %v", err, tt.wantErr)
return
}
// redact the schema block
re := regexp.MustCompile(`(?s)"schema":\W*\{.*?},?`)
subject := re.ReplaceAllString(sbomFile.String(), `"schema":{}`)
assert.JSONEq(t, tt.wantSbomFile, subject)
})
}
}
func Test_attestCommand(t *testing.T) {
cmdPrefix := cosignBinName
lp, err := exec.LookPath(cosignBinName)
if err == nil {
cmdPrefix = lp
}
fullCmd := func(args string) string {
return fmt.Sprintf("%s %s", cmdPrefix, args)
}
type args struct {
sbomFilepath string
opts attestOptions
userInput string
}
tests := []struct {
name string
args args
wantCmd string
wantEnvVars map[string]string
notEnvVars []string
wantErr require.ErrorAssertionFunc
}{
{
name: "with key and password",
args: args{
userInput: "myimage",
sbomFilepath: "/tmp/sbom-filepath.json",
opts: func() attestOptions {
def := defaultAttestOptions()
def.Outputs = []string{"syft-json"}
def.Attest.Key = "key"
def.Attest.Password = "password"
return def
}(),
},
wantCmd: fullCmd("attest myimage --predicate /tmp/sbom-filepath.json --type custom -y --key key"),
wantEnvVars: map[string]string{
"COSIGN_PASSWORD": "password",
},
notEnvVars: []string{
"COSIGN_EXPERIMENTAL", // only for keyless
},
},
{
name: "keyless",
args: args{
userInput: "myimage",
sbomFilepath: "/tmp/sbom-filepath.json",
opts: func() attestOptions {
def := defaultAttestOptions()
def.Outputs = []string{"syft-json"}
return def
}(),
},
wantCmd: fullCmd("attest myimage --predicate /tmp/sbom-filepath.json --type custom -y"),
wantEnvVars: map[string]string{
"COSIGN_EXPERIMENTAL": "1",
},
notEnvVars: []string{
"COSIGN_PASSWORD",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = require.NoError
}
got, err := attestCommand(tt.args.sbomFilepath, &tt.args.opts, tt.args.userInput)
tt.wantErr(t, err)
if err != nil {
return
}
require.NotNil(t, got)
assert.Equal(t, tt.wantCmd, got.String())
gotEnv := strset.New(got.Env...)
for k, v := range tt.wantEnvVars {
assert.True(t, gotEnv.Has(fmt.Sprintf("%s=%s", k, v)))
}
for _, k := range tt.notEnvVars {
for _, env := range got.Env {
fields := strings.Split(env, "=")
if fields[0] == k {
t.Errorf("attestCommand() unexpected environment variable %s", k)
}
}
}
})
}
}
func Test_predicateType(t *testing.T) {
tests := []struct {
name string
want string
}{
{
name: "cyclonedx-json",
want: "cyclonedx",
},
{
name: "spdx-tag-value",
want: "spdx",
},
{
name: "spdx-tv",
want: "spdx",
},
{
name: "spdx-json",
want: "spdxjson",
},
{
name: "json",
want: "spdxjson",
},
{
name: "syft-json",
want: "custom",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, predicateType(tt.name), "predicateType(%v)", tt.name)
})
}
}
func Test_buildSBOMForAttestation(t *testing.T) {
// note: this test is only meant to test that the filter function is wired
// and not the correctness of the function in depth
type args struct {
id clio.Identification
opts *options.Catalog
userInput string
}
tests := []struct {
name string
args args
want *sbom.SBOM
wantErr require.ErrorAssertionFunc
}{
{
name: "do not allow directory scans",
args: args{
opts: func() *options.Catalog {
def := defaultAttestOptions()
return &def.Catalog
}(),
userInput: "dir:/tmp/something",
},
wantErr: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = require.NoError
}
_, err := generateSBOMForAttestation(tt.args.id, tt.args.opts, tt.args.userInput)
tt.wantErr(t, err)
if err != nil {
return
}
})
}
}

View file

@ -117,51 +117,10 @@ func runPackages(id clio.Identification, opts *packagesOptions, userInput string
return err
}
detection, err := source.Detect(
userInput,
source.DetectConfig{
DefaultImageSource: opts.DefaultImagePullSource,
},
)
if err != nil {
return fmt.Errorf("could not deteremine source: %w", err)
}
var platform *image.Platform
if opts.Platform != "" {
platform, err = image.NewPlatform(opts.Platform)
if err != nil {
return fmt.Errorf("invalid platform: %w", err)
}
}
hashers, err := file.Hashers(opts.Source.File.Digests...)
if err != nil {
return fmt.Errorf("invalid hash: %w", err)
}
src, err := detection.NewSource(
source.DetectionSourceConfig{
Alias: source.Alias{
Name: opts.Source.Name,
Version: opts.Source.Version,
},
RegistryOptions: opts.Registry.ToOptions(),
Platform: platform,
Exclude: source.ExcludeConfig{
Paths: opts.Exclusions,
},
DigestAlgorithms: hashers,
BasePath: opts.BasePath,
},
)
src, err := getSource(&opts.Catalog, userInput)
if err != nil {
if userInput == "power-user" {
bus.Notify("Note: the 'power-user' command has been removed.")
}
return fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
return err
}
defer func() {
@ -188,6 +147,63 @@ func runPackages(id clio.Identification, opts *packagesOptions, userInput string
return nil
}
func getSource(opts *options.Catalog, userInput string, filters ...func(*source.Detection) error) (source.Source, error) {
detection, err := source.Detect(
userInput,
source.DetectConfig{
DefaultImageSource: opts.DefaultImagePullSource,
},
)
if err != nil {
return nil, fmt.Errorf("could not deteremine source: %w", err)
}
for _, filter := range filters {
if err := filter(detection); err != nil {
return nil, err
}
}
var platform *image.Platform
if opts.Platform != "" {
platform, err = image.NewPlatform(opts.Platform)
if err != nil {
return nil, fmt.Errorf("invalid platform: %w", err)
}
}
hashers, err := file.Hashers(opts.Source.File.Digests...)
if err != nil {
return nil, fmt.Errorf("invalid hash: %w", err)
}
src, err := detection.NewSource(
source.DetectionSourceConfig{
Alias: source.Alias{
Name: opts.Source.Name,
Version: opts.Source.Version,
},
RegistryOptions: opts.Registry.ToOptions(),
Platform: platform,
Exclude: source.ExcludeConfig{
Paths: opts.Exclusions,
},
DigestAlgorithms: hashers,
BasePath: opts.BasePath,
},
)
if err != nil {
if userInput == "power-user" {
bus.Notify("Note: the 'power-user' command has been removed.")
}
return nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
}
return src, nil
}
func generateSBOM(id clio.Identification, src source.Source, opts *options.Catalog) (*sbom.SBOM, error) {
tasks, err := eventloop.Tasks(opts)
if err != nil {

View file

@ -32,8 +32,12 @@ func (o FormatCyclonedxJSON) formatEncoders() ([]sbom.FormatEncoder, error) {
}
func (o FormatCyclonedxJSON) buildConfig(version string) cyclonedxjson.EncoderConfig {
var pretty bool
if o.Pretty != nil {
pretty = *o.Pretty
}
return cyclonedxjson.EncoderConfig{
Version: version,
Pretty: *o.Pretty,
Pretty: pretty,
}
}

View file

@ -32,8 +32,12 @@ func (o FormatCyclonedxXML) formatEncoders() ([]sbom.FormatEncoder, error) {
}
func (o FormatCyclonedxXML) buildConfig(version string) cyclonedxxml.EncoderConfig {
var pretty bool
if o.Pretty != nil {
pretty = *o.Pretty
}
return cyclonedxxml.EncoderConfig{
Version: version,
Pretty: *o.Pretty,
Pretty: pretty,
}
}

View file

@ -32,8 +32,12 @@ func (o FormatSPDXJSON) formatEncoders() ([]sbom.FormatEncoder, error) {
}
func (o FormatSPDXJSON) buildConfig(v string) spdxjson.EncoderConfig {
var pretty bool
if o.Pretty != nil {
pretty = *o.Pretty
}
return spdxjson.EncoderConfig{
Version: v,
Pretty: *o.Pretty,
Pretty: pretty,
}
}

View file

@ -22,8 +22,12 @@ func (o FormatSyftJSON) formatEncoders() ([]sbom.FormatEncoder, error) {
}
func (o FormatSyftJSON) buildConfig() syftjson.EncoderConfig {
var pretty bool
if o.Pretty != nil {
pretty = *o.Pretty
}
return syftjson.EncoderConfig{
Legacy: o.Legacy,
Pretty: *o.Pretty,
Pretty: pretty,
}
}

View file

@ -30,6 +30,7 @@ var _ interface {
type Output struct {
AllowableOptions []string `yaml:"-" json:"-" mapstructure:"-"`
AllowMultipleOutputs bool `yaml:"-" json:"-" mapstructure:"-"`
AllowToFile bool `yaml:"-" json:"-" mapstructure:"-"`
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
OutputFile `yaml:",inline" json:"" mapstructure:",squash"`
Format `yaml:"format" json:"format" mapstructure:"format"`
@ -38,6 +39,7 @@ type Output struct {
func DefaultOutput() Output {
return Output{
AllowMultipleOutputs: true,
AllowToFile: true,
Outputs: []string{string(table.ID)},
OutputFile: OutputFile{
Enabled: true,
@ -86,6 +88,14 @@ func (o Output) SBOMWriter() (sbom.Writer, error) {
return nil, err
}
if !o.AllowToFile {
for _, opt := range o.Outputs {
if strings.Contains(opt, "=") {
return nil, fmt.Errorf("file output is not allowed ('-o format=path' should be '-o format')")
}
}
}
return makeSBOMWriter(o.Outputs, o.File, encoders)
}

View file

@ -179,3 +179,25 @@ func Test_EncoderCollection_ByString_IDOnly_Defaults(t *testing.T) {
})
}
}
func Test_OutputHonorsAllowFile(t *testing.T) {
o := DefaultOutput()
t.Run("file is not allowed", func(t *testing.T) {
o.AllowToFile = false
o.Outputs = []string{"table=/tmp/somefile"}
w, err := o.SBOMWriter()
assert.Nil(t, w)
assert.ErrorContains(t, err, "file output is not allowed")
})
t.Run("file is allowed", func(t *testing.T) {
o.AllowToFile = true
o.Outputs = []string{"table=/tmp/somefile"}
w, err := o.SBOMWriter()
assert.NotNil(t, w)
assert.NoError(t, err)
})
}

View file

@ -78,10 +78,9 @@ func TestConvertCmd(t *testing.T) {
}()
opts := &commands.ConvertOptions{
Output: options.Output{
Outputs: []string{fmt.Sprintf("%s=%s", test.format.ID().String(), formatFile.Name())},
},
Output: options.DefaultOutput(),
}
opts.Outputs = []string{fmt.Sprintf("%s=%s", test.format.ID().String(), formatFile.Name())}
require.NoError(t, opts.PostLoad())
// stdout reduction of test noise