github.com/wolfi-dev/wolfictl@v0.16.11/pkg/advisory/validate_test.go (about)

     1  package advisory
     2  
     3  import (
     4  	"context"
     5  	"path/filepath"
     6  	"testing"
     7  
     8  	"chainguard.dev/melange/pkg/config"
     9  	"github.com/chainguard-dev/go-apk/pkg/apk"
    10  	"github.com/stretchr/testify/require"
    11  	"github.com/wolfi-dev/wolfictl/pkg/configs"
    12  	v2 "github.com/wolfi-dev/wolfictl/pkg/configs/advisory/v2"
    13  	"github.com/wolfi-dev/wolfictl/pkg/configs/build"
    14  	rwos "github.com/wolfi-dev/wolfictl/pkg/configs/rwfs/os"
    15  )
    16  
    17  func TestValidate(t *testing.T) {
    18  	// The diff validation tests use the test fixtures for advisory.IndexDiff.
    19  
    20  	t.Run("diff", func(t *testing.T) {
    21  		cases := []struct {
    22  			name          string
    23  			shouldBeValid bool
    24  		}{
    25  			{
    26  				name:          "same",
    27  				shouldBeValid: true,
    28  			},
    29  			{
    30  				name:          "added-document",
    31  				shouldBeValid: true,
    32  			},
    33  			{
    34  				name:          "removed-document",
    35  				shouldBeValid: false,
    36  			},
    37  			{
    38  				name:          "added-advisory",
    39  				shouldBeValid: true,
    40  			},
    41  			{
    42  				name:          "removed-advisory",
    43  				shouldBeValid: false,
    44  			},
    45  			{
    46  				name:          "added-event",
    47  				shouldBeValid: true,
    48  			},
    49  			{
    50  				name:          "removed-event",
    51  				shouldBeValid: false,
    52  			},
    53  			{
    54  				name:          "modified-advisory-outside-of-events",
    55  				shouldBeValid: true,
    56  			},
    57  			{
    58  				name:          "added-event-with-non-recent-timestamp",
    59  				shouldBeValid: false,
    60  			},
    61  		}
    62  
    63  		for _, tt := range cases {
    64  			t.Run(tt.name, func(t *testing.T) {
    65  				aDir := filepath.Join("testdata", "diff", tt.name, "a")
    66  				bDir := filepath.Join("testdata", "diff", tt.name, "b")
    67  				aFsys := rwos.DirFS(aDir)
    68  				bFsys := rwos.DirFS(bDir)
    69  				aIndex, err := v2.NewIndex(context.Background(), aFsys)
    70  				require.NoError(t, err)
    71  				bIndex, err := v2.NewIndex(context.Background(), bFsys)
    72  				require.NoError(t, err)
    73  
    74  				err = Validate(context.Background(), ValidateOptions{
    75  					AdvisoryDocs:     bIndex,
    76  					BaseAdvisoryDocs: aIndex,
    77  					Now:              now,
    78  				})
    79  				if tt.shouldBeValid && err != nil {
    80  					t.Errorf("should be valid but got error: %v", err)
    81  				}
    82  				if !tt.shouldBeValid && err == nil {
    83  					t.Error("shouldn't be valid but got no error")
    84  				}
    85  			})
    86  		}
    87  
    88  		t.Run("with existence conditions", func(t *testing.T) {
    89  			cases := []struct {
    90  				name            string
    91  				subcase         string
    92  				packageCfgsFunc func(t *testing.T) *configs.Index[config.Configuration]
    93  				apkindex        *apk.APKIndex
    94  				shouldBeValid   bool
    95  			}{
    96  				{
    97  					name:            "added-document", // these must be in distro
    98  					subcase:         "package in APKINDEX but not distro",
    99  					packageCfgsFunc: distroWithNothing,
   100  					apkindex: &apk.APKIndex{
   101  						Packages: []*apk.Package{
   102  							{
   103  								Name: "ko",
   104  							},
   105  						},
   106  					},
   107  					shouldBeValid: false,
   108  				},
   109  				{
   110  					name:            "added-document",
   111  					subcase:         "package in distro and APKINDEX",
   112  					packageCfgsFunc: distroWithKo,
   113  					apkindex: &apk.APKIndex{
   114  						Packages: []*apk.Package{
   115  							{
   116  								Name:    "kaf",
   117  								Version: "0.2.6-r5",
   118  							},
   119  							{
   120  								Name:    "kaf",
   121  								Version: "0.2.6-r6",
   122  							},
   123  							{
   124  								Name: "ko",
   125  							},
   126  						},
   127  					},
   128  					shouldBeValid: true,
   129  				},
   130  				{
   131  					name:            "added-document",
   132  					subcase:         "package not in distro or APKINDEX",
   133  					packageCfgsFunc: distroWithNothing,
   134  					apkindex: &apk.APKIndex{
   135  						Packages: []*apk.Package{
   136  							{
   137  								Name:    "kaf",
   138  								Version: "0.2.6-r5",
   139  							},
   140  							{
   141  								Name:    "kaf",
   142  								Version: "0.2.6-r6",
   143  							},
   144  						},
   145  					},
   146  					shouldBeValid: false,
   147  				},
   148  				{
   149  					name:            "added-advisory", // i.e. "modified-document", can be just in APKINDEX
   150  					subcase:         "package in APKINDEX but not distro",
   151  					packageCfgsFunc: distroWithNothing,
   152  					apkindex: &apk.APKIndex{
   153  						Packages: []*apk.Package{
   154  							{
   155  								Name: "ko",
   156  							},
   157  						},
   158  					},
   159  					shouldBeValid: true,
   160  				},
   161  				{
   162  					name:            "added-advisory",
   163  					subcase:         "package in distro and APKINDEX",
   164  					packageCfgsFunc: distroWithKo,
   165  					apkindex: &apk.APKIndex{
   166  						Packages: []*apk.Package{
   167  							{
   168  								Name: "ko",
   169  							},
   170  						},
   171  					},
   172  					shouldBeValid: true,
   173  				},
   174  				{
   175  					name:            "added-advisory",
   176  					subcase:         "package not in distro or APKINDEX",
   177  					packageCfgsFunc: distroWithNothing,
   178  					apkindex:        &apk.APKIndex{},
   179  					shouldBeValid:   false,
   180  				},
   181  			}
   182  
   183  			for _, tt := range cases {
   184  				t.Run(tt.name+" -- "+tt.subcase, func(t *testing.T) {
   185  					aDir := filepath.Join("testdata", "diff", tt.name, "a")
   186  					bDir := filepath.Join("testdata", "diff", tt.name, "b")
   187  					aFsys := rwos.DirFS(aDir)
   188  					bFsys := rwos.DirFS(bDir)
   189  					aIndex, err := v2.NewIndex(context.Background(), aFsys)
   190  					require.NoError(t, err)
   191  					bIndex, err := v2.NewIndex(context.Background(), bFsys)
   192  					require.NoError(t, err)
   193  
   194  					err = Validate(context.Background(), ValidateOptions{
   195  						AdvisoryDocs:          bIndex,
   196  						BaseAdvisoryDocs:      aIndex,
   197  						Now:                   now,
   198  						PackageConfigurations: tt.packageCfgsFunc(t),
   199  						APKIndex:              tt.apkindex,
   200  					})
   201  					if tt.shouldBeValid && err != nil {
   202  						t.Errorf("should be valid but got error: %v", err)
   203  					}
   204  					if !tt.shouldBeValid && err == nil {
   205  						t.Error("shouldn't be valid but got no error")
   206  					}
   207  				})
   208  			}
   209  		})
   210  	})
   211  
   212  	t.Run("alias completeness", func(t *testing.T) {
   213  		cases := []struct {
   214  			name          string
   215  			shouldBeValid bool
   216  		}{
   217  			{
   218  				name:          "alias-missing-cve",
   219  				shouldBeValid: false,
   220  			},
   221  			{
   222  				name:          "alias-missing-ghsa",
   223  				shouldBeValid: false,
   224  			},
   225  			{
   226  				name:          "alias-not-missing",
   227  				shouldBeValid: true,
   228  			},
   229  		}
   230  
   231  		mockAF := &mockAliasFinder{
   232  			cveByGHSA: map[string]string{
   233  				"GHSA-2222-2222-2222": "CVE-2222-2222",
   234  			},
   235  			ghsasByCVE: map[string][]string{
   236  				"CVE-2222-2222": {"GHSA-2222-2222-2222"},
   237  			},
   238  		}
   239  
   240  		for _, tt := range cases {
   241  			t.Run(tt.name, func(t *testing.T) {
   242  				dir := filepath.Join("testdata", "validate", tt.name)
   243  				fsys := rwos.DirFS(dir)
   244  				index, err := v2.NewIndex(context.Background(), fsys)
   245  				require.NoError(t, err)
   246  
   247  				err = Validate(context.Background(), ValidateOptions{
   248  					AdvisoryDocs: index,
   249  					AliasFinder:  mockAF,
   250  				})
   251  				if tt.shouldBeValid && err != nil {
   252  					t.Errorf("should be valid but got error: %v", err)
   253  				}
   254  				if !tt.shouldBeValid && err == nil {
   255  					t.Error("shouldn't be valid but got no error")
   256  				}
   257  			})
   258  		}
   259  	})
   260  
   261  	t.Run("fixed versions", func(t *testing.T) {
   262  		t.Run("must exist in APKINDEX", func(t *testing.T) {
   263  			cases := []struct {
   264  				name          string
   265  				apkindex      *apk.APKIndex
   266  				shouldBeValid bool
   267  			}{
   268  				{
   269  					name: "package-missing",
   270  					apkindex: &apk.APKIndex{
   271  						Packages: nil,
   272  					},
   273  					shouldBeValid: false,
   274  				},
   275  				{
   276  					name: "fixed-version-missing",
   277  					apkindex: &apk.APKIndex{
   278  						Packages: []*apk.Package{
   279  							{
   280  								Name:    "ko",
   281  								Version: "1.0.0-r1",
   282  							},
   283  						},
   284  					},
   285  					shouldBeValid: false,
   286  				},
   287  				{
   288  					name: "fixed-version-present-and-first", // which is not allowed
   289  					apkindex: &apk.APKIndex{
   290  						Packages: []*apk.Package{
   291  							{
   292  								Name:    "ko",
   293  								Version: "1.0.0-r2",
   294  							},
   295  						},
   296  					},
   297  					shouldBeValid: false,
   298  				},
   299  				{
   300  					name: "fixed-version-present-and-not-first",
   301  					apkindex: &apk.APKIndex{
   302  						Packages: []*apk.Package{
   303  							{
   304  								Name:    "ko",
   305  								Version: "1.0.0-r1",
   306  							},
   307  							{
   308  								Name:    "ko",
   309  								Version: "1.0.0-r2",
   310  							},
   311  							{
   312  								Name:    "mo",
   313  								Version: "1.0.0-r8",
   314  							},
   315  							{
   316  								Name:    "mo",
   317  								Version: "1.0.0-r9",
   318  							},
   319  							{
   320  								Name:    "mo",
   321  								Version: "1.0.0-r10",
   322  							},
   323  						},
   324  					},
   325  					shouldBeValid: true,
   326  				},
   327  				{
   328  					name: "fixed-version-present-and-not-first-missing-rs",
   329  					apkindex: &apk.APKIndex{
   330  						Packages: []*apk.Package{
   331  							{
   332  								Name:    "ko",
   333  								Version: "1.0.0-r1",
   334  							},
   335  							{
   336  								Name:    "ko",
   337  								Version: "1.0.0-r2",
   338  							},
   339  							{
   340  								Name:    "mo",
   341  								Version: "1.0.0-r8",
   342  							},
   343  							{
   344  								Name:    "mo",
   345  								Version: "1.0.0-r9",
   346  							},
   347  							{
   348  								Name:    "mo",
   349  								Version: "1.0.0-r10",
   350  							},
   351  						},
   352  					},
   353  					shouldBeValid: true,
   354  				},
   355  			}
   356  
   357  			for _, tt := range cases {
   358  				t.Run(tt.name, func(t *testing.T) {
   359  					dir := filepath.Join("testdata", "validate", "fixed-version")
   360  					fsys := rwos.DirFS(dir)
   361  					index, err := v2.NewIndex(context.Background(), fsys)
   362  					require.NoError(t, err)
   363  
   364  					err = Validate(context.Background(), ValidateOptions{
   365  						AdvisoryDocs: index,
   366  						APKIndex:     tt.apkindex,
   367  					})
   368  					if tt.shouldBeValid && err != nil {
   369  						t.Errorf("should be valid but got error: %v", err)
   370  					}
   371  					if !tt.shouldBeValid && err == nil {
   372  						t.Error("shouldn't be valid but got no error")
   373  					}
   374  				})
   375  			}
   376  		})
   377  	})
   378  
   379  	t.Run("duplicate advisories", func(t *testing.T) {
   380  		cases := []struct {
   381  			name          string
   382  			shouldBeValid bool
   383  		}{
   384  			{
   385  				name:          "duplicate-advisory-by-id",
   386  				shouldBeValid: false,
   387  			},
   388  			{
   389  				name:          "duplicate-advisory-by-id-and-alias",
   390  				shouldBeValid: false,
   391  			},
   392  			{
   393  				name:          "no-duplicates",
   394  				shouldBeValid: true,
   395  			},
   396  		}
   397  
   398  		for _, tt := range cases {
   399  			t.Run(tt.name, func(t *testing.T) {
   400  				dir := filepath.Join("testdata", "validate", tt.name)
   401  				fsys := rwos.DirFS(dir)
   402  				index, err := v2.NewIndex(context.Background(), fsys)
   403  				require.NoError(t, err)
   404  
   405  				err = Validate(context.Background(), ValidateOptions{
   406  					AdvisoryDocs: index,
   407  				})
   408  				if tt.shouldBeValid && err != nil {
   409  					t.Errorf("should be valid but got error: %v", err)
   410  				}
   411  				if !tt.shouldBeValid && err == nil {
   412  					t.Error("shouldn't be valid but got no error")
   413  				}
   414  			})
   415  		}
   416  	})
   417  }
   418  
   419  func distroWithKo(t *testing.T) *configs.Index[config.Configuration] {
   420  	fsys := rwos.DirFS(filepath.Join("testdata", "validate", "package-existence", "distro"))
   421  	index, err := build.NewIndex(context.Background(), fsys)
   422  	require.NoError(t, err)
   423  	return index
   424  }
   425  
   426  func distroWithNothing(t *testing.T) *configs.Index[config.Configuration] {
   427  	fsys := rwos.DirFS(filepath.Join("testdata", "validate", "package-existence", "distro-empty"))
   428  	index, err := build.NewIndex(context.Background(), fsys)
   429  	require.NoError(t, err)
   430  	return index
   431  }