Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package syft for openSUSE:Factory checked in at 2026-02-19 14:23:45 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/syft (Old) and /work/SRC/openSUSE:Factory/.syft.new.1977 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "syft" Thu Feb 19 14:23:45 2026 rev:119 rq:1333874 version:1.42.1 Changes: -------- --- /work/SRC/openSUSE:Factory/syft/syft.changes 2026-02-11 18:49:39.707429070 +0100 +++ /work/SRC/openSUSE:Factory/.syft.new.1977/syft.changes 2026-02-19 14:23:52.291946596 +0100 @@ -1,0 +2,21 @@ +Thu Feb 19 06:12:52 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 1.42.1: + * Bug Fixes + - Use redhat as namespace for hummingbird rpms [#4615 @scoheb] + - False Positive: Emacs snap package version CVE-2024-39331 + [#4485] + * Additional Changes + - call cleanup on tmpfile and replace some io.ReadAlls with + streams [#4629 @willmurphyscode] + - bumps go mod version to 1.25; ci takes latest patch [#4628 + @spiffcs] + * Dependencies + - chore(deps): update tools to latest versions (#4614) + - chore(deps): bump the actions-minor-patch group across 1 + directory with 2 updates (#4622) + - chore(deps): bump the go-minor-patch group with 2 updates + (#4621) + - chore(deps): update CPE dictionary index (#4623) + +------------------------------------------------------------------- Old: ---- syft-1.42.0.obscpio New: ---- syft-1.42.1.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ syft.spec ++++++ --- /var/tmp/diff_new_pack.4X4nFr/_old 2026-02-19 14:23:54.768049326 +0100 +++ /var/tmp/diff_new_pack.4X4nFr/_new 2026-02-19 14:23:54.768049326 +0100 @@ -17,7 +17,7 @@ Name: syft -Version: 1.42.0 +Version: 1.42.1 Release: 0 Summary: CLI tool and library for generating a Software Bill of Materials License: Apache-2.0 @@ -26,8 +26,8 @@ Source1: vendor.tar.gz BuildRequires: bash-completion BuildRequires: fish -BuildRequires: go >= 1.24 BuildRequires: zsh +BuildRequires: golang(API) >= 1.25 %description A CLI tool and Go library for generating a Software Bill of Materials (SBOM) ++++++ _service ++++++ --- /var/tmp/diff_new_pack.4X4nFr/_old 2026-02-19 14:23:54.804050820 +0100 +++ /var/tmp/diff_new_pack.4X4nFr/_new 2026-02-19 14:23:54.808050986 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/anchore/syft</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v1.42.0</param> + <param name="revision">v1.42.1</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.4X4nFr/_old 2026-02-19 14:23:54.828051816 +0100 +++ /var/tmp/diff_new_pack.4X4nFr/_new 2026-02-19 14:23:54.832051982 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/anchore/syft</param> - <param name="changesrevision">9872ff36ba5881f977b6e1b46c7bb33a1147f09e</param></service></servicedata> + <param name="changesrevision">0a3f7bb06ee168495ee54b8b66fccb22a056fe99</param></service></servicedata> (No newline at EOF) ++++++ syft-1.42.0.obscpio -> syft-1.42.1.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/syft-1.42.0/.binny.yaml new/syft-1.42.1/.binny.yaml --- old/syft-1.42.0/.binny.yaml 2026-02-10 18:19:56.000000000 +0100 +++ new/syft-1.42.1/.binny.yaml 2026-02-17 23:32:35.000000000 +0100 @@ -2,7 +2,7 @@ # we want to use a pinned version of binny to manage the toolchain (so binny manages itself!) - name: binny version: - want: v0.11.2 + want: v0.11.3 method: github-release with: repo: anchore/binny @@ -26,7 +26,7 @@ # used for linting - name: golangci-lint version: - want: v2.8.0 + want: v2.9.0 method: github-release with: repo: golangci/golangci-lint @@ -114,7 +114,7 @@ # used to upload test fixture cache - name: yq version: - want: v4.52.2 + want: v4.52.4 method: github-release with: repo: mikefarah/yq diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/syft-1.42.0/.golangci.yaml new/syft-1.42.1/.golangci.yaml --- old/syft-1.42.0/.golangci.yaml 2026-02-10 18:19:56.000000000 +0100 +++ new/syft-1.42.1/.golangci.yaml 2026-02-17 23:32:35.000000000 +0100 @@ -50,10 +50,9 @@ - legacy - std-error-handling rules: - # internal/os contains OS feature detection logic; the name reflects its purpose + # we have multiple packages in syft that might overlap with the stblib; their names reflect their purpose - linters: - revive - path: internal/os/ text: "var-naming: avoid package names that conflict" paths: - third_party$ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/syft-1.42.0/go.mod new/syft-1.42.1/go.mod --- old/syft-1.42.0/go.mod 2026-02-10 18:19:56.000000000 +0100 +++ new/syft-1.42.1/go.mod 2026-02-17 23:32:35.000000000 +0100 @@ -1,10 +1,10 @@ module github.com/anchore/syft -go 1.24.6 +go 1.25 require ( github.com/BurntSushi/toml v1.6.0 - github.com/CycloneDX/cyclonedx-go v0.9.3 + github.com/CycloneDX/cyclonedx-go v0.10.0 github.com/Masterminds/semver/v3 v3.4.0 github.com/Masterminds/sprig/v3 v3.3.0 github.com/OneOfOne/xxhash v1.2.8 @@ -29,7 +29,7 @@ github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/bmatcuk/doublestar/v4 v4.10.0 - github.com/charmbracelet/bubbles v0.21.0 + github.com/charmbracelet/bubbles v0.21.1 github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 github.com/dave/jennifer v1.7.1 @@ -121,11 +121,11 @@ github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/sevenzip v1.6.1 // indirect github.com/bodgit/windows v1.0.1 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/colorprofile v0.4.1 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/x/ansi v0.10.1 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/charmbracelet/x/ansi v0.11.5 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect github.com/cloudflare/circl v1.6.1 // indirect github.com/containerd/containerd/api v1.10.0 // indirect github.com/containerd/continuity v0.4.5 // indirect @@ -177,7 +177,7 @@ github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/maruel/natural v1.1.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -307,9 +307,9 @@ github.com/aws/smithy-go v1.24.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect - github.com/clipperhouse/displaywidth v0.6.2 // indirect + github.com/clipperhouse/displaywidth v0.9.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/clipperhouse/uax29/v2 v2.5.0 // indirect github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect github.com/containerd/cgroups/v3 v3.1.2 // indirect github.com/containerd/containerd/v2 v2.2.1 // indirect diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/syft-1.42.0/go.sum new/syft-1.42.1/go.sum --- old/syft-1.42.0/go.sum 2026-02-10 18:19:56.000000000 +0100 +++ new/syft-1.42.1/go.sum 2026-02-17 23:32:35.000000000 +0100 @@ -81,8 +81,8 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/CycloneDX/cyclonedx-go v0.9.3 h1:Pyk/lwavPz7AaZNvugKFkdWOm93MzaIyWmBwmBo3aUI= -github.com/CycloneDX/cyclonedx-go v0.9.3/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg= +github.com/CycloneDX/cyclonedx-go v0.10.0 h1:7xyklU7YD+CUyGzSFIARG18NYLsKVn4QFg04qSsu+7Y= +github.com/CycloneDX/cyclonedx-go v0.10.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= @@ -215,8 +215,8 @@ github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= @@ -252,24 +252,24 @@ github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= -github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbles v0.21.1 h1:nj0decPiixaZeL9diI4uzzQTkkz1kYY8+jgzCZXSmW0= +github.com/charmbracelet/bubbles v0.21.1/go.mod h1:HHvIYRCpbkCJw2yo0vNX1O5loCwSr9/mWS8GYSg50Sk= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= +github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= -github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/ansi v0.11.5 h1:NBWeBpj/lJPE3Q5l+Lusa4+mH6v7487OP8K0r1IhRg4= +github.com/charmbracelet/x/ansi v0.11.5/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= @@ -282,12 +282,12 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= -github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= +github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= +github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= -github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= +github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -683,8 +683,8 @@ github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/syft-1.42.0/syft/pkg/cataloger/debian/parse_copyright.go new/syft-1.42.1/syft/pkg/cataloger/debian/parse_copyright.go --- old/syft-1.42.0/syft/pkg/cataloger/debian/parse_copyright.go 2026-02-10 18:19:56.000000000 +0100 +++ new/syft-1.42.1/syft/pkg/cataloger/debian/parse_copyright.go 2026-02-17 23:32:35.000000000 +0100 @@ -1,6 +1,7 @@ package debian import ( + "bufio" "io" "regexp" "sort" @@ -14,23 +15,32 @@ // For more information see: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-syntax var ( - licensePattern = regexp.MustCompile(`^License: (?P<license>\S*)`) - commonLicensePathPattern = regexp.MustCompile(`/usr/share/common-licenses/(?P<license>[0-9A-Za-z_.\-]+)`) - licenseFirstSentenceAfterHeadingPattern = regexp.MustCompile(`(?is)^[^\n]+?\n[-]+?\n+(?P<license>.*?\.)`) - licenseAgreementHeadingPattern = regexp.MustCompile(`(?i)^\s*(?P<license>LICENSE AGREEMENT(?: FOR .+?)?)\s*$`) + licensePattern = regexp.MustCompile(`^License: (?P<license>\S*)`) + commonLicensePathPattern = regexp.MustCompile(`/usr/share/common-licenses/(?P<license>[0-9A-Za-z_.\-]+)`) + licenseAgreementHeadingPattern = regexp.MustCompile(`(?i)^\s*(?P<license>LICENSE AGREEMENT(?: FOR .+?)?)\s*$`) ) func parseLicensesFromCopyright(reader io.Reader) []string { findings := strset.New() - data, err := io.ReadAll(reader) - if err != nil { - // Fail-safe: return nothing if unable to read - return []string{} - } + scanner := bufio.NewScanner(reader) + + // State machine replacing licenseFirstSentenceAfterHeadingPattern. + // That regex only matched at the start of the file: a non-empty heading, + // a line of dashes, blank lines, then text up to the first period. + const ( + expectHeading = iota + expectDashes + skipBlanks + captureLicense + headingDone // matched or impossible — stop checking + ) + headingState := expectHeading + var licenseText strings.Builder - content := string(data) - lines := strings.Split(content, "\n") - for _, line := range lines { + for scanner.Scan() { + line := scanner.Text() + + // per-line regex checks (applied to every line) if value := findLicenseClause(licensePattern, line); value != "" { findings.Add(value) } @@ -40,13 +50,39 @@ if value := findLicenseClause(licenseAgreementHeadingPattern, line); value != "" { findings.Add(value) } - } - // some copyright files have a license declaration after the heading ex: - // End User License Agreement\n-------------------------- - // we want to try and find these multi-line license declarations and make exceptions for them - if value := findLicenseClause(licenseFirstSentenceAfterHeadingPattern, content); value != "" { - findings.Add(value) + // multi-line heading detection (only at start of file) + switch headingState { + case expectHeading: + if strings.TrimSpace(line) != "" { + headingState = expectDashes + } else { + headingState = headingDone + } + case expectDashes: + trimmed := strings.TrimSpace(line) + if len(trimmed) > 0 && strings.Trim(trimmed, "-") == "" { + headingState = skipBlanks + } else { + headingState = headingDone + } + case skipBlanks: + if strings.TrimSpace(line) != "" { + headingState = captureLicense + licenseText.WriteString(line) + if value := extractUpToFirstPeriod(licenseText.String()); value != "" { + findings.Add(value) + headingState = headingDone + } + } + case captureLicense: + licenseText.WriteString(" ") + licenseText.WriteString(line) + if value := extractUpToFirstPeriod(licenseText.String()); value != "" { + findings.Add(value) + headingState = headingDone + } + } } results := findings.List() @@ -55,6 +91,15 @@ return results } +// extractUpToFirstPeriod returns the license text up to the first period, +// processed through ensureIsSingleLicense, or "" if no period found yet. +func extractUpToFirstPeriod(s string) string { + if idx := strings.Index(s, "."); idx >= 0 { + return ensureIsSingleLicense(s[:idx+1]) + } + return "" +} + func findLicenseClause(pattern *regexp.Regexp, line string) string { valueGroup := "license" matchesByGroup := internal.MatchNamedCaptureGroups(pattern, line) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/syft-1.42.0/syft/pkg/cataloger/debian/parse_deb_archive.go new/syft-1.42.1/syft/pkg/cataloger/debian/parse_deb_archive.go --- old/syft-1.42.0/syft/pkg/cataloger/debian/parse_deb_archive.go 2026-02-10 18:19:56.000000000 +0100 +++ new/syft-1.42.1/syft/pkg/cataloger/debian/parse_deb_archive.go 2026-02-17 23:32:35.000000000 +0100 @@ -2,7 +2,6 @@ import ( "archive/tar" - "bytes" "context" "fmt" "io" @@ -107,132 +106,77 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) { defer internal.CloseAndLogError(dcReader, "") - // Extract control, md5sums, and conffiles files from control.tar tarReader := tar.NewReader(dcReader) - controlFileContent, md5Content, confContent, err := readControlFiles(tarReader) - if err != nil { - return nil, fmt.Errorf("failed to read control files: %w", err) - } - - if controlFileContent == nil { - return nil, fmt.Errorf("control file not found in archive") - } - metadata, err := newDpkgArchiveMetadata(controlFileContent, md5Content, confContent) - if err != nil { - return nil, fmt.Errorf("failed to create package metadata: %w", err) - } - - return &metadata, nil -} - -func newDpkgArchiveMetadata(controlFile, md5sums, confFiles []byte) (pkg.DpkgArchiveEntry, error) { - // parse the control file to get package metadata - metadata, err := parseControlFile(string(controlFile)) - if err != nil { - return pkg.DpkgArchiveEntry{}, fmt.Errorf("failed to parse control file: %w", err) - } - - // parse MD5 sums to get file records + var metadata *pkg.DpkgArchiveEntry var files []pkg.DpkgFileRecord - if len(md5sums) > 0 { - files = parseDpkgMD5Info(bytes.NewReader(md5sums)) - } - - // mark config files - if len(confFiles) > 0 { - markConfigFiles(confFiles, files) - } - - metadata.Files = files - return metadata, nil -} - -func decompressionStream(ctx context.Context, r io.Reader, filePath string) (io.ReadCloser, error) { - format, stream, err := archives.Identify(ctx, filePath, r) - if err != nil { - return nil, fmt.Errorf("failed to identify compression format: %w", err) - } - - decompressor, ok := format.(archives.Decompressor) - if !ok { - return nil, fmt.Errorf("file format does not support decompression: %s", filePath) - } - - rc, err := decompressor.OpenReader(stream) - if err != nil { - return nil, fmt.Errorf("failed to create decompression reader: %w", err) - } + var confFileRecords []pkg.DpkgFileRecord - return rc, nil -} - -// readControlFiles extracts important files from the control.tar archive -func readControlFiles(tarReader *tar.Reader) (controlFile, md5sums, conffiles []byte, err error) { for { header, err := tarReader.Next() if err == io.EOF { break } if err != nil { - return nil, nil, nil, err + return nil, fmt.Errorf("failed to read control tar: %w", err) } switch filepath.Base(header.Name) { case "control": - controlFile, err = io.ReadAll(tarReader) + // parseDpkgStatus already streams via bufio.Reader + entries, err := parseDpkgStatus(tarReader) if err != nil { - return nil, nil, nil, err + return nil, fmt.Errorf("failed to parse control file: %w", err) } - case "md5sums": - md5sums, err = io.ReadAll(tarReader) - if err != nil { - return nil, nil, nil, err + if len(entries) == 0 { + return nil, fmt.Errorf("no package entries found in control file") } + entry := pkg.DpkgArchiveEntry(entries[0]) + metadata = &entry + case "md5sums": + // parseDpkgMD5Info already streams via bufio.Scanner + files = parseDpkgMD5Info(tarReader) case "conffiles": - conffiles, err = io.ReadAll(tarReader) - if err != nil { - return nil, nil, nil, err + // parseDpkgConffileInfo already streams via bufio.Scanner + confFileRecords = parseDpkgConffileInfo(tarReader) + } + } + + if metadata == nil { + return nil, fmt.Errorf("control file not found in archive") + } + + if len(confFileRecords) > 0 && len(files) > 0 { + configPaths := make(map[string]struct{}, len(confFileRecords)) + for _, cf := range confFileRecords { + configPaths[cf.Path] = struct{}{} + } + for i, f := range files { + if _, isConfig := configPaths[f.Path]; isConfig { + files[i].IsConfigFile = true } } } - return controlFile, md5sums, conffiles, nil + metadata.Files = files + return metadata, nil } -// parseControlFile parses the content of a debian control file into package metadata -func parseControlFile(controlFileContent string) (pkg.DpkgArchiveEntry, error) { - // Reuse the existing dpkg status file parsing logic - reader := strings.NewReader(controlFileContent) - - entries, err := parseDpkgStatus(reader) +func decompressionStream(ctx context.Context, r io.Reader, filePath string) (io.ReadCloser, error) { + format, stream, err := archives.Identify(ctx, filePath, r) if err != nil { - return pkg.DpkgArchiveEntry{}, fmt.Errorf("failed to parse control file: %w", err) + return nil, fmt.Errorf("failed to identify compression format: %w", err) } - if len(entries) == 0 { - return pkg.DpkgArchiveEntry{}, fmt.Errorf("no package entries found in control file") + decompressor, ok := format.(archives.Decompressor) + if !ok { + return nil, fmt.Errorf("file format does not support decompression: %s", filePath) } - // We expect only one entry from a .deb control file - return pkg.DpkgArchiveEntry(entries[0]), nil -} - -// markConfigFiles marks files that are listed in conffiles as configuration files -func markConfigFiles(conffilesContent []byte, files []pkg.DpkgFileRecord) { - // Parse the conffiles content into DpkgFileRecord entries - confFiles := parseDpkgConffileInfo(bytes.NewReader(conffilesContent)) - - // Create a map for quick lookup of config files by path - configPathMap := make(map[string]struct{}) - for _, confFile := range confFiles { - configPathMap[confFile.Path] = struct{}{} + rc, err := decompressor.OpenReader(stream) + if err != nil { + return nil, fmt.Errorf("failed to create decompression reader: %w", err) } - // Mark files as config files if they're in the conffiles list - for i := range files { - if _, exists := configPathMap[files[i].Path]; exists { - files[i].IsConfigFile = true - } - } + return rc, nil } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/syft-1.42.0/syft/pkg/cataloger/debian/parse_deb_archive_test.go new/syft-1.42.1/syft/pkg/cataloger/debian/parse_deb_archive_test.go --- old/syft-1.42.0/syft/pkg/cataloger/debian/parse_deb_archive_test.go 2026-02-10 18:19:56.000000000 +0100 +++ new/syft-1.42.1/syft/pkg/cataloger/debian/parse_deb_archive_test.go 2026-02-17 23:32:35.000000000 +0100 @@ -3,140 +3,85 @@ import ( "archive/tar" "bytes" + "io" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/pkg" ) -func TestReadControlFiles(t *testing.T) { +func TestProcessControlTar(t *testing.T) { tarBytes := createTestTarWithControlFiles(t) - tarReader := bytes.NewReader(tarBytes) - reader := tar.NewReader(tarReader) - - controlFile, md5sums, conffiles, err := readControlFiles(reader) + metadata, err := processControlTar(io.NopCloser(bytes.NewReader(tarBytes))) require.NoError(t, err) - assert.NotNil(t, controlFile, "expected control file to be found") - assert.NotNil(t, md5sums, "expected md5sums file to be found") - assert.NotNil(t, conffiles, "expected conffiles file to be found") - - assert.Contains(t, string(controlFile), "Package: test-package") - assert.Contains(t, string(md5sums), "d41d8cd98f00b204e9800998ecf8427e") - assert.Contains(t, string(conffiles), "/etc/test") + require.NotNil(t, metadata) + + assert.Equal(t, "test-package", metadata.Package) + assert.Equal(t, "1.0.0", metadata.Version) + + // md5sums should have been parsed into file records + require.Len(t, metadata.Files, 1) + assert.Equal(t, "/usr/bin/test-command", metadata.Files[0].Path) + assert.Equal(t, "d41d8cd98f00b204e9800998ecf8427e", metadata.Files[0].Digest.Value) + + // conffiles should have marked config files + assert.True(t, metadata.Files[0].IsConfigFile, "file listed in conffiles should be marked as config") } -// createTestTarWithControlFiles creates a simple in-memory tar file with test control files -func createTestTarWithControlFiles(t *testing.T) []byte { +func TestProcessControlTar_ConfigFileMarking(t *testing.T) { + // Create a tar where conffiles lists paths that overlap with md5sums entries var buf bytes.Buffer tw := tar.NewWriter(&buf) - // Add control file - controlContent := `Package: test-package -Version: 1.0.0 -Architecture: all -Maintainer: Test <[email protected]> -Description: Test package -` - err := tw.WriteHeader(&tar.Header{ - Name: "control", - Mode: 0644, - Size: int64(len(controlContent)), - }) - require.NoError(t, err) - _, err = tw.Write([]byte(controlContent)) - require.NoError(t, err) + controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\n" + writeTarEntry(t, tw, "control", controlContent) - // Add md5sums file - md5Content := "d41d8cd98f00b204e9800998ecf8427e usr/bin/test-command\n" - err = tw.WriteHeader(&tar.Header{ - Name: "md5sums", - Mode: 0644, - Size: int64(len(md5Content)), - }) - require.NoError(t, err) - _, err = tw.Write([]byte(md5Content)) - require.NoError(t, err) + md5Content := "d41d8cd98f00b204e9800998ecf8427e usr/bin/test-command\n" + + "d41d8cd98f00b204e9800998ecf8427e etc/test/config.conf\n" + + "d41d8cd98f00b204e9800998ecf8427e usr/bin/other-command\n" + writeTarEntry(t, tw, "md5sums", md5Content) - // Add conffiles file - conffilesContent := "/etc/test/config.conf\n" - err = tw.WriteHeader(&tar.Header{ - Name: "conffiles", - Mode: 0644, - Size: int64(len(conffilesContent)), - }) - require.NoError(t, err) - _, err = tw.Write([]byte(conffilesContent)) - require.NoError(t, err) + conffilesContent := "/usr/bin/test-command\n/etc/test/config.conf\n" + writeTarEntry(t, tw, "conffiles", conffilesContent) - // Close the tar writer - err = tw.Close() + require.NoError(t, tw.Close()) + + metadata, err := processControlTar(io.NopCloser(bytes.NewReader(buf.Bytes()))) require.NoError(t, err) + require.Len(t, metadata.Files, 3) - return buf.Bytes() + assert.True(t, metadata.Files[0].IsConfigFile, "first file should be marked as config file") + assert.True(t, metadata.Files[1].IsConfigFile, "second file should be marked as config file") + assert.False(t, metadata.Files[2].IsConfigFile, "third file should not be marked as config file") } -func TestMarkConfigFiles(t *testing.T) { - // Create test data - conffilesContent := []byte("/usr/bin/test-command\n/etc/test/config.conf\n") - - files := []pkg.DpkgFileRecord{ - { - Path: "/usr/bin/test-command", - Digest: &file.Digest{ - Algorithm: "md5", - Value: "d41d8cd98f00b204e9800998ecf8427e", - }, - }, - { - Path: "/etc/test/config.conf", - Digest: &file.Digest{ - Algorithm: "md5", - Value: "d41d8cd98f00b204e9800998ecf8427e", - }, - }, - { - Path: "/usr/bin/other-command", - Digest: &file.Digest{ - Algorithm: "md5", - Value: "d41d8cd98f00b204e9800998ecf8427e", - }, - }, - } - - markConfigFiles(conffilesContent, files) - - assert.True(t, files[0].IsConfigFile, "first file should be marked as config file") - assert.True(t, files[1].IsConfigFile, "second file should be marked as config file") - assert.False(t, files[2].IsConfigFile, "third file should not be marked as config file") -} +// createTestTarWithControlFiles creates a simple in-memory tar file with test control files +func createTestTarWithControlFiles(t *testing.T) []byte { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + + controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\nMaintainer: Test <[email protected]>\nDescription: Test package\n" + writeTarEntry(t, tw, "control", controlContent) + + md5Content := "d41d8cd98f00b204e9800998ecf8427e usr/bin/test-command\n" + writeTarEntry(t, tw, "md5sums", md5Content) -func TestParseControlFile(t *testing.T) { - controlContent := `Package: test-package -Version: 1.2.3-4 -Architecture: amd64 -Maintainer: Test User <[email protected]> -Installed-Size: 1234 -Depends: libc6, libtest -Description: This is a test package - More description text - And even more details -` + conffilesContent := "/usr/bin/test-command\n" + writeTarEntry(t, tw, "conffiles", conffilesContent) - metadata, err := parseControlFile(controlContent) + require.NoError(t, tw.Close()) + return buf.Bytes() +} +func writeTarEntry(t *testing.T, tw *tar.Writer, name, content string) { + t.Helper() + require.NoError(t, tw.WriteHeader(&tar.Header{ + Name: name, + Mode: 0644, + Size: int64(len(content)), + })) + _, err := tw.Write([]byte(content)) require.NoError(t, err) - assert.Equal(t, "test-package", metadata.Package) - assert.Equal(t, "1.2.3-4", metadata.Version) - assert.Equal(t, "amd64", metadata.Architecture) - assert.Equal(t, "Test User <[email protected]>", metadata.Maintainer) - assert.Equal(t, 1234, metadata.InstalledSize) - assert.Contains(t, metadata.Description, "This is a test package") - assert.Len(t, metadata.Depends, 2) - assert.Contains(t, metadata.Depends, "libc6") - assert.Contains(t, metadata.Depends, "libtest") } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/syft-1.42.0/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json new/syft-1.42.1/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json --- old/syft-1.42.0/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json 2026-02-10 18:19:56.000000000 +0100 +++ new/syft-1.42.1/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json 2026-02-17 23:32:35.000000000 +0100 @@ -4,6 +4,9 @@ "aahframe.work": [ "cpe:2.3:a:aahframework:aah:*:*:*:*:*:go:*:*" ], + "chainguard.dev/apko": [ + "cpe:2.3:a:chainguard:apko:*:*:*:*:*:go:*:*" + ], "github.com/ClickHouse/ch-go": [ "cpe:2.3:a:clickhouse:ch:*:*:*:*:*:go:*:*" ], @@ -165,6 +168,9 @@ "golang.org/x/net": [ "cpe:2.3:a:golang:networking:*:*:*:*:*:go:*:*" ], + "golang.org/x/net/html": [ + "cpe:2.3:a:go:html:*:*:*:*:*:go:*:*" + ], "golang.org/x/net/http2": [ "cpe:2.3:a:golang:http2:*:*:*:*:*:*:*:*", "cpe:2.3:a:golang:http2:*:*:*:*:*:go:*:*" @@ -2371,6 +2377,9 @@ "@nuxt/devalue": [ "cpe:2.3:a:nuxtjs:\\@nuxt\\/devalue:*:*:*:*:*:node.js:*:*" ], + "@nyariv/sandboxjs": [ + "cpe:2.3:a:nyariv:sandboxjs:*:*:*:*:*:node.js:*:*" + ], "@octokit/webhooks": [ "cpe:2.3:a:octokit:webhooks:*:*:*:*:*:node.js:*:*" ], @@ -6014,6 +6023,9 @@ "fonttools": [ "cpe:2.3:a:fonttools:fonttools:*:*:*:*:*:python:*:*" ], + "geopandas": [ + "cpe:2.3:a:geopandas:geopandas:*:*:*:*:*:python:*:*" + ], "global-workqueue": [ "cpe:2.3:a:global-workqueue_project:global-workqueue:*:*:*:*:*:python:*:*" ], @@ -8807,7 +8819,8 @@ "cpe:2.3:a:solidwp:solid_security:*:*:*:*:*:wordpress:*:*" ], "betterdocs": [ - "cpe:2.3:a:wpdeveloper:betterdocs:*:*:*:*:*:wordpress:*:*" + "cpe:2.3:a:wpdeveloper:betterdocs:*:*:*:*:*:wordpress:*:*", + "cpe:2.3:a:wpdeveloper:betterdocs:*:*:*:*:free:wordpress:*:*" ], "betterlinks": [ "cpe:2.3:a:wpdeveloper:betterlinks:*:*:*:*:free:wordpress:*:*" @@ -15061,7 +15074,8 @@ "cpe:2.3:a:andreamarinucci:notification_for_telegram:*:*:*:*:*:wordpress:*:*" ], "notificationx": [ - "cpe:2.3:a:wpdeveloper:notificationx:*:*:*:*:*:wordpress:*:*" + "cpe:2.3:a:wpdeveloper:notificationx:*:*:*:*:*:wordpress:*:*", + "cpe:2.3:a:wpdeveloper:notificationx:*:*:*:*:free:wordpress:*:*" ], "notifier": [ "cpe:2.3:a:wanotifier:wanotifier:*:*:*:*:*:wordpress:*:*" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/syft-1.42.0/syft/pkg/cataloger/redhat/package.go new/syft-1.42.1/syft/pkg/cataloger/redhat/package.go --- old/syft-1.42.0/syft/pkg/cataloger/redhat/package.go 2026-02-10 18:19:56.000000000 +0100 +++ new/syft-1.42.1/syft/pkg/cataloger/redhat/package.go 2026-02-17 23:32:35.000000000 +0100 @@ -84,13 +84,13 @@ }, nil } -// packageURL returns the PURL for the specific RHEL package (see https://github.com/package-url/purl-spec) +// packageURL returns the PURL for the specific RHEL or Hummingbird package (see https://github.com/package-url/purl-spec) func packageURL(name, arch string, epoch *int, srpm string, version, release string, distro *linux.Release) string { var namespace string if distro != nil { namespace = distro.ID } - if namespace == "rhel" { + if namespace == "rhel" || namespace == "hummingbird" { namespace = "redhat" } if strings.HasPrefix(namespace, "opensuse") { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/syft-1.42.0/syft/pkg/cataloger/redhat/package_test.go new/syft-1.42.1/syft/pkg/cataloger/redhat/package_test.go --- old/syft-1.42.0/syft/pkg/cataloger/redhat/package_test.go 2026-02-10 18:19:56.000000000 +0100 +++ new/syft-1.42.1/syft/pkg/cataloger/redhat/package_test.go 2026-02-17 23:32:35.000000000 +0100 @@ -56,6 +56,20 @@ expected: "pkg:rpm/p@v-r", }, { + name: "hummingbird distro maps to redhat namespace", + distro: &linux.Release{ + ID: "hummingbird", + VersionID: "1.0", + }, + metadata: pkg.RpmDBEntry{ + Name: "p", + Version: "v", + Release: "r", + Epoch: nil, + }, + expected: "pkg:rpm/redhat/p@v-r?distro=hummingbird-1.0", + }, + { name: "with upstream source rpm info", distro: &linux.Release{ ID: "rhel", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/syft-1.42.0/syft/pkg/cataloger/snap/parse_kernel_changelog.go new/syft-1.42.1/syft/pkg/cataloger/snap/parse_kernel_changelog.go --- old/syft-1.42.0/syft/pkg/cataloger/snap/parse_kernel_changelog.go 2026-02-10 18:19:56.000000000 +0100 +++ new/syft-1.42.1/syft/pkg/cataloger/snap/parse_kernel_changelog.go 2026-02-17 23:32:35.000000000 +0100 @@ -1,10 +1,10 @@ package snap import ( + "bufio" "compress/gzip" "context" "fmt" - "io" "regexp" "strings" @@ -22,16 +22,28 @@ majorVersion string // e.g., "5.4" } -// parseKernelChangelog parses changelog files from kernel snaps to extract kernel version +// parseKernelChangelog parses changelog files from kernel snaps to extract kernel version. +// The changelog is gzip-compressed and may be very large, so we stream it line-by-line +// rather than reading it entirely into memory. func parseKernelChangelog(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { - // The file should be gzipped - lines, err := readChangelogLines(reader) + gzReader, err := gzip.NewReader(reader) if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("failed to create gzip reader for changelog: %w", err) + } + defer gzReader.Close() + + scanner := bufio.NewScanner(gzReader) + + // read the first line to extract kernel version + // Format: "linux (5.4.0-195.215) focal; urgency=medium" + if !scanner.Scan() { + if err := scanner.Err(); err != nil { + return nil, nil, fmt.Errorf("failed to read changelog content: %w", err) + } + return nil, nil, fmt.Errorf("changelog file is empty") } - // pull from first line - versionInfo, err := extractKernelVersion(lines[0]) + versionInfo, err := extractKernelVersion(scanner.Text()) if err != nil { return nil, nil, err } @@ -42,38 +54,21 @@ packages := createMainKernelPackage(versionInfo, snapMetadata, reader.Location) - // Check for base kernel package - basePackage := findBaseKernelPackage(lines, versionInfo, snapMetadata, reader.Location) - if basePackage != nil { - packages = append(packages, *basePackage) + // stream remaining lines looking for the base kernel entry + baseKernelEntry := fmt.Sprintf("%s/linux:", strings.ReplaceAll(versionInfo.releaseVersion, ";", "/")) + for scanner.Scan() { + line := scanner.Text() + if strings.Contains(line, baseKernelEntry) { + if basePackage := parseBaseKernelLine(line, versionInfo.majorVersion, snapMetadata, reader.Location); basePackage != nil { + packages = append(packages, *basePackage) + } + break + } } return packages, nil, nil } -// readChangelogLines reads and decompresses the changelog content -func readChangelogLines(reader file.LocationReadCloser) ([]string, error) { - gzReader, err := gzip.NewReader(reader) - if err != nil { - return nil, fmt.Errorf("failed to create gzip reader for changelog: %w", err) - } - defer gzReader.Close() - - content, err := io.ReadAll(gzReader) - if err != nil { - return nil, fmt.Errorf("failed to read changelog content: %w", err) - } - - lines := strings.Split(string(content), "\n") - if len(lines) == 0 { - return nil, fmt.Errorf("changelog file is empty") - } - - // Parse the first line to extract kernel version information - // Format: "linux (5.4.0-195.215) focal; urgency=medium" - return lines, nil -} - // extractKernelVersion parses version information from the first changelog line func extractKernelVersion(firstLine string) (*kernelVersionInfo, error) { // Format: "linux (5.4.0-195.215) focal; urgency=medium" @@ -117,19 +112,6 @@ return []pkg.Package{kernelPkg} } -// findBaseKernelPackage searches for and creates base kernel package if present -func findBaseKernelPackage(lines []string, versionInfo *kernelVersionInfo, snapMetadata pkg.SnapEntry, location file.Location) *pkg.Package { - baseKernelEntry := fmt.Sprintf("%s/linux:", strings.ReplaceAll(versionInfo.releaseVersion, ";", "/")) - - for _, line := range lines { - if strings.Contains(line, baseKernelEntry) { - return parseBaseKernelLine(line, versionInfo.majorVersion, snapMetadata, location) - } - } - - return nil -} - // parseBaseKernelLine extracts base kernel version from a changelog line func parseBaseKernelLine(line string, majorVersion string, snapMetadata pkg.SnapEntry, location file.Location) *pkg.Package { baseKernelRegex := regexp.MustCompile(fmt.Sprintf(`(%s-[0-9]+)\.?[0-9]*`, regexp.QuoteMeta(majorVersion))) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/syft-1.42.0/syft/pkg/cataloger/snap/parse_kernel_changelog_test.go new/syft-1.42.1/syft/pkg/cataloger/snap/parse_kernel_changelog_test.go --- old/syft-1.42.0/syft/pkg/cataloger/snap/parse_kernel_changelog_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/syft-1.42.1/syft/pkg/cataloger/snap/parse_kernel_changelog_test.go 2026-02-17 23:32:35.000000000 +0100 @@ -0,0 +1,312 @@ +package snap + +import ( + "bytes" + "compress/gzip" + "context" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +func gzipContent(t *testing.T, content string) []byte { + t.Helper() + var buf bytes.Buffer + w := gzip.NewWriter(&buf) + _, err := w.Write([]byte(content)) + require.NoError(t, err) + require.NoError(t, w.Close()) + return buf.Bytes() +} + +func locationReadCloser(t *testing.T, data []byte) file.LocationReadCloser { + t.Helper() + return file.LocationReadCloser{ + Location: file.NewLocation("test-fixtures/changelog.Debian.gz"), + ReadCloser: io.NopCloser(bytes.NewReader(data)), + } +} + +func TestExtractKernelVersion(t *testing.T) { + tests := []struct { + name string + firstLine string + expected *kernelVersionInfo + expectError string + }{ + { + name: "standard focal kernel", + firstLine: "linux (5.4.0-195.215) focal; urgency=medium", + expected: &kernelVersionInfo{ + baseVersion: "5.4.0-195", + releaseVersion: "215", + fullVersion: "5.4.0-195.215", + majorVersion: "5.4", + }, + }, + { + name: "noble kernel 6.x", + firstLine: "linux (6.8.0-50.51) noble; urgency=medium", + expected: &kernelVersionInfo{ + baseVersion: "6.8.0-50", + releaseVersion: "51", + fullVersion: "6.8.0-50.51", + majorVersion: "6.8", + }, + }, + { + name: "jammy kernel", + firstLine: "linux (5.15.0-130.140) jammy; urgency=medium", + expected: &kernelVersionInfo{ + baseVersion: "5.15.0-130", + releaseVersion: "140", + fullVersion: "5.15.0-130.140", + majorVersion: "5.15", + }, + }, + { + name: "empty string", + firstLine: "", + expectError: "could not parse kernel version from changelog", + }, + { + name: "no version match", + firstLine: "not a valid changelog line", + expectError: "could not parse kernel version from changelog", + }, + { + name: "missing release version", + firstLine: "linux (5.4.0-195) focal; urgency=medium", + expectError: "could not parse kernel version from changelog", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := extractKernelVersion(tt.firstLine) + if tt.expectError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.expectError) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expected.baseVersion, result.baseVersion) + assert.Equal(t, tt.expected.releaseVersion, result.releaseVersion) + assert.Equal(t, tt.expected.fullVersion, result.fullVersion) + assert.Equal(t, tt.expected.majorVersion, result.majorVersion) + }) + } +} + +func TestCreateMainKernelPackage(t *testing.T) { + location := file.NewLocation("test-fixtures/changelog.Debian.gz") + versionInfo := &kernelVersionInfo{ + baseVersion: "5.4.0-195", + releaseVersion: "215", + fullVersion: "5.4.0-195.215", + majorVersion: "5.4", + } + snapMetadata := pkg.SnapEntry{ + SnapType: pkg.SnapTypeKernel, + } + + packages := createMainKernelPackage(versionInfo, snapMetadata, location) + + require.Len(t, packages, 1) + p := packages[0] + assert.Equal(t, "linux-image-5.4.0-195-generic", p.Name) + assert.Equal(t, "5.4.0-195.215", p.Version) + assert.Equal(t, pkg.DebPkg, p.Type) + + metadata, ok := p.Metadata.(pkg.SnapEntry) + require.True(t, ok) + assert.Equal(t, pkg.SnapTypeKernel, metadata.SnapType) +} + +func TestParseBaseKernelLine(t *testing.T) { + location := file.NewLocation("test-fixtures/changelog.Debian.gz") + snapMetadata := pkg.SnapEntry{ + SnapType: pkg.SnapTypeKernel, + } + + tests := []struct { + name string + line string + majorVersion string + expectNil bool + expectedName string + expectedVer string + }{ + { + name: "standard base kernel entry", + line: " [ Ubuntu: 5.4-100.200 ]", + majorVersion: "5.4", + expectedName: "linux-image-5.4-100-generic", + expectedVer: "5.4-100.200", + }, + { + name: "6.x base kernel entry", + line: " [ Ubuntu: 6.8-40.41 ]", + majorVersion: "6.8", + expectedName: "linux-image-6.8-40-generic", + expectedVer: "6.8-40.41", + }, + { + name: "no matching version", + line: " * some random changelog text here", + majorVersion: "5.4", + expectNil: true, + }, + { + name: "empty line", + line: "", + majorVersion: "5.4", + expectNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := parseBaseKernelLine(tt.line, tt.majorVersion, snapMetadata, location) + if tt.expectNil { + assert.Nil(t, result) + return + } + require.NotNil(t, result) + assert.Equal(t, tt.expectedName, result.Name) + assert.Equal(t, tt.expectedVer, result.Version) + assert.Equal(t, pkg.DebPkg, result.Type) + + metadata, ok := result.Metadata.(pkg.SnapEntry) + require.True(t, ok) + assert.Equal(t, pkg.SnapTypeKernel, metadata.SnapType) + }) + } +} + +func TestParseKernelChangelog(t *testing.T) { + // Realistic changelog content modeled on Ubuntu kernel changelogs. + // The first line declares the patched kernel version. + // Somewhere later a line references the base upstream kernel. + fullChangelog := strings.Join([]string{ + "linux (5.4.0-195.215) focal; urgency=medium", + "", + " * focal/linux: 5.4.0-195.215 -proposed tracker (LP: #2083390)", + "", + " [ Ubuntu: 5.4-100.200 ]", + "", + " * Some other entry", + "", + " -- Ubuntu Kernel Team <[email protected]> Mon, 01 Jan 2024 00:00:00 +0000", + "", + }, "\n") + + // Changelog where the base kernel entry line uses the release version pattern + // The code builds: fmt.Sprintf("%s/linux:", releaseVersion) → "215/linux:" + changelogWithBaseEntry := strings.Join([]string{ + "linux (5.4.0-195.215) focal; urgency=medium", + "", + " * focal/linux: 5.4.0-195.215 -proposed tracker", + "", + " 215/linux: 5.4-100.200 base entry", + "", + " -- Ubuntu Kernel Team <[email protected]> Mon, 01 Jan 2024 00:00:00 +0000", + "", + }, "\n") + + // Changelog with only the header line and no base kernel match + minimalChangelog := "linux (6.8.0-50.51) noble; urgency=medium\n" + + tests := []struct { + name string + input []byte + expectedCount int + expectedNames []string + expectedVers []string + expectError bool + errorContains string + }{ + { + name: "full changelog with base kernel via release version pattern", + input: gzipContent(t, changelogWithBaseEntry), + expectedCount: 2, + expectedNames: []string{"linux-image-5.4.0-195-generic", "linux-image-5.4-100-generic"}, + expectedVers: []string{"5.4.0-195.215", "5.4-100.200"}, + }, + { + name: "changelog without base kernel match returns only main package", + input: gzipContent(t, minimalChangelog), + expectedCount: 1, + expectedNames: []string{"linux-image-6.8.0-50-generic"}, + expectedVers: []string{"6.8.0-50.51"}, + }, + { + name: "full changelog without matching release version pattern returns only main package", + input: gzipContent(t, fullChangelog), + expectedCount: 1, + expectedNames: []string{"linux-image-5.4.0-195-generic"}, + expectedVers: []string{"5.4.0-195.215"}, + }, + { + name: "invalid gzip data", + input: []byte("not gzip data"), + expectError: true, + errorContains: "failed to create gzip reader", + }, + { + // The old (slurp) implementation produces "could not parse kernel version" + // because strings.Split("", "\n") yields [""], not an empty slice. + // The new (streaming) implementation produces "changelog file is empty" + // because bufio.Scanner.Scan() returns false immediately. + // Both correctly reject empty content; only the message differs. + name: "empty gzip content", + input: gzipContent(t, ""), + expectError: true, + }, + { + name: "gzip content with unparseable first line", + input: gzipContent(t, "this is not a valid kernel changelog\n"), + expectError: true, + errorContains: "could not parse kernel version from changelog", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reader := locationReadCloser(t, tt.input) + + packages, relationships, err := parseKernelChangelog( + context.Background(), nil, &generic.Environment{}, reader, + ) + + if tt.expectError { + require.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + return + } + + require.NoError(t, err) + assert.Nil(t, relationships) + require.Len(t, packages, tt.expectedCount) + + for i, p := range packages { + assert.Equal(t, tt.expectedNames[i], p.Name, "package %d name", i) + assert.Equal(t, tt.expectedVers[i], p.Version, "package %d version", i) + assert.Equal(t, pkg.DebPkg, p.Type, "package %d type", i) + + metadata, ok := p.Metadata.(pkg.SnapEntry) + require.True(t, ok, "package %d metadata type", i) + assert.Equal(t, pkg.SnapTypeKernel, metadata.SnapType, "package %d snap type", i) + } + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/syft-1.42.0/syft/source/filesource/file_source.go new/syft-1.42.1/syft/source/filesource/file_source.go --- old/syft-1.42.0/syft/source/filesource/file_source.go 2026-02-10 18:19:56.000000000 +0100 +++ new/syft-1.42.1/syft/source/filesource/file_source.go 2026-02-17 23:32:35.000000000 +0100 @@ -76,6 +76,11 @@ analysisPath, cleanupFn, err := fileAnalysisPath(cfg.Path, cfg.SkipExtractArchive) if err != nil { + if cleanupFn != nil { + if cleanupErr := cleanupFn(); cleanupErr != nil { + log.Warnf("failed to cleanup temporary directory: %v", cleanupErr) + } + } return nil, fmt.Errorf("unable to extract file analysis path=%q: %w", cfg.Path, err) } @@ -211,7 +216,7 @@ if unarchiver, ok := envelopedUnarchiver.(archives.Extractor); err == nil && ok { analysisPath, cleanupFn, err = unarchiveToTmp(path, unarchiver) if err != nil { - return "", nil, fmt.Errorf("unable to unarchive source file: %w", err) + return "", cleanupFn, fmt.Errorf("unable to unarchive source file: %w", err) } log.Debugf("source path is an archive") ++++++ syft.obsinfo ++++++ --- /var/tmp/diff_new_pack.4X4nFr/_old 2026-02-19 14:24:00.140272212 +0100 +++ /var/tmp/diff_new_pack.4X4nFr/_new 2026-02-19 14:24:00.152272709 +0100 @@ -1,5 +1,5 @@ name: syft -version: 1.42.0 -mtime: 1770743996 -commit: 9872ff36ba5881f977b6e1b46c7bb33a1147f09e +version: 1.42.1 +mtime: 1771367555 +commit: 0a3f7bb06ee168495ee54b8b66fccb22a056fe99 ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/syft/vendor.tar.gz /work/SRC/openSUSE:Factory/.syft.new.1977/vendor.tar.gz differ: char 15, line 1
