github.com/anchore/syft@v1.38.2/syft/format/cyclonedxxml/encoder_test.go (about)

     1  package cyclonedxxml
     2  
     3  import (
     4  	"bytes"
     5  	"flag"
     6  	"regexp"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"github.com/anchore/syft/syft/format/internal/cyclonedxutil"
    14  	"github.com/anchore/syft/syft/format/internal/testutil"
    15  	"github.com/anchore/syft/syft/sbom"
    16  )
    17  
    18  var updateSnapshot = flag.Bool("update-cyclonedx-xml", false, "update the *.golden files for cyclone-dx XML encoders")
    19  var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
    20  
    21  func getEncoder(t testing.TB) sbom.FormatEncoder {
    22  	cfg := DefaultEncoderConfig()
    23  	cfg.Pretty = true
    24  
    25  	enc, err := NewFormatEncoderWithConfig(cfg)
    26  	require.NoError(t, err)
    27  	return enc
    28  }
    29  
    30  func TestPrettyOutput(t *testing.T) {
    31  	enc, err := NewFormatEncoderWithConfig(EncoderConfig{
    32  		Version: cyclonedxutil.DefaultVersion,
    33  		Pretty:  false,
    34  	})
    35  	require.NoError(t, err)
    36  
    37  	dir := t.TempDir()
    38  	s := testutil.DirectoryInput(t, dir)
    39  
    40  	var buffer bytes.Buffer
    41  	err = enc.Encode(&buffer, s)
    42  	require.NoError(t, err)
    43  
    44  	actual := buffer.String()
    45  	lines := strings.Split(actual, "\n")
    46  	require.NotEmpty(t, lines)
    47  	whitespace := regexp.MustCompile(`^\s+`)
    48  	for _, line := range lines {
    49  		if len(line) == 0 {
    50  			continue
    51  		}
    52  
    53  		// require a non-whitespace character (tab, space, etc) as the first character of the line
    54  		require.False(t, whitespace.Match([]byte(line)), "line should not start with whitespace: %q", line)
    55  	}
    56  }
    57  
    58  func TestCycloneDxDirectoryEncoder(t *testing.T) {
    59  	dir := t.TempDir()
    60  	testutil.AssertEncoderAgainstGoldenSnapshot(t,
    61  		testutil.EncoderSnapshotTestConfig{
    62  			Subject:                     testutil.DirectoryInput(t, dir),
    63  			Format:                      getEncoder(t),
    64  			UpdateSnapshot:              *updateSnapshot,
    65  			PersistRedactionsInSnapshot: true,
    66  			IsJSON:                      false,
    67  			Redactor:                    redactor(dir),
    68  		},
    69  	)
    70  }
    71  
    72  func TestCycloneDxImageEncoder(t *testing.T) {
    73  	testImage := "image-simple"
    74  	testutil.AssertEncoderAgainstGoldenImageSnapshot(t,
    75  		testutil.ImageSnapshotTestConfig{
    76  			Image:               testImage,
    77  			UpdateImageSnapshot: *updateImage,
    78  		},
    79  		testutil.EncoderSnapshotTestConfig{
    80  			Subject:                     testutil.ImageInput(t, testImage),
    81  			Format:                      getEncoder(t),
    82  			UpdateSnapshot:              *updateSnapshot,
    83  			PersistRedactionsInSnapshot: true,
    84  			IsJSON:                      false,
    85  			Redactor:                    redactor(),
    86  		},
    87  	)
    88  }
    89  
    90  func redactor(values ...string) testutil.Redactor {
    91  	return testutil.NewRedactions().
    92  		WithValuesRedacted(values...).
    93  		WithPatternRedactorSpec(
    94  			testutil.PatternReplacement{
    95  				// only the source component bom-ref (not package or other component bom-refs)
    96  				Search:  regexp.MustCompile(`<component bom-ref="(?P<redact>[^"]*)" type="file">`),
    97  				Groups:  []string{"redact"}, // use the regex to anchor the search, but only replace bytes within the capture group
    98  				Replace: "redacted",
    99  			},
   100  		).
   101  		WithPatternRedactorSpec(
   102  			testutil.PatternReplacement{
   103  				// only the source component bom-ref (not package or other component bom-refs)
   104  				Search:  regexp.MustCompile(`<component bom-ref="(?P<redact>[^"]*)" type="container">`),
   105  				Groups:  []string{"redact"}, // use the regex to anchor the search, but only replace bytes within the capture group
   106  				Replace: "redacted",
   107  			},
   108  		).
   109  		WithPatternRedactors(
   110  			map[string]string{
   111  				// dates
   112  				`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`: `redacted`,
   113  
   114  				// image hashes
   115  				`sha256:[A-Za-z0-9]{64}`: `sha256:redacted`,
   116  
   117  				// serial numbers
   118  				`(serialNumber)="[^"]+"`: `$1="redacted"`,
   119  			},
   120  		)
   121  }
   122  
   123  func TestSupportedVersions(t *testing.T) {
   124  	encs := defaultFormatEncoders()
   125  	require.NotEmpty(t, encs)
   126  
   127  	versions := SupportedVersions()
   128  	require.Equal(t, len(versions), len(encs))
   129  
   130  	subject := testutil.DirectoryInput(t, t.TempDir())
   131  	dec := NewFormatDecoder()
   132  
   133  	for _, enc := range encs {
   134  		t.Run(enc.Version(), func(t *testing.T) {
   135  			require.Contains(t, versions, enc.Version())
   136  
   137  			var buf bytes.Buffer
   138  			require.NoError(t, enc.Encode(&buf, subject))
   139  
   140  			id, version := dec.Identify(bytes.NewReader(buf.Bytes()))
   141  			require.Equal(t, enc.ID(), id)
   142  			require.Equal(t, enc.Version(), version)
   143  
   144  			var s *sbom.SBOM
   145  			var err error
   146  			s, id, version, err = dec.Decode(bytes.NewReader(buf.Bytes()))
   147  			require.NoError(t, err)
   148  			require.Equal(t, enc.ID(), id)
   149  			require.Equal(t, enc.Version(), version)
   150  
   151  			require.NotEmpty(t, s.Artifacts.Packages.PackageCount())
   152  
   153  			assert.Equal(t, len(subject.Relationships), len(s.Relationships), "mismatched relationship count")
   154  
   155  			if !assert.Equal(t, subject.Artifacts.Packages.PackageCount(), s.Artifacts.Packages.PackageCount(), "mismatched package count") {
   156  				t.Logf("expected: %d", subject.Artifacts.Packages.PackageCount())
   157  				for _, p := range subject.Artifacts.Packages.Sorted() {
   158  					t.Logf("  - %s", p.String())
   159  				}
   160  				t.Logf("actual: %d", s.Artifacts.Packages.PackageCount())
   161  				for _, p := range s.Artifacts.Packages.Sorted() {
   162  					t.Logf("  - %s", p.String())
   163  				}
   164  			}
   165  		})
   166  	}
   167  }
   168  
   169  func defaultFormatEncoders() []sbom.FormatEncoder {
   170  	var encs []sbom.FormatEncoder
   171  	for _, version := range SupportedVersions() {
   172  		enc, err := NewFormatEncoderWithConfig(EncoderConfig{Version: version})
   173  		if err != nil {
   174  			panic(err)
   175  		}
   176  		encs = append(encs, enc)
   177  	}
   178  	return encs
   179  }