Compare commits

..
Author SHA1 Message Date
Alex Goodman 5ea14d6e8b [wip] prototype
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-03-11 10:00:29 -04:00
90 changed files with 3159 additions and 1071 deletions
+6 -6
View File
@@ -2,7 +2,7 @@ tools:
# we want to use a pinned version of binny to manage the toolchain (so binny manages itself!)
- name: binny
version:
want: v0.12.0
want: v0.11.3
method: github-release
with:
repo: anchore/binny
@@ -18,7 +18,7 @@ tools:
# used to sign mac binaries at release
- name: quill
version:
want: v0.7.1
want: v0.5.1
method: github-release
with:
repo: anchore/quill
@@ -26,7 +26,7 @@ tools:
# used for linting
- name: golangci-lint
version:
want: v2.11.4
want: v2.11.2
method: github-release
with:
repo: golangci/golangci-lint
@@ -58,7 +58,7 @@ tools:
# used to release all artifacts
- name: goreleaser
version:
want: v2.14.3
want: v2.14.2
method: github-release
with:
repo: goreleaser/goreleaser
@@ -98,7 +98,7 @@ tools:
# used for triggering a release
- name: gh
version:
want: v2.88.1
want: v2.87.3
method: github-release
with:
repo: cli/cli
@@ -106,7 +106,7 @@ tools:
# used to upload test fixture cache
- name: oras
version:
want: v1.3.1
want: v1.3.0
method: github-release
with:
repo: oras-project/oras
-5
View File
@@ -23,11 +23,6 @@ updates:
open-pull-requests-limit: 10
labels:
- "dependencies"
ignore:
- dependency-name: "github.com/aquasecurity/go-pep440-version"
- dependency-name: "github.com/aquasecurity/go-version"
- dependency-name: "github.com/knqyf263/go-apk-version"
- dependency-name: "github.com/knqyf263/go-deb-version"
groups:
go-minor-patch:
applies-to: version-updates # security updates get individual PRs
+3 -3
View File
@@ -48,7 +48,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -59,7 +59,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -73,4 +73,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
@@ -37,8 +37,6 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
repository: anchore/syft # IMPORTANT! An additional protection that this is checking out code from the expected repository
ref: main # IMPORTANT! It is CRITICAL that this only ever considers the code from main and NEVER EVER from a fork.
- run: python .github/scripts/labeler.py
env:
+2 -2
View File
@@ -150,13 +150,13 @@ jobs:
uses: ./.github/actions/bootstrap
- name: Login to Docker Hub
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 #v4.0.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 #v3.7.0
with:
username: ${{ secrets.ANCHOREOSSWRITE_DH_USERNAME }}
password: ${{ secrets.ANCHOREOSSWRITE_DH_PAT }}
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 #v4.0.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 #v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -0,0 +1,51 @@
name: PR to update Anchore dependencies
on:
workflow_dispatch:
inputs:
repos:
description: "List of dependencies to update"
required: true
type: string
permissions:
contents: read
jobs:
update:
runs-on: ubuntu-latest
if: github.repository_owner == 'anchore' # only run for main repo (not forks)
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
with:
tools: false
bootstrap-apt-packages: ""
- name: Update dependencies
id: update
uses: anchore/workflows/.github/actions/update-go-dependencies@main
with:
repos: ${{ github.event.inputs.repos }}
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf #v2.2.1
id: generate-token
with:
app-id: ${{ secrets.TOKEN_APP_ID }}
private-key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 #v8.1.0
with:
signoff: true
delete-branch: true
draft: ${{ steps.update.outputs.draft }}
# do not change this branch, as other workflows depend on it
branch: auto/integration
labels: dependencies,pre-release
commit-message: "chore(deps): update anchore dependencies"
title: "chore(deps): update anchore dependencies"
body: ${{ steps.update.outputs.summary }}
token: ${{ steps.generate-token.outputs.token }}
@@ -0,0 +1,67 @@
name: PR for latest versions of tools
on:
schedule:
- cron: "0 8 * * *" # 3 AM EST
workflow_dispatch:
permissions:
contents: read
jobs:
update-bootstrap-tools:
runs-on: ubuntu-latest
if: github.repository == 'anchore/syft' # only run for main repo
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
with:
bootstrap-apt-packages: ""
go-dependencies: false
- name: "Update tool versions"
id: latest-versions
run: |
make update-tools
make list-tools
export NO_COLOR=1
delimiter="$(openssl rand -hex 8)"
{
echo "status<<${delimiter}"
make list-tool-updates
echo "${delimiter}"
} >> $GITHUB_OUTPUT
{
echo "### Tool version status"
echo "\`\`\`"
make list-tool-updates
echo "\`\`\`"
} >> $GITHUB_STEP_SUMMARY
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf #v2.2.1
id: generate-token
with:
app-id: ${{ secrets.TOKEN_APP_ID }}
private-key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 #v8.1.0
with:
signoff: true
delete-branch: true
branch: auto/latest-tools
labels: dependencies
commit-message: 'chore(deps): update tools to latest versions'
title: 'chore(deps): update tools to latest versions'
body: |
```
${{ steps.latest-versions.outputs.status }}
```
This is an auto-generated pull request to update all of the tools to the latest versions.
token: ${{ steps.generate-token.outputs.token }}
@@ -0,0 +1,84 @@
name: PR to update CPE dictionary index
on:
schedule:
- cron: "0 1 * * 1" # every monday at 1 AM
workflow_dispatch:
permissions:
contents: read
env:
SLACK_NOTIFICATIONS: true
jobs:
upgrade-cpe-dictionary-index:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
if: github.repository == 'anchore/syft' # only run for main repo
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
id: bootstrap
- name: Login to GitHub Container Registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | .tool/oras login ghcr.io -u "$ACTOR" --password-stdin
env:
ACTOR: ${{ github.actor }}
- name: Pull CPE cache from registry
run: make generate:cpe-index:cache:pull
- name: Update CPE cache from NVD API
run: make generate:cpe-index:cache:update
env:
NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
- name: Generate CPE dictionary index
run: make generate:cpe-index:build
- name: Push updated CPE cache to registry
run: make generate:cpe-index:cache:push
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf #v2.2.1
id: generate-token
with:
app-id: ${{ secrets.TOKEN_APP_ID }}
private-key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 #v8.1.0
with:
signoff: true
delete-branch: true
branch: auto/latest-cpe-dictionary-index
labels: dependencies
commit-message: "chore(deps): update CPE dictionary index"
title: "chore(deps): update CPE dictionary index"
body: |
Update CPE dictionary index based on the latest available CPE dictionary
token: ${{ steps.generate-token.outputs.token }}
- name: Notify Slack on failure
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a #v2.1.1
with:
webhook: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "Syft CPE dictionary index update failed"
blocks:
- type: section
text:
type: mrkdwn
text: |
*Syft CPE dictionary index update failed*
• Workflow: `${{ github.workflow }}`
• Event: `${{ github.event_name }}`
• Job Status: `${{ job.status }}`
• <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>
if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }}
@@ -0,0 +1,64 @@
name: PR to update SPDX license list
on:
schedule:
- cron: "0 6 * * 1" # every monday at 6 AM UTC
workflow_dispatch:
permissions:
contents: read
env:
SLACK_NOTIFICATIONS: true
jobs:
upgrade-spdx-license-list:
runs-on: ubuntu-latest
if: github.repository == 'anchore/syft' # only run for main repo
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- run: |
make generate-license-list
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
id: generate-token
with:
app_id: ${{ secrets.TOKEN_APP_ID }}
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 #v8.1.0
with:
signoff: true
delete-branch: true
branch: auto/latest-spdx-license-list
labels: dependencies
commit-message: "chore(deps): update SPDX license list"
title: "chore(deps): update SPDX license list"
body: |
Update SPDX license list based on the latest available list from spdx.org
token: ${{ steps.generate-token.outputs.token }}
- name: Notify Slack on failure
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a #v2.1.1
with:
webhook: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "Syft SPDX license list update failed"
blocks:
- type: section
text:
type: mrkdwn
text: |
*Syft SPDX license list update failed*
• Workflow: `${{ github.workflow }}`
• Event: `${{ github.event_name }}`
• Job Status: `${{ job.status }}`
• <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>
if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }}
+1
View File
@@ -48,6 +48,7 @@ func create(id clio.Identification, out io.Writer) (clio.Application, *cobra.Com
commands.Cataloger(app),
commands.Attest(app),
commands.Convert(app),
commands.Split(app),
clio.VersionCommand(id, schemaVersion),
clio.ConfigCommand(app, nil),
cranecmd.NewCmdAuthLogin(id.Name), // syft login uses the same command as crane
+264
View File
@@ -0,0 +1,264 @@
package commands
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"github.com/anchore/fangs"
"github.com/spf13/cobra"
"github.com/anchore/clio"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/cmd/syft/internal/split"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/format/syftjson"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
)
const (
splitExample = ` {{.appName}} {{.command}} image.sbom.json split SBOM into one file per package
{{.appName}} {{.command}} image.sbom.json --pkg musl split only the musl package
{{.appName}} {{.command}} image.sbom.json --pkg musl --dir /tmp write output to /tmp directory
{{.appName}} {{.command}} image.sbom.json --drop source --drop location:fsid drop source and filesystem IDs
`
)
// SplitOptions defines the options for the split command
type SplitOptions struct {
options.Config `yaml:",inline" mapstructure:",squash"`
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
Packages []string `yaml:"packages" json:"packages" mapstructure:"packages"`
OutputDir string `yaml:"output-dir" json:"output-dir" mapstructure:"output-dir"`
Drop []string `yaml:"drop" json:"drop" mapstructure:"drop"`
}
var _ interface {
clio.FlagAdder
fangs.FieldDescriber
} = (*SplitOptions)(nil)
func (o *SplitOptions) AddFlags(flags clio.FlagSet) {
flags.StringArrayVarP(&o.Packages, "pkg", "p", "package(s) to split (can be specified multiple times)")
flags.StringVarP(&o.OutputDir, "dir", "d", "output directory for split SBOMs (default: current directory)")
flags.StringArrayVarP(&o.Drop, "drop", "", "drop options to apply (source, descriptor, distro, pkg:*, file:*, location:*, all)")
}
func (o *SplitOptions) DescribeFields(descriptions fangs.FieldDescriptionSet) {
descriptions.Add(&o.Packages, "package queries to match for splitting (ID, PURL, name, or name@version)")
descriptions.Add(&o.OutputDir, "output directory for split SBOM files")
descriptions.Add(&o.Drop, "options for dropping SBOM sections (source, descriptor, distro, pkg:licenses, pkg:metadata.files, file:metadata, file:digests, file:executable, file:unknowns, file:licenses, file:contents, location:fsid, location:non-primary-evidence, all)")
}
// Split creates the split command
func Split(app clio.Application) *cobra.Command {
id := app.ID()
opts := &SplitOptions{
UpdateCheck: options.DefaultUpdateCheck(),
OutputDir: ".",
}
return app.SetupCommand(&cobra.Command{
Use: "split [SOURCE-SBOM] [flags]",
Short: "Split an SBOM into separate SBOMs per package",
Long: `[Experimental] Split a syft-format SBOM into one or more SBOMs, one per package.
Each output SBOM contains only the target package, its related packages (connected via
ownership-by-file-overlap and evident-by relationships), and their associated files.
If no --pkg flags are specified, creates one SBOM file per package in the source SBOM.
If --pkg flags are specified, only creates SBOM files for the matching packages.
Package matching (in order of precedence):
1. Exact package ID
2. Exact PURL or PURL prefix
3. Case-insensitive package name
4. name@version format
Drop options:
source Drop the source object entirely
descriptor Drop the descriptor object
distro Drop the distro (Linux distribution) object
pkg:licenses Drop package licenses
pkg:metadata.files Drop files from package metadata (for FileOwner types)
file:metadata Drop file metadata (size, permissions, etc.)
file:digests Drop file digests
file:executable Drop executable metadata
file:unknowns Drop unknown file entries
file:licenses Drop file-level licenses
file:contents Drop file contents
location:fsid Clear FileSystemID from all coordinates
location:non-primary-evidence Drop locations without "evidence": "primary" annotation
all Apply all drop options above`,
Example: internal.Tprintf(splitExample, map[string]interface{}{
"appName": id.Name,
"command": "split",
}),
Args: validateSplitArgs,
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
RunE: func(_ *cobra.Command, args []string) error {
restoreStdout := ui.CaptureStdoutToTraceLog()
defer restoreStdout()
return RunSplit(opts, args[0])
},
}, opts)
}
func validateSplitArgs(cmd *cobra.Command, args []string) error {
return validateArgs(cmd, args, "an SBOM argument is required")
}
// RunSplit executes the split operation
func RunSplit(opts *SplitOptions, userInput string) error {
log.Warn("split is an experimental feature, run `syft split -h` for help")
// validate drop options
for _, d := range opts.Drop {
if !split.ValidDropOption(d) {
return fmt.Errorf("invalid drop option: %q", d)
}
}
// parse drop options
dropOpts := split.ParseDropOptions(opts.Drop)
dropLocationFSID := split.HasDropLocationFSID(dropOpts)
dropNonPrimaryEvidence := split.HasDropLocationNonPrimaryEvidence(dropOpts)
// read SBOM
var reader io.ReadSeekCloser
if userInput == "-" {
reader = internal.NewBufferedSeeker(os.Stdin)
} else {
f, err := os.Open(userInput)
if err != nil {
return fmt.Errorf("failed to open SBOM file: %w", err)
}
defer func() {
_ = f.Close()
}()
reader = f
}
s, formatID, _, err := format.Decode(reader)
if err != nil {
return fmt.Errorf("failed to decode SBOM: %w", err)
}
if s == nil {
return fmt.Errorf("no SBOM produced")
}
// warn if not syft-json format
if formatID != syftjson.ID {
log.Warnf("input SBOM is not syft-json format (detected: %s), some data may be lost", formatID)
}
// determine target packages
var targetPackages []pkg.Package
if len(opts.Packages) == 0 {
// split all packages
targetPackages = s.Artifacts.Packages.Sorted()
} else {
// match specified packages
targetPackages = split.MatchPackages(s.Artifacts.Packages, opts.Packages)
if len(targetPackages) == 0 {
return fmt.Errorf("no packages matched the specified queries: %v", opts.Packages)
}
log.Infof("matched %d package(s) for splitting", len(targetPackages))
}
// ensure output directory exists
if err := os.MkdirAll(opts.OutputDir, 0o755); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
// split SBOM
results := split.Split(*s, targetPackages, dropLocationFSID, dropNonPrimaryEvidence)
// create encoder
encoder, err := syftjson.NewFormatEncoderWithConfig(syftjson.EncoderConfig{
Pretty: true,
})
if err != nil {
return fmt.Errorf("failed to create encoder: %w", err)
}
// get fields to remove from JSON output
fieldsToRemove := split.GetJSONFieldsToRemove(dropOpts)
// write output files
for _, result := range results {
// apply drop options (excluding location:* options which are handled in Split)
filteredDropOpts := make([]split.DropOption, 0, len(dropOpts))
for _, opt := range dropOpts {
if opt != split.DropLocationFSID && opt != split.DropLocationNonPrimaryEvidence {
filteredDropOpts = append(filteredDropOpts, opt)
}
}
split.ApplyDropOptions(&result.SBOM, filteredDropOpts)
// generate output filename using package ID
outputFile := filepath.Join(opts.OutputDir, fmt.Sprintf("%s.json", result.TargetPackage.ID()))
if err := writeSBOMWithFieldRemoval(encoder, result.SBOM, outputFile, fieldsToRemove); err != nil {
return fmt.Errorf("failed to write SBOM for package %s: %w", result.TargetPackage.Name, err)
}
log.Infof("wrote %s (%s@%s)", outputFile, result.TargetPackage.Name, result.TargetPackage.Version)
}
log.Infof("split complete: %d SBOM(s) written to %s", len(results), opts.OutputDir)
return nil
}
func writeSBOMWithFieldRemoval(encoder sbom.FormatEncoder, s sbom.SBOM, outputFile string, fieldsToRemove []string) error {
// if no fields to remove, use direct encoding
if len(fieldsToRemove) == 0 {
f, err := os.Create(outputFile)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer func() {
_ = f.Close()
}()
return encoder.Encode(f, s)
}
// encode to buffer first
var buf bytes.Buffer
if err := encoder.Encode(&buf, s); err != nil {
return fmt.Errorf("failed to encode SBOM: %w", err)
}
// parse as generic map to remove fields
var doc map[string]interface{}
if err := json.Unmarshal(buf.Bytes(), &doc); err != nil {
return fmt.Errorf("failed to parse encoded SBOM: %w", err)
}
// remove specified fields
for _, field := range fieldsToRemove {
delete(doc, field)
}
// re-encode with pretty printing
output, err := json.MarshalIndent(doc, "", " ")
if err != nil {
return fmt.Errorf("failed to re-encode SBOM: %w", err)
}
// write to file
if err := os.WriteFile(outputFile, append(output, '\n'), 0o644); err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
return nil
}
+119
View File
@@ -0,0 +1,119 @@
package commands
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRunSplit_BasicExecution(t *testing.T) {
// use the example SBOM
sbomPath := "../../../../examples/decode_sbom/alpine.syft.json"
if _, err := os.Stat(sbomPath); os.IsNotExist(err) {
t.Skip("example SBOM not found, skipping integration test")
}
// create temporary output directory
tmpDir, err := os.MkdirTemp("", "syft-split-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
opts := &SplitOptions{
Packages: []string{"alpine-baselayout"},
OutputDir: tmpDir,
Drop: []string{},
}
err = RunSplit(opts, sbomPath)
require.NoError(t, err)
// verify output files were created
entries, err := os.ReadDir(tmpDir)
require.NoError(t, err)
assert.NotEmpty(t, entries, "expected at least one output file")
// verify output file is valid JSON
for _, entry := range entries {
if entry.IsDir() {
continue
}
content, err := os.ReadFile(filepath.Join(tmpDir, entry.Name()))
require.NoError(t, err)
assert.NotEmpty(t, content)
// basic JSON validation - should start with {
assert.True(t, len(content) > 0 && content[0] == '{', "output should be valid JSON")
}
}
func TestRunSplit_InvalidDropOption(t *testing.T) {
opts := &SplitOptions{
Packages: []string{"test"},
OutputDir: ".",
Drop: []string{"invalid-option"},
}
err := RunSplit(opts, "some-sbom.json")
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid drop option")
}
func TestRunSplit_NoMatchingPackages(t *testing.T) {
sbomPath := "../../../../examples/decode_sbom/alpine.syft.json"
if _, err := os.Stat(sbomPath); os.IsNotExist(err) {
t.Skip("example SBOM not found, skipping integration test")
}
tmpDir, err := os.MkdirTemp("", "syft-split-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
opts := &SplitOptions{
Packages: []string{"nonexistent-package-xyz"},
OutputDir: tmpDir,
Drop: []string{},
}
err = RunSplit(opts, sbomPath)
require.Error(t, err)
assert.Contains(t, err.Error(), "no packages matched")
}
func TestRunSplit_WithDropOptions(t *testing.T) {
sbomPath := "../../../../examples/decode_sbom/alpine.syft.json"
if _, err := os.Stat(sbomPath); os.IsNotExist(err) {
t.Skip("example SBOM not found, skipping integration test")
}
tmpDir, err := os.MkdirTemp("", "syft-split-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
opts := &SplitOptions{
Packages: []string{"alpine-baselayout"},
OutputDir: tmpDir,
Drop: []string{"source", "location:fsid"},
}
err = RunSplit(opts, sbomPath)
require.NoError(t, err)
// verify output file was created
entries, err := os.ReadDir(tmpDir)
require.NoError(t, err)
assert.NotEmpty(t, entries)
}
func TestRunSplit_FileNotFound(t *testing.T) {
opts := &SplitOptions{
Packages: []string{"test"},
OutputDir: ".",
Drop: []string{},
}
err := RunSplit(opts, "/nonexistent/path/sbom.json")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to open SBOM file")
}
+1 -1
View File
@@ -107,7 +107,7 @@ func fetchLatestApplicationVersion(id clio.Identification) (*hashiVersion.Versio
return nil, fmt.Errorf("HTTP %d on fetching latest version: %s", resp.StatusCode, resp.Status)
}
versionBytes, err := io.ReadAll(io.LimitReader(resp.Body, 500))
versionBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read latest version: %w", err)
}
+307
View File
@@ -0,0 +1,307 @@
package split
import (
"slices"
"strings"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
// DropOption represents a drop option for filtering SBOM contents
type DropOption string
const (
DropSource DropOption = "source"
DropDescriptor DropOption = "descriptor"
DropDistro DropOption = "distro"
DropPkgLicenses DropOption = "pkg:licenses"
DropPkgMetadataFiles DropOption = "pkg:metadata.files"
DropFileMetadata DropOption = "file:metadata"
DropFileDigests DropOption = "file:digests"
DropFileExecutable DropOption = "file:executable"
DropFileUnknowns DropOption = "file:unknowns"
DropFileLicenses DropOption = "file:licenses"
DropFileContents DropOption = "file:contents"
DropLocationFSID DropOption = "location:fsid"
DropLocationNonPrimaryEvidence DropOption = "location:non-primary-evidence"
DropAll DropOption = "all"
)
// AllDropOptions returns all valid drop options (excluding "all")
func AllDropOptions() []DropOption {
return []DropOption{
DropSource,
DropDescriptor,
DropDistro,
DropPkgLicenses,
DropPkgMetadataFiles,
DropFileMetadata,
DropFileDigests,
DropFileExecutable,
DropFileUnknowns,
DropFileLicenses,
DropFileContents,
DropLocationFSID,
DropLocationNonPrimaryEvidence,
}
}
// ParseDropOptions parses string values into DropOption values
func ParseDropOptions(values []string) []DropOption {
var opts []DropOption
for _, v := range values {
opt := DropOption(strings.ToLower(strings.TrimSpace(v)))
if opt == DropAll {
return AllDropOptions()
}
opts = append(opts, opt)
}
return opts
}
// ApplyDropOptions applies the specified drop options to the SBOM
func ApplyDropOptions(s *sbom.SBOM, opts []DropOption) {
if s == nil || len(opts) == 0 {
return
}
for _, opt := range opts {
switch opt {
case DropSource:
s.Source = source.Description{}
case DropDescriptor:
s.Descriptor = sbom.Descriptor{}
case DropDistro:
s.Artifacts.LinuxDistribution = nil
case DropPkgLicenses:
clearPackageLicenses(s)
case DropPkgMetadataFiles:
clearPackageMetadataFiles(s)
case DropFileMetadata:
s.Artifacts.FileMetadata = nil
case DropFileDigests:
s.Artifacts.FileDigests = nil
case DropFileExecutable:
s.Artifacts.Executables = nil
case DropFileUnknowns:
s.Artifacts.Unknowns = nil
case DropFileLicenses:
s.Artifacts.FileLicenses = nil
case DropFileContents:
s.Artifacts.FileContents = nil
case DropLocationFSID:
clearFileSystemIDs(s)
case DropLocationNonPrimaryEvidence:
clearNonPrimaryEvidenceLocations(s)
}
}
}
// GetJSONFieldsToRemove returns the JSON field names that should be completely removed from output
func GetJSONFieldsToRemove(opts []DropOption) []string {
var fields []string
for _, opt := range opts {
switch opt {
case DropSource:
fields = append(fields, "source")
case DropDescriptor:
fields = append(fields, "descriptor")
case DropDistro:
fields = append(fields, "distro")
}
}
return fields
}
// clearFileSystemIDs clears FileSystemID from all coordinates in file artifacts and relationships.
// Note: package locations are handled separately in the splitter when creating new packages.
func clearFileSystemIDs(s *sbom.SBOM) {
// clear from file metadata
if s.Artifacts.FileMetadata != nil {
newMetadata := make(map[file.Coordinates]file.Metadata)
for coord, meta := range s.Artifacts.FileMetadata {
newCoord := file.Coordinates{RealPath: coord.RealPath}
newMetadata[newCoord] = meta
}
s.Artifacts.FileMetadata = newMetadata
}
// clear from file digests
if s.Artifacts.FileDigests != nil {
newDigests := make(map[file.Coordinates][]file.Digest)
for coord, digests := range s.Artifacts.FileDigests {
newCoord := file.Coordinates{RealPath: coord.RealPath}
newDigests[newCoord] = digests
}
s.Artifacts.FileDigests = newDigests
}
// clear from file contents
if s.Artifacts.FileContents != nil {
newContents := make(map[file.Coordinates]string)
for coord, contents := range s.Artifacts.FileContents {
newCoord := file.Coordinates{RealPath: coord.RealPath}
newContents[newCoord] = contents
}
s.Artifacts.FileContents = newContents
}
// clear from file licenses
if s.Artifacts.FileLicenses != nil {
newLicenses := make(map[file.Coordinates][]file.License)
for coord, licenses := range s.Artifacts.FileLicenses {
newCoord := file.Coordinates{RealPath: coord.RealPath}
newLicenses[newCoord] = licenses
}
s.Artifacts.FileLicenses = newLicenses
}
// clear from executables
if s.Artifacts.Executables != nil {
newExec := make(map[file.Coordinates]file.Executable)
for coord, exec := range s.Artifacts.Executables {
newCoord := file.Coordinates{RealPath: coord.RealPath}
newExec[newCoord] = exec
}
s.Artifacts.Executables = newExec
}
// clear from unknowns
if s.Artifacts.Unknowns != nil {
newUnknowns := make(map[file.Coordinates][]string)
for coord, unknowns := range s.Artifacts.Unknowns {
newCoord := file.Coordinates{RealPath: coord.RealPath}
newUnknowns[newCoord] = unknowns
}
s.Artifacts.Unknowns = newUnknowns
}
// clear from relationships that reference file coordinates
newRelationships := make([]artifact.Relationship, 0, len(s.Relationships))
for _, rel := range s.Relationships {
newRel := rel
if coord, ok := rel.From.(file.Coordinates); ok {
newRel.From = file.Coordinates{RealPath: coord.RealPath}
}
if coord, ok := rel.To.(file.Coordinates); ok {
newRel.To = file.Coordinates{RealPath: coord.RealPath}
}
newRelationships = append(newRelationships, newRel)
}
s.Relationships = newRelationships
}
// clearPackageLicenses removes licenses from all packages in the SBOM
func clearPackageLicenses(s *sbom.SBOM) {
if s.Artifacts.Packages == nil {
return
}
for p := range s.Artifacts.Packages.Enumerate() {
p.Licenses = pkg.NewLicenseSet()
s.Artifacts.Packages.Delete(p.ID())
s.Artifacts.Packages.Add(p)
}
}
// clearNonPrimaryEvidenceLocations removes locations that don't have "evidence": "primary" annotation
func clearNonPrimaryEvidenceLocations(s *sbom.SBOM) {
if s.Artifacts.Packages == nil {
return
}
for p := range s.Artifacts.Packages.Enumerate() {
newLocations := file.NewLocationSet()
for _, loc := range p.Locations.ToSlice() {
if loc.Annotations != nil && loc.Annotations["evidence"] == "primary" {
newLocations.Add(loc)
}
}
p.Locations = newLocations
s.Artifacts.Packages.Delete(p.ID())
s.Artifacts.Packages.Add(p)
}
}
// clearPackageMetadataFiles clears the Files field from any package metadata that implements FileOwner
func clearPackageMetadataFiles(s *sbom.SBOM) {
if s.Artifacts.Packages == nil {
return
}
for p := range s.Artifacts.Packages.Enumerate() {
if p.Metadata == nil {
continue
}
newMetadata := clearMetadataFiles(p.Metadata)
if newMetadata != nil {
p.Metadata = newMetadata
s.Artifacts.Packages.Delete(p.ID())
s.Artifacts.Packages.Add(p)
}
}
}
// clearMetadataFiles returns a copy of the metadata with Files field cleared if it implements FileOwner.
// Returns nil if the metadata type is not recognized as a FileOwner implementer.
func clearMetadataFiles(metadata any) any {
switch m := metadata.(type) {
case pkg.ApkDBEntry:
m.Files = nil
return m
case pkg.RpmDBEntry:
m.Files = nil
return m
case pkg.DpkgDBEntry:
m.Files = nil
return m
case pkg.AlpmDBEntry:
m.Files = nil
return m
case pkg.PortageEntry:
m.Files = nil
return m
case pkg.NixStoreEntry:
m.Files = nil
return m
case pkg.PythonPackage:
m.Files = nil
return m
case pkg.CondaMetaPackage:
m.Files = nil
return m
case pkg.BitnamiSBOMEntry:
m.Files = nil
return m
case pkg.JavaVMInstallation:
m.Files = nil
return m
}
return nil
}
// ValidDropOption checks if a string is a valid drop option
func ValidDropOption(s string) bool {
opt := DropOption(strings.ToLower(strings.TrimSpace(s)))
if opt == DropAll {
return true
}
return slices.Contains(AllDropOptions(), opt)
}
// HasDropLocationFSID checks if the drop options include location:fsid
func HasDropLocationFSID(opts []DropOption) bool {
return slices.Contains(opts, DropLocationFSID)
}
// HasDropLocationNonPrimaryEvidence checks if the drop options include location:non-primary-evidence
func HasDropLocationNonPrimaryEvidence(opts []DropOption) bool {
return slices.Contains(opts, DropLocationNonPrimaryEvidence)
}
+499
View File
@@ -0,0 +1,499 @@
package split
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func TestParseDropOptions(t *testing.T) {
tests := []struct {
name string
values []string
want []DropOption
}{
{
name: "single option",
values: []string{"source"},
want: []DropOption{DropSource},
},
{
name: "multiple options",
values: []string{"source", "descriptor", "file:digests"},
want: []DropOption{DropSource, DropDescriptor, DropFileDigests},
},
{
name: "all option expands",
values: []string{"all"},
want: AllDropOptions(),
},
{
name: "case insensitive",
values: []string{"SOURCE", "File:Digests"},
want: []DropOption{DropSource, DropFileDigests},
},
{
name: "with whitespace",
values: []string{" source ", " location:fsid"},
want: []DropOption{DropSource, DropLocationFSID},
},
{
name: "empty values",
values: []string{},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ParseDropOptions(tt.values)
assert.Equal(t, tt.want, got)
})
}
}
func TestValidDropOption(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
{name: "valid source", input: "source", want: true},
{name: "valid descriptor", input: "descriptor", want: true},
{name: "valid distro", input: "distro", want: true},
{name: "valid file:metadata", input: "file:metadata", want: true},
{name: "valid file:digests", input: "file:digests", want: true},
{name: "valid file:executable", input: "file:executable", want: true},
{name: "valid file:unknowns", input: "file:unknowns", want: true},
{name: "valid file:licenses", input: "file:licenses", want: true},
{name: "valid file:contents", input: "file:contents", want: true},
{name: "valid location:fsid", input: "location:fsid", want: true},
{name: "valid location:non-primary-evidence", input: "location:non-primary-evidence", want: true},
{name: "valid pkg:licenses", input: "pkg:licenses", want: true},
{name: "valid pkg:metadata.files", input: "pkg:metadata.files", want: true},
{name: "valid all", input: "all", want: true},
{name: "case insensitive", input: "SOURCE", want: true},
{name: "invalid option", input: "invalid", want: false},
{name: "partial match", input: "file", want: false},
{name: "empty string", input: "", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidDropOption(tt.input)
assert.Equal(t, tt.want, got)
})
}
}
func TestApplyDropOptions(t *testing.T) {
coord := file.Coordinates{
RealPath: "/test/path",
FileSystemID: "layer123",
}
baseSBOM := func() *sbom.SBOM {
return &sbom.SBOM{
Source: source.Description{
ID: "test-id",
Name: "test-source",
},
Descriptor: sbom.Descriptor{
Name: "syft",
Version: "1.0.0",
},
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(),
FileMetadata: map[file.Coordinates]file.Metadata{coord: {}},
FileDigests: map[file.Coordinates][]file.Digest{coord: {{Algorithm: "sha256", Value: "abc"}}},
FileContents: map[file.Coordinates]string{coord: "contents"},
FileLicenses: map[file.Coordinates][]file.License{coord: {{Value: "MIT"}}},
Executables: map[file.Coordinates]file.Executable{coord: {}},
Unknowns: map[file.Coordinates][]string{coord: {"unknown"}},
},
Relationships: []artifact.Relationship{
{
From: coord,
To: coord,
Type: artifact.EvidentByRelationship,
},
},
}
}
tests := []struct {
name string
opts []DropOption
verify func(t *testing.T, s *sbom.SBOM)
}{
{
name: "drop source",
opts: []DropOption{DropSource},
verify: func(t *testing.T, s *sbom.SBOM) {
assert.Empty(t, s.Source.ID)
assert.Empty(t, s.Source.Name)
// other fields should be preserved
assert.Equal(t, "syft", s.Descriptor.Name)
},
},
{
name: "drop descriptor",
opts: []DropOption{DropDescriptor},
verify: func(t *testing.T, s *sbom.SBOM) {
assert.Empty(t, s.Descriptor.Name)
assert.Empty(t, s.Descriptor.Version)
// other fields should be preserved
assert.Equal(t, "test-id", s.Source.ID)
},
},
{
name: "drop distro",
opts: []DropOption{DropDistro},
verify: func(t *testing.T, s *sbom.SBOM) {
assert.Nil(t, s.Artifacts.LinuxDistribution)
// other fields should be preserved
assert.Equal(t, "test-id", s.Source.ID)
},
},
{
name: "drop file:metadata",
opts: []DropOption{DropFileMetadata},
verify: func(t *testing.T, s *sbom.SBOM) {
assert.Nil(t, s.Artifacts.FileMetadata)
// other file artifacts should be preserved
assert.NotNil(t, s.Artifacts.FileDigests)
},
},
{
name: "drop file:digests",
opts: []DropOption{DropFileDigests},
verify: func(t *testing.T, s *sbom.SBOM) {
assert.Nil(t, s.Artifacts.FileDigests)
// other file artifacts should be preserved
assert.NotNil(t, s.Artifacts.FileMetadata)
},
},
{
name: "drop file:executable",
opts: []DropOption{DropFileExecutable},
verify: func(t *testing.T, s *sbom.SBOM) {
assert.Nil(t, s.Artifacts.Executables)
},
},
{
name: "drop file:unknowns",
opts: []DropOption{DropFileUnknowns},
verify: func(t *testing.T, s *sbom.SBOM) {
assert.Nil(t, s.Artifacts.Unknowns)
},
},
{
name: "drop file:licenses",
opts: []DropOption{DropFileLicenses},
verify: func(t *testing.T, s *sbom.SBOM) {
assert.Nil(t, s.Artifacts.FileLicenses)
},
},
{
name: "drop file:contents",
opts: []DropOption{DropFileContents},
verify: func(t *testing.T, s *sbom.SBOM) {
assert.Nil(t, s.Artifacts.FileContents)
},
},
{
name: "drop location:fsid clears FileSystemID",
opts: []DropOption{DropLocationFSID},
verify: func(t *testing.T, s *sbom.SBOM) {
// check that FileSystemID is cleared from file metadata
for coord := range s.Artifacts.FileMetadata {
assert.Empty(t, coord.FileSystemID, "FileSystemID should be empty in FileMetadata")
assert.Equal(t, "/test/path", coord.RealPath)
}
// check relationships
for _, rel := range s.Relationships {
if c, ok := rel.From.(file.Coordinates); ok {
assert.Empty(t, c.FileSystemID, "FileSystemID should be empty in relationship From")
}
if c, ok := rel.To.(file.Coordinates); ok {
assert.Empty(t, c.FileSystemID, "FileSystemID should be empty in relationship To")
}
}
},
},
{
name: "drop pkg:metadata.files clears files from ApkDBEntry",
opts: []DropOption{DropPkgMetadataFiles},
verify: func(t *testing.T, s *sbom.SBOM) {
// first add a package with ApkDBEntry metadata
p := pkg.Package{
Name: "test-apk",
Version: "1.0.0",
Type: pkg.ApkPkg,
Metadata: pkg.ApkDBEntry{
Package: "test-apk",
Files: []pkg.ApkFileRecord{
{Path: "/usr/bin/test"},
},
},
}
s.Artifacts.Packages.Add(p)
ApplyDropOptions(s, []DropOption{DropPkgMetadataFiles})
// verify files were cleared
for p := range s.Artifacts.Packages.Enumerate() {
if p.Name == "test-apk" {
meta, ok := p.Metadata.(pkg.ApkDBEntry)
require.True(t, ok)
assert.Nil(t, meta.Files)
}
}
},
},
{
name: "drop pkg:metadata.files clears files from RpmDBEntry",
opts: []DropOption{DropPkgMetadataFiles},
verify: func(t *testing.T, s *sbom.SBOM) {
p := pkg.Package{
Name: "test-rpm",
Version: "1.0.0",
Type: pkg.RpmPkg,
Metadata: pkg.RpmDBEntry{
Name: "test-rpm",
Files: []pkg.RpmFileRecord{
{Path: "/usr/bin/test"},
},
},
}
s.Artifacts.Packages.Add(p)
ApplyDropOptions(s, []DropOption{DropPkgMetadataFiles})
for p := range s.Artifacts.Packages.Enumerate() {
if p.Name == "test-rpm" {
meta, ok := p.Metadata.(pkg.RpmDBEntry)
require.True(t, ok)
assert.Nil(t, meta.Files)
}
}
},
},
{
name: "drop pkg:metadata.files clears files from DpkgDBEntry",
opts: []DropOption{DropPkgMetadataFiles},
verify: func(t *testing.T, s *sbom.SBOM) {
p := pkg.Package{
Name: "test-deb",
Version: "1.0.0",
Type: pkg.DebPkg,
Metadata: pkg.DpkgDBEntry{
Package: "test-deb",
Files: []pkg.DpkgFileRecord{
{Path: "/usr/bin/test"},
},
},
}
s.Artifacts.Packages.Add(p)
ApplyDropOptions(s, []DropOption{DropPkgMetadataFiles})
for p := range s.Artifacts.Packages.Enumerate() {
if p.Name == "test-deb" {
meta, ok := p.Metadata.(pkg.DpkgDBEntry)
require.True(t, ok)
assert.Nil(t, meta.Files)
}
}
},
},
{
name: "drop pkg:metadata.files clears files from PythonPackage",
opts: []DropOption{DropPkgMetadataFiles},
verify: func(t *testing.T, s *sbom.SBOM) {
p := pkg.Package{
Name: "test-python",
Version: "1.0.0",
Type: pkg.PythonPkg,
Metadata: pkg.PythonPackage{
Name: "test-python",
Files: []pkg.PythonFileRecord{
{Path: "/usr/lib/python/test.py"},
},
},
}
s.Artifacts.Packages.Add(p)
ApplyDropOptions(s, []DropOption{DropPkgMetadataFiles})
for p := range s.Artifacts.Packages.Enumerate() {
if p.Name == "test-python" {
meta, ok := p.Metadata.(pkg.PythonPackage)
require.True(t, ok)
assert.Nil(t, meta.Files)
}
}
},
},
{
name: "drop pkg:metadata.files does nothing for non-FileOwner metadata",
opts: []DropOption{DropPkgMetadataFiles},
verify: func(t *testing.T, s *sbom.SBOM) {
// add a package with metadata that doesn't implement FileOwner
p := pkg.Package{
Name: "test-npm",
Version: "1.0.0",
Type: pkg.NpmPkg,
Metadata: pkg.NpmPackage{
Name: "test-npm",
Version: "1.0.0",
},
}
s.Artifacts.Packages.Add(p)
ApplyDropOptions(s, []DropOption{DropPkgMetadataFiles})
// verify package still exists with metadata intact
for p := range s.Artifacts.Packages.Enumerate() {
if p.Name == "test-npm" {
meta, ok := p.Metadata.(pkg.NpmPackage)
require.True(t, ok)
assert.Equal(t, "test-npm", meta.Name)
}
}
},
},
{
name: "nil SBOM does not panic",
opts: []DropOption{DropSource},
verify: func(t *testing.T, s *sbom.SBOM) {
// nothing to verify, just ensure no panic
},
},
{
name: "empty options does nothing",
opts: []DropOption{},
verify: func(t *testing.T, s *sbom.SBOM) {
assert.Equal(t, "test-id", s.Source.ID)
assert.Equal(t, "syft", s.Descriptor.Name)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var s *sbom.SBOM
if tt.name != "nil SBOM does not panic" {
s = baseSBOM()
}
// should not panic
require.NotPanics(t, func() {
ApplyDropOptions(s, tt.opts)
})
if s != nil {
tt.verify(t, s)
}
})
}
}
func TestHasDropLocationFSID(t *testing.T) {
tests := []struct {
name string
opts []DropOption
want bool
}{
{
name: "has location:fsid",
opts: []DropOption{DropSource, DropLocationFSID, DropDescriptor},
want: true,
},
{
name: "no location:fsid",
opts: []DropOption{DropSource, DropDescriptor},
want: false,
},
{
name: "empty opts",
opts: []DropOption{},
want: false,
},
{
name: "nil opts",
opts: nil,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := HasDropLocationFSID(tt.opts)
assert.Equal(t, tt.want, got)
})
}
}
func TestGetJSONFieldsToRemove(t *testing.T) {
tests := []struct {
name string
opts []DropOption
want []string
}{
{
name: "source only",
opts: []DropOption{DropSource},
want: []string{"source"},
},
{
name: "descriptor only",
opts: []DropOption{DropDescriptor},
want: []string{"descriptor"},
},
{
name: "distro only",
opts: []DropOption{DropDistro},
want: []string{"distro"},
},
{
name: "all three",
opts: []DropOption{DropSource, DropDescriptor, DropDistro},
want: []string{"source", "descriptor", "distro"},
},
{
name: "mixed with file options",
opts: []DropOption{DropSource, DropFileDigests, DropDescriptor, DropFileMetadata},
want: []string{"source", "descriptor"},
},
{
name: "file options only returns empty",
opts: []DropOption{DropFileDigests, DropFileMetadata, DropLocationFSID},
want: nil,
},
{
name: "empty opts",
opts: []DropOption{},
want: nil,
},
{
name: "nil opts",
opts: nil,
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetJSONFieldsToRemove(tt.opts)
assert.Equal(t, tt.want, got)
})
}
}
+92
View File
@@ -0,0 +1,92 @@
package split
import (
"strings"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
)
// MatchPackages finds packages in the collection that match the given queries.
// Match order (first match wins):
// 1. Exact package ID match
// 2. Exact PURL match or PURL prefix match
// 3. Case-insensitive name match
// 4. name@version format match
func MatchPackages(collection *pkg.Collection, queries []string) []pkg.Package {
if collection == nil || len(queries) == 0 {
return nil
}
// build indexes for efficient lookups
byID := make(map[artifact.ID]pkg.Package)
byPURL := make(map[string]pkg.Package)
byNameLower := make(map[string][]pkg.Package)
byNameVersion := make(map[string]pkg.Package)
for p := range collection.Enumerate() {
byID[p.ID()] = p
if p.PURL != "" {
byPURL[p.PURL] = p
}
nameLower := strings.ToLower(p.Name)
byNameLower[nameLower] = append(byNameLower[nameLower], p)
nameVersion := strings.ToLower(p.Name + "@" + p.Version)
byNameVersion[nameVersion] = p
}
// track matched packages to avoid duplicates
matched := make(map[artifact.ID]pkg.Package)
for _, query := range queries {
// 1. exact package ID match
if p, ok := byID[artifact.ID(query)]; ok {
matched[p.ID()] = p
continue
}
// 2. exact PURL match
if p, ok := byPURL[query]; ok {
matched[p.ID()] = p
continue
}
// 2b. PURL prefix match (e.g., "pkg:apk/alpine/musl" matches "pkg:apk/alpine/musl@1.2.2")
if strings.HasPrefix(query, "pkg:") {
for purl, p := range byPURL {
if strings.HasPrefix(purl, query) {
matched[p.ID()] = p
}
}
if len(matched) > 0 {
continue
}
}
queryLower := strings.ToLower(query)
// 3. case-insensitive name match
if pkgs, ok := byNameLower[queryLower]; ok {
for _, p := range pkgs {
matched[p.ID()] = p
}
continue
}
// 4. name@version format match
if p, ok := byNameVersion[queryLower]; ok {
matched[p.ID()] = p
continue
}
}
// convert map to slice
result := make([]pkg.Package, 0, len(matched))
for _, p := range matched {
result = append(result, p)
}
// sort for stable output
pkg.Sort(result)
return result
}
+124
View File
@@ -0,0 +1,124 @@
package split
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/pkg"
)
func TestMatchPackages(t *testing.T) {
// create test packages
pkgA := pkg.Package{
Name: "alpine-baselayout",
Version: "3.2.0-r7",
PURL: "pkg:apk/alpine/alpine-baselayout@3.2.0-r7",
}
pkgA.SetID()
pkgB := pkg.Package{
Name: "musl",
Version: "1.2.2-r0",
PURL: "pkg:apk/alpine/musl@1.2.2-r0",
}
pkgB.SetID()
pkgC := pkg.Package{
Name: "Musl", // different case
Version: "2.0.0",
PURL: "pkg:apk/alpine/Musl@2.0.0",
}
pkgC.SetID()
collection := pkg.NewCollection(pkgA, pkgB, pkgC)
tests := []struct {
name string
queries []string
want []pkg.Package
}{
{
name: "match by exact package ID",
queries: []string{string(pkgA.ID())},
want: []pkg.Package{pkgA},
},
{
name: "match by exact PURL",
queries: []string{"pkg:apk/alpine/musl@1.2.2-r0"},
want: []pkg.Package{pkgB},
},
{
name: "match by PURL prefix",
queries: []string{"pkg:apk/alpine/musl"},
want: []pkg.Package{pkgB},
},
{
name: "match by case-insensitive name",
queries: []string{"musl"},
want: []pkg.Package{pkgB, pkgC},
},
{
name: "match by name@version",
queries: []string{"musl@1.2.2-r0"},
want: []pkg.Package{pkgB},
},
{
name: "multiple queries",
queries: []string{"alpine-baselayout", "musl@1.2.2-r0"},
want: []pkg.Package{pkgA, pkgB},
},
{
name: "no match",
queries: []string{"nonexistent"},
want: []pkg.Package{},
},
{
name: "empty queries",
queries: []string{},
want: nil,
},
{
name: "nil collection returns nil",
queries: []string{"musl"},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
coll := collection
if tt.name == "nil collection returns nil" {
coll = nil
}
got := MatchPackages(coll, tt.queries)
if tt.want == nil {
require.Nil(t, got)
return
}
require.Len(t, got, len(tt.want))
if len(tt.want) == 0 {
return
}
// sort both for comparison
pkg.Sort(tt.want)
// compare using cmp.Diff, ignoring unexported fields
opts := []cmp.Option{
cmpopts.IgnoreUnexported(pkg.Package{}),
cmpopts.IgnoreFields(pkg.Package{}, "Locations", "Licenses", "CPEs"),
}
if diff := cmp.Diff(tt.want, got, opts...); diff != "" {
t.Errorf("MatchPackages() mismatch (-want +got):\n%s", diff)
}
})
}
}
@@ -0,0 +1,158 @@
package split
import (
"slices"
"strings"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
)
// relationshipIndex indexes relationships for efficient lookup by from/to IDs.
// This is a simplified version tailored for the split functionality.
type relationshipIndex struct {
relationships []*sortableRelationship
fromID map[artifact.ID]*mappedRelationships
toID map[artifact.ID]*mappedRelationships
}
// newRelationshipIndex creates a new relationship index from the given relationships
func newRelationshipIndex(relationships ...artifact.Relationship) *relationshipIndex {
idx := &relationshipIndex{
fromID: make(map[artifact.ID]*mappedRelationships),
toID: make(map[artifact.ID]*mappedRelationships),
}
for _, r := range relationships {
// prevent duplicates
if idx.contains(r) {
continue
}
fromID := r.From.ID()
toID := r.To.ID()
sr := &sortableRelationship{
from: fromID,
to: toID,
relationship: r,
}
idx.relationships = append(idx.relationships, sr)
// add from -> to mapping
if idx.fromID[fromID] == nil {
idx.fromID[fromID] = &mappedRelationships{}
}
idx.fromID[fromID].add(toID, sr)
// add to -> from mapping
if idx.toID[toID] == nil {
idx.toID[toID] = &mappedRelationships{}
}
idx.toID[toID].add(fromID, sr)
}
return idx
}
// from returns all relationships from the given identifiable, filtered by types
func (i *relationshipIndex) from(identifiable artifact.Identifiable, types ...artifact.RelationshipType) []artifact.Relationship {
return toSortedSlice(fromMapped(i.fromID, identifiable), types)
}
// to returns all relationships to the given identifiable, filtered by types
func (i *relationshipIndex) to(identifiable artifact.Identifiable, types ...artifact.RelationshipType) []artifact.Relationship {
return toSortedSlice(fromMapped(i.toID, identifiable), types)
}
// coordinates returns all file coordinates referenced by relationships for the given identifiable
func (i *relationshipIndex) coordinates(identifiable artifact.Identifiable, types ...artifact.RelationshipType) []file.Coordinates {
// get relationships in both directions
rels := append(fromMapped(i.fromID, identifiable), fromMapped(i.toID, identifiable)...)
sorted := toSortedSlice(rels, types)
var coords []file.Coordinates
for _, rel := range sorted {
if c, ok := rel.From.(file.Coordinates); ok {
coords = append(coords, c)
}
if c, ok := rel.To.(file.Coordinates); ok {
coords = append(coords, c)
}
}
return coords
}
// all returns all relationships, optionally filtered by types
func (i *relationshipIndex) all(types ...artifact.RelationshipType) []artifact.Relationship {
return toSortedSlice(i.relationships, types)
}
// contains checks if the relationship is already in the index
func (i *relationshipIndex) contains(r artifact.Relationship) bool {
mapped := i.fromID[r.From.ID()]
if mapped == nil {
return false
}
typeMap := mapped.typeMap[r.Type]
if typeMap == nil {
return false
}
return typeMap[r.To.ID()] != nil
}
type mappedRelationships struct {
typeMap map[artifact.RelationshipType]map[artifact.ID]*sortableRelationship
allRelated []*sortableRelationship
}
func (m *mappedRelationships) add(id artifact.ID, sr *sortableRelationship) {
m.allRelated = append(m.allRelated, sr)
if m.typeMap == nil {
m.typeMap = make(map[artifact.RelationshipType]map[artifact.ID]*sortableRelationship)
}
if m.typeMap[sr.relationship.Type] == nil {
m.typeMap[sr.relationship.Type] = make(map[artifact.ID]*sortableRelationship)
}
m.typeMap[sr.relationship.Type][id] = sr
}
type sortableRelationship struct {
from artifact.ID
to artifact.ID
relationship artifact.Relationship
}
func fromMapped(idMap map[artifact.ID]*mappedRelationships, identifiable artifact.Identifiable) []*sortableRelationship {
if identifiable == nil {
return nil
}
mapped := idMap[identifiable.ID()]
if mapped == nil {
return nil
}
return mapped.allRelated
}
func toSortedSlice(relationships []*sortableRelationship, types []artifact.RelationshipType) []artifact.Relationship {
slices.SortFunc(relationships, func(a, b *sortableRelationship) int {
cmp := strings.Compare(string(a.relationship.Type), string(b.relationship.Type))
if cmp != 0 {
return cmp
}
cmp = strings.Compare(string(a.from), string(b.from))
if cmp != 0 {
return cmp
}
return strings.Compare(string(a.to), string(b.to))
})
var out []artifact.Relationship
for _, r := range relationships {
if len(types) == 0 || slices.Contains(types, r.relationship.Type) {
out = append(out, r.relationship)
}
}
return out
}
+314
View File
@@ -0,0 +1,314 @@
package split
import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
)
// allowedRelationshipTypes are the only relationship types allowed in split output
var allowedRelationshipTypes = []artifact.RelationshipType{
artifact.OwnershipByFileOverlapRelationship,
artifact.EvidentByRelationship,
}
// Result represents the result of splitting an SBOM for a single target package
type Result struct {
TargetPackage pkg.Package
SBOM sbom.SBOM
}
// Split splits the source SBOM into separate SBOMs, one for each target package.
// Each output SBOM contains the target package, its connected packages (via ownership-by-file-overlap
// and evident-by relationships), and all related files.
func Split(source sbom.SBOM, targetPackages []pkg.Package, dropLocationFSID, dropNonPrimaryEvidence bool) []Result {
if len(targetPackages) == 0 {
return nil
}
relIndex := newRelationshipIndex(source.Relationships...)
results := make([]Result, 0, len(targetPackages))
for _, target := range targetPackages {
result := splitForPackage(source, target, relIndex, dropLocationFSID, dropNonPrimaryEvidence)
results = append(results, result)
}
return results
}
// splitForPackage creates a new SBOM containing only the target package and its related artifacts
func splitForPackage(source sbom.SBOM, target pkg.Package, relIndex *relationshipIndex, dropLocationFSID, dropNonPrimaryEvidence bool) Result {
// find all connected packages via BFS
connectedPkgs := findConnectedPackages(source, target, relIndex)
// collect all kept package IDs for relationship filtering
keptPkgIDs := make(map[artifact.ID]bool)
for _, p := range connectedPkgs {
keptPkgIDs[p.ID()] = true
}
// find related file coordinates from relationships and package locations
keptCoords := collectFileCoordinates(connectedPkgs, relIndex, dropLocationFSID, dropNonPrimaryEvidence)
// build filtered SBOM
filteredSBOM := buildFilteredSBOM(source, connectedPkgs, keptPkgIDs, keptCoords, relIndex, dropLocationFSID, dropNonPrimaryEvidence)
return Result{
TargetPackage: target,
SBOM: filteredSBOM,
}
}
// findConnectedPackages uses BFS to find all packages connected to the target via traversal relationships
func findConnectedPackages(source sbom.SBOM, target pkg.Package, relIndex *relationshipIndex) []pkg.Package {
visited := make(map[artifact.ID]bool)
var result []pkg.Package
queue := []pkg.Package{target}
visited[target.ID()] = true
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
result = append(result, current)
// find connected packages through relationships (both directions)
for _, rel := range relIndex.from(current, allowedRelationshipTypes...) {
toID := rel.To.ID()
if visited[toID] {
continue
}
// check if the target is a package in the source collection
if p := source.Artifacts.Packages.Package(toID); p != nil {
visited[toID] = true
queue = append(queue, *p)
}
}
for _, rel := range relIndex.to(current, allowedRelationshipTypes...) {
fromID := rel.From.ID()
if visited[fromID] {
continue
}
// check if the source is a package in the source collection
if p := source.Artifacts.Packages.Package(fromID); p != nil {
visited[fromID] = true
queue = append(queue, *p)
}
}
}
return result
}
// collectFileCoordinates gathers all file coordinates related to the kept packages
func collectFileCoordinates(packages []pkg.Package, relIndex *relationshipIndex, dropLocationFSID, dropNonPrimaryEvidence bool) map[file.Coordinates]bool {
coords := make(map[file.Coordinates]bool)
for _, p := range packages {
// collect coordinates from package locations
for _, loc := range p.Locations.ToSlice() {
// skip non-primary evidence locations if requested
if dropNonPrimaryEvidence {
if loc.Annotations == nil || loc.Annotations["evidence"] != "primary" {
continue
}
}
coord := loc.Coordinates
if dropLocationFSID {
coord = file.Coordinates{RealPath: coord.RealPath}
}
coords[coord] = true
}
// collect coordinates from allowed relationship types only
for _, c := range relIndex.coordinates(p, allowedRelationshipTypes...) {
coord := c
if dropLocationFSID {
coord = file.Coordinates{RealPath: c.RealPath}
}
coords[coord] = true
}
}
return coords
}
// buildFilteredSBOM creates a new SBOM with only the kept packages, files, and relationships
func buildFilteredSBOM(source sbom.SBOM, packages []pkg.Package, keptPkgIDs map[artifact.ID]bool, keptCoords map[file.Coordinates]bool, relIndex *relationshipIndex, dropLocationFSID, dropNonPrimaryEvidence bool) sbom.SBOM {
// create new package collection
newPkgCollection := pkg.NewCollection()
for _, p := range packages {
// filter non-primary evidence locations if requested
if dropNonPrimaryEvidence {
p = filterPackageNonPrimaryLocations(p)
}
// if dropLocationFSID is enabled, we need to clear FileSystemID from package locations
if dropLocationFSID {
p = clearPackageFileSystemIDs(p)
}
newPkgCollection.Add(p)
}
// filter file artifacts
filteredSBOM := sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: newPkgCollection,
FileMetadata: filterFileMap(source.Artifacts.FileMetadata, keptCoords, dropLocationFSID),
FileDigests: filterFileMap(source.Artifacts.FileDigests, keptCoords, dropLocationFSID),
FileContents: filterFileMap(source.Artifacts.FileContents, keptCoords, dropLocationFSID),
FileLicenses: filterFileMap(source.Artifacts.FileLicenses, keptCoords, dropLocationFSID),
Executables: filterFileMap(source.Artifacts.Executables, keptCoords, dropLocationFSID),
Unknowns: filterFileMap(source.Artifacts.Unknowns, keptCoords, dropLocationFSID),
LinuxDistribution: source.Artifacts.LinuxDistribution,
},
Relationships: filterRelationships(relIndex.all(), keptPkgIDs, keptCoords, dropLocationFSID),
Source: source.Source,
Descriptor: source.Descriptor,
}
return filteredSBOM
}
// filterPackageNonPrimaryLocations creates a copy of the package with only primary evidence locations
func filterPackageNonPrimaryLocations(p pkg.Package) pkg.Package {
newLocations := file.NewLocationSet()
for _, loc := range p.Locations.ToSlice() {
if loc.Annotations != nil && loc.Annotations["evidence"] == "primary" {
newLocations.Add(loc)
}
}
p.Locations = newLocations
return p
}
// filterFileMap filters a map of file.Coordinates to only include kept coordinates
func filterFileMap[T any](m map[file.Coordinates]T, keptCoords map[file.Coordinates]bool, dropLocationFSID bool) map[file.Coordinates]T {
if m == nil {
return nil
}
result := make(map[file.Coordinates]T)
for coord, value := range m {
checkCoord := coord
if dropLocationFSID {
checkCoord = file.Coordinates{RealPath: coord.RealPath}
}
if keptCoords[checkCoord] {
outputCoord := coord
if dropLocationFSID {
outputCoord = file.Coordinates{RealPath: coord.RealPath}
}
result[outputCoord] = value
}
}
if len(result) == 0 {
return nil
}
return result
}
// filterRelationships filters relationships to only include allowed types referencing kept artifacts
func filterRelationships(relationships []artifact.Relationship, keptPkgIDs map[artifact.ID]bool, keptCoords map[file.Coordinates]bool, dropLocationFSID bool) []artifact.Relationship {
var result []artifact.Relationship
for _, rel := range relationships {
// only keep allowed relationship types
if !isAllowedRelationshipType(rel.Type) {
continue
}
// check if both ends of the relationship reference kept artifacts
fromKept := isArtifactKept(rel.From, keptPkgIDs, keptCoords, dropLocationFSID)
toKept := isArtifactKept(rel.To, keptPkgIDs, keptCoords, dropLocationFSID)
if fromKept && toKept {
newRel := rel
if dropLocationFSID {
if coord, ok := rel.From.(file.Coordinates); ok {
newRel.From = file.Coordinates{RealPath: coord.RealPath}
}
if coord, ok := rel.To.(file.Coordinates); ok {
newRel.To = file.Coordinates{RealPath: coord.RealPath}
}
}
result = append(result, newRel)
}
}
return result
}
// isAllowedRelationshipType checks if a relationship type is in the allowed list
func isAllowedRelationshipType(t artifact.RelationshipType) bool {
for _, allowed := range allowedRelationshipTypes {
if t == allowed {
return true
}
}
return false
}
// isArtifactKept checks if an artifact (package or file) is in the kept set
func isArtifactKept(a artifact.Identifiable, keptPkgIDs map[artifact.ID]bool, keptCoords map[file.Coordinates]bool, dropLocationFSID bool) bool {
if a == nil {
return false
}
// check if it's a file coordinate
if coord, ok := a.(file.Coordinates); ok {
checkCoord := coord
if dropLocationFSID {
checkCoord = file.Coordinates{RealPath: coord.RealPath}
}
return keptCoords[checkCoord]
}
// otherwise check package ID
return keptPkgIDs[a.ID()]
}
// clearPackageFileSystemIDs creates a copy of the package with FileSystemID cleared from all locations
func clearPackageFileSystemIDs(p pkg.Package) pkg.Package {
newLocations := file.NewLocationSet()
for _, loc := range p.Locations.ToSlice() {
newLoc := file.Location{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: loc.RealPath,
// FileSystemID intentionally left empty
},
AccessPath: loc.AccessPath,
},
LocationMetadata: loc.LocationMetadata,
}
newLocations.Add(newLoc)
}
p.Locations = newLocations
// also clear from license locations
newLicenses := pkg.NewLicenseSet()
for _, lic := range p.Licenses.ToSlice() {
newLicLocs := file.NewLocationSet()
for _, loc := range lic.Locations.ToSlice() {
newLoc := file.Location{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: loc.RealPath,
},
AccessPath: loc.AccessPath,
},
LocationMetadata: loc.LocationMetadata,
}
newLicLocs.Add(newLoc)
}
lic.Locations = newLicLocs
newLicenses.Add(lic)
}
p.Licenses = newLicenses
return p
}
+606
View File
@@ -0,0 +1,606 @@
package split
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func TestSplit(t *testing.T) {
// create test coordinates
coord1 := file.Coordinates{RealPath: "/lib/apk/db/installed", FileSystemID: "layer1"}
coord2 := file.Coordinates{RealPath: "/usr/lib/libmusl.so", FileSystemID: "layer1"}
coord3 := file.Coordinates{RealPath: "/unrelated/file", FileSystemID: "layer1"}
// create test packages
pkgA := pkg.Package{
Name: "alpine-baselayout",
Version: "3.2.0-r7",
Type: pkg.ApkPkg,
Locations: file.NewLocationSet(file.NewLocationFromCoordinates(coord1)),
}
pkgA.SetID()
pkgB := pkg.Package{
Name: "musl",
Version: "1.2.2-r0",
Type: pkg.ApkPkg,
Locations: file.NewLocationSet(file.NewLocationFromCoordinates(coord2)),
}
pkgB.SetID()
pkgC := pkg.Package{
Name: "unrelated",
Version: "1.0.0",
Type: pkg.ApkPkg,
Locations: file.NewLocationSet(file.NewLocationFromCoordinates(coord3)),
}
pkgC.SetID()
// create source SBOM with relationships
sourceSBOM := sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(pkgA, pkgB, pkgC),
FileMetadata: map[file.Coordinates]file.Metadata{
coord1: {MIMEType: "text/plain"},
coord2: {MIMEType: "application/x-sharedlib"},
coord3: {MIMEType: "text/plain"},
},
FileDigests: map[file.Coordinates][]file.Digest{
coord1: {{Algorithm: "sha256", Value: "abc123"}},
coord2: {{Algorithm: "sha256", Value: "def456"}},
coord3: {{Algorithm: "sha256", Value: "ghi789"}},
},
LinuxDistribution: &linux.Release{ID: "alpine", VersionID: "3.12"},
},
Relationships: []artifact.Relationship{
// pkgA owns pkgB via file overlap
{
From: pkgA,
To: pkgB,
Type: artifact.OwnershipByFileOverlapRelationship,
},
// pkgA is evident by coord1
{
From: pkgA,
To: coord1,
Type: artifact.EvidentByRelationship,
},
// pkgB is evident by coord2
{
From: pkgB,
To: coord2,
Type: artifact.EvidentByRelationship,
},
// pkgC is evident by coord3 (unrelated)
{
From: pkgC,
To: coord3,
Type: artifact.EvidentByRelationship,
},
},
Source: source.Description{
ID: "test-source-id",
Name: "test-image",
},
Descriptor: sbom.Descriptor{
Name: "syft",
Version: "test",
},
}
tests := []struct {
name string
targetPackages []pkg.Package
dropLocationFSID bool
dropNonPrimaryEvidence bool
wantCount int
verify func(t *testing.T, results []Result)
}{
{
name: "split single package with connected packages",
targetPackages: []pkg.Package{pkgA},
dropLocationFSID: false,
wantCount: 1,
verify: func(t *testing.T, results []Result) {
require.Len(t, results, 1)
result := results[0]
// target package should be pkgA
assert.Equal(t, pkgA.Name, result.TargetPackage.Name)
// should include both pkgA and pkgB (connected via ownership)
assert.Equal(t, 2, result.SBOM.Artifacts.Packages.PackageCount())
// should include coord1 and coord2 (related to pkgA and pkgB)
assert.Contains(t, result.SBOM.Artifacts.FileMetadata, coord1)
assert.Contains(t, result.SBOM.Artifacts.FileMetadata, coord2)
// should NOT include coord3 (unrelated)
assert.NotContains(t, result.SBOM.Artifacts.FileMetadata, coord3)
// source and descriptor should be preserved
assert.Equal(t, "test-source-id", result.SBOM.Source.ID)
assert.Equal(t, "syft", result.SBOM.Descriptor.Name)
// linux distribution should be preserved
require.NotNil(t, result.SBOM.Artifacts.LinuxDistribution)
assert.Equal(t, "alpine", result.SBOM.Artifacts.LinuxDistribution.ID)
},
},
{
name: "split unrelated package",
targetPackages: []pkg.Package{pkgC},
dropLocationFSID: false,
wantCount: 1,
verify: func(t *testing.T, results []Result) {
require.Len(t, results, 1)
result := results[0]
// should only include pkgC
assert.Equal(t, 1, result.SBOM.Artifacts.Packages.PackageCount())
// should only include coord3
assert.Contains(t, result.SBOM.Artifacts.FileMetadata, coord3)
assert.NotContains(t, result.SBOM.Artifacts.FileMetadata, coord1)
assert.NotContains(t, result.SBOM.Artifacts.FileMetadata, coord2)
},
},
{
name: "split with dropLocationFSID",
targetPackages: []pkg.Package{pkgA},
dropLocationFSID: true,
wantCount: 1,
verify: func(t *testing.T, results []Result) {
require.Len(t, results, 1)
result := results[0]
// FileSystemID should be cleared from coordinates
for coord := range result.SBOM.Artifacts.FileMetadata {
assert.Empty(t, coord.FileSystemID, "FileSystemID should be empty")
}
// package locations should also have FileSystemID cleared
for p := range result.SBOM.Artifacts.Packages.Enumerate() {
for _, loc := range p.Locations.ToSlice() {
assert.Empty(t, loc.FileSystemID, "package location FileSystemID should be empty")
}
}
},
},
{
name: "split multiple packages",
targetPackages: []pkg.Package{pkgA, pkgC},
dropLocationFSID: false,
wantCount: 2,
verify: func(t *testing.T, results []Result) {
require.Len(t, results, 2)
// first result should be for pkgA
assert.Equal(t, pkgA.Name, results[0].TargetPackage.Name)
// second result should be for pkgC
assert.Equal(t, pkgC.Name, results[1].TargetPackage.Name)
},
},
{
name: "empty target packages",
targetPackages: []pkg.Package{},
dropLocationFSID: false,
wantCount: 0,
verify: func(t *testing.T, results []Result) {
assert.Nil(t, results)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
results := Split(sourceSBOM, tt.targetPackages, tt.dropLocationFSID, tt.dropNonPrimaryEvidence)
if tt.wantCount == 0 {
assert.Nil(t, results)
} else {
require.Len(t, results, tt.wantCount)
}
if tt.verify != nil {
tt.verify(t, results)
}
})
}
}
func TestFindConnectedPackages(t *testing.T) {
// create test packages
pkgA := pkg.Package{Name: "a", Version: "1.0"}
pkgA.SetID()
pkgB := pkg.Package{Name: "b", Version: "1.0"}
pkgB.SetID()
pkgC := pkg.Package{Name: "c", Version: "1.0"}
pkgC.SetID()
pkgD := pkg.Package{Name: "d", Version: "1.0"}
pkgD.SetID()
tests := []struct {
name string
target pkg.Package
relationships []artifact.Relationship
wantNames []string
}{
{
name: "single package no relationships",
target: pkgA,
relationships: []artifact.Relationship{
// pkgB -> pkgC (unrelated)
{From: pkgB, To: pkgC, Type: artifact.OwnershipByFileOverlapRelationship},
},
wantNames: []string{"a"},
},
{
name: "connected via ownership-by-file-overlap",
target: pkgA,
relationships: []artifact.Relationship{
{From: pkgA, To: pkgB, Type: artifact.OwnershipByFileOverlapRelationship},
},
wantNames: []string{"a", "b"},
},
{
name: "connected via evident-by (to package)",
target: pkgA,
relationships: []artifact.Relationship{
{From: pkgA, To: pkgB, Type: artifact.EvidentByRelationship},
},
wantNames: []string{"a", "b"},
},
{
name: "chain of connections",
target: pkgA,
relationships: []artifact.Relationship{
{From: pkgA, To: pkgB, Type: artifact.OwnershipByFileOverlapRelationship},
{From: pkgB, To: pkgC, Type: artifact.OwnershipByFileOverlapRelationship},
},
wantNames: []string{"a", "b", "c"},
},
{
name: "circular relationships",
target: pkgA,
relationships: []artifact.Relationship{
{From: pkgA, To: pkgB, Type: artifact.OwnershipByFileOverlapRelationship},
{From: pkgB, To: pkgC, Type: artifact.OwnershipByFileOverlapRelationship},
{From: pkgC, To: pkgA, Type: artifact.OwnershipByFileOverlapRelationship},
},
wantNames: []string{"a", "b", "c"},
},
{
name: "ignores contains relationship",
target: pkgA,
relationships: []artifact.Relationship{
{From: pkgA, To: pkgB, Type: artifact.ContainsRelationship},
},
wantNames: []string{"a"},
},
{
name: "ignores dependency-of relationship",
target: pkgA,
relationships: []artifact.Relationship{
{From: pkgA, To: pkgB, Type: artifact.DependencyOfRelationship},
},
wantNames: []string{"a"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sourceSBOM := sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(pkgA, pkgB, pkgC, pkgD),
},
Relationships: tt.relationships,
}
relIndex := newRelationshipIndex(sourceSBOM.Relationships...)
got := findConnectedPackages(sourceSBOM, tt.target, relIndex)
gotNames := make([]string, len(got))
for i, p := range got {
gotNames[i] = p.Name
}
// sort for comparison
assert.ElementsMatch(t, tt.wantNames, gotNames)
})
}
}
func TestClearPackageFileSystemIDs(t *testing.T) {
coord := file.Coordinates{RealPath: "/test/path", FileSystemID: "layer123"}
loc := file.NewLocationFromCoordinates(coord)
loc.AccessPath = "/test/access"
licLoc := file.NewLocationFromCoordinates(coord)
lic := pkg.License{
Value: "MIT",
Locations: file.NewLocationSet(licLoc),
}
p := pkg.Package{
Name: "test",
Version: "1.0",
Locations: file.NewLocationSet(loc),
Licenses: pkg.NewLicenseSet(lic),
}
result := clearPackageFileSystemIDs(p)
// check package locations
for _, l := range result.Locations.ToSlice() {
assert.Empty(t, l.FileSystemID, "package location FileSystemID should be empty")
assert.Equal(t, "/test/path", l.RealPath)
assert.Equal(t, "/test/access", l.AccessPath)
}
// check license locations
for _, l := range result.Licenses.ToSlice() {
for _, ll := range l.Locations.ToSlice() {
assert.Empty(t, ll.FileSystemID, "license location FileSystemID should be empty")
}
}
}
func TestPackageIDStabilityThroughSplit(t *testing.T) {
// this test verifies that package IDs remain stable through all split transformations,
// including dropping licenses, filtering locations, and clearing filesystem IDs.
// Package IDs are content-addressable and should NOT change when we modify packages
// during the split process.
// create a package with multiple locations (primary and non-primary evidence)
primaryLoc := file.NewLocation("/lib/apk/db/installed")
primaryLoc.Annotations = map[string]string{"evidence": "primary"}
nonPrimaryLoc := file.NewLocation("/some/other/path")
nonPrimaryLoc.Annotations = map[string]string{"evidence": "supporting"}
// add filesystem IDs to both
primaryLoc.Coordinates.FileSystemID = "layer123"
nonPrimaryLoc.Coordinates.FileSystemID = "layer456"
// create license with location
licLoc := file.NewLocation("/lib/apk/db/installed")
licLoc.Coordinates.FileSystemID = "layer123"
lic := pkg.License{
Value: "MIT",
Locations: file.NewLocationSet(licLoc),
}
testPkg := pkg.Package{
Name: "test-package",
Version: "1.0.0",
Type: pkg.ApkPkg,
Locations: file.NewLocationSet(primaryLoc, nonPrimaryLoc),
Licenses: pkg.NewLicenseSet(lic),
}
testPkg.SetID()
originalID := testPkg.ID()
// verify original ID is set
require.NotEmpty(t, originalID, "original package ID should be set")
// create a simple SBOM for the split operation
sourceSBOM := sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(testPkg),
FileMetadata: map[file.Coordinates]file.Metadata{
primaryLoc.Coordinates: {MIMEType: "text/plain"},
nonPrimaryLoc.Coordinates: {MIMEType: "text/plain"},
},
},
Relationships: []artifact.Relationship{
{
From: testPkg,
To: primaryLoc.Coordinates,
Type: artifact.EvidentByRelationship,
},
},
Source: source.Description{
ID: "test-source",
Name: "test",
},
Descriptor: sbom.Descriptor{
Name: "syft",
Version: "test",
},
}
tests := []struct {
name string
dropLocationFSID bool
dropNonPrimaryEvidence bool
}{
{
name: "no drop options",
dropLocationFSID: false,
dropNonPrimaryEvidence: false,
},
{
name: "drop location:fsid only",
dropLocationFSID: true,
dropNonPrimaryEvidence: false,
},
{
name: "drop location:non-primary-evidence only",
dropLocationFSID: false,
dropNonPrimaryEvidence: true,
},
{
name: "drop both location options",
dropLocationFSID: true,
dropNonPrimaryEvidence: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
results := Split(sourceSBOM, []pkg.Package{testPkg}, tt.dropLocationFSID, tt.dropNonPrimaryEvidence)
require.Len(t, results, 1)
resultSBOM := results[0].SBOM
// verify there's exactly one package in the result
require.Equal(t, 1, resultSBOM.Artifacts.Packages.PackageCount())
// get the package from the result
var resultPkg *pkg.Package
for p := range resultSBOM.Artifacts.Packages.Enumerate() {
resultPkg = &p
break
}
require.NotNil(t, resultPkg)
// THE CRITICAL ASSERTION: package ID must remain stable
assert.Equal(t, originalID, resultPkg.ID(),
"package ID changed after split with dropLocationFSID=%v, dropNonPrimaryEvidence=%v",
tt.dropLocationFSID, tt.dropNonPrimaryEvidence)
// verify the target package ID also matches
assert.Equal(t, originalID, results[0].TargetPackage.ID(),
"target package ID changed")
// additional verification: if we dropped non-primary evidence, locations should be filtered
if tt.dropNonPrimaryEvidence {
locs := resultPkg.Locations.ToSlice()
for _, loc := range locs {
assert.Equal(t, "primary", loc.Annotations["evidence"],
"non-primary locations should be filtered out")
}
}
// additional verification: if we dropped fsid, it should be empty
if tt.dropLocationFSID {
for _, loc := range resultPkg.Locations.ToSlice() {
assert.Empty(t, loc.FileSystemID, "FileSystemID should be cleared")
}
}
})
}
}
func TestPackageIDStabilityWithDropOptions(t *testing.T) {
// this test verifies that package IDs remain stable when using ApplyDropOptions
// which includes pkg:licenses
coord := file.Coordinates{RealPath: "/test/path", FileSystemID: "layer123"}
loc := file.NewLocationFromCoordinates(coord)
loc.Annotations = map[string]string{"evidence": "primary"}
lic := pkg.License{
Value: "GPL-2.0",
Locations: file.NewLocationSet(loc),
}
testPkg := pkg.Package{
Name: "license-test-pkg",
Version: "2.0.0",
Type: pkg.ApkPkg,
Locations: file.NewLocationSet(loc),
Licenses: pkg.NewLicenseSet(lic),
Metadata: pkg.ApkDBEntry{
Package: "license-test-pkg",
Files: []pkg.ApkFileRecord{
{Path: "/usr/bin/test"},
},
},
}
testPkg.SetID()
originalID := testPkg.ID()
require.NotEmpty(t, originalID)
// create SBOM
testSBOM := sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(testPkg),
},
Source: source.Description{
ID: "test",
Name: "test",
},
Descriptor: sbom.Descriptor{
Name: "syft",
Version: "test",
},
}
tests := []struct {
name string
opts []DropOption
}{
{
name: "drop pkg:licenses",
opts: []DropOption{DropPkgLicenses},
},
{
name: "drop location:fsid via ApplyDropOptions",
opts: []DropOption{DropLocationFSID},
},
{
name: "drop location:non-primary-evidence via ApplyDropOptions",
opts: []DropOption{DropLocationNonPrimaryEvidence},
},
{
name: "drop all location and pkg options",
opts: []DropOption{DropPkgLicenses, DropLocationFSID, DropLocationNonPrimaryEvidence},
},
{
name: "drop pkg:metadata.files",
opts: []DropOption{DropPkgMetadataFiles},
},
{
name: "drop all pkg options",
opts: []DropOption{DropPkgLicenses, DropPkgMetadataFiles},
},
{
name: "drop source and descriptor (should not affect package ID)",
opts: []DropOption{DropSource, DropDescriptor, DropDistro},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// make a copy of the SBOM for this test
sbomCopy := sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(testPkg),
},
Source: testSBOM.Source,
Descriptor: testSBOM.Descriptor,
}
// apply drop options
ApplyDropOptions(&sbomCopy, tt.opts)
// get the package from the modified SBOM
var resultPkg *pkg.Package
for p := range sbomCopy.Artifacts.Packages.Enumerate() {
resultPkg = &p
break
}
require.NotNil(t, resultPkg)
// THE CRITICAL ASSERTION: package ID must remain stable
assert.Equal(t, originalID, resultPkg.ID(),
"package ID changed after applying drop options: %v", tt.opts)
})
}
}
@@ -94,7 +94,7 @@ func getVersion(resolver file.Resolver) (string, []file.Location, error) {
}
defer internal.CloseAndLogError(reader, locations[0].RealPath)
version, err := io.ReadAll(reader) //nolint:gocritic // example code
version, err := io.ReadAll(reader)
if err != nil {
return "", nil, fmt.Errorf("unable to read alpine version: %w", err)
}
@@ -117,7 +117,7 @@ func getAPKKeys(resolver file.Resolver) (map[string]string, []file.Location, err
if err != nil {
return nil, nil, fmt.Errorf("unable to resolve file contents by location at %s: %w", location.RealPath, err)
}
content, err := io.ReadAll(reader) //nolint:gocritic // example code
content, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("unable to read apk key content at %s: %w", location.RealPath, err)
}
+52 -50
View File
@@ -1,6 +1,6 @@
module github.com/anchore/syft
go 1.25.8
go 1.25.6
require (
github.com/BurntSushi/toml v1.6.0
@@ -9,6 +9,7 @@ require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/OneOfOne/xxhash v1.2.8
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/acobaugh/osrelease v0.1.0
github.com/adrg/xdg v0.5.3
github.com/anchore/bubbly v0.0.0-20231115134915-def0aba654a9
github.com/anchore/clio v0.0.0-20250319180342-2cfe4b0cb716
@@ -21,7 +22,7 @@ require (
github.com/anchore/go-sync v0.0.0-20250326131806-4eda43a485b6
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115
github.com/anchore/stereoscope v0.1.22
github.com/anchore/stereoscope v0.1.21
github.com/aquasecurity/go-pep440-version v0.0.1
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
@@ -44,12 +45,12 @@ require (
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/gohugoio/hashstructure v0.6.0
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.21.2
github.com/google/go-containerregistry v0.21.1
github.com/google/licensecheck v0.3.1
github.com/google/uuid v1.6.0
github.com/gookit/color v1.6.0
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-getter v1.8.5
github.com/hashicorp/go-getter v1.8.4
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/hcl/v2 v2.24.0
github.com/iancoleman/strcase v0.3.0
@@ -61,7 +62,7 @@ require (
github.com/mholt/archives v0.1.5
github.com/moby/sys/mountinfo v0.7.2
github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1
github.com/olekukonko/tablewriter v1.1.4
github.com/olekukonko/tablewriter v1.1.3
github.com/opencontainers/go-digest v1.0.0
github.com/pelletier/go-toml v1.9.5
github.com/quasilyte/go-ruleguard/dsl v0.3.23
@@ -86,24 +87,24 @@ require (
go.uber.org/goleak v1.3.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
golang.org/x/mod v0.34.0
golang.org/x/net v0.52.0
golang.org/x/mod v0.33.0
golang.org/x/net v0.51.0
modernc.org/sqlite v1.46.1
)
require (
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.18.1 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/storage v1.60.0 // indirect
cloud.google.com/go/storage v1.58.0 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/DataDog/zstd v1.5.5 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.14.0-rc.1 // indirect
github.com/ProtonMail/go-crypto v1.4.0 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/agext/levenshtein v1.2.1 // indirect; indirectt
github.com/anchore/go-lzo v0.1.0 // indirect
@@ -136,7 +137,7 @@ require (
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/cli v29.3.0+incompatible // indirect
github.com/docker/cli v29.2.1+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/docker/go-connections v0.6.0 // indirect
@@ -159,8 +160,8 @@ require (
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
@@ -221,7 +222,7 @@ require (
github.com/spf13/viper v1.20.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/sylabs/sif/v2 v2.24.0 // indirect
github.com/sylabs/sif/v2 v2.23.0 // indirect
github.com/sylabs/squashfs v1.0.6 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
@@ -246,20 +247,20 @@ require (
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/term v0.41.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.15.0
golang.org/x/tools v0.43.0
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0
golang.org/x/tools v0.42.0
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/api v0.267.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/api v0.256.0 // indirect
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1
@@ -269,7 +270,6 @@ require (
)
require (
github.com/acobaugh/osrelease v0.1.0
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/cespare/xxhash/v2 v2.3.0
github.com/gpustack/gguf-parser-go v0.24.0
@@ -277,12 +277,12 @@ require (
)
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
cyphar.com/go-pathrs v0.2.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.10 // indirect
@@ -291,44 +291,46 @@ require (
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
github.com/aws/smithy-go v1.24.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.2 // indirect
github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // 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
github.com/containerd/plugin v1.0.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 // indirect
github.com/henvic/httpretty v0.1.4 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/moby/moby/api v1.54.0 // indirect
github.com/moby/moby/client v0.3.0 // indirect
github.com/moby/moby/api v1.53.0 // indirect
github.com/moby/moby/client v0.2.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.6 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/zeebo/errs v1.4.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
gonum.org/v1/gonum v0.16.0 // indirect
+112 -108
View File
@@ -1,5 +1,5 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -31,8 +31,8 @@ cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0c
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
@@ -48,12 +48,12 @@ cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -63,10 +63,10 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8=
cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo=
cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8=
cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
@@ -84,14 +84,14 @@ github.com/CycloneDX/cyclonedx-go v0.10.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByX
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=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
@@ -106,8 +106,8 @@ github.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boS
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ=
github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
@@ -148,8 +148,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
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.22 h1:L807G/kk0WZzOCGuRGF7knxMKzwW2PGdbPVRystryd8=
github.com/anchore/stereoscope v0.1.22/go.mod h1:FikPtAb/WnbqwgLHAvQA9O+fWez0K4pbjxzghz++iy4=
github.com/anchore/stereoscope v0.1.21 h1:rIwPDjJQdOdIvMrkNyZFkUS5dSyQPDjAwUqU0XjVF74=
github.com/anchore/stereoscope v0.1.21/go.mod h1:8652axKmO8itOcGX8gzAvHW3cVWvbxpJ4woLNMEO4a0=
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=
@@ -187,18 +187,18 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoy
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
@@ -237,8 +237,8 @@ github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
@@ -276,10 +276,12 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38
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.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
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.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.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
@@ -292,8 +294,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4=
github.com/containerd/cgroups/v3 v3.1.2/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o=
@@ -343,8 +345,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v29.3.0+incompatible h1:z3iWveU7h19Pqx7alZES8j+IeFQZ1lhTwb2F+V9SVvk=
github.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
@@ -375,16 +377,16 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c/go.mod h1:QGzNH9ujQ2ZUr/CjDGZGWeDAVStrWNjHeEcjJL96Nuk=
@@ -431,8 +433,8 @@ github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
@@ -513,8 +515,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.21.2 h1:vYaMU4nU55JJGFC9JR/s8NZcTjbE9DBBbvusTW9NeS0=
github.com/google/go-containerregistry v0.21.2/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0=
github.com/google/go-containerregistry v0.21.1 h1:sOt/o9BS2b87FnR7wxXPvRKU1XVJn2QCwOS5g8zQXlc=
github.com/google/go-containerregistry v0.21.1/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs=
github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY=
@@ -549,14 +551,14 @@ github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
@@ -576,8 +578,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-getter v1.8.5 h1:DMPV5CSw5JrNg/IK7kDZt3+l2REKXOi3oAw7uYLh2NM=
github.com/hashicorp/go-getter v1.8.5/go.mod h1:WIffejwAyDSJhoVptc3UEshEMkR9O63rw34V7k43O3Q=
github.com/hashicorp/go-getter v1.8.4 h1:hGEd2xsuVKgwkMtPVufq73fAmZU/x65PPcqH3cb0D9A=
github.com/hashicorp/go-getter v1.8.4/go.mod h1:x27pPGSg9kzoB147QXI8d/nDvp2IgYGcwuRjpaXE9Yg=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
@@ -725,10 +727,10 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0=
github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs=
github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ=
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM=
github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
@@ -764,12 +766,12 @@ github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKi
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM=
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -900,8 +902,8 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -923,8 +925,8 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/sylabs/sif/v2 v2.24.0 h1:1wB5uMDUQYjk8AckTySaDcP9YnpMb1LyDRr1Jt9A10w=
github.com/sylabs/sif/v2 v2.24.0/go.mod h1:DbXWqWZ1hdLSU+K9ipdds5AmZeHWsyxCOj/oQakBa88=
github.com/sylabs/sif/v2 v2.23.0 h1:VWJC7iryINdIgyIK8EdxMCJkx8EDPVlvFaskgY+ruFk=
github.com/sylabs/sif/v2 v2.23.0/go.mod h1:0UJk7MMSQS2n/95yYd+CvAfK7K2mE0aWHRYYybd+ouw=
github.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI=
github.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE=
github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo=
@@ -982,6 +984,8 @@ github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1 h1:V+UsotZpAVvfj3X/LMoEytoLzSiP6Lg0F7wdVyu9gGg=
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1/go.mod h1:ly2RBz4mnz1yeuVbQA/VFwGjK3mnHGRj1JuoG336Bis=
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
@@ -998,16 +1002,16 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
@@ -1042,8 +1046,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1083,8 +1087,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1129,8 +1133,8 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1148,8 +1152,8 @@ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1162,8 +1166,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1238,13 +1242,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1255,13 +1259,13 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -1317,8 +1321,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1359,8 +1363,8 @@ google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdr
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1431,12 +1435,12 @@ google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ6
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc=
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1464,8 +1468,8 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+1 -1
View File
@@ -215,7 +215,7 @@ func write(schema []byte) {
panic(err)
}
existingSchemaBytes, err := io.ReadAll(existingFh) //nolint:gocritic // offline code generator
existingSchemaBytes, err := io.ReadAll(existingFh)
if err != nil {
panic(err)
}
+1 -1
View File
@@ -10,7 +10,7 @@ func (s *scanner) FindEvidence(_ context.Context, reader io.Reader) (evidence []
return nil, nil, nil
}
content, err = io.ReadAll(reader) //nolint:gocritic // license scanner requires full content
content, err = io.ReadAll(reader)
if err != nil {
return nil, nil, err
}
+2 -4
View File
@@ -31,7 +31,7 @@ func UpdateGoldenFileContents(t *testing.T, contents []byte) {
t.Log(dangerText("!!! UPDATING GOLDEN FILE !!!"), goldenFilePath)
err := os.WriteFile(goldenFilePath, contents, 0o600)
err := os.WriteFile(goldenFilePath, contents, 0600)
if err != nil {
t.Fatalf("could not update golden file (%s): %+v", goldenFilePath, err)
}
@@ -50,9 +50,7 @@ func GetGoldenFileContents(t *testing.T) []byte {
}
defer f.Close()
// suppress lint prohibiting ReadAll since golden files are source files in Syft's repository
// and not user provided artifacts.
bytes, err := io.ReadAll(f) //nolint:gocritic // golden files are trusted source files, not user artifacts
bytes, err := io.ReadAll(f)
if err != nil {
t.Fatalf("could not read file (%s): %+v", goldenPath, err)
}
-121
View File
@@ -1,121 +0,0 @@
package tmpdir
import (
"context"
"fmt"
"os"
"sync"
"sync/atomic"
)
type ctxKey struct{}
// Root creates a new root temp directory with the given prefix and returns a context with the
// TempDir attached. Callers should defer Cleanup() on the returned TempDir to ensure all
// temp files are removed.
func Root(ctx context.Context, prefix string) (context.Context, *TempDir) {
td := &TempDir{prefix: prefix}
return context.WithValue(ctx, ctxKey{}, td), td
}
// FromPath creates a TempDir backed by an existing directory. The caller owns the lifecycle of
// the directory; Cleanup() is a no-op. This is useful for wrapping a directory from t.TempDir()
// where the test framework handles cleanup automatically.
func FromPath(dir string) *TempDir {
td := &TempDir{}
td.root = dir
td.initOnce.Do(func() {}) // mark as initialized
return td
}
// WithValue returns a new context with the given TempDir attached. Use this to inject an
// existing TempDir into a context (e.g., sharing a TempDir across multiple test contexts).
func WithValue(ctx context.Context, td *TempDir) context.Context {
return context.WithValue(ctx, ctxKey{}, td)
}
// FromContext returns the TempDir from the context, or nil if none is set.
func FromContext(ctx context.Context) *TempDir {
td, _ := ctx.Value(ctxKey{}).(*TempDir)
return td
}
// TempDir manages a tree of temporary directories. All files and child directories live under
// a single root path that can be removed in one shot via Cleanup(). After initialization, the
// struct has no mutable state — NewChild and NewFile delegate uniqueness to os.MkdirTemp and
// os.CreateTemp respectively, so no locking is needed on the hot path.
type TempDir struct {
prefix string
root string // set exactly once by initOnce
initOnce sync.Once
initErr error
cleanupOnce sync.Once
cleaned atomic.Bool
}
func noop() {}
// path returns the root directory, lazily creating it on the first call.
func (t *TempDir) path() (string, error) {
t.initOnce.Do(func() {
t.root, t.initErr = os.MkdirTemp("", t.prefix+"-")
})
if t.initErr != nil {
return "", fmt.Errorf("failed to create root temp dir: %w", t.initErr)
}
if t.cleaned.Load() {
return "", fmt.Errorf("temp dir has been cleaned up")
}
return t.root, nil
}
// NewChild creates a named subdirectory under this TempDir. The returned cleanup function removes
// the subdirectory and all contents; callers should defer it to reclaim space early. The root
// Cleanup acts as a safety net if the per-child cleanup is missed. The cleanup function is safe
// to call multiple times and is safe to call after the root has already been cleaned up.
func (t *TempDir) NewChild(name string) (string, func(), error) {
root, err := t.path()
if err != nil {
return "", noop, err
}
dir, err := os.MkdirTemp(root, name+"-")
if err != nil {
return "", noop, fmt.Errorf("failed to create child temp dir: %w", err)
}
cleanup := func() {
_ = os.RemoveAll(dir)
}
return dir, cleanup, nil
}
// NewFile creates a new temp file under this TempDir with the given name pattern (as in os.CreateTemp).
// The caller is responsible for closing the file. The returned cleanup function removes the file;
// callers should defer it to reclaim space early. The root Cleanup acts as a safety net if the
// per-file cleanup is missed. The cleanup function is safe to call multiple times.
func (t *TempDir) NewFile(pattern string) (*os.File, func(), error) {
root, err := t.path()
if err != nil {
return nil, noop, err
}
f, err := os.CreateTemp(root, pattern)
if err != nil {
return nil, noop, fmt.Errorf("failed to create temp file: %w", err)
}
cleanup := func() {
_ = os.Remove(f.Name())
}
return f, cleanup, nil
}
// Cleanup removes the entire root directory and all contents. Safe to call multiple times.
func (t *TempDir) Cleanup() error {
var err error
t.cleanupOnce.Do(func() {
t.cleaned.Store(true)
if t.root == "" {
return
}
err = os.RemoveAll(t.root)
})
return err
}
-295
View File
@@ -1,295 +0,0 @@
package tmpdir
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRootAndFromContext(t *testing.T) {
ctx := context.Background()
assert.Nil(t, FromContext(ctx))
ctx, td := Root(ctx, "test")
require.NotNil(t, FromContext(ctx))
assert.Same(t, td, FromContext(ctx))
}
func TestWithValue(t *testing.T) {
_, td := Root(context.Background(), "test")
defer td.Cleanup()
// inject the existing TempDir into a fresh context
ctx := WithValue(context.Background(), td)
assert.Same(t, td, FromContext(ctx))
// the injected TempDir is fully functional
f, cleanup, err := FromContext(ctx).NewFile("with-value-*.txt")
require.NoError(t, err)
defer cleanup()
require.NoError(t, f.Close())
}
func TestNewChild(t *testing.T) {
ctx, td := Root(context.Background(), "test")
defer td.Cleanup()
_ = ctx
child1, cleanup1, err := td.NewChild("sub")
require.NoError(t, err)
defer cleanup1()
child2, cleanup2, err := td.NewChild("sub")
require.NoError(t, err)
defer cleanup2()
// children are distinct
assert.NotEqual(t, child1, child2)
// both exist and are under the same root
info1, err := os.Stat(child1)
require.NoError(t, err)
assert.True(t, info1.IsDir())
info2, err := os.Stat(child2)
require.NoError(t, err)
assert.True(t, info2.IsDir())
assert.Equal(t, filepath.Dir(child1), filepath.Dir(child2))
}
func TestNewFile(t *testing.T) {
_, td := Root(context.Background(), "test")
defer td.Cleanup()
f, cleanup, err := td.NewFile("hello-*.txt")
require.NoError(t, err)
defer cleanup()
_, err = f.WriteString("hello")
require.NoError(t, err)
require.NoError(t, f.Close())
content, err := os.ReadFile(f.Name())
require.NoError(t, err)
assert.Equal(t, "hello", string(content))
}
func TestCleanup(t *testing.T) {
_, td := Root(context.Background(), "test")
child, _, err := td.NewChild("sub")
require.NoError(t, err)
f, _, err := td.NewFile("file-*")
require.NoError(t, err)
fname := f.Name()
f.Close()
// write a file inside the child dir too
require.NoError(t, os.WriteFile(filepath.Join(child, "inner.txt"), []byte("x"), 0600))
// everything exists
_, err = os.Stat(child)
require.NoError(t, err)
_, err = os.Stat(fname)
require.NoError(t, err)
// cleanup
require.NoError(t, td.Cleanup())
// everything is gone
_, err = os.Stat(child)
assert.True(t, os.IsNotExist(err))
_, err = os.Stat(fname)
assert.True(t, os.IsNotExist(err))
// double cleanup is safe
require.NoError(t, td.Cleanup())
}
func TestCleanupPreventsNewAllocation(t *testing.T) {
_, td := Root(context.Background(), "test")
require.NoError(t, td.Cleanup())
_, _, err := td.NewChild("nope")
assert.Error(t, err)
_, _, err = td.NewFile("nope-*")
assert.Error(t, err)
}
func TestEarlyCleanupFile(t *testing.T) {
_, td := Root(context.Background(), "test")
defer td.Cleanup()
f, cleanup, err := td.NewFile("early-*.txt")
require.NoError(t, err)
fname := f.Name()
require.NoError(t, f.Close())
// file exists before cleanup
_, err = os.Stat(fname)
require.NoError(t, err)
// early cleanup removes the file
cleanup()
_, err = os.Stat(fname)
assert.True(t, os.IsNotExist(err))
// calling cleanup again is safe (idempotent)
cleanup()
}
func TestEarlyCleanupChild(t *testing.T) {
_, td := Root(context.Background(), "test")
defer td.Cleanup()
child, cleanup, err := td.NewChild("early")
require.NoError(t, err)
// child dir exists
_, err = os.Stat(child)
require.NoError(t, err)
// early cleanup removes it
cleanup()
_, err = os.Stat(child)
assert.True(t, os.IsNotExist(err))
// calling cleanup again is safe (idempotent)
cleanup()
}
func TestEarlyCleanupThenRootCleanup(t *testing.T) {
_, td := Root(context.Background(), "test")
f, cleanupFile, err := td.NewFile("combo-*.txt")
require.NoError(t, err)
fname := f.Name()
f.Close()
child, cleanupChild, err := td.NewChild("combo")
require.NoError(t, err)
// early cleanup both
cleanupFile()
cleanupChild()
// files are already gone
_, err = os.Stat(fname)
assert.True(t, os.IsNotExist(err))
_, err = os.Stat(child)
assert.True(t, os.IsNotExist(err))
// root cleanup still works (no error on already-removed contents)
require.NoError(t, td.Cleanup())
}
func TestConcurrentNewChildAndNewFile(t *testing.T) {
_, td := Root(context.Background(), "test")
defer td.Cleanup()
const goroutines = 20
errs := make(chan error, goroutines)
paths := make(chan string, goroutines)
for i := 0; i < goroutines; i++ {
go func(i int) {
if i%2 == 0 {
child, cleanup, err := td.NewChild("concurrent")
if err != nil {
errs <- err
return
}
defer cleanup()
paths <- child
} else {
f, cleanup, err := td.NewFile("concurrent-*.txt")
if err != nil {
errs <- err
return
}
defer cleanup()
_ = f.Close()
paths <- f.Name()
}
errs <- nil
}(i)
}
seen := make(map[string]bool)
for i := 0; i < goroutines; i++ {
err := <-errs
require.NoError(t, err)
}
close(paths)
for p := range paths {
assert.False(t, seen[p], "duplicate path: %s", p)
seen[p] = true
}
assert.Len(t, seen, goroutines)
}
func TestConcurrentNewChildDuringCleanup(t *testing.T) {
_, td := Root(context.Background(), "test")
// trigger root creation
_, cleanup, err := td.NewChild("init")
require.NoError(t, err)
cleanup()
// cleanup and concurrent NewChild should not panic
done := make(chan struct{})
go func() {
_ = td.Cleanup()
close(done)
}()
// try creating children concurrently with cleanup — should get errors, not panics
for i := 0; i < 10; i++ {
_, c, _ := td.NewChild("race")
if c != nil {
c()
}
}
<-done
}
func TestLazyCreation(t *testing.T) {
_, td := Root(context.Background(), "test")
// root dir is not created until needed
assert.Equal(t, "", td.root)
_, _, err := td.NewFile("trigger-*")
require.NoError(t, err)
assert.NotEmpty(t, td.root)
require.NoError(t, td.Cleanup())
}
func TestFromPath(t *testing.T) {
dir := t.TempDir()
td := FromPath(dir)
// can create children
child, cleanup, err := td.NewChild("sub")
require.NoError(t, err)
defer cleanup()
assert.DirExists(t, child)
// can create files
f, cleanupFile, err := td.NewFile("file-*.txt")
require.NoError(t, err)
defer cleanupFile()
require.NoError(t, f.Close())
assert.FileExists(t, f.Name())
// root is the provided dir
assert.Equal(t, dir, filepath.Dir(child))
}
+1 -23
View File
@@ -12,10 +12,8 @@ import (
"github.com/anchore/go-sync"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/licenses"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/internal/task"
"github.com/anchore/syft/internal/tmpdir"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/event/monitor"
@@ -26,8 +24,6 @@ import (
// CreateSBOM creates a software bill-of-materials from the given source. If the CreateSBOMConfig is nil, then
// default options will be used.
//
//nolint:funlen
func CreateSBOM(ctx context.Context, src source.Source, cfg *CreateSBOMConfig) (*sbom.SBOM, error) {
if cfg == nil {
cfg = DefaultCreateSBOMConfig()
@@ -69,25 +65,12 @@ func CreateSBOM(ctx context.Context, src source.Source, cfg *CreateSBOMConfig) (
},
}
// check if the caller already provided a TempDir; if so, don't clean it up (the caller owns it)
callerOwnsTempDir := tmpdir.FromContext(ctx) != nil
// setup everything we need in context: license scanner, executors, temp storage, etc.
// setup everything we need in context: license scanner, executors, etc.
ctx, err = setupContext(ctx, cfg)
if err != nil {
return nil, err
}
if !callerOwnsTempDir {
if td := tmpdir.FromContext(ctx); td != nil {
defer func() {
if err := td.Cleanup(); err != nil {
log.Warnf("failed to clean up temp dir: %v", err)
}
}()
}
}
catalogingProgress := monitorCatalogingTask(src.ID(), taskGroups)
packageCatalogingProgress := monitorPackageCatalogingTask()
@@ -112,11 +95,6 @@ func setupContext(ctx context.Context, cfg *CreateSBOMConfig) (context.Context,
// configure parallel executors
ctx = setContextExecutors(ctx, cfg)
// configure temp dir factory for catalogers (if not already set)
if tmpdir.FromContext(ctx) == nil {
ctx, _ = tmpdir.Root(ctx, "syft-cataloger")
}
// configure license scanner
// skip injecting a license scanner if one already set on context
if licenses.IsContextLicenseScannerSet(ctx) {
+2 -2
View File
@@ -22,7 +22,7 @@ tools-check:
# docker buildx build --platform linux/amd64 -t $(TOOL_IMAGE) .
tools:
@(docker image inspect $(TOOL_IMAGE) > /dev/null 2>&1 && make tools-check) || (docker build -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256)
@(docker inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256)
build: tools
mkdir -p $(BIN)
@@ -31,7 +31,7 @@ build: tools
verify: tools
@rm -f $(VERIFY_FILE)
docker run -i -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make verify > $(VERIFY_FILE)
@python3 ./differ expected_verify $(VERIFY_FILE)
@python ./differ expected_verify $(VERIFY_FILE)
debug:
docker run -i --rm -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) bash
@@ -1,9 +1,10 @@
# syntax=docker/dockerfile:1
ARG OSXCROSS_VERSION=13.1
FROM --platform=linux/amd64 crazymax/osxcross:${OSXCROSS_VERSION}-ubuntu AS osxcross
ARG BUILDPLATFORM=linux/amd64
FROM --platform=$BUILDPLATFORM crazymax/osxcross:${OSXCROSS_VERSION}-ubuntu AS osxcross
FROM --platform=linux/amd64 ubuntu:22.04
FROM --platform=$BUILDPLATFORM ubuntu:22.04
RUN apt update -y && apt install -y \
curl wget \
@@ -19,7 +19,7 @@ tools-check:
@sha256sum -c Dockerfile.sha256 || (echo "Tools Dockerfile has changed" && exit 1)
tools:
@(docker image inspect $(TOOL_IMAGE) > /dev/null 2>&1 && make tools-check) || (docker build --platform linux/amd64 -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256)
@(docker inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256)
build: tools
@mkdir -p $(BIN)
@@ -20,7 +20,7 @@ func SeekableReader(reader io.Reader) (io.ReadSeeker, error) {
return getOffsetReadSeeker(r)
}
content, err := io.ReadAll(reader) //nolint:gocritic // buffering non-seekable to seekable reader
content, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
+1 -1
View File
@@ -62,7 +62,7 @@ func GetUnionReader(readerCloser io.ReadCloser) (UnionReader, error) {
return newReaderAtAdapter(r), nil
}
b, err := io.ReadAll(readerCloser) //nolint:gocritic // buffering non-seekable to ReaderAt
b, err := io.ReadAll(readerCloser)
if err != nil {
return nil, fmt.Errorf("unable to read contents from binary: %w", err)
}
+1 -1
View File
@@ -92,7 +92,7 @@ func tryParseReleaseInfo(resolver file.Resolver, location file.Location, logger
}
defer internal.CloseAndLogError(contentReader, location.AccessPath)
content, err := io.ReadAll(io.LimitReader(contentReader, 5*1024*1024))
content, err := io.ReadAll(contentReader)
if err != nil {
logger.WithFields("error", err, "path", location.RealPath).Trace("unable to read contents")
return nil
+1 -1
View File
@@ -38,7 +38,7 @@ func readDebianVersionFile(resolver file.Resolver, location file.Location) strin
return ""
}
defer internal.CloseAndLogError(rdr, location.RealPath)
contents, err := io.ReadAll(io.LimitReader(rdr, 5*1024*1024))
contents, err := io.ReadAll(rdr)
if err != nil {
log.Debugf("error reading %s: %v", location.RealPath, err)
return ""
+6 -8
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
@@ -14,7 +15,6 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/tmpdir"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
@@ -24,19 +24,17 @@ import (
// parseGGUFModel parses a GGUF model file and returns the discovered package.
// This implementation only reads the header portion of the file, not the entire model.
func parseGGUFModel(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
func parseGGUFModel(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
defer internal.CloseAndLogError(reader, reader.Path())
td := tmpdir.FromContext(ctx)
if td == nil {
return nil, nil, fmt.Errorf("no temp dir factory in context")
}
tempFile, cleanup, err := td.NewFile("syft-gguf-*.gguf")
// Create a temporary file for the library to parse
// The library requires a file path, so we create a temp file
tempFile, err := os.CreateTemp("", "syft-gguf-*.gguf")
if err != nil {
return nil, nil, fmt.Errorf("failed to create temp file: %w", err)
}
defer cleanup()
tempPath := tempFile.Name()
defer os.Remove(tempPath)
// Copy and validate the GGUF file header using LimitedReader to prevent OOM
// We use LimitedReader to cap reads at maxHeaderSize (50MB)
+1 -1
View File
@@ -171,7 +171,7 @@ func findReleases(resolver file.Resolver, dbPath string) []linux.Release {
func parseReleasesFromAPKRepository(reader file.LocationReadCloser) []linux.Release {
var releases []linux.Release
reposB, err := io.ReadAll(reader) //nolint:gocritic // regex matching requires full buffer
reposB, err := io.ReadAll(reader)
if err != nil {
log.Tracef("unable to read APK repositories file %q: %+v", reader.RealPath, err)
return nil
@@ -61,28 +61,6 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
Metadata: metadata("arangodb-binary"),
},
},
{
logicalFixture: "arangodb/3.12.5/linux-amd64",
expected: pkg.Package{
Name: "arangodb",
Version: "3.12.5",
Type: "binary",
PURL: "pkg:generic/arangodb@3.12.5",
Locations: locations("arangosh"),
Metadata: metadata("arangodb-binary"),
},
},
{
logicalFixture: "arangodb/3.12.5-2/linux-amd64",
expected: pkg.Package{
Name: "arangodb",
Version: "3.12.5-2",
Type: "binary",
PURL: "pkg:generic/arangodb@3.12.5-2",
Locations: locations("arangosh"),
Metadata: metadata("arangodb-binary"),
},
},
{
logicalFixture: "postgres/15beta4/linux-amd64",
expected: pkg.Package{
@@ -749,16 +727,6 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
{
// TODO: find original binary...
// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
logicalFixture: "go-version-hint/1.15-dev/any",
expected: pkg.Package{
Name: "go",
Version: "1.15",
PURL: "pkg:generic/go@1.15",
Locations: locations("bin/go", "VERSION"),
Metadata: metadata("go-binary"),
},
},
{
logicalFixture: "go-version-hint/1.15/any",
expected: pkg.Package{
Name: "go",
+3 -3
View File
@@ -76,10 +76,10 @@ func DefaultClassifiers() []binutils.Classifier {
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+)?)\x00`),
binutils.SupportingEvidenceMatcher("VERSION*",
m.FileContentsVersionMatcher(
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+|-[_0-9a-z]+)?)`)),
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+|-[_0-9a-z]+)?)\s`)),
binutils.SupportingEvidenceMatcher("../VERSION*",
m.FileContentsVersionMatcher(
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+|-[_0-9a-z]+)?)`)),
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+|-[_0-9a-z]+)?)\s`)),
),
Package: "go",
PURL: mustPURL("pkg:generic/go@version"),
@@ -232,7 +232,7 @@ func DefaultClassifiers() []binutils.Classifier {
Class: "arangodb-binary",
FileGlob: "**/arangosh",
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)\x00*(?P<version>[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?)\s(enterprise\s)?\[linux\]`),
`(?m)\x00*(?P<version>[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?)\s\[linux\]`),
Package: "arangodb",
PURL: mustPURL("pkg:generic/arangodb@version"),
CPEs: singleCPE("cpe:2.3:a:arangodb:arangodb:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
@@ -127,83 +127,23 @@ func pullDockerImage(imageReference, platform string) error {
}
func checkArchitecturesMatch(imageReference, platform string) (bool, string, error) {
// first check if the image exists locally
cmd := exec.Command("docker", "image", "inspect", imageReference)
if err := cmd.Run(); err != nil {
return false, "", err
}
// prefer the manifest list for platform info — with Docker's containerd image store,
// platform metadata lives on the manifest list entry, not in the image config.
if found, err := platformInManifest(imageReference, platform); err == nil {
return found, platform, nil
}
// fall back to image config for older Docker daemons that don't support "docker manifest inspect"
gotPlatform, err := platformFromImageInspect(imageReference)
if err != nil {
return false, "", err
}
return gotPlatform == platform, gotPlatform, nil
}
func platformFromImageInspect(imageReference string) (string, error) {
cmd := exec.Command("docker", "image", "inspect", imageReference)
out, err := cmd.CombinedOutput()
if err != nil {
return "", err
return false, "", err
}
var inspect []imageInspect
if err := json.Unmarshal(out, &inspect); err != nil {
return "", fmt.Errorf("unable to unmarshal image inspect: %w", err)
return false, "", fmt.Errorf("unable to unmarshal image inspect: %w", err)
}
if len(inspect) != 1 {
return "", fmt.Errorf("expected 1 image inspect, got %d", len(inspect))
return false, "", fmt.Errorf("expected 1 image inspect, got %d", len(inspect))
}
gotPlatform := inspect[0].Platform()
return inspect[0].Platform(), nil
}
type manifestList struct {
Manifests []manifestEntry `json:"manifests"`
}
type manifestEntry struct {
Platform manifestPlatform `json:"platform"`
}
type manifestPlatform struct {
Architecture string `json:"architecture"`
OS string `json:"os"`
}
// platformInManifest checks whether the wanted platform is available in the image's manifest list.
// With Docker's containerd image store, all images are distributed via manifest lists (indexes),
// and platform metadata may only be on the index entry — not in the image config that
// "docker image inspect" reads. "docker manifest inspect" reads the index directly.
func platformInManifest(imageReference, wantPlatform string) (bool, error) {
cmd := exec.Command("docker", "manifest", "inspect", imageReference)
out, err := cmd.CombinedOutput()
if err != nil {
return false, fmt.Errorf("manifest inspect failed: %w", err)
}
var ml manifestList
if err := json.Unmarshal(out, &ml); err != nil {
return false, fmt.Errorf("unable to unmarshal manifest: %w", err)
}
for _, m := range ml.Manifests {
p := fmt.Sprintf("%s/%s", m.Platform.OS, m.Platform.Architecture)
if p == wantPlatform {
return true, nil
}
}
return false, nil
return gotPlatform == platform, gotPlatform, nil
}
func copyBinariesFromDockerImages(config config.BinaryFromImage, destination string) (err error) {
+1 -1
View File
@@ -151,4 +151,4 @@ while $CONTINUE_LOOP; do
done
go run ../internal/manager write-snippet "$BINARY_FILE" --offset "$OFFSET" --length "$LENGTH" --name "$GROUP_NAME" --version "$VERSION"
go run ./manager write-snippet "$BINARY_FILE" --offset "$OFFSET" --length "$LENGTH" --name "$GROUP_NAME" --version "$VERSION"
@@ -1 +0,0 @@
go1.15 Fri 2003
@@ -1 +0,0 @@
no version in this binary
@@ -1 +1 @@
go1.15
go1.15 Fri 2003
-14
View File
@@ -458,20 +458,6 @@ from-images:
platform: linux/amd64
paths:
- /usr/bin/arangosh
- name: arangodb
version: 3.12.5
images:
- ref: arangodb:3.12.5@sha256:1f9278fe17b200cf3aea2c7bd7fd571221b5b41a49b835a397c47eb970c952d6
platform: linux/amd64
paths:
- /usr/bin/arangosh
- name: arangodb
version: 3.12.5-2
images:
- ref: arangodb:3.12.5.2@sha256:5b0d1d2911ea864ea61d7e2357789004fe912606f5980cf481739601d7cb17a1
platform: linux/amd64
paths:
- /usr/bin/arangosh
- version: 15.1
images:
- ref: postgres:15.1@sha256:b4140dd3a62f364f16a82c1bd88d28b9887ecb47f07dbe2941237d073574d428
+1 -1
View File
@@ -47,7 +47,7 @@ func node(value interface{}) erlangNode {
// parseErlang basic parser for erlang, used by rebar.lock
func parseErlang(reader io.Reader) (erlangNode, error) {
data, err := io.ReadAll(reader) //nolint:gocritic // custom parser requires []byte
data, err := io.ReadAll(reader)
if err != nil {
return node(nil), err
}
@@ -3,6 +3,7 @@ package githubactions
import (
"context"
"fmt"
"io"
"go.yaml.in/yaml/v3"
@@ -24,9 +25,13 @@ type compositeActionRunsDef struct {
}
func parseCompositeActionForActionUsage(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
contents, errs := io.ReadAll(reader)
if errs != nil {
return nil, nil, fmt.Errorf("unable to read yaml composite action file: %w", errs)
}
var ca compositeActionDef
var errs error
if errs = yaml.NewDecoder(reader).Decode(&ca); errs != nil {
if errs = yaml.Unmarshal(contents, &ca); errs != nil {
return nil, nil, fmt.Errorf("unable to parse yaml composite action file: %w", errs)
}
@@ -3,6 +3,7 @@ package githubactions
import (
"context"
"fmt"
"io"
"regexp"
"go.yaml.in/yaml/v3"
@@ -40,10 +41,14 @@ type stepDef struct {
}
func parseWorkflowForWorkflowUsage(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
contents, errs := io.ReadAll(reader)
if errs != nil {
return nil, nil, fmt.Errorf("unable to read yaml workflow file: %w", errs)
}
// parse the yaml file into a generic node to preserve comments
var node yaml.Node
var errs error
if errs = yaml.NewDecoder(reader).Decode(&node); errs != nil {
if errs = yaml.Unmarshal(contents, &node); errs != nil {
return nil, nil, fmt.Errorf("unable to parse yaml workflow file: %w", errs)
}
@@ -74,10 +79,14 @@ func parseWorkflowForWorkflowUsage(_ context.Context, _ file.Resolver, _ *generi
}
func parseWorkflowForActionUsage(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
contents, errs := io.ReadAll(reader)
if errs != nil {
return nil, nil, fmt.Errorf("unable to read yaml workflow file: %w", errs)
}
// parse the yaml file into a generic node to preserve comments
var node yaml.Node
var errs error
if errs = yaml.NewDecoder(reader).Decode(&node); errs != nil {
if errs = yaml.Unmarshal(contents, &node); errs != nil {
return nil, nil, fmt.Errorf("unable to parse yaml workflow file: %w", errs)
}
+12 -52
View File
@@ -23,7 +23,6 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/cache"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/tmpdir"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/licenses"
@@ -164,10 +163,7 @@ func (c *goLicenseResolver) getLicensesFromRemote(ctx context.Context, moduleNam
return c.licenseCache.Resolve(fmt.Sprintf("%s/%s", moduleName, moduleVersion), func() ([]pkg.License, error) {
proxies := remotesForModule(c.opts.Proxies, c.opts.NoProxy, moduleName)
urlPrefix, fsys, cleanup, err := getModule(ctx, proxies, moduleName, moduleVersion)
if cleanup != nil {
defer cleanup()
}
urlPrefix, fsys, err := getModule(proxies, moduleName, moduleVersion)
if err != nil {
return nil, err
}
@@ -262,7 +258,7 @@ func processCaps(s string) string {
})
}
func getModule(ctx context.Context, proxies []string, moduleName, moduleVersion string) (urlPrefix string, fsys fs.FS, cleanup func(), err error) {
func getModule(proxies []string, moduleName, moduleVersion string) (urlPrefix string, fsys fs.FS, err error) {
for _, proxy := range proxies {
u, _ := url.Parse(proxy)
if proxy == "direct" {
@@ -271,7 +267,7 @@ func getModule(ctx context.Context, proxies []string, moduleName, moduleVersion
}
switch u.Scheme {
case "https", "http":
urlPrefix, fsys, cleanup, err = getModuleProxy(ctx, proxy, moduleName, moduleVersion)
urlPrefix, fsys, err = getModuleProxy(proxy, moduleName, moduleVersion)
case "file":
p := filepath.Join(u.Path, moduleName, "@v", moduleVersion)
urlPrefix = path.Join("file://", p) + "/"
@@ -285,78 +281,42 @@ func getModule(ctx context.Context, proxies []string, moduleName, moduleVersion
return
}
// getModuleProxy downloads a Go module zip from the given proxy and returns a filesystem view of its contents.
// The returned cleanup function closes the underlying temp file and removes it; callers must not use the
// returned fs.FS after calling cleanup.
func getModuleProxy(ctx context.Context, proxy string, moduleName string, moduleVersion string) (moduleURL string, out fs.FS, cleanup func(), _ error) {
func getModuleProxy(proxy string, moduleName string, moduleVersion string) (moduleURL string, out fs.FS, _ error) {
u := fmt.Sprintf("%s/%s/@v/%s.zip", proxy, moduleName, moduleVersion)
// get the module zip
log.WithFields("url", u).Info("downloading go module from proxy")
resp, err := http.Get(u) //nolint:gosec
if err != nil {
return "", nil, nil, err
return "", nil, err
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
// close the first response before retrying with a lowercased module name
_ = resp.Body.Close()
u = fmt.Sprintf("%s/%s/@v/%s.zip", proxy, strings.ToLower(moduleName), moduleVersion)
// try lowercasing it; some packages have mixed casing that really messes up the proxy
resp, err = http.Get(u) //nolint:gosec
if err != nil {
return "", nil, nil, err
return "", nil, err
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return "", nil, nil, fmt.Errorf("failed to get module zip: %s", resp.Status)
return "", nil, fmt.Errorf("failed to get module zip: %s", resp.Status)
}
}
// stream the zip to a temp file to avoid unbounded memory usage
td := tmpdir.FromContext(ctx)
if td == nil {
return "", nil, nil, fmt.Errorf("no temp dir factory in context")
}
tmpFile, cleanupFile, err := td.NewFile("gomodule-*.zip") //nolint:gocritic // cleanup is returned to caller, not deferred here
// read the zip
b, err := io.ReadAll(resp.Body)
if err != nil {
return "", nil, nil, fmt.Errorf("failed to create temp file for module zip: %w", err)
return "", nil, err
}
cleanup = func() {
_ = tmpFile.Close()
cleanupFile()
}
// cap downloads at 500MB to prevent disk exhaustion from malicious proxies
const maxModuleZipSize = 500 * 1024 * 1024
size, err := io.Copy(tmpFile, io.LimitReader(resp.Body, maxModuleZipSize))
if err != nil {
cleanup()
return "", nil, nil, fmt.Errorf("failed to download module zip: %w", err)
}
if size >= maxModuleZipSize {
cleanup()
return "", nil, nil, fmt.Errorf("module zip exceeds %d byte size limit", maxModuleZipSize)
}
if _, err := tmpFile.Seek(0, io.SeekStart); err != nil {
cleanup()
return "", nil, nil, fmt.Errorf("failed to seek module zip: %w", err)
}
out, err = zip.NewReader(tmpFile, size)
if err != nil {
cleanup()
return "", nil, nil, fmt.Errorf("failed to read module zip: %w", err)
}
out, err = zip.NewReader(bytes.NewReader(b), resp.ContentLength)
versionPath := findVersionPath(out, ".")
out = getSubFS(out, versionPath)
return u + "#" + versionPath + "/", out, cleanup, err
return u + "#" + versionPath + "/", out, err
}
func findVersionPath(f fs.FS, dir string) string {
+2 -2
View File
@@ -24,7 +24,7 @@ import (
)
func Test_LicenseSearch(t *testing.T) {
ctx := pkgtest.Context(t)
ctx := pkgtest.Context()
loc1 := file.NewLocation("github.com/someorg/somename@v0.3.2/LICENSE")
loc2 := file.NewLocation("github.com/!cap!o!r!g/!cap!project@v4.111.5/LICENSE.txt")
@@ -321,7 +321,7 @@ func Test_noLocalGoModDir(t *testing.T) {
validTmp := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(validTmp, "mod@ver"), 0700|os.ModeDir))
ctx := pkgtest.Context(t)
ctx := pkgtest.Context()
tests := []struct {
name string
dir string
+1 -1
View File
@@ -324,7 +324,7 @@ func buildModuleRelationships(
}
func (c *goModCataloger) parseModFileContents(reader file.LocationReadCloser) (*modfile.File, error) {
contents, err := io.ReadAll(reader) //nolint:gocritic // modfile.Parse requires []byte
contents, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("failed to read go module: %w", err)
}
@@ -3,6 +3,7 @@ package haskell
import (
"context"
"fmt"
"io"
"strings"
"go.yaml.in/yaml/v3"
@@ -41,11 +42,16 @@ type completedSnapshot struct {
// parseStackLock is a parser function for stack.yaml.lock contents, returning all packages discovered.
func parseStackLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
bytes, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to load stack.yaml.lock file: %w", err)
}
var lockFile stackLock
if err := yaml.NewDecoder(reader).Decode(&lockFile); err != nil {
if err := yaml.Unmarshal(bytes, &lockFile); err != nil {
log.WithFields("error", err, "path", reader.RealPath).Trace("failed to parse stack.yaml.lock")
return nil, nil, fmt.Errorf("failed to parse stack.yaml.lock file: %w", err)
return nil, nil, fmt.Errorf("failed to parse stack.yaml.lock file")
}
var (
@@ -3,6 +3,7 @@ package haskell
import (
"context"
"fmt"
"io"
"go.yaml.in/yaml/v3"
@@ -22,11 +23,16 @@ type stackYaml struct {
// parseStackYaml is a parser function for stack.yaml contents, returning all packages discovered.
func parseStackYaml(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
bytes, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to load stack.yaml file: %w", err)
}
var stackFile stackYaml
if err := yaml.NewDecoder(reader).Decode(&stackFile); err != nil {
if err := yaml.Unmarshal(bytes, &stackFile); err != nil {
log.WithFields("error", err, "path", reader.RealPath).Trace("failed to parse stack.yaml")
return nil, nil, fmt.Errorf("failed to parse stack.yaml file: %w", err)
return nil, nil, fmt.Errorf("failed to parse stack.yaml file")
}
var pkgs []pkg.Package
@@ -52,12 +52,6 @@
"github.com/ecnepsnai/web": [
"cpe:2.3:a:web_project:web:*:*:*:*:*:go:*:*"
],
"github.com/free5gc/amf": [
"cpe:2.3:a:free5gc:amf:*:*:*:*:*:go:*:*"
],
"github.com/free5gc/go-upf": [
"cpe:2.3:a:free5gc:go-upf:*:*:*:*:*:go:*:*"
],
"github.com/free5gc/smf": [
"cpe:2.3:a:free5gc:smf:*:*:*:*:*:go:*:*"
],
@@ -142,9 +136,6 @@
"github.com/quic-go/webtransport-go#section-readme": [
"cpe:2.3:a:quic-go:webtransport-go:*:*:*:*:*:go:*:*"
],
"github.com/rezmoss/axios4go": [
"cpe:2.3:a:rezmoss:axios4go:*:*:*:*:*:go:*:*"
],
"github.com/sap/cloud-security-client-go": [
"cpe:2.3:a:sap:cloud-security-client-go:*:*:*:*:*:go:*:*"
],
@@ -2320,9 +2311,6 @@
"@ckeditor/ckeditor5-widget": [
"cpe:2.3:a:ckeditor:ckeditor5-widget:*:*:*:*:*:node.js:*:*"
],
"@coding-solo/godot-mcp": [
"cpe:2.3:a:coding-solo:godot_mcp:*:*:*:*:*:*:*:*"
],
"@cookiex/deep": [
"cpe:2.3:a:cookiex-deep_project:cookiex-deep:*:*:*:*:*:node.js:*:*"
],
@@ -2362,9 +2350,6 @@
"@google-cloud/firestore": [
"cpe:2.3:a:google:cloud_firestore:*:*:*:*:*:node.js:*:*"
],
"@graphprotocol/contracts": [
"cpe:2.3:a:thegraph:graph_protocol_contracts:*:*:*:*:*:node.js:*:*"
],
"@grpc/grpc-js": [
"cpe:2.3:a:grpc:grpc:*:*:*:*:*:node.js:*:*"
],
@@ -2486,12 +2471,6 @@
"@thi.ng/egf": [
"cpe:2.3:a:\\@thi.ng\\/egf_project:\\@thi.ng\\/egf:*:*:*:*:*:node.js:*:*"
],
"@tinacms/cli": [
"cpe:2.3:a:ssw:tinacms\\/cli:*:*:*:*:*:node.js:*:*"
],
"@tinacms/graphql": [
"cpe:2.3:a:ssw:tinacms\\/graphql:*:*:*:*:*:node.js:*:*"
],
"@tiptap/extension-link": [
"cpe:2.3:a:tiptap:tiptap\\/extension-link:*:*:*:*:*:node.js:*:*"
],
@@ -3091,9 +3070,6 @@
"defaults-deep": [
"cpe:2.3:a:defaults-deep_project:defaults-deep:*:*:*:*:*:node.js:*:*"
],
"defuddle": [
"cpe:2.3:a:kepano:defuddle:*:*:*:*:*:node.js:*:*"
],
"desafio": [
"cpe:2.3:a:desafio_project:desafio:*:*:*:*:*:node.js:*:*"
],
@@ -3297,9 +3273,6 @@
"express-openid-connect": [
"cpe:2.3:a:auth0:express_openid_connect:*:*:*:*:*:node.js:*:*"
],
"express-rate-limit": [
"cpe:2.3:a:express-rate-limit:express-rate-limit:*:*:*:*:*:node.js:*:*"
],
"express-restify-mongoose": [
"cpe:2.3:a:express-restify-mongoose_project:express-restify-mongoose:*:*:*:*:*:node.js:*:*"
],
@@ -3346,6 +3319,7 @@
"cpe:2.3:a:fast_string_search_project:fast_string_search:*:*:*:*:*:node.js:*:*"
],
"fast-xml-parser": [
"cpe:2.3:a:fast-xml-parser_project:fast-xml-parser:*:*:*:*:*:node.js:*:*",
"cpe:2.3:a:naturalintelligence:fast-xml-parser:*:*:*:*:*:*:*:*"
],
"fastest-json-copy": [
@@ -3382,7 +3356,7 @@
"cpe:2.3:a:fibjs_project:fibjs:*:*:*:*:*:node.js:*:*"
],
"file-type": [
"cpe:2.3:a:sindresorhus:file-type:*:*:*:*:*:node.js:*:*"
"cpe:2.3:a:file-type_project:file-type:*:*:*:*:*:node.js:*:*"
],
"file-upload-with-preview": [
"cpe:2.3:a:johndatserakis:file-upload-with-preview:*:*:*:*:*:node.js:*:*"
@@ -5428,9 +5402,6 @@
"stimulsoft-dashboards-js": [
"cpe:2.3:a:stimulsoft:dashboard.js:*:*:*:*:*:node.js:*:*"
],
"storybook": [
"cpe:2.3:a:storybook:storybook:*:*:*:*:*:node.js:*:*"
],
"strapi": [
"cpe:2.3:a:strapi:strapi:*:*:*:*:*:*:*:*",
"cpe:2.3:a:strapi:strapi:*:*:*:*:*:node.js:*:*"
@@ -5497,9 +5468,6 @@
"terminal-kit": [
"cpe:2.3:a:terminal-kit_project:terminal-kit:*:*:*:*:*:node.js:*:*"
],
"terriajs-server": [
"cpe:2.3:a:terria:terriajs-server:*:*:*:*:*:node.js:*:*"
],
"terser": [
"cpe:2.3:a:terser:terser:*:*:*:*:*:node.js:*:*"
],
@@ -5515,9 +5483,6 @@
"timespan": [
"cpe:2.3:a:timespan_project:timespan:*:*:*:*:*:node.js:*:*"
],
"tinacms": [
"cpe:2.3:a:ssw:tinacms:*:*:*:*:*:node.js:*:*"
],
"tiny-conf": [
"cpe:2.3:a:tiny-conf_project:tiny-conf:*:*:*:*:*:node.js:*:*"
],
@@ -5626,9 +5591,6 @@
"ungit": [
"cpe:2.3:a:ungit_project:ungit:*:*:*:*:*:node.js:*:*"
],
"unhead": [
"cpe:2.3:a:unjs:unhead:*:*:*:*:*:node.js:*:*"
],
"unicode": [
"cpe:2.3:a:unicode_project:unicode:*:*:*:*:*:node.js:*:*"
],
@@ -6010,9 +5972,6 @@
"b2sdk": [
"cpe:2.3:a:backblaze:b2-sdk-python:*:*:*:*:*:*:*:*"
],
"black": [
"cpe:2.3:a:python:black:*:*:*:*:*:python:*:*"
],
"blackduck": [
"cpe:2.3:a:synopsys:hub-rest-api-python:*:*:*:*:*:*:*:*"
],
@@ -6080,9 +6039,6 @@
"datapizza-ai": [
"cpe:2.3:a:datapizza:datapizza_ai:*:*:*:*:*:*:*:*"
],
"dbt-common": [
"cpe:2.3:a:getdbt:dbt-common:*:*:*:*:*:*:*:*"
],
"decorator": [
"cpe:2.3:a:python:decorator:*:*:*:*:*:*:*:*"
],
@@ -6207,18 +6163,12 @@
"lxml-html-clean": [
"cpe:2.3:a:fedoralovespython:lxml_html_clean:*:*:*:*:*:python:*:*"
],
"mad-proxy": [
"cpe:2.3:a:machphy:mad-proxy:*:*:*:*:*:python:*:*"
],
"mage-ai": [
"cpe:2.3:a:mage:mage-ai:*:*:*:*:*:python:*:*"
],
"marshmallow": [
"cpe:2.3:a:marshmallow_project:marshmallow:*:*:*:*:*:python:*:*"
],
"mcp-memory-service": [
"cpe:2.3:a:doobidoo:mcp-memory-service:*:*:*:*:*:*:*:*"
],
"mltable": [
"cpe:2.3:a:microsoft:azure_machine_learning_software_development_kit:*:*:*:*:*:*:*:*"
],
@@ -7044,15 +6994,6 @@
"autorand": [
"cpe:2.3:a:autorand_project:autorand:*:*:*:*:*:rust:*:*"
],
"aws-lc-fips-sys": [
"cpe:2.3:a:amazon:aws-lc-fips-sys:*:*:*:*:*:rust:*:*"
],
"aws-lc-rs": [
"cpe:2.3:a:amazon:aws-lc-rs:*:*:*:*:*:rust:*:*"
],
"aws-lc-sys": [
"cpe:2.3:a:amazon:aws-lc-sys:*:*:*:*:*:rust:*:*"
],
"axum-core": [
"cpe:2.3:a:axum-core_project:axum-core:*:*:*:*:*:rust:*:*"
],
@@ -9231,9 +9172,6 @@
"bravo-translate": [
"cpe:2.3:a:guelbetech:bravo_translate:*:*:*:*:*:wordpress:*:*"
],
"bread-butter": [
"cpe:2.3:a:breadbutter:bread_\\\u0026_butter:*:*:*:*:*:wordpress:*:*"
],
"breadcrumbs-by-menu": [
"cpe:2.3:a:holest:breadcrumbs_by_menu:*:*:*:*:*:wordpress:*:*"
],
@@ -9472,7 +9410,8 @@
"cpe:2.3:a:kieranoshea:calendar:*:*:*:*:*:wordpress:*:*"
],
"calendar-booking": [
"cpe:2.3:a:startbooking:scheduling_plugin:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:startbooking:scheduling_plugin:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:startbooking:scheduling_plugin_-_online_booking:*:*:*:*:*:wordpress:*:*"
],
"calendar-plugin": [
"cpe:2.3:a:calendar_plugin_project:calendar_plugin:*:*:*:*:*:wordpress:*:*"
@@ -10087,9 +10026,6 @@
"contact-form-7-paypal-add-on": [
"cpe:2.3:a:wpplugin:paypal_\\\u0026_stripe_add-on:*:*:*:*:*:wordpress:*:*"
],
"contact-form-7-recaptcha": [
"cpe:2.3:a:iambriansreed:contact_form_7_recaptcha:*:*:*:*:*:wordpress:*:*"
],
"contact-form-7-simple-recaptcha": [
"cpe:2.3:a:contact_form_7_captcha_project:contact_form_7_captcha:*:*:*:*:*:wordpress:*:*"
],
@@ -13513,7 +13449,7 @@
"cpe:2.3:a:brijeshk89:ip_based_login:*:*:*:*:*:wordpress:*:*"
],
"ip-blacklist-cloud": [
"cpe:2.3:a:ad33lx:ip_blacklist_cloud:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:ip_blacklist_cloud_project:ip_blacklist_cloud:*:*:*:*:*:wordpress:*:*"
],
"ip-vault-wp-firewall": [
"cpe:2.3:a:youtag:two-factor_authentication:*:*:*:*:*:wordpress:*:*"
@@ -270,7 +270,7 @@ func (c *NVDAPIClient) fetchPage(ctx context.Context, startIndex int, start, end
// handleRateLimit handles HTTP 429 responses by parsing Retry-After and waiting
func (c *NVDAPIClient) handleRateLimit(ctx context.Context, httpResp *http.Response, attempt int) error {
body, _ := io.ReadAll(httpResp.Body) //nolint:gocritic // offline code generator
body, _ := io.ReadAll(httpResp.Body)
httpResp.Body.Close()
// parse Retry-After header
@@ -298,7 +298,7 @@ func checkHTTPStatus(httpResp *http.Response) error {
if httpResp.StatusCode == http.StatusOK {
return nil
}
body, _ := io.ReadAll(httpResp.Body) //nolint:gocritic // offline code generator
body, _ := io.ReadAll(httpResp.Body)
httpResp.Body.Close()
return fmt.Errorf("NVD API error (status %d): %s", httpResp.StatusCode, string(body))
}
@@ -24,7 +24,6 @@ import (
"github.com/anchore/syft/internal/cmptest"
"github.com/anchore/syft/internal/licenses"
"github.com/anchore/syft/internal/relationship"
"github.com/anchore/syft/internal/tmpdir"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/fileresolver"
@@ -63,7 +62,7 @@ type CatalogTester struct {
skipTestObservations bool
}
func initLicenseScanner() {
func Context() context.Context {
once.Do(func() {
// most of the time in testing is initializing the scanner. Let's do that just once
sc := &licenses.ScannerConfig{Scanner: licensecheck.Scan, CoverageThreshold: 75}
@@ -73,20 +72,13 @@ func initLicenseScanner() {
}
licenseScanner = &scanner
})
}
// Context returns a context with a shared license scanner and a TempDir backed by t.TempDir(),
// so cleanup is handled automatically when the test finishes.
func Context(t *testing.T) context.Context {
t.Helper()
initLicenseScanner()
td := tmpdir.FromPath(t.TempDir())
ctx := tmpdir.WithValue(context.Background(), td)
return licenses.SetContextLicenseScanner(ctx, *licenseScanner)
return licenses.SetContextLicenseScanner(context.Background(), *licenseScanner)
}
func NewCatalogTester() *CatalogTester {
return &CatalogTester{
context: Context(),
locationComparer: cmptest.DefaultLocationComparer,
licenseComparer: cmptest.DefaultLicenseComparer,
packageStringer: stringPackage,
@@ -280,18 +272,9 @@ func (p *CatalogTester) WithoutTestObserver() *CatalogTester {
return p
}
func (p *CatalogTester) ensureContext(t *testing.T) context.Context {
t.Helper()
if p.context != nil {
return p.context
}
return Context(t)
}
func (p *CatalogTester) TestParser(t *testing.T, parser generic.Parser) {
t.Helper()
ctx := p.ensureContext(t)
pkgs, relationships, err := parser(ctx, p.resolver, p.env, p.reader)
pkgs, relationships, err := parser(p.context, p.resolver, p.env, p.reader)
// only test for errors if explicitly requested
if p.wantErr != nil {
@@ -306,11 +289,10 @@ func (p *CatalogTester) TestParser(t *testing.T, parser generic.Parser) {
func (p *CatalogTester) TestCataloger(t *testing.T, cataloger pkg.Cataloger) {
t.Helper()
ctx := p.ensureContext(t)
resolver := NewObservingResolver(p.resolver)
pkgs, relationships, err := cataloger.Catalog(ctx, resolver)
pkgs, relationships, err := cataloger.Catalog(p.context, resolver)
// this is a minimum set, the resolver may return more that just this list
for _, path := range p.expectedPathResponses {
+1 -6
View File
@@ -18,7 +18,6 @@ import (
"github.com/anchore/syft/internal"
intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/tmpdir"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
@@ -105,11 +104,7 @@ func newJavaArchiveParser(ctx context.Context, reader file.LocationReadCloser, d
virtualElements := strings.Split(reader.Path(), ":")
currentFilepath := virtualElements[len(virtualElements)-1]
td := tmpdir.FromContext(ctx)
if td == nil {
return nil, func() {}, fmt.Errorf("no temp dir factory in context")
}
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(td, currentFilepath, reader)
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(currentFilepath, reader)
if err != nil {
return nil, cleanupFn, fmt.Errorf("unable to process java archive: %w", err)
}
+14 -13
View File
@@ -2,6 +2,7 @@ package java
import (
"bufio"
"context"
"fmt"
"io"
"os"
@@ -29,7 +30,7 @@ import (
func TestSearchMavenForLicenses(t *testing.T) {
url := maventest.MockRepo(t, "internal/maven/testdata/maven-repo")
ctx := pkgtest.Context(t)
ctx := pkgtest.Context()
tests := []struct {
name string
@@ -71,7 +72,7 @@ func TestSearchMavenForLicenses(t *testing.T) {
require.NoError(t, err)
// setup parser
ap, cleanupFn, err := newJavaArchiveParser(pkgtest.Context(t),
ap, cleanupFn, err := newJavaArchiveParser(context.Background(),
file.LocationReadCloser{
Location: file.NewLocation(fixture.Name()),
ReadCloser: fixture,
@@ -80,17 +81,17 @@ func TestSearchMavenForLicenses(t *testing.T) {
require.NoError(t, err)
// assert licenses are discovered from upstream
_, _, _, parsedPom := ap.discoverMainPackageFromPomInfo(pkgtest.Context(t))
_, _, _, parsedPom := ap.discoverMainPackageFromPomInfo(context.Background())
require.NotNil(t, parsedPom, "expected to find pom information in the fixture")
require.NotNil(t, parsedPom.project, "expected parsedPom to have a project")
resolvedLicenses, _ := ap.maven.ResolveLicenses(pkgtest.Context(t), parsedPom.project)
resolvedLicenses, _ := ap.maven.ResolveLicenses(context.Background(), parsedPom.project)
assert.Equal(t, tc.expectedLicenses, toPkgLicenses(ctx, nil, resolvedLicenses))
})
}
}
func TestParseJar(t *testing.T) {
ctx := pkgtest.Context(t)
ctx := pkgtest.Context()
tests := []struct {
name string
fixture string
@@ -371,7 +372,7 @@ func TestParseJar(t *testing.T) {
UseNetwork: false,
UseMavenLocalRepository: false,
}
parser, cleanupFn, err := newJavaArchiveParser(pkgtest.Context(t),
parser, cleanupFn, err := newJavaArchiveParser(context.Background(),
file.LocationReadCloser{
Location: file.NewLocation(fixture.Name()),
ReadCloser: fixture,
@@ -661,7 +662,7 @@ func TestParseNestedJar(t *testing.T) {
require.NoError(t, err)
gap := newGenericArchiveParserAdapter(ArchiveCatalogerConfig{})
actual, _, err := gap.processJavaArchive(pkgtest.Context(t), file.LocationReadCloser{
actual, _, err := gap.processJavaArchive(context.Background(), file.LocationReadCloser{
Location: file.NewLocation(fixture.Name()),
ReadCloser: fixture,
}, nil)
@@ -1079,7 +1080,7 @@ func Test_newPackageFromMavenData(t *testing.T) {
test.expectedParent.Locations = locations
r := maven.NewResolver(nil, maven.DefaultConfig())
actualPackage := newPackageFromMavenData(pkgtest.Context(t), r, test.props, test.project, test.parent, file.NewLocation(virtualPath))
actualPackage := newPackageFromMavenData(context.Background(), r, test.props, test.project, test.parent, file.NewLocation(virtualPath))
if test.expectedPackage == nil {
require.Nil(t, actualPackage)
} else {
@@ -1119,7 +1120,7 @@ func Test_artifactIDMatchesFilename(t *testing.T) {
}
func Test_parseJavaArchive_regressions(t *testing.T) {
ctx := pkgtest.Context(t)
ctx := context.TODO()
apiAll := pkg.Package{
Name: "api-all",
Version: "2.0.0",
@@ -1498,7 +1499,7 @@ func Test_deterministicMatchingPomProperties(t *testing.T) {
fixture, err := os.Open(fixturePath)
require.NoError(t, err)
parser, cleanupFn, err := newJavaArchiveParser(pkgtest.Context(t),
parser, cleanupFn, err := newJavaArchiveParser(context.Background(),
file.LocationReadCloser{
Location: file.NewLocation(fixture.Name()),
ReadCloser: fixture,
@@ -1506,7 +1507,7 @@ func Test_deterministicMatchingPomProperties(t *testing.T) {
defer cleanupFn()
require.NoError(t, err)
groupID, artifactID, version, _ := parser.discoverMainPackageFromPomInfo(pkgtest.Context(t))
groupID, artifactID, version, _ := parser.discoverMainPackageFromPomInfo(context.TODO())
require.Equal(t, test.expected, maven.NewID(groupID, artifactID, version))
}()
}
@@ -1633,9 +1634,9 @@ func Test_jarPomPropertyResolutionDoesNotPanic(t *testing.T) {
fixture, err := os.Open(jarName)
require.NoError(t, err)
ctx := pkgtest.Context(t)
ctx := context.TODO()
// setup parser
ap, cleanupFn, err := newJavaArchiveParser(pkgtest.Context(t),
ap, cleanupFn, err := newJavaArchiveParser(context.Background(),
file.LocationReadCloser{
Location: file.NewLocation(fixture.Name()),
ReadCloser: fixture,
@@ -40,7 +40,7 @@ func ParsePomXML(content io.Reader) (project *Project, err error) {
}
func getUtf8Reader(content io.Reader) (io.Reader, error) {
pomContents, err := io.ReadAll(content) //nolint:gocritic // charset detection requires full buffer
pomContents, err := io.ReadAll(content)
if err != nil {
return nil, err
}
@@ -463,7 +463,7 @@ func (r *Resolver) cacheResolveReader(key string, resolve func() (io.ReadCloser,
defer internal.CloseAndLogError(contentReader, key)
// store the contents to return a new reader with the same content
contents, err := io.ReadAll(contentReader) //nolint:gocritic // caching requires full buffer
contents, err := io.ReadAll(contentReader)
if err != nil {
return nil, err
}
@@ -1,6 +1,7 @@
package java
import (
"context"
"os"
"testing"
@@ -193,7 +194,7 @@ func Test_parseCommonsTextPomXMLProject(t *testing.T) {
func Test_parsePomXMLProject(t *testing.T) {
// TODO: ideally we would have the path to the contained pom.xml, not the jar
jarLocation := file.NewLocation("path/to/archive.jar")
ctx := pkgtest.Context(t)
ctx := context.TODO()
tests := []struct {
name string
project *pkg.JavaPomProject
@@ -264,11 +265,11 @@ func Test_parsePomXMLProject(t *testing.T) {
pom, err := maven.ParsePomXML(fixture)
require.NoError(t, err)
actual := newPomProject(pkgtest.Context(t), r, fixture.Name(), pom)
actual := newPomProject(context.Background(), r, fixture.Name(), pom)
assert.NoError(t, err)
assert.Equal(t, test.project, actual)
licenses, err := r.ResolveLicenses(pkgtest.Context(t), pom)
licenses, err := r.ResolveLicenses(context.Background(), pom)
//assert.NoError(t, err)
assert.Equal(t, test.licenses, toPkgLicenses(ctx, &jarLocation, licenses))
})
@@ -330,7 +331,7 @@ func Test_pomParent(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
r := maven.NewResolver(nil, maven.DefaultConfig())
assert.Equal(t, test.expected, pomParent(pkgtest.Context(t), r, &maven.Project{Parent: test.input}))
assert.Equal(t, test.expected, pomParent(context.Background(), r, &maven.Project{Parent: test.input}))
})
}
}
@@ -432,7 +433,7 @@ func Test_resolveLicenses(t *testing.T) {
fr, err := ds.FileResolver(source.AllLayersScope)
require.NoError(t, err)
ctx := pkgtest.Context(t)
ctx := context.TODO()
pkgs, _, err := cat.Catalog(ctx, fr)
require.NoError(t, err)
+10 -6
View File
@@ -6,16 +6,23 @@ import (
"os"
"path/filepath"
"github.com/anchore/syft/internal/tmpdir"
"github.com/anchore/syft/internal/log"
)
func saveArchiveToTmp(td *tmpdir.TempDir, archiveVirtualPath string, reader io.Reader) (string, string, func(), error) {
func saveArchiveToTmp(archiveVirtualPath string, reader io.Reader) (string, string, func(), error) {
name := filepath.Base(archiveVirtualPath)
tempDir, cleanupFn, err := td.NewChild("archive-contents") //nolint:gocritic // cleanup is returned to caller, not deferred here
tempDir, err := os.MkdirTemp("", "syft-archive-contents-")
if err != nil {
return "", "", func() {}, fmt.Errorf("unable to create tempdir for archive processing: %w", err)
}
cleanupFn := func() {
err = os.RemoveAll(tempDir)
if err != nil {
log.Errorf("unable to cleanup archive tempdir: %+v", err)
}
}
archivePath := filepath.Join(tempDir, "archive-"+name)
contentDir := filepath.Join(tempDir, "contents")
@@ -30,9 +37,6 @@ func saveArchiveToTmp(td *tmpdir.TempDir, archiveVirtualPath string, reader io.R
}
defer archiveFile.Close()
// note: no size limit here — the reader comes from a file already enumerated by the
// resolver, not from an untrusted network source. The file size is bounded by the
// source image/directory being scanned.
_, err = io.Copy(archiveFile, reader)
if err != nil {
return contentDir, archivePath, cleanupFn, fmt.Errorf("unable to copy archive: %w", err)
@@ -5,7 +5,6 @@ import (
"fmt"
intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/tmpdir"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
@@ -59,11 +58,7 @@ func newGenericTarWrappedJavaArchiveParser(cfg ArchiveCatalogerConfig) genericTa
}
func (gtp genericTarWrappedJavaArchiveParser) parseTarWrappedJavaArchive(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
td := tmpdir.FromContext(ctx)
if td == nil {
return nil, nil, fmt.Errorf("no temp dir factory in context")
}
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(td, reader.Path(), reader)
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(reader.Path(), reader)
// note: even on error, we should always run cleanup functions
defer cleanupFn()
if err != nil {
@@ -1,6 +1,7 @@
package java
import (
"context"
"os"
"path"
"testing"
@@ -49,7 +50,7 @@ func Test_parseTarWrappedJavaArchive(t *testing.T) {
}
gtp := newGenericTarWrappedJavaArchiveParser(ArchiveCatalogerConfig{})
actualPkgs, _, err := gtp.parseTarWrappedJavaArchive(pkgtest.Context(t), nil, nil, file.LocationReadCloser{
actualPkgs, _, err := gtp.parseTarWrappedJavaArchive(context.Background(), nil, nil, file.LocationReadCloser{
Location: file.NewLocation(test.fixture),
ReadCloser: fixture,
})
@@ -5,7 +5,6 @@ import (
"fmt"
intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/tmpdir"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
@@ -31,11 +30,7 @@ func newGenericZipWrappedJavaArchiveParser(cfg ArchiveCatalogerConfig) genericZi
}
func (gzp genericZipWrappedJavaArchiveParser) parseZipWrappedJavaArchive(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
td := tmpdir.FromContext(ctx)
if td == nil {
return nil, nil, fmt.Errorf("no temp dir factory in context")
}
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(td, reader.Path(), reader)
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(reader.Path(), reader)
// note: even on error, we should always run cleanup functions
defer cleanupFn()
if err != nil {
@@ -1,6 +1,7 @@
package java
import (
"context"
"os"
"path"
"testing"
@@ -9,7 +10,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func Test_parseZipWrappedJavaArchive(t *testing.T) {
@@ -36,7 +36,7 @@ func Test_parseZipWrappedJavaArchive(t *testing.T) {
gzp := newGenericZipWrappedJavaArchiveParser(ArchiveCatalogerConfig{})
actualPkgs, _, err := gzp.parseZipWrappedJavaArchive(pkgtest.Context(t), nil, nil, file.LocationReadCloser{
actualPkgs, _, err := gzp.parseZipWrappedJavaArchive(context.Background(), nil, nil, file.LocationReadCloser{
Location: file.NewLocation(test.fixture),
ReadCloser: fixture,
})
+16 -2
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
@@ -256,12 +257,19 @@ func getLicenseFromNpmRegistry(baseURL, packageName, version string) (string, er
}
}()
bytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("unable to parse package from npm registry: %w", err)
}
dec := json.NewDecoder(strings.NewReader(string(bytes)))
// Read "license" from the response
var license struct {
License string `json:"license"`
}
if err := json.NewDecoder(resp.Body).Decode(&license); err != nil {
if err := dec.Decode(&license); err != nil {
return "", fmt.Errorf("unable to parse license from npm registry: %w", err)
}
@@ -316,8 +324,14 @@ func parseLicensesFromLocation(l file.Location, resolver file.Resolver, pkgFile
}
defer internal.CloseAndLogError(contentReader, l.RealPath)
contents, err := io.ReadAll(contentReader)
if err != nil {
log.Debugf("error reading file contents for %s: %v", pkgFile, err)
return nil, err
}
var pkgJSON packageJSON
err = json.NewDecoder(contentReader).Decode(&pkgJSON)
err = json.Unmarshal(contents, &pkgJSON)
if err != nil {
log.Debugf("error parsing %s: %v", pkgFile, err)
return nil, err
@@ -179,7 +179,7 @@ func newPnpmLockfileParser(version float64) pnpmLockfileParser {
// parsePnpmLock is the main parser function for pnpm-lock.yaml files.
func (a genericPnpmLockAdapter) parsePnpmLock(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
data, err := io.ReadAll(reader) //nolint:gocritic // multi-pass parse requires []byte
data, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to load pnpm-lock.yaml file: %w", err)
}
@@ -166,8 +166,7 @@ func findDevOnlyPkgs(yarnPkgs []yarnPackage, prodDeps, devDeps map[string]string
}
func parseYarnV1LockFile(reader io.ReadCloser) ([]yarnPackage, error) {
// TODO: refactor to use bufio.Scanner for streaming line-by-line parsing instead of reading the entire file
content, err := io.ReadAll(reader) //nolint:gocritic // stateful multi-line parser; candidate for streaming refactor
content, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("failed to read yarn.lock file: %w", err)
}
@@ -266,8 +265,7 @@ func (a genericYarnLockAdapter) parseYarnLock(ctx context.Context, resolver file
return nil, nil, nil
}
// TODO: refactor to detect version from the first line via bufio.Scanner, then dispatch to a streaming parser
data, err := io.ReadAll(reader) //nolint:gocritic // two-pass parse: version detection then format-specific parsing
data, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to load yarn.lock file: %w", err)
}
+1 -1
View File
@@ -39,7 +39,7 @@ var noReturn = rockspec{
// parseRockspec basic parser for rockspec
func parseRockspecData(reader io.Reader) (rockspec, error) {
data, err := io.ReadAll(reader) //nolint:gocritic // custom parser requires []byte
data, err := io.ReadAll(reader)
if err != nil {
return noReturn, err
}
+1 -1
View File
@@ -46,7 +46,7 @@ func (c cataloger) Name() string {
func (c cataloger) Catalog(ctx context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
// always try the DB cataloger first (based off of information recorded by actions taken by nix tooling)
pkgs, rels, err := c.dbParser.catalog(ctx, resolver)
pkgs, rels, err := c.dbParser.catalog(resolver)
if err != nil {
return nil, nil, fmt.Errorf("failed to catalog nix packages from database: %w", err)
}
+7 -8
View File
@@ -1,9 +1,8 @@
package nix
import (
"bufio"
"context"
"fmt"
"io"
"path"
"strconv"
"strings"
@@ -18,7 +17,7 @@ import (
const defaultSchema = 10
type dbProcessor func(ctx context.Context, config Config, dbLocation file.Location, resolver file.Resolver, catalogerName string) ([]pkg.Package, []artifact.Relationship, error)
type dbProcessor func(config Config, dbLocation file.Location, resolver file.Resolver, catalogerName string) ([]pkg.Package, []artifact.Relationship, error)
type dbCataloger struct {
config Config
@@ -46,7 +45,7 @@ type dbPackageEntry struct {
Files []string
}
func (c dbCataloger) catalog(ctx context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
func (c dbCataloger) catalog(resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
dbLocs, err := resolver.FilesByGlob("**/nix/var/nix/db/db.sqlite")
if err != nil {
return nil, nil, fmt.Errorf("failed to find Nix database: %w", err)
@@ -66,7 +65,7 @@ func (c dbCataloger) catalog(ctx context.Context, resolver file.Resolver) ([]pkg
continue
}
newPkgs, newRelationships, err := parser(ctx, c.config, dbLoc, resolver, c.catalogerName)
newPkgs, newRelationships, err := parser(c.config, dbLoc, resolver, c.catalogerName)
if err != nil {
errs = unknown.Append(errs, dbLoc.Coordinates, err)
continue
@@ -93,13 +92,13 @@ func (c dbCataloger) selectDBParser(dbLocation file.Location, resolver file.Reso
return c.schemaProcessor[defaultSchema], 0
}
scanner := bufio.NewScanner(schemaContents)
if !scanner.Scan() {
contents, err := io.ReadAll(schemaContents)
if err != nil {
log.WithFields("path", loc.RealPath).Tracef("failed to read Nix database schema file, assuming %d", defaultSchema)
return c.schemaProcessor[defaultSchema], 0
}
schema, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
schema, err := strconv.Atoi(strings.TrimSpace(string(contents)))
if err != nil {
log.WithFields("path", loc.RealPath).Tracef("failed to parse Nix database schema file, assuming %d", defaultSchema)
return c.schemaProcessor[defaultSchema], 0
+11 -22
View File
@@ -1,7 +1,6 @@
package nix
import (
"context"
"database/sql"
"fmt"
"io"
@@ -9,7 +8,6 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/tmpdir"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
@@ -17,33 +15,26 @@ import (
var _ dbProcessor = processV10DB
func processV10DB(ctx context.Context, config Config, dbLocation file.Location, resolver file.Resolver, catalogerName string) ([]pkg.Package, []artifact.Relationship, error) {
func processV10DB(config Config, dbLocation file.Location, resolver file.Resolver, catalogerName string) ([]pkg.Package, []artifact.Relationship, error) {
dbContents, err := resolver.FileContentsByLocation(dbLocation)
defer internal.CloseAndLogError(dbContents, dbLocation.RealPath)
if err != nil {
return nil, nil, fmt.Errorf("unable to read Nix database: %w", err)
}
td := tmpdir.FromContext(ctx)
if td == nil {
return nil, nil, fmt.Errorf("no temp dir factory in context")
}
tempDB, cleanupDB, err := createTempDB(td, dbContents)
tempDB, err := createTempDB(dbContents)
if err != nil {
return nil, nil, fmt.Errorf("failed to create temporary database: %w", err)
}
// defer order is LIFO: cleanupDB (remove file) must run after db.Close and tempDB.Close
defer cleanupDB()
// close order is LIFO: db.Close() (SQLite conn) → tempDB.Close() (file handle) → cleanupDB (remove file)
defer tempDB.Close()
defer os.RemoveAll(tempDB.Name())
db, err := sql.Open("sqlite", tempDB.Name())
if err != nil {
return nil, nil, fmt.Errorf("failed to open database: %w", err)
}
defer db.Close()
db.SetConnMaxLifetime(0)
defer db.Close()
packageEntries, err := extractV10DBPackages(config, db, dbLocation, resolver)
if err != nil {
@@ -241,20 +232,18 @@ func finalizeV10DBResults(db *sql.DB, packageEntries map[int]*dbPackageEntry, ca
return pkgs, relationships, nil
}
func createTempDB(td *tmpdir.TempDir, content io.ReadCloser) (*os.File, func(), error) {
noop := func() {}
tempFile, cleanup, err := td.NewFile("nix-db-*.sqlite") //nolint:gocritic // cleanup is returned to caller, not deferred here
func createTempDB(content io.ReadCloser) (*os.File, error) {
tempFile, err := os.CreateTemp("", "nix-db-*.sqlite")
if err != nil {
return nil, noop, err
return nil, err
}
_, err = io.Copy(tempFile, content)
if err != nil {
tempFile.Close()
cleanup()
return nil, noop, err
os.Remove(tempFile.Name())
return nil, err
}
return tempFile, cleanup, nil
return tempFile, nil
}
+1 -1
View File
@@ -25,7 +25,7 @@ func parseOpamPackage(ctx context.Context, _ file.Resolver, _ *generic.Environme
homepageRe := regexp.MustCompile(`(?m)homepage:\s*"(?P<url>[^"]+)"`)
urlRe := regexp.MustCompile(`(?m)url\s*{(?P<url>[^}]+)}`)
data, err := io.ReadAll(reader) //nolint:gocritic // regex matching requires full buffer
data, err := io.ReadAll(reader)
if err != nil {
log.WithFields("error", err).Trace("unable to read opam package")
return nil, nil, nil
+1 -1
View File
@@ -58,7 +58,7 @@ func parsePear(ctx context.Context, _ file.Resolver, _ *generic.Environment, rea
// parsePeclPearSerialized is a parser function for Pear metadata contents, returning "Default" php packages discovered.
func parsePeclPearSerialized(reader file.LocationReadCloser) (*peclPearData, error) {
data, err := io.ReadAll(reader) //nolint:gocritic // phpserialize requires []byte
data, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
+1 -1
View File
@@ -473,7 +473,7 @@ func Test_PackageCataloger_IgnorePackage(t *testing.T) {
t.Run(test.MetadataFixture, func(t *testing.T) {
resolver := file.NewMockResolverForPaths(test.MetadataFixture)
actual, _, err := NewInstalledPackageCataloger().Catalog(pkgtest.Context(t), resolver)
actual, _, err := NewInstalledPackageCataloger().Catalog(pkgtest.Context(), resolver)
require.NoError(t, err)
if len(actual) != 0 {
+10 -1
View File
@@ -4,8 +4,10 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/anchore/syft/internal/cache"
@@ -98,6 +100,13 @@ func getLicenseFromPypiRegistry(baseURL, packageName, version string) (string, e
return "", fmt.Errorf("unable to get package from pypi registry")
}
bytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("unable to parse package from pypi registry: %w", err)
}
dec := json.NewDecoder(strings.NewReader(string(bytes)))
// Read "license" from the response
var pypiResponse struct {
Info struct {
@@ -106,7 +115,7 @@ func getLicenseFromPypiRegistry(baseURL, packageName, version string) (string, e
} `json:"info"`
}
if err := json.NewDecoder(resp.Body).Decode(&pypiResponse); err != nil {
if err := dec.Decode(&pypiResponse); err != nil {
return "", fmt.Errorf("unable to parse license from pypi registry: %w", err)
}
+1 -1
View File
@@ -143,7 +143,7 @@ func (ulp uvLockParser) uvLockPackages(ctx context.Context, reader file.Location
var parsedLockFileVersion uvLockFileVersion
// we cannot use the reader twice, so we read the contents first --uv.lock files tend to be small enough
contents, err := io.ReadAll(reader) //nolint:gocritic // multi-pass parse requires []byte
contents, err := io.ReadAll(reader)
if err != nil {
return nil, unknown.New(reader.Location, fmt.Errorf("failed to read uv lock file: %w", err))
}
+7 -1
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"context"
"encoding/json"
"io"
"path"
"path/filepath"
"strings"
@@ -173,8 +174,13 @@ func fetchDirectURLData(resolver file.Resolver, metadataLocation file.Location)
}
defer internal.CloseAndLogError(directURLContents, directURLLocation.AccessPath)
buffer, err := io.ReadAll(directURLContents)
if err != nil {
return nil, nil, err
}
var directURLJson directURLOrigin
if err := json.NewDecoder(directURLContents).Decode(&directURLJson); err != nil {
if err := json.Unmarshal(buffer, &directURLJson); err != nil {
return nil, nil, err
}
+13 -8
View File
@@ -5,11 +5,11 @@ import (
"errors"
"fmt"
"io"
"os"
"strings"
rpmdb "github.com/anchore/go-rpmdb/pkg"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/tmpdir"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
@@ -22,16 +22,21 @@ import (
//
//nolint:funlen
func parseRpmDB(ctx context.Context, resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
td := tmpdir.FromContext(ctx)
if td == nil {
return nil, nil, fmt.Errorf("no temp dir factory in context")
}
f, cleanup, err := td.NewFile("rpmdb-*")
f, err := os.CreateTemp("", "rpmdb")
if err != nil {
return nil, nil, fmt.Errorf("failed to create temp rpmdb file: %w", err)
}
defer cleanup()
defer f.Close()
defer func() {
err = f.Close()
if err != nil {
log.Errorf("failed to close temp rpmdb file: %+v", err)
}
err = os.Remove(f.Name())
if err != nil {
log.Errorf("failed to remove temp rpmdb file: %+v", err)
}
}()
_, err = io.Copy(f, reader)
if err != nil {
+1 -1
View File
@@ -85,6 +85,6 @@ func adaptToReadSeeker(reader io.Reader) (io.ReadSeeker, error) {
log.Debug("SBOM cataloger reader is not a ReadSeeker, reading entire SBOM into memory")
var buff bytes.Buffer
_, err := io.Copy(&buff, reader) //nolint:gocritic // buffering to ReadSeeker
_, err := io.Copy(&buff, reader)
return bytes.NewReader(buff.Bytes()), err
}
@@ -3,6 +3,7 @@ package swift
import (
"context"
"fmt"
"io"
"strings"
"go.yaml.in/yaml/v3"
@@ -27,8 +28,12 @@ type podfileLock struct {
// parsePodfileLock is a parser function for Podfile.lock contents, returning all cocoapods pods discovered.
func parsePodfileLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
bytes, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("unable to read file: %w", err)
}
var podfile podfileLock
if err := yaml.NewDecoder(reader).Decode(&podfile); err != nil {
if err = yaml.Unmarshal(bytes, &podfile); err != nil {
return nil, nil, fmt.Errorf("unable to parse yaml: %w", err)
}
+1 -1
View File
@@ -20,7 +20,7 @@ func parsePackPackage(ctx context.Context, resolver file.Resolver, _ *generic.En
homeRe := regexp.MustCompile(`home\(\s*'([^']+)'\s*\)`)
authorRe := regexp.MustCompile(`(author|packager)\(\s*'([^']+)'\s*(?:,\s*'([^']+)'\s*)?\)`)
data, err := io.ReadAll(reader) //nolint:gocritic // regex matching requires full buffer
data, err := io.ReadAll(reader)
if err != nil {
log.WithFields("error", err).Trace("unable to parse Rockspec app")
return nil, nil, nil
@@ -20,7 +20,7 @@ type terraformLockFile struct {
func parseTerraformLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var lockFile terraformLockFile
contents, err := io.ReadAll(reader) //nolint:gocritic // hclsimple.Decode requires []byte
contents, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to read terraform lock file: %w", err)
}
+1 -1
View File
@@ -411,7 +411,7 @@ func (b *licenseBuilder) licenseFromContentHash(content string) License {
}
func contentFromReader(r io.Reader) (string, error) {
bytes, err := io.ReadAll(r) //nolint:gocritic // reading license content for storage
bytes, err := io.ReadAll(r)
if err != nil {
return "", err
}
+13 -2
View File
@@ -3,6 +3,7 @@ package snapsource
import (
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strconv"
@@ -214,8 +215,13 @@ func (c *snapcraftClient) GetSnapDownloadURL(id snapIdentity) (string, error) {
return "", fmt.Errorf("API request failed with status code %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}
var info snapcraftInfo
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
if err := json.Unmarshal(body, &info); err != nil {
return "", fmt.Errorf("failed to parse JSON response: %w", err)
}
@@ -253,8 +259,13 @@ func (c *snapcraftClient) CheckSnapExists(snapName string) (bool, string, error)
return false, "", fmt.Errorf("find API request failed with status code %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, "", fmt.Errorf("failed to read find response body: %w", err)
}
var findResp snapFindResponse
if err := json.NewDecoder(resp.Body).Decode(&findResp); err != nil {
if err := json.Unmarshal(body, &findResp); err != nil {
return false, "", fmt.Errorf("failed to parse find JSON response: %w", err)
}
-44
View File
@@ -24,50 +24,6 @@ func isPtr(ctx *dsl.VarFilterContext) bool {
return strings.HasPrefix(ctx.Type.String(), "*") || strings.HasPrefix(ctx.Type.Underlying().String(), "*")
}
// nolint:unused
func noUnboundedReads(m dsl.Matcher) {
// flag io.ReadAll where the argument is not already wrapped in io.LimitReader
m.Match(`io.ReadAll($reader)`).
Where(!m["reader"].Text.Matches(`(?i)LimitReader|LimitedReader`)).
Report("do not use unbounded io.ReadAll; wrap the reader with io.LimitReader or use a streaming parser")
// flag io.Copy only when the destination is an in-memory buffer
// io.Copy to files, hash writers, encoders, etc. is streaming and safe
m.Match(`io.Copy($dst, $src)`).
Where((m["dst"].Type.Is(`*bytes.Buffer`) || m["dst"].Type.Is(`*strings.Builder`)) && !m["src"].Text.Matches(`(?i)LimitReader|LimitedReader`)).
Report("do not use unbounded io.Copy to in-memory buffer; wrap the source reader with io.LimitReader")
}
// nolint:unused
func noDirectTempFiles(m dsl.Matcher) {
// catalogers must use tmpdir.FromContext(ctx) instead of creating temp files/dirs directly,
// so that all temp storage is centrally managed and cleaned up
m.Match(
`os.CreateTemp($*_)`,
`os.MkdirTemp($*_)`,
).
Where(m.File().PkgPath.Matches(`/cataloger/`)).
Report("do not use os.CreateTemp/os.MkdirTemp in catalogers; use tmpdir.FromContext(ctx) instead")
}
// nolint:unused
func tmpCleanupDeferred(m dsl.Matcher) {
// ensure the cleanup function returned by NewFile/NewChild is deferred, not discarded
m.Match(
`$_, $cleanup, $err := $x.NewFile($*_); if $*_ { $*_ }; $next`,
`$_, $cleanup, $err = $x.NewFile($*_); if $*_ { $*_ }; $next`,
).
Where(!m["next"].Text.Matches(`^defer `)).
Report("defer the cleanup function returned by NewFile immediately after the error check")
m.Match(
`$_, $cleanup, $err := $x.NewChild($*_); if $*_ { $*_ }; $next`,
`$_, $cleanup, $err = $x.NewChild($*_); if $*_ { $*_ }; $next`,
).
Where(!m["next"].Text.Matches(`^defer `)).
Report("defer the cleanup function returned by NewChild immediately after the error check")
}
// nolint:unused
func packagesInRelationshipsAsValues(m dsl.Matcher) {
m.Import("github.com/anchore/syft/syft/artifact")