github.com/anchore/syft@v1.38.2/syft/format/cyclonedxjson/encoder_test.go (about) 1 package cyclonedxjson 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/pkg" 16 "github.com/anchore/syft/syft/sbom" 17 ) 18 19 var updateSnapshot = flag.Bool("update-cyclonedx-json", false, "update the *.golden files for cyclone-dx JSON encoders") 20 var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing") 21 22 func getEncoder(t testing.TB) sbom.FormatEncoder { 23 cfg := DefaultEncoderConfig() 24 cfg.Pretty = true 25 26 enc, err := NewFormatEncoderWithConfig(cfg) 27 require.NoError(t, err) 28 return enc 29 } 30 31 func TestPrettyOutput(t *testing.T) { 32 run := func(opt bool) string { 33 enc, err := NewFormatEncoderWithConfig(EncoderConfig{ 34 Version: cyclonedxutil.DefaultVersion, 35 Pretty: opt, 36 }) 37 require.NoError(t, err) 38 39 dir := t.TempDir() 40 s := testutil.DirectoryInput(t, dir) 41 42 var buffer bytes.Buffer 43 err = enc.Encode(&buffer, s) 44 require.NoError(t, err) 45 46 return strings.TrimSpace(buffer.String()) 47 } 48 49 t.Run("pretty", func(t *testing.T) { 50 actual := run(true) 51 assert.Contains(t, actual, "\n") 52 }) 53 54 t.Run("compact", func(t *testing.T) { 55 actual := run(false) 56 assert.NotContains(t, actual, "\n") 57 }) 58 } 59 60 func TestEscapeHTML(t *testing.T) { 61 dir := t.TempDir() 62 s := testutil.DirectoryInput(t, dir) 63 s.Artifacts.Packages.Add(pkg.Package{ 64 Name: "<html-package>", 65 }) 66 67 // by default we do not escape HTML 68 t.Run("default", func(t *testing.T) { 69 cfg := DefaultEncoderConfig() 70 71 enc, err := NewFormatEncoderWithConfig(cfg) 72 require.NoError(t, err) 73 74 var buffer bytes.Buffer 75 err = enc.Encode(&buffer, s) 76 require.NoError(t, err) 77 78 actual := buffer.String() 79 assert.Contains(t, actual, "<html-package>") 80 assert.NotContains(t, actual, "\\u003chtml-package\\u003e") 81 }) 82 83 } 84 85 func TestCycloneDxDirectoryEncoder(t *testing.T) { 86 dir := t.TempDir() 87 testutil.AssertEncoderAgainstGoldenSnapshot(t, 88 testutil.EncoderSnapshotTestConfig{ 89 Subject: testutil.DirectoryInput(t, dir), 90 Format: getEncoder(t), 91 UpdateSnapshot: *updateSnapshot, 92 PersistRedactionsInSnapshot: true, 93 IsJSON: true, 94 Redactor: redactor(dir), 95 }, 96 ) 97 } 98 99 func TestCycloneDxImageEncoder(t *testing.T) { 100 testImage := "image-simple" 101 testutil.AssertEncoderAgainstGoldenImageSnapshot(t, 102 testutil.ImageSnapshotTestConfig{ 103 Image: testImage, 104 UpdateImageSnapshot: *updateImage, 105 }, 106 testutil.EncoderSnapshotTestConfig{ 107 Subject: testutil.ImageInput(t, testImage), 108 Format: getEncoder(t), 109 UpdateSnapshot: *updateSnapshot, 110 PersistRedactionsInSnapshot: true, 111 IsJSON: true, 112 Redactor: redactor(), 113 }, 114 ) 115 } 116 117 func redactor(values ...string) testutil.Redactor { 118 return testutil.NewRedactions(). 119 WithValuesRedacted(values...). 120 WithPatternRedactorSpec( 121 testutil.PatternReplacement{ 122 // only the source component bom-ref (not package or other component bom-refs) 123 Search: regexp.MustCompile(`"component": \{[^}]*"bom-ref":\s*"(?P<redact>.+)"[^}]*}`), 124 Groups: []string{"redact"}, // use the regex to anchore the search, but only replace bytes within the capture group 125 Replace: "redacted", 126 }, 127 ). 128 WithPatternRedactors( 129 map[string]string{ 130 // UUIDs 131 `urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`: `urn:uuid:redacted`, 132 133 // timestamps 134 `([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]))`: `timestamp:redacted`, 135 136 // image hashes 137 `sha256:[A-Fa-f0-9]{64}`: `sha256:redacted`, 138 }, 139 ) 140 } 141 142 func TestSupportedVersions(t *testing.T) { 143 encs := defaultFormatEncoders() 144 require.NotEmpty(t, encs) 145 146 versions := SupportedVersions() 147 require.Equal(t, len(versions), len(encs)) 148 149 subject := testutil.DirectoryInput(t, t.TempDir()) 150 dec := NewFormatDecoder() 151 152 for _, enc := range encs { 153 t.Run(enc.Version(), func(t *testing.T) { 154 require.Contains(t, versions, enc.Version()) 155 156 var buf bytes.Buffer 157 require.NoError(t, enc.Encode(&buf, subject)) 158 159 id, version := dec.Identify(bytes.NewReader(buf.Bytes())) 160 require.Equal(t, enc.ID(), id) 161 require.Equal(t, enc.Version(), version) 162 163 var s *sbom.SBOM 164 var err error 165 s, id, version, err = dec.Decode(bytes.NewReader(buf.Bytes())) 166 require.NoError(t, err) 167 require.Equal(t, enc.ID(), id) 168 require.Equal(t, enc.Version(), version) 169 170 require.NotEmpty(t, s.Artifacts.Packages.PackageCount()) 171 172 assert.Equal(t, len(subject.Relationships), len(s.Relationships), "mismatched relationship count") 173 174 if !assert.Equal(t, subject.Artifacts.Packages.PackageCount(), s.Artifacts.Packages.PackageCount(), "mismatched package count") { 175 t.Logf("expected: %d", subject.Artifacts.Packages.PackageCount()) 176 for _, p := range subject.Artifacts.Packages.Sorted() { 177 t.Logf(" - %s", p.String()) 178 } 179 t.Logf("actual: %d", s.Artifacts.Packages.PackageCount()) 180 for _, p := range s.Artifacts.Packages.Sorted() { 181 t.Logf(" - %s", p.String()) 182 } 183 } 184 }) 185 } 186 } 187 188 func defaultFormatEncoders() []sbom.FormatEncoder { 189 var encs []sbom.FormatEncoder 190 for _, version := range SupportedVersions() { 191 enc, err := NewFormatEncoderWithConfig(EncoderConfig{Version: version}) 192 if err != nil { 193 panic(err) 194 } 195 encs = append(encs, enc) 196 } 197 return encs 198 }