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 }