Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package grype-db for openSUSE:Factory checked in at 2025-10-24 17:23:40 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/grype-db (Old) and /work/SRC/openSUSE:Factory/.grype-db.new.1980 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "grype-db" Fri Oct 24 17:23:40 2025 rev:20 rq:1313312 version:0.46.0 Changes: -------- --- /work/SRC/openSUSE:Factory/grype-db/grype-db.changes 2025-10-17 17:27:13.029618302 +0200 +++ /work/SRC/openSUSE:Factory/.grype-db.new.1980/grype-db.changes 2025-10-24 17:24:30.546124998 +0200 @@ -1,0 +2,21 @@ +Fri Oct 24 05:08:16 UTC 2025 - Johannes Kastl <[email protected]> + +- Update to version 0.46.0: + * Added Features + - enable emitting unaffected packages for AlmaLinux RPMs from + Alma OSV data [#686 @willmurphyscode] + * Additional Changes + - remove io.ReadAll in tarutils [#718 @willmurphyscode] + - Switch to using runs-on [#720 @wagoodman] + * Dependencies + - chore(deps): update anchore dependencies (#725) + - chore(deps): Bump astral-sh/setup-uv in + /.github/actions/bootstrap (#721) + - chore(deps): Bump github.com/klauspost/compress from 1.18.0 + to 1.18.1 (#722) + - chore(deps): update tools to latest versions (#724) + - chore(deps): Bump github.com/anchore/grype from 0.101.0 to + 0.101.1 (#717) + - chore(deps): update tools to latest versions (#715) + +------------------------------------------------------------------- Old: ---- grype-db-0.45.0.obscpio New: ---- grype-db-0.46.0.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ grype-db.spec ++++++ --- /var/tmp/diff_new_pack.WvbeXv/_old 2025-10-24 17:24:32.274197668 +0200 +++ /var/tmp/diff_new_pack.WvbeXv/_new 2025-10-24 17:24:32.274197668 +0200 @@ -17,7 +17,7 @@ Name: grype-db -Version: 0.45.0 +Version: 0.46.0 Release: 0 Summary: A vulnerability scanner for container images and filesystems License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.WvbeXv/_old 2025-10-24 17:24:32.326199855 +0200 +++ /var/tmp/diff_new_pack.WvbeXv/_new 2025-10-24 17:24:32.330200023 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/anchore/grype-db</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v0.45.0</param> + <param name="revision">v0.46.0</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.WvbeXv/_old 2025-10-24 17:24:32.374201874 +0200 +++ /var/tmp/diff_new_pack.WvbeXv/_new 2025-10-24 17:24:32.378202042 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/anchore/grype-db</param> - <param name="changesrevision">342891aa516b3f52bd4e04ad052be454c261cdc0</param></service></servicedata> + <param name="changesrevision">127e29f2e1331d0368bed7b241c8f19069ecee54</param></service></servicedata> (No newline at EOF) ++++++ grype-db-0.45.0.obscpio -> grype-db-0.46.0.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grype-db-0.45.0/.binny.yaml new/grype-db-0.46.0/.binny.yaml --- old/grype-db-0.45.0/.binny.yaml 2025-10-15 19:59:51.000000000 +0200 +++ new/grype-db-0.46.0/.binny.yaml 2025-10-23 13:33:17.000000000 +0200 @@ -26,7 +26,7 @@ # used to release all artifacts - name: goreleaser version: - want: v2.12.5 + want: v2.12.6 method: github-release with: repo: goreleaser/goreleaser @@ -58,7 +58,7 @@ # used for triggering a release - name: gh version: - want: v2.81.0 + want: v2.82.1 method: github-release with: repo: cli/cli diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grype-db-0.45.0/data/vulnerability-match-labels/labels/docker.io+centos@sha256:3688aa867eb84332460e172b9250c9c198fdfd8d987605fd53f246f498c60bcf/f91f8814-9cdc-4325-8ae6-eb87305ff29b.json new/grype-db-0.46.0/data/vulnerability-match-labels/labels/docker.io+centos@sha256:3688aa867eb84332460e172b9250c9c198fdfd8d987605fd53f246f498c60bcf/f91f8814-9cdc-4325-8ae6-eb87305ff29b.json --- old/grype-db-0.45.0/data/vulnerability-match-labels/labels/docker.io+centos@sha256:3688aa867eb84332460e172b9250c9c198fdfd8d987605fd53f246f498c60bcf/f91f8814-9cdc-4325-8ae6-eb87305ff29b.json 2025-10-15 19:59:51.000000000 +0200 +++ new/grype-db-0.46.0/data/vulnerability-match-labels/labels/docker.io+centos@sha256:3688aa867eb84332460e172b9250c9c198fdfd8d987605fd53f246f498c60bcf/f91f8814-9cdc-4325-8ae6-eb87305ff29b.json 2025-10-23 13:33:17.000000000 +0200 @@ -1 +1 @@ -{"ID": "f91f8814-9cdc-4325-8ae6-eb87305ff29b", "effective_cve": "CVE-2016-2183", "image": {"exact": "docker.io/centos@sha256:3688aa867eb84332460e172b9250c9c198fdfd8d987605fd53f246f498c60bcf"}, "label": "TP", "package": {"name": "openssl", "version": "1.0.1e-57.el6"}, "timestamp": "2022-09-15T18:56:49-04:00", "user": "wagoodman", "vulnerability_id": "CVE-2016-2183"} \ No newline at end of file +{"ID": "f91f8814-9cdc-4325-8ae6-eb87305ff29b", "effective_cve": "CVE-2016-2183", "image": {"exact": "docker.io/centos@sha256:3688aa867eb84332460e172b9250c9c198fdfd8d987605fd53f246f498c60bcf"}, "label": "FP", "package": {"name": "openssl", "version": "1.0.1e-57.el6"}, "timestamp": "2022-09-15T18:56:49-04:00", "user": "wagoodman", "vulnerability_id": "CVE-2016-2183"} \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grype-db-0.45.0/go.mod new/grype-db-0.46.0/go.mod --- old/grype-db-0.45.0/go.mod 2025-10-15 19:59:51.000000000 +0200 +++ new/grype-db-0.46.0/go.mod 2025-10-23 13:33:17.000000000 +0200 @@ -8,9 +8,9 @@ github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/adrg/xdg v0.5.3 github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 - github.com/anchore/grype v0.101.0 + github.com/anchore/grype v0.102.0 github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 - github.com/anchore/syft v1.34.1 + github.com/anchore/syft v1.36.0 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/dave/jennifer v1.7.1 github.com/dustin/go-humanize v1.0.1 @@ -26,7 +26,7 @@ github.com/hashicorp/go-multierror v1.1.1 github.com/iancoleman/strcase v0.3.0 github.com/jinzhu/copier v0.4.0 - github.com/klauspost/compress v1.18.0 + github.com/klauspost/compress v1.18.1 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.5.0 github.com/openvex/go-vex v0.2.7 @@ -78,7 +78,7 @@ github.com/anchore/archiver/v3 v3.5.3-0.20241210171143-5b1d8d1c7c51 // indirect github.com/anchore/clio v0.0.0-20250715152405-a0fa658e5084 // indirect github.com/anchore/fangs v0.0.0-20250716230140-94c22408c232 // indirect - github.com/anchore/go-collections v0.0.0-20241211140901-567f400e9a46 // indirect + github.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c // indirect github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d // indirect github.com/anchore/go-lzo v0.1.0 // indirect github.com/anchore/go-macholibre v0.0.0-20250320151634-807da7ad2331 // indirect @@ -162,7 +162,7 @@ github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect - github.com/github/go-spdx/v2 v2.3.3 // indirect + github.com/github/go-spdx/v2 v2.3.4 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grype-db-0.45.0/go.sum new/grype-db-0.46.0/go.sum --- old/grype-db-0.45.0/go.sum 2025-10-15 19:59:51.000000000 +0200 +++ new/grype-db-0.46.0/go.sum 2025-10-23 13:33:17.000000000 +0200 @@ -134,8 +134,8 @@ github.com/anchore/clio v0.0.0-20250715152405-a0fa658e5084/go.mod h1:42dWox8z4//b898OIELsQnSdYq9q1aCXkwp5fKF+BEU= github.com/anchore/fangs v0.0.0-20250716230140-94c22408c232 h1:aVC6r9h5wGNh8BYTW3CXxOdPoZzY/bBRWne1NvSTlO8= github.com/anchore/fangs v0.0.0-20250716230140-94c22408c232/go.mod h1:Zees1AEKNpXIRgdVAMYWITncarLFiPOtEQ7rl45V/h0= -github.com/anchore/go-collections v0.0.0-20241211140901-567f400e9a46 h1:huvprHsfzhrIIkk7kja1Fm5Wn3mnwPv4CeHrGlGD3ds= -github.com/anchore/go-collections v0.0.0-20241211140901-567f400e9a46/go.mod h1:1aiktV46ATCkuVg0O573ZrH56BUawTECPETbZyBcqT8= +github.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c h1:eoJXyC0n7DZ4YvySG/ETdYkTar2Due7eH+UmLK6FbrA= +github.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c/go.mod h1:1aiktV46ATCkuVg0O573ZrH56BUawTECPETbZyBcqT8= github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d h1:gT69osH9AsdpOfqxbRwtxcNnSZ1zg4aKy2BevO3ZBdc= github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d/go.mod h1:PhSnuFYknwPZkOWKB1jXBNToChBA+l0FjwOxtViIc50= github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4VyBzhjLkRF/3gDrcpUBj8LjvvO6OOM= @@ -155,14 +155,14 @@ github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ= github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 h1:rmZG77uXgE+o2gozGEBoUMpX27lsku+xrMwlmBZJtbg= github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= -github.com/anchore/grype v0.101.0 h1:8b+Oj0BZLNSBMhryWckSjGKdvqVx2NNYTM+pnbL3UkA= -github.com/anchore/grype v0.101.0/go.mod h1:+PcBoFeG0beVdTPbMUNIxkNdQU0IR1FUXOXdpX2peuM= +github.com/anchore/grype v0.102.0 h1:yf8YKGklukxIobObK2WQEYEp8mU3ofTnB4+cBR7S9vE= +github.com/anchore/grype v0.102.0/go.mod h1:9EFzjrlx81aP37YKpxktXSbAhgJ8jNCm4jP9tim9jww= github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY= github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI= github.com/anchore/stereoscope v0.1.11 h1:YP/XUNcJyMbOOPAWPkeZNCVlKKTRO2cnBTEeUW6I40Y= github.com/anchore/stereoscope v0.1.11/go.mod h1:G3PZlzPbxFhylj9pQwtqfVPaahuWmy/UCtv5FTIIMvg= -github.com/anchore/syft v1.34.1 h1:OdM9guARidtMPBL6ju83vV/GauZ6Tb6UwhFlLyLHbNw= -github.com/anchore/syft v1.34.1/go.mod h1:J9fOxYe2o9I5sML6ntNF2uiPYZ+vwcWVPM26tCSyf3M= +github.com/anchore/syft v1.36.0 h1:vmrQz/eCPEdniHi2XRqEXxpvO3Q3wHL9o+YcE45XtUI= +github.com/anchore/syft v1.36.0/go.mod h1:DdJMDHhI2V7pOjC/5FL98BKbG2DkbIT5zYmig6AORdU= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= @@ -415,8 +415,8 @@ github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/github/go-spdx/v2 v2.3.3 h1:QI7evnHWEfWkT54eJwkoV/f3a0xD3gLlnVmT5wQG6LE= -github.com/github/go-spdx/v2 v2.3.3/go.mod h1:2ZxKsOhvBp+OYBDlsGnUMcchLeo2mrpEBn2L1C+U3IQ= +github.com/github/go-spdx/v2 v2.3.4 h1:6VNAsYWvQge+SOeoubTlH81MY21d5uekXNIRGfXMNXo= +github.com/github/go-spdx/v2 v2.3.4/go.mod h1:7LYNCshU2Gj17qZ0heJ5CQUKWWmpd98K7o93K8fJSMk= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= @@ -667,8 +667,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grype-db-0.45.0/internal/tarutil/reader_entry.go new/grype-db-0.46.0/internal/tarutil/reader_entry.go --- old/grype-db-0.45.0/internal/tarutil/reader_entry.go 2025-10-15 19:59:51.000000000 +0200 +++ new/grype-db-0.46.0/internal/tarutil/reader_entry.go 2025-10-23 13:33:17.000000000 +0200 @@ -3,6 +3,7 @@ import ( "archive/tar" "bytes" + "fmt" "io" "os" @@ -32,6 +33,65 @@ }) } +// autoDeleteFile wraps an *os.File and deletes it when closed. +type autoDeleteFile struct { + *os.File +} + +func (f *autoDeleteFile) Close() error { + name := f.Name() + err := f.File.Close() + if removeErr := os.Remove(name); removeErr != nil && err == nil { + err = removeErr + } + return err +} + +// readerWithSize determines the size of the reader's content without reading the entire content into memory. +// For known reader types (bytes.Reader, os.File), it queries the size directly. +// For unknown types, it copies to a temp file to avoid loading into memory. +// Returns the size, a ReadCloser for the content (may be different from input), and any error. +func readerWithSize(reader io.Reader) (int64, io.ReadCloser, error) { + switch r := reader.(type) { + case *bytes.Reader: + // For bytes.Reader (used by NewEntryFromBytes), get actual size + return r.Size(), io.NopCloser(reader), nil + case interface{ Stat() (os.FileInfo, error) }: + // For *os.File, use Stat to get size + stat, err := r.Stat() + if err != nil { + return 0, nil, err + } + // Check if it's already a ReadCloser + if rc, ok := reader.(io.ReadCloser); ok { + return stat.Size(), rc, nil + } + return 0, nil, fmt.Errorf("reader with Stat() must implement io.ReadCloser") + default: + // Fallback for unknown reader types: copy to temp file to avoid loading into memory + tmpFile, err := os.CreateTemp("", "grype-db-tar-*") + if err != nil { + return 0, nil, fmt.Errorf("unable to create temp file: %w", err) + } + + size, err := io.Copy(tmpFile, reader) + if err != nil { + tmpFile.Close() + os.Remove(tmpFile.Name()) + return 0, nil, fmt.Errorf("unable to copy to temp file: %w", err) + } + + // Seek back to beginning for reading + if _, err := tmpFile.Seek(0, 0); err != nil { + tmpFile.Close() + os.Remove(tmpFile.Name()) + return 0, nil, fmt.Errorf("unable to seek temp file: %w", err) + } + + return size, &autoDeleteFile{File: tmpFile}, nil + } +} + func writeEntry(tw lowLevelWriter, filename string, fileInfo os.FileInfo, opener func() (io.Reader, error)) error { log.WithFields("path", filename).Trace("adding file to archive") @@ -69,17 +129,20 @@ return err } - contents, err := io.ReadAll(reader) + size, readCloser, err := readerWithSize(reader) if err != nil { return err } - header.Size = int64(len(contents)) + defer readCloser.Close() + + header.Size = size if err := tw.WriteHeader(header); err != nil { return err } - if _, err := tw.Write(contents); err != nil { + // Stream the file contents directly to the tar writer + if _, err := io.Copy(tw, readCloser); err != nil { return err } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grype-db-0.45.0/internal/tarutil/reader_entry_test.go new/grype-db-0.46.0/internal/tarutil/reader_entry_test.go --- old/grype-db-0.45.0/internal/tarutil/reader_entry_test.go 2025-10-15 19:59:51.000000000 +0200 +++ new/grype-db-0.46.0/internal/tarutil/reader_entry_test.go 2025-10-23 13:33:17.000000000 +0200 @@ -2,9 +2,12 @@ import ( "archive/tar" + "bytes" + "io" "io/fs" "os" "path/filepath" + "strings" "testing" "time" @@ -141,3 +144,97 @@ }) } } + +func Test_readerWithSize(t *testing.T) { + testData := "hello world from test" + + tests := []struct { + name string + reader func(t *testing.T) io.Reader + wantSize int64 + wantErr require.ErrorAssertionFunc + }{ + { + name: "bytes.Reader", + reader: func(t *testing.T) io.Reader { + return bytes.NewReader([]byte(testData)) + }, + wantSize: int64(len(testData)), + }, + { + name: "os.File success", + reader: func(t *testing.T) io.Reader { + dir := t.TempDir() + path := filepath.Join(dir, "test.txt") + require.NoError(t, os.WriteFile(path, []byte(testData), 0644)) + f, err := os.Open(path) + require.NoError(t, err) + t.Cleanup(func() { f.Close() }) + return f + }, + wantSize: int64(len(testData)), + }, + { + name: "os.File stat fails", + reader: func(t *testing.T) io.Reader { + dir := t.TempDir() + path := filepath.Join(dir, "test.txt") + require.NoError(t, os.WriteFile(path, []byte(testData), 0644)) + f, err := os.Open(path) + require.NoError(t, err) + f.Close() + return f + }, + wantErr: require.Error, + }, + { + name: "unknown reader creates temp file", + reader: func(t *testing.T) io.Reader { + return strings.NewReader(testData) + }, + wantSize: int64(len(testData)), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantErr == nil { + tt.wantErr = require.NoError + } + + reader := tt.reader(t) + size, rc, err := readerWithSize(reader) + tt.wantErr(t, err) + if err != nil { + return + } + defer rc.Close() + + assert.Equal(t, tt.wantSize, size) + + content, err := io.ReadAll(rc) + require.NoError(t, err) + assert.Equal(t, testData, string(content)) + }) + } +} + +func Test_autoDeleteFile(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "test.txt") + require.NoError(t, os.WriteFile(path, []byte("test content"), 0644)) + + f, err := os.Open(path) + require.NoError(t, err) + + adf := &autoDeleteFile{File: f} + + _, err = os.Stat(path) + require.NoError(t, err) + + err = adf.Close() + require.NoError(t, err) + + _, err = os.Stat(path) + assert.True(t, os.IsNotExist(err)) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grype-db-0.45.0/pkg/process/v6/transformers/osv/test-fixtures/ALSA-2025-7467.json new/grype-db-0.46.0/pkg/process/v6/transformers/osv/test-fixtures/ALSA-2025-7467.json --- old/grype-db-0.45.0/pkg/process/v6/transformers/osv/test-fixtures/ALSA-2025-7467.json 1970-01-01 01:00:00.000000000 +0100 +++ new/grype-db-0.46.0/pkg/process/v6/transformers/osv/test-fixtures/ALSA-2025-7467.json 2025-10-23 13:33:17.000000000 +0200 @@ -0,0 +1,61 @@ +{ + "id": "ALSA-2025:7467", + "summary": "Moderate: skopeo security update", + "aliases": [ + "CVE-2025-27144" + ], + "affected": [ + { + "package": { + "ecosystem": "AlmaLinux:10", + "name": "skopeo" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "0" + }, + { + "fixed": "2:1.18.1-1.el10_0" + } + ] + } + ] + }, + { + "package": { + "ecosystem": "AlmaLinux:10", + "name": "skopeo-tests" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "0" + }, + { + "fixed": "2:1.18.1-1.el10_0" + } + ] + } + ] + } + ], + "published": "2025-05-13T00:00:00Z", + "modified": "2025-07-02T12:50:06Z", + "details": "The skopeo command lets you inspect images from container image registries.", + "references": [ + { + "url": "https://errata.almalinux.org/10/ALSA-2025-7467.html", + "type": "ADVISORY" + } + ], + "database_specific": { + "anchore": { + "record_type": "advisory" + } + } +} \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grype-db-0.45.0/pkg/process/v6/transformers/osv/transform.go new/grype-db-0.46.0/pkg/process/v6/transformers/osv/transform.go --- old/grype-db-0.45.0/pkg/process/v6/transformers/osv/transform.go 2025-10-15 19:59:51.000000000 +0200 +++ new/grype-db-0.46.0/pkg/process/v6/transformers/osv/transform.go 2025-10-23 13:33:17.000000000 +0200 @@ -4,11 +4,13 @@ "fmt" "regexp" "sort" + "strconv" "strings" "github.com/google/osv-scanner/pkg/models" "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/internal/codename" "github.com/anchore/grype-db/pkg/process/internal/common" "github.com/anchore/grype-db/pkg/process/v6/transformers" "github.com/anchore/grype-db/pkg/process/v6/transformers/internal" @@ -19,12 +21,23 @@ "github.com/anchore/syft/syft/pkg" ) +const ( + almaLinux = "almalinux" +) + func Transform(vulnerability unmarshal.OSVVulnerability, state provider.State) ([]data.Entry, error) { severities, err := getSeverities(vulnerability) if err != nil { return nil, fmt.Errorf("unable to obtain severities: %w", err) } + isAdvisory := isAdvisoryRecord(vulnerability) + aliases := vulnerability.Aliases + + if isAdvisory { + aliases = append(aliases, vulnerability.Related...) + } + in := []any{ grypeDB.VulnerabilityHandle{ Name: vulnerability.ID, @@ -38,14 +51,23 @@ Assigners: nil, Description: vulnerability.Details, References: getReferences(vulnerability), - Aliases: vulnerability.Aliases, + Aliases: aliases, Severities: severities, }, }, } - for _, a := range getAffectedPackages(vulnerability) { - in = append(in, a) + // Check if this is an advisory record + if isAdvisory { + // For advisory records, emit unaffected packages + for _, u := range getUnaffectedPackages(vulnerability) { + in = append(in, u) + } + } else { + // For vulnerability records, emit affected packages + for _, a := range getAffectedPackages(vulnerability) { + in = append(in, a) + } } return transformers.NewEntries(in...), nil @@ -67,19 +89,20 @@ var aphs []grypeDB.AffectedPackageHandle for _, affected := range vuln.Affected { aph := grypeDB.AffectedPackageHandle{ - Package: getPackage(affected.Package), - BlobValue: &grypeDB.PackageBlob{CVEs: vuln.Aliases}, + Package: getPackage(affected.Package), + OperatingSystem: getOperatingSystemFromEcosystem(string(affected.Package.Ecosystem)), + BlobValue: &grypeDB.PackageBlob{CVEs: vuln.Aliases}, } - if withCPE { - aph.BlobValue.Qualifiers = &grypeDB.PackageQualifiers{ - PlatformCPEs: cpes.([]string), - } + // Extract qualifiers (CPE and RPM modularity) + qualifiers := getPackageQualifiers(affected, cpes, withCPE) + if qualifiers != nil { + aph.BlobValue.Qualifiers = qualifiers } var ranges []grypeDB.Range for _, r := range affected.Ranges { - ranges = append(ranges, getGrypeRangesFromRange(r)...) + ranges = append(ranges, getGrypeRangesFromRange(r, string(affected.Package.Ecosystem))...) } aph.BlobValue.Ranges = ranges aphs = append(aphs, aph) @@ -91,6 +114,49 @@ return aphs } +// getPackageQualifiers extracts package qualifiers from affected package data +// including CPE information and RPM modularity +func getPackageQualifiers(affected models.Affected, cpes any, withCPE bool) *grypeDB.PackageQualifiers { + var qualifiers *grypeDB.PackageQualifiers + + // Handle CPE qualifiers (existing logic) + if withCPE { + qualifiers = &grypeDB.PackageQualifiers{ + PlatformCPEs: cpes.([]string), + } + } + + // Extract RPM modularity from ecosystem_specific + rpmModularity := extractRpmModularity(affected) + if rpmModularity != "" { + if qualifiers == nil { + qualifiers = &grypeDB.PackageQualifiers{} + } + qualifiers.RpmModularity = &rpmModularity + } + + return qualifiers +} + +// extractRpmModularity extracts RPM modularity information from affected package ecosystem_specific +func extractRpmModularity(affected models.Affected) string { + if affected.EcosystemSpecific == nil { + return "" + } + + rpmModularity, ok := affected.EcosystemSpecific["rpm_modularity"] + if !ok { + return "" + } + + rpmModularityStr, ok := rpmModularity.(string) + if !ok { + return "" + } + + return rpmModularityStr +} + // OSV supports flattered ranges, so both formats below are valid: // "ranges": [ // @@ -139,7 +205,7 @@ // } // // ] -func getGrypeRangesFromRange(r models.Range) []grypeDB.Range { // nolint: gocognit +func getGrypeRangesFromRange(r models.Range, ecosystem string) []grypeDB.Range { // nolint: gocognit var ranges []grypeDB.Range if len(r.Events) == 0 { return nil @@ -184,7 +250,7 @@ } } - rangeType := normalizeRangeType(r.Type) + rangeType := normalizeRangeType(r.Type, ecosystem) for _, e := range r.Events { switch { case e.Introduced != "" && e.Introduced != "0": @@ -234,7 +300,7 @@ } func normalizeConstraint(constraint string, rangeType string) string { - if rangeType == "semver" { + if rangeType == "semver" || rangeType == "bitnami" { return common.EnforceSemVerConstraint(constraint) } return constraint @@ -254,7 +320,12 @@ } } -func normalizeRangeType(t models.RangeType) string { +func normalizeRangeType(t models.RangeType, ecosystem string) string { + // For Bitnami ecosystem, use "bitnami" format instead of "semver" + if ecosystem == "Bitnami" && t == models.RangeSemVer { + return "bitnami" + } + switch t { case models.RangeSemVer, models.RangeEcosystem, models.RangeGit: return strings.ToLower(string(t)) @@ -264,17 +335,63 @@ } func getPackage(p models.Package) *grypeDB.Package { + // Try to determine package type from ecosystem or PURL + var pkgType pkg.Type + var ecosystem string + + if p.Purl != "" { + pkgType = pkg.TypeFromPURL(p.Purl) + ecosystem = string(p.Ecosystem) + } else { + pkgType = getPackageTypeFromEcosystem(string(p.Ecosystem)) + // If we found a package type from OS ecosystem, use it; otherwise use original ecosystem + if pkgType != "" { + ecosystem = string(pkgType) + } else { + ecosystem = string(p.Ecosystem) + } + } + return &grypeDB.Package{ - Ecosystem: string(p.Ecosystem), - Name: name.Normalize(p.Name, pkg.TypeFromPURL(p.Purl)), + Ecosystem: ecosystem, + Name: name.Normalize(p.Name, pkgType), } } +// getPackageTypeFromEcosystem determines package type from OSV ecosystem +// Currently only supports AlmaLinux; other ecosystems use PURL-based detection +func getPackageTypeFromEcosystem(ecosystem string) pkg.Type { + if ecosystem == "" { + return "" + } + + // Split ecosystem by colon to get OS name + parts := strings.Split(ecosystem, ":") + osName := strings.ToLower(parts[0]) + + // Only handle AlmaLinux + if osName == almaLinux { + return pkg.RpmPkg + } + + // For other ecosystems (like Bitnami, npm, pypi, etc.), return empty type + // The package type will be determined from PURL if available + return "" +} + func getReferences(vuln unmarshal.OSVVulnerability) []grypeDB.Reference { var refs []grypeDB.Reference for _, ref := range vuln.References { + // For advisory references, use the vulnerability ID as the advisory ID + // This allows tools consuming the data to link back to the specific advisory + refID := "" + if ref.Type == models.ReferenceAdvisory && isAdvisoryRecord(vuln) { + refID = vuln.ID + } + refs = append(refs, grypeDB.Reference{ + ID: refID, URL: ref.URL, Tags: []string{string(ref.Type)}, }, @@ -341,3 +458,232 @@ return severities, nil } + +// getOperatingSystemFromEcosystem extracts operating system information from OSV ecosystem field +// Currently only supports AlmaLinux ecosystems +// Example: "AlmaLinux:8" -> almalinux 8 +func getOperatingSystemFromEcosystem(ecosystem string) *grypeDB.OperatingSystem { + if ecosystem == "" { + return nil + } + + // Split ecosystem by colon to get components + parts := strings.Split(ecosystem, ":") + if len(parts) < 2 { + return nil + } + + osName := strings.ToLower(parts[0]) + + // Only handle AlmaLinux + if osName != almaLinux { + return nil + } + + osVersion := parts[1] + + // Parse version into major/minor components + versionFields := strings.Split(osVersion, ".") + var majorVersion, minorVersion string + if len(versionFields) > 0 { + majorVersion = versionFields[0] + // Check if the first field is actually a number + if _, err := strconv.Atoi(majorVersion[0:1]); err != nil { + // If not numeric, treat the whole thing as a label version + return &grypeDB.OperatingSystem{ + Name: normalizeOSName(osName), + LabelVersion: osVersion, + Codename: codename.LookupOS(normalizeOSName(osName), "", ""), + } + } + if len(versionFields) > 1 { + minorVersion = versionFields[1] + } + } + + return &grypeDB.OperatingSystem{ + Name: normalizeOSName(osName), + MajorVersion: majorVersion, + MinorVersion: minorVersion, + Codename: codename.LookupOS(normalizeOSName(osName), majorVersion, minorVersion), + } +} + +// normalizeOSName normalizes operating system names for consistency +// Currently only supports AlmaLinux +func normalizeOSName(osName string) string { + osName = strings.ToLower(osName) + + // Only handle AlmaLinux + if osName == almaLinux { + return almaLinux + } + + return osName +} + +// isAdvisoryRecord checks if the OSV record is marked as an advisory +func isAdvisoryRecord(vuln unmarshal.OSVVulnerability) bool { + if vuln.DatabaseSpecific == nil { + return false + } + + anchoreData, ok := vuln.DatabaseSpecific["anchore"] + if !ok { + return false + } + + anchoreMap, ok := anchoreData.(map[string]any) + if !ok { + return false + } + + recordType, ok := anchoreMap["record_type"] + if !ok { + return false + } + + recordTypeStr, ok := recordType.(string) + if !ok { + return false + } + + return recordTypeStr == "advisory" +} + +// getUnaffectedPackages creates UnaffectedPackageHandle entries for advisory records +func getUnaffectedPackages(vuln unmarshal.OSVVulnerability) []grypeDB.UnaffectedPackageHandle { + if len(vuln.Affected) == 0 { + return nil + } + + var uphs []grypeDB.UnaffectedPackageHandle + for _, affected := range vuln.Affected { + uph := grypeDB.UnaffectedPackageHandle{ + Package: getPackage(affected.Package), + OperatingSystem: getOperatingSystemFromEcosystem(string(affected.Package.Ecosystem)), + BlobValue: getUnaffectedBlob(vuln.Aliases, affected.Ranges, affected), + } + uphs = append(uphs, uph) + } + + // stable ordering + sort.Sort(internal.ByUnaffectedPackage(uphs)) + + return uphs +} + +// getUnaffectedBlob creates a package blob for unaffected packages (advisories) +// For advisories, we need to invert the ranges to represent unaffected versions +func getUnaffectedBlob(aliases []string, ranges []models.Range, affected models.Affected) *grypeDB.PackageBlob { + var grypeRanges []grypeDB.Range + ecosystem := string(affected.Package.Ecosystem) + for _, r := range ranges { + grypeRanges = append(grypeRanges, getGrypeUnaffectedRangesFromRange(r, ecosystem)...) + } + + // Extract qualifiers including RPM modularity + qualifiers := getPackageQualifiers(affected, nil, false) + + return &grypeDB.PackageBlob{ + CVEs: aliases, + Ranges: grypeRanges, + Qualifiers: qualifiers, + } +} + +// getGrypeUnaffectedRangesFromRange converts OSV ranges to unaffected version ranges for unaffected packages +// This inverts the logic: instead of "< fix_version" (affected), we create ">= fix_version" (unaffected) +func getGrypeUnaffectedRangesFromRange(r models.Range, ecosystem string) []grypeDB.Range { + if len(r.Events) == 0 { + return nil + } + + fixByVersion := extractFixAvailability(r) + rangeType := normalizeRangeType(r.Type, ecosystem) + + return buildUnaffectedRangesFromEvents(r.Events, fixByVersion, rangeType) +} + +// extractFixAvailability extracts fix availability information from DatabaseSpecific +func extractFixAvailability(r models.Range) map[string]grypeDB.FixAvailability { + fixByVersion := make(map[string]grypeDB.FixAvailability) + + dbSpecific, hasDBSpecific := r.DatabaseSpecific["anchore"] + if !hasDBSpecific { + return fixByVersion + } + + anchoreInfo, isMap := dbSpecific.(map[string]any) + if !isMap { + return fixByVersion + } + + fixes, hasFixes := anchoreInfo["fixes"] + if !hasFixes { + return fixByVersion + } + + fixList, isList := fixes.([]any) + if !isList { + return fixByVersion + } + + for _, fixEntry := range fixList { + parseSingleFixEntry(fixEntry, fixByVersion) + } + + return fixByVersion +} + +// parseSingleFixEntry parses a single fix entry and adds it to the fixByVersion map +func parseSingleFixEntry(fixEntry any, fixByVersion map[string]grypeDB.FixAvailability) { + fixMap, isMap := fixEntry.(map[string]any) + if !isMap { + return + } + + version, vOk := fixMap["version"].(string) + kind, kOk := fixMap["kind"].(string) + date, dOk := fixMap["date"].(string) + + if vOk && kOk && dOk { + fixByVersion[version] = grypeDB.FixAvailability{ + Date: internal.ParseTime(date), + Kind: kind, + } + } +} + +// buildUnaffectedRangesFromEvents processes events to create unaffected version ranges +func buildUnaffectedRangesFromEvents(events []models.Event, fixByVersion map[string]grypeDB.FixAvailability, rangeType string) []grypeDB.Range { + var ranges []grypeDB.Range + + for _, e := range events { + if e.Fixed != "" { + unaffectedRange := createUnaffectedRange(e.Fixed, fixByVersion, rangeType) + ranges = append(ranges, unaffectedRange) + } + } + + return ranges +} + +// createUnaffectedRange creates a single safe range for a fixed version +func createUnaffectedRange(fixedVersion string, fixByVersion map[string]grypeDB.FixAvailability, rangeType string) grypeDB.Range { + var detail *grypeDB.FixDetail + if f, ok := fixByVersion[fixedVersion]; ok { + detail = &grypeDB.FixDetail{ + Available: &f, + } + } + + constraint := fmt.Sprintf(">= %s", fixedVersion) + return grypeDB.Range{ + Fix: normalizeFix(fixedVersion, detail), + Version: grypeDB.Version{ + Type: rangeType, + Constraint: normalizeConstraint(constraint, rangeType), + }, + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/grype-db-0.45.0/pkg/process/v6/transformers/osv/transform_test.go new/grype-db-0.46.0/pkg/process/v6/transformers/osv/transform_test.go --- old/grype-db-0.45.0/pkg/process/v6/transformers/osv/transform_test.go 2025-10-15 19:59:51.000000000 +0200 +++ new/grype-db-0.46.0/pkg/process/v6/transformers/osv/transform_test.go 2025-10-23 13:33:17.000000000 +0200 @@ -68,6 +68,14 @@ return r } +func unaffectedPkgSlice(u ...grypeDB.UnaffectedPackageHandle) []any { + var r []any + for _, v := range u { + r = append(r, v) + } + return r +} + func TestTransform(t *testing.T) { tests := []struct { name string @@ -115,7 +123,7 @@ CVEs: []string{"CVE-2020-11984"}, Ranges: []grypeDB.Range{{ Version: grypeDB.Version{ - Type: "semver", + Type: "bitnami", Constraint: ">=2.4.32,<=2.4.43", }, }}, @@ -165,7 +173,7 @@ CVEs: []string{"CVE-2020-8201"}, Ranges: []grypeDB.Range{{ Version: grypeDB.Version{ - Type: "semver", + Type: "bitnami", Constraint: ">=12.0.0,<12.18.4", }, Fix: &grypeDB.Fix{ @@ -180,7 +188,7 @@ }, }, { Version: grypeDB.Version{ - Type: "semver", + Type: "bitnami", Constraint: ">=14.0.0,<14.11.0", }, Fix: &grypeDB.Fix{ @@ -199,6 +207,79 @@ ), }}, }, + { + name: "AlmaLinux Advisory", + fixturePath: "test-fixtures/ALSA-2025-7467.json", + want: []transformers.RelatedEntries{{ + VulnerabilityHandle: &grypeDB.VulnerabilityHandle{ + Name: "ALSA-2025:7467", + Status: grypeDB.VulnerabilityActive, + ProviderID: "osv", + Provider: expectedProvider(), + ModifiedDate: timeRef(time.Date(2025, time.July, 2, 12, 50, 6, 0, time.UTC)), + PublishedDate: timeRef(time.Date(2025, time.May, 13, 0, 0, 0, 0, time.UTC)), + BlobValue: &grypeDB.VulnerabilityBlob{ + ID: "ALSA-2025:7467", + Description: "The skopeo command lets you inspect images from container image registries.", + References: []grypeDB.Reference{{ + ID: "ALSA-2025:7467", + URL: "https://errata.almalinux.org/10/ALSA-2025-7467.html", + Tags: []string{"ADVISORY"}, + }}, + Aliases: []string{"CVE-2025-27144"}, + Severities: nil, + }, + }, + Related: unaffectedPkgSlice( + grypeDB.UnaffectedPackageHandle{ + Package: &grypeDB.Package{ + Name: "skopeo", + Ecosystem: "rpm", + }, + OperatingSystem: &grypeDB.OperatingSystem{ + Name: "almalinux", + MajorVersion: "10", + }, + BlobValue: &grypeDB.PackageBlob{ + CVEs: []string{"CVE-2025-27144"}, + Ranges: []grypeDB.Range{{ + Version: grypeDB.Version{ + Type: "ecosystem", + Constraint: ">= 2:1.18.1-1.el10_0", + }, + Fix: &grypeDB.Fix{ + Version: "2:1.18.1-1.el10_0", + State: grypeDB.FixedStatus, + }, + }}, + }, + }, + grypeDB.UnaffectedPackageHandle{ + Package: &grypeDB.Package{ + Name: "skopeo-tests", + Ecosystem: "rpm", + }, + OperatingSystem: &grypeDB.OperatingSystem{ + Name: "almalinux", + MajorVersion: "10", + }, + BlobValue: &grypeDB.PackageBlob{ + CVEs: []string{"CVE-2025-27144"}, + Ranges: []grypeDB.Range{{ + Version: grypeDB.Version{ + Type: "ecosystem", + Constraint: ">= 2:1.18.1-1.el10_0", + }, + Fix: &grypeDB.Fix{ + Version: "2:1.18.1-1.el10_0", + State: grypeDB.FixedStatus, + }, + }}, + }, + }, + ), + }}, + }, } t.Parallel() for _, testToRun := range tests { @@ -225,12 +306,14 @@ } func Test_getGrypeRangesFromRange(t *testing.T) { tests := []struct { - name string - rnge models.Range - want []grypeDB.Range + name string + rnge models.Range + ecosystem string + want []grypeDB.Range }{ { - name: "single range with 'fixed' status", + name: "single range with 'fixed' status", + ecosystem: "npm", rnge: models.Range{ Type: models.RangeSemVer, Events: []models.Event{{ @@ -251,7 +334,8 @@ }}, }, { - name: "single range with 'last affected' status", + name: "single range with 'last affected' status", + ecosystem: "npm", rnge: models.Range{ Type: models.RangeSemVer, Events: []models.Event{{ @@ -268,7 +352,8 @@ }}, }, { - name: "single range with no 'fixed' or 'last affected' status", + name: "single range with no 'fixed' or 'last affected' status", + ecosystem: "npm", rnge: models.Range{ Type: models.RangeSemVer, Events: []models.Event{{ @@ -283,7 +368,8 @@ }}, }, { - name: "single range introduced with '0'", + name: "single range introduced with '0'", + ecosystem: "npm", rnge: models.Range{ Type: models.RangeSemVer, Events: []models.Event{{ @@ -300,7 +386,8 @@ }}, }, { - name: "multiple ranges", + name: "multiple ranges", + ecosystem: "npm", rnge: models.Range{ Type: models.RangeSemVer, Events: []models.Event{{ @@ -335,7 +422,8 @@ }, }, { - name: "single range with database-specific fix availability", + name: "single range with database-specific fix availability", + ecosystem: "npm", rnge: models.Range{ Type: models.RangeSemVer, Events: []models.Event{{ @@ -378,7 +466,7 @@ test := testToRun t.Run(test.name, func(tt *testing.T) { tt.Parallel() - if got := getGrypeRangesFromRange(test.rnge); !reflect.DeepEqual(got, test.want) { + if got := getGrypeRangesFromRange(test.rnge, test.ecosystem); !reflect.DeepEqual(got, test.want) { t.Errorf("getGrypeRangesFromRange() = %v, want %v", got, test.want) } }) @@ -500,3 +588,137 @@ }) } } + +func Test_extractRpmModularity(t *testing.T) { + tests := []struct { + name string + affected models.Affected + want string + }{ + { + name: "with rpm_modularity", + affected: models.Affected{ + EcosystemSpecific: map[string]interface{}{ + "rpm_modularity": "mariadb:10.3", + }, + }, + want: "mariadb:10.3", + }, + { + name: "no ecosystem_specific", + affected: models.Affected{ + EcosystemSpecific: nil, + }, + want: "", + }, + { + name: "no rpm_modularity key", + affected: models.Affected{ + EcosystemSpecific: map[string]interface{}{ + "other_key": "some_value", + }, + }, + want: "", + }, + { + name: "rpm_modularity not string", + affected: models.Affected{ + EcosystemSpecific: map[string]interface{}{ + "rpm_modularity": 123, + }, + }, + want: "", + }, + { + name: "nodejs modularity", + affected: models.Affected{ + EcosystemSpecific: map[string]interface{}{ + "rpm_modularity": "nodejs:16", + }, + }, + want: "nodejs:16", + }, + } + + for _, testToRun := range tests { + test := testToRun + t.Run(test.name, func(tt *testing.T) { + got := extractRpmModularity(test.affected) + if got != test.want { + t.Errorf("extractRpmModularity() = %v, want %v", got, test.want) + } + }) + } +} + +func Test_getPackageQualifiers(t *testing.T) { + tests := []struct { + name string + affected models.Affected + cpes any + withCPE bool + want *grypeDB.PackageQualifiers + }{ + { + name: "with rpm_modularity only", + affected: models.Affected{ + EcosystemSpecific: map[string]interface{}{ + "rpm_modularity": "mariadb:10.3", + }, + }, + cpes: nil, + withCPE: false, + want: &grypeDB.PackageQualifiers{ + RpmModularity: stringRef("mariadb:10.3"), + }, + }, + { + name: "with CPE only", + affected: models.Affected{ + EcosystemSpecific: nil, + }, + cpes: []string{"cpe:2.3:a:vendor:product:*:*:*:*:*:*:*:*"}, + withCPE: true, + want: &grypeDB.PackageQualifiers{ + PlatformCPEs: []string{"cpe:2.3:a:vendor:product:*:*:*:*:*:*:*:*"}, + }, + }, + { + name: "with both rpm_modularity and CPE", + affected: models.Affected{ + EcosystemSpecific: map[string]interface{}{ + "rpm_modularity": "nodejs:16", + }, + }, + cpes: []string{"cpe:2.3:a:nodejs:nodejs:*:*:*:*:*:*:*:*"}, + withCPE: true, + want: &grypeDB.PackageQualifiers{ + PlatformCPEs: []string{"cpe:2.3:a:nodejs:nodejs:*:*:*:*:*:*:*:*"}, + RpmModularity: stringRef("nodejs:16"), + }, + }, + { + name: "no qualifiers", + affected: models.Affected{ + EcosystemSpecific: nil, + }, + cpes: nil, + withCPE: false, + want: nil, + }, + } + + for _, testToRun := range tests { + test := testToRun + t.Run(test.name, func(tt *testing.T) { + got := getPackageQualifiers(test.affected, test.cpes, test.withCPE) + if !reflect.DeepEqual(got, test.want) { + t.Errorf("getPackageQualifiers() = %v, want %v", got, test.want) + } + }) + } +} + +func stringRef(s string) *string { + return &s +} ++++++ grype-db.obsinfo ++++++ --- /var/tmp/diff_new_pack.WvbeXv/_old 2025-10-24 17:24:50.874979883 +0200 +++ /var/tmp/diff_new_pack.WvbeXv/_new 2025-10-24 17:24:50.882980220 +0200 @@ -1,5 +1,5 @@ name: grype-db -version: 0.45.0 -mtime: 1760551191 -commit: 342891aa516b3f52bd4e04ad052be454c261cdc0 +version: 0.46.0 +mtime: 1761219197 +commit: 127e29f2e1331d0368bed7b241c8f19069ecee54 ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/grype-db/vendor.tar.gz /work/SRC/openSUSE:Factory/.grype-db.new.1980/vendor.tar.gz differ: char 15, line 1
