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 }