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  }