github.com/anchore/syft@v1.38.2/syft/configuration_audit_trail_test.go (about)

     1  package syft
     2  
     3  import (
     4  	"bytes"
     5  	"crypto"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"reflect"
    10  	"sort"
    11  	"testing"
    12  
    13  	"github.com/hashicorp/go-multierror"
    14  	"github.com/iancoleman/strcase"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  
    18  	"github.com/anchore/syft/syft/cataloging/filecataloging"
    19  	"github.com/anchore/syft/syft/file"
    20  )
    21  
    22  func Test_configurationAuditTrail_StructTags(t *testing.T) {
    23  	// we need to ensure that the output for any configuration is well-formed and follows conventions.
    24  	// We ensure that:
    25  	// 1. all fields have a JSON tag
    26  	// 2. the tag value follows lowercase kebab-case style
    27  
    28  	jsonTags := getJSONTags(t, configurationAuditTrail{})
    29  
    30  	for _, tag := range jsonTags {
    31  		assertLowercaseKebab(t, tag)
    32  	}
    33  
    34  }
    35  
    36  func getJSONTags(t *testing.T, v interface{}) []string {
    37  	var tags []string
    38  	err := collectJSONTags(t, reflect.ValueOf(v), &tags, "", "")
    39  	require.NoError(t, err)
    40  	return tags
    41  }
    42  
    43  func collectJSONTags(t *testing.T, v reflect.Value, tags *[]string, parentTag string, path string) error {
    44  	var errs error
    45  
    46  	if v.Kind() == reflect.Ptr {
    47  		v = v.Elem()
    48  	}
    49  
    50  	if v.Kind() != reflect.Struct {
    51  		return errs
    52  	}
    53  
    54  	tType := v.Type()
    55  	for i := 0; i < v.NumField(); i++ {
    56  		field := v.Field(i)
    57  		fieldType := tType.Field(i)
    58  
    59  		curPath := path + "." + fieldType.Name
    60  
    61  		// account for embeddings
    62  		if fieldType.Anonymous {
    63  			embeddedField := field
    64  
    65  			if embeddedField.Kind() == reflect.Ptr {
    66  				// this can be enhanced in the future if the need arises...
    67  				errs = multierror.Append(errs, fmt.Errorf("field '%s' is a pointer to an embedded struct, this is not supported in the test helper", curPath))
    68  			}
    69  
    70  			if embeddedField.Kind() == reflect.Struct {
    71  				err := collectJSONTags(t, field, tags, parentTag, curPath)
    72  				if err != nil {
    73  					errs = multierror.Append(errs, err)
    74  				}
    75  			}
    76  
    77  			continue
    78  		}
    79  
    80  		var tag string
    81  		var ok bool
    82  		if fieldType.PkgPath == "" {
    83  			tag, ok = fieldType.Tag.Lookup("json")
    84  			if !ok || (tag == "" && parentTag == "") {
    85  				errs = multierror.Append(errs, fmt.Errorf("field '%s' does not have a json tag", curPath))
    86  				return errs
    87  			}
    88  			if tag != "" && tag != "-" {
    89  				*tags = append(*tags, tag)
    90  			}
    91  		}
    92  
    93  		if field.Kind() == reflect.Struct || (field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct) {
    94  			err := collectJSONTags(t, field, tags, tag, curPath)
    95  			if err != nil {
    96  				errs = multierror.Append(errs, err)
    97  			}
    98  		}
    99  	}
   100  	return errs
   101  }
   102  
   103  func assertLowercaseKebab(t *testing.T, tag string) {
   104  	t.Helper()
   105  	require.NotEmpty(t, tag)
   106  	assert.Equal(t, strcase.ToKebab(tag), tag)
   107  }
   108  
   109  func Test_collectJSONTags(t *testing.T) {
   110  	// though this is not used in production, this is a sensitive and complex enough of a check to warrant testing the test helper.
   111  	type good struct {
   112  		A string `json:"a"`
   113  	}
   114  
   115  	type missing struct {
   116  		A string `json:"a"`
   117  		B string
   118  	}
   119  
   120  	type exclude struct {
   121  		A string `json:"a"`
   122  		B string `json:"-"`
   123  	}
   124  
   125  	type goodEmbedded struct {
   126  		good `json:""`
   127  	}
   128  
   129  	type badEmbedded struct {
   130  		missing `json:""`
   131  	}
   132  
   133  	// simply not covered and require further development to support
   134  	type goodPtrEmbedded struct {
   135  		*good `json:""`
   136  	}
   137  
   138  	// simply not covered and require further development to support
   139  	type badPtrEmbedded struct {
   140  		*missing `json:""`
   141  	}
   142  
   143  	tests := []struct {
   144  		name    string
   145  		v       interface{}
   146  		want    []string
   147  		wantErr require.ErrorAssertionFunc
   148  	}{
   149  		{
   150  			name: "good",
   151  			v:    good{},
   152  			want: []string{
   153  				"a",
   154  			},
   155  		},
   156  		{
   157  			name:    "missing",
   158  			v:       missing{},
   159  			wantErr: require.Error,
   160  		},
   161  		{
   162  			name: "exclude",
   163  			v:    exclude{},
   164  			want: []string{
   165  				"a",
   166  			},
   167  		},
   168  		{
   169  			name:    "bad embedded",
   170  			v:       badEmbedded{},
   171  			wantErr: require.Error,
   172  		},
   173  		{
   174  			name: "good embedded",
   175  			v:    goodEmbedded{},
   176  			want: []string{
   177  				"a",
   178  			},
   179  		},
   180  		// these cases are simply not covered and require further development to support
   181  		{
   182  			name:    "bad ptr embedded",
   183  			v:       badPtrEmbedded{},
   184  			wantErr: require.Error,
   185  		},
   186  		{
   187  			name: "good ptr embedded",
   188  			v:    goodPtrEmbedded{},
   189  			want: []string{
   190  				"a",
   191  			},
   192  			wantErr: require.Error,
   193  		},
   194  	}
   195  
   196  	for _, tt := range tests {
   197  		t.Run(tt.name, func(t *testing.T) {
   198  			if tt.wantErr == nil {
   199  				tt.wantErr = require.NoError
   200  			}
   201  
   202  			var tags []string
   203  
   204  			err := collectJSONTags(t, reflect.ValueOf(tt.v), &tags, "", "")
   205  
   206  			tt.wantErr(t, err)
   207  			if err != nil {
   208  				return
   209  			}
   210  
   211  			assert.Equal(t, tt.want, tags)
   212  		})
   213  	}
   214  
   215  }
   216  
   217  func Test_configurationAuditTrail_MarshalJSON(t *testing.T) {
   218  
   219  	tests := []struct {
   220  		name   string
   221  		cfg    configurationAuditTrail
   222  		assert func(t *testing.T, got []byte)
   223  	}{
   224  		{
   225  			name: "ensure other marshallers are called",
   226  			cfg: configurationAuditTrail{
   227  
   228  				Files: filecataloging.Config{
   229  					Selection: file.FilesOwnedByPackageSelection,
   230  					Hashers: []crypto.Hash{
   231  						crypto.SHA256,
   232  					},
   233  				},
   234  			},
   235  			// the custom file marshaller swaps ints for strings for hashers
   236  			assert: func(t *testing.T, got []byte) {
   237  				assert.Contains(t, string(got), `"hashers":["sha-256"]`)
   238  			},
   239  		},
   240  		{
   241  			name: "ensure maps are sorted",
   242  			cfg:  configurationAuditTrail{},
   243  			assert: func(t *testing.T, got []byte) {
   244  				assert.NoError(t, assertJSONKeysSorted(got))
   245  			},
   246  		},
   247  	}
   248  	for _, tt := range tests {
   249  		t.Run(tt.name, func(t *testing.T) {
   250  
   251  			got, err := tt.cfg.MarshalJSON()
   252  			require.NoError(t, err)
   253  			if tt.assert == nil {
   254  				t.Fatal("assert function must be provided")
   255  			}
   256  			tt.assert(t, got)
   257  
   258  		})
   259  	}
   260  }
   261  
   262  // assertJSONKeysSorted asserts that all keys in JSON maps are sorted.
   263  func assertJSONKeysSorted(jsonBytes []byte) error {
   264  	var errs error
   265  	decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
   266  	var keys []string
   267  	var inObject bool
   268  
   269  	for {
   270  		token, err := decoder.Token()
   271  		if err != nil {
   272  			if err == io.EOF {
   273  				break
   274  			}
   275  			errs = multierror.Append(errs, fmt.Errorf("error decoding JSON: %w", err))
   276  		}
   277  
   278  		switch v := token.(type) {
   279  		case json.Delim:
   280  			switch v {
   281  			case '{':
   282  				inObject = true
   283  				keys = nil // Reset keys for a new object
   284  			case '}':
   285  				inObject = false
   286  				if !sort.StringsAreSorted(keys) {
   287  					errs = multierror.Append(errs, fmt.Errorf("Keys are not sorted: %v", keys))
   288  				}
   289  			}
   290  		case string:
   291  			if inObject && v != "" {
   292  				keys = append(keys, v)
   293  			}
   294  		}
   295  	}
   296  	return errs
   297  }
   298  
   299  func Test_assertJSONKeysSorted(t *testing.T) {
   300  	// this test function is sufficiently complicated enough to warrant its own test...
   301  
   302  	sorted := []byte(`{"a":1,"b":2}`)
   303  	unsorted := []byte(`{"b":2,"a":1}`)
   304  
   305  	nestedSorted := []byte(`{"a":1,"b":{"a":1,"b":2}}`)
   306  	nestedUnsorted := []byte(`{"a":1,"b":{"b":2,"a":1}}`)
   307  
   308  	tests := []struct {
   309  		name    string
   310  		json    []byte
   311  		wantErr require.ErrorAssertionFunc
   312  	}{
   313  		{
   314  			name:    "sorted",
   315  			json:    sorted,
   316  			wantErr: require.NoError,
   317  		},
   318  		{
   319  			name:    "unsorted",
   320  			json:    unsorted,
   321  			wantErr: require.Error,
   322  		},
   323  		{
   324  			name:    "nested sorted",
   325  			json:    nestedSorted,
   326  			wantErr: require.NoError,
   327  		},
   328  		{
   329  			name:    "nested unsorted",
   330  			json:    nestedUnsorted,
   331  			wantErr: require.Error,
   332  		},
   333  	}
   334  
   335  	for _, tt := range tests {
   336  		t.Run(tt.name, func(t *testing.T) {
   337  			if tt.wantErr == nil {
   338  				tt.wantErr = require.NoError
   339  			}
   340  
   341  			err := assertJSONKeysSorted(tt.json)
   342  			tt.wantErr(t, err)
   343  		})
   344  
   345  	}
   346  }