Compare commits

..
Author SHA1 Message Date
Alex Goodman 677f4c0110 remove workflows that update the code
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-03-27 12:24:02 -04:00
anchore-actions-token-generator[bot]andwagoodman 673c85754c chore(deps): update CPE dictionary index (#4689)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: wagoodman <590471+wagoodman@users.noreply.github.com>
2026-03-25 08:38:49 -04:00
Will Murphy c5114fd745 chore(deps): ignore some dependabot deps (#4696)
Prevent some packages from being updated.

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
2026-03-24 08:12:50 -04:00
Weston Steimel f68a7cc899 ci: further pr target code checkout assurances (#4695)
Signed-off-by: Weston Steimel <author@code.w.steimel.me.uk>
2026-03-24 07:16:16 -04:00
witchcraze 7800b16529 fix: update arangodb classifier and capture-snippet.sh (#4662)
Signed-off-by: witchcraze <witchcraze@gmail.com>
2026-03-23 16:29:39 -04:00
Keith Zantow 834ddcb1c0 fix: golang version file regex (#4694)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
2026-03-23 15:56:29 -04:00
Weston Steimel f5d318d934 ci: add explicit ref to main and warning for pull_request_target workflow (#4693)
Signed-off-by: Weston Steimel <author@code.w.steimel.me.uk>
2026-03-23 16:45:18 +00:00
anchore-actions-token-generator[bot]andspiffcs 8531e1917b chore(deps): update tools to latest versions (#4690)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: spiffcs <32073428+spiffcs@users.noreply.github.com>
2026-03-23 12:01:27 -04:00
anchore-actions-token-generator[bot]andwillmurphyscode 860126c650 chore(deps): update anchore dependencies (#4681)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: willmurphyscode <12529630+willmurphyscode@users.noreply.github.com>
2026-03-19 16:44:55 +00:00
Will Murphy 36639f136b chore(deps): bump github.com/buger/jsonsparser to v1.1.2 (#4680)
Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
2026-03-19 15:08:18 +00:00
dependabot[bot] f32238c268 chore(deps): bump the go-minor-patch group with 2 updates (#4678)
Bumps the go-minor-patch group with 2 updates: [golang.org/x/net](https://github.com/golang/net) and [golang.org/x/tools](https://github.com/golang/tools).


Updates `golang.org/x/net` from 0.51.0 to 0.52.0
- [Commits](https://github.com/golang/net/compare/v0.51.0...v0.52.0)

Updates `golang.org/x/tools` from 0.42.0 to 0.43.0
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.42.0...v0.43.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.52.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-minor-patch
- dependency-name: golang.org/x/tools
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 10:25:19 -04:00
dependabot[bot] 0c8eef65f0 chore(deps): bump google.golang.org/grpc from 1.78.0 to 1.79.3 (#4675)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.78.0 to 1.79.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.78.0...v1.79.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.79.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 16:55:30 -04:00
dependabot[bot] 4d42f8af32 chore(deps): bump the go-minor-patch group with 2 updates (#4674)
Bumps the go-minor-patch group with 2 updates: [github.com/hashicorp/go-getter](https://github.com/hashicorp/go-getter) and [golang.org/x/mod](https://github.com/golang/mod).


Updates `github.com/hashicorp/go-getter` from 1.8.4 to 1.8.5
- [Release notes](https://github.com/hashicorp/go-getter/releases)
- [Commits](https://github.com/hashicorp/go-getter/compare/v1.8.4...v1.8.5)

Updates `golang.org/x/mod` from 0.33.0 to 0.34.0
- [Commits](https://github.com/golang/mod/compare/v0.33.0...v0.34.0)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/go-getter
  dependency-version: 1.8.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-minor-patch
- dependency-name: golang.org/x/mod
  dependency-version: 0.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 16:13:35 -04:00
Will Murphy e38851143e chore: centralize temp files and prefer streaming IO (#4668)
* chore: centralize temp files and prefer streaming IO

Catalogers that create temp files ad-hoc can easily forget cleanup,
leaking files on disk. Similarly, io.ReadAll is convenient but risks
OOM on large or malicious inputs.

Introduce internal/tmpdir to manage all cataloger temp storage under
a single root directory with automatic cleanup. Prefer streaming
parsers (bufio.Scanner, json/yaml.NewDecoder, io.LimitReader) over
buffering entire inputs into memory. Add ruleguard rules to enforce
both practices going forward.

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

* chore: go back to old release parsing

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

* simplify to limit reader in version check

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

* chore: regex change postponed

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

* simplify supplement release to limitreader

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

---------

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
2026-03-18 10:53:51 -04:00
anchore-actions-token-generator[bot]andspiffcs a3dacf5ecd chore(deps): update tools to latest versions (#4663)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: spiffcs <32073428+spiffcs@users.noreply.github.com>
2026-03-16 11:26:06 -04:00
dependabot[bot] cccc9bf7f9 chore(deps): bump the go-minor-patch group with 3 updates (#4669)
Bumps the go-minor-patch group with 3 updates: [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry), [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) and [golang.org/x/time](https://github.com/golang/time).


Updates `github.com/google/go-containerregistry` from 0.21.1 to 0.21.2
- [Release notes](https://github.com/google/go-containerregistry/releases)
- [Commits](https://github.com/google/go-containerregistry/compare/v0.21.1...v0.21.2)

Updates `github.com/olekukonko/tablewriter` from 1.1.3 to 1.1.4
- [Release notes](https://github.com/olekukonko/tablewriter/releases)
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.1.3...v1.1.4)

Updates `golang.org/x/time` from 0.14.0 to 0.15.0
- [Commits](https://github.com/golang/time/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: github.com/google/go-containerregistry
  dependency-version: 0.21.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-minor-patch
- dependency-name: github.com/olekukonko/tablewriter
  dependency-version: 1.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-minor-patch
- dependency-name: golang.org/x/time
  dependency-version: 0.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:25:41 -04:00
dependabot[bot] 59f7725d0d chore(deps): bump github/codeql-action (#4670)
Bumps the actions-minor-patch group with 1 update in the / directory: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 4.32.3 to 4.32.6
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/9e907b5e64f6b83e7804b09294d44122997950d6...0d579ffd059c29b07949a3cce3983f0780820c98)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:25:27 -04:00
dependabot[bot] 7a6b1575ae chore(deps): bump docker/login-action from 3.7.0 to 4.0.0 (#4671)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.7.0 to 4.0.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/c94ce9fb468520275223c153574b00df6fe4bcc9...b45d80f862d83dbcd57f89517bcf500b2ab88fb2)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:25:16 -04:00
anchore-actions-token-generator[bot]andwagoodman 92a6b36e89 chore(deps): update CPE dictionary index (#4673)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: wagoodman <590471+wagoodman@users.noreply.github.com>
2026-03-16 11:25:05 -04:00
Will Murphy 7158535fe6 chore(tests): fix test fixture build on modern ARM Mac (#4666)
BUILDPLATFORM is automatically set to the host's platform in new Docker,
so having it defined as an arg results in it being overridden by this
automatic value. Since it was always assigned to a literal string in the
test files, just use that string.

Additionally, image platform is better pulled from the manifest, not the
image config, in containerd store, so try that first.

Additionally, python3 is on PATH on new macs by default, but not python.

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
2026-03-11 09:37:40 -04:00
90 changed files with 1071 additions and 3159 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.11.3
want: v0.12.0
method: github-release
with:
repo: anchore/binny
@@ -18,7 +18,7 @@ tools:
# used to sign mac binaries at release
- name: quill
version:
want: v0.5.1
want: v0.7.1
method: github-release
with:
repo: anchore/quill
@@ -26,7 +26,7 @@ tools:
# used for linting
- name: golangci-lint
version:
want: v2.11.2
want: v2.11.4
method: github-release
with:
repo: golangci/golangci-lint
@@ -58,7 +58,7 @@ tools:
# used to release all artifacts
- name: goreleaser
version:
want: v2.14.2
want: v2.14.3
method: github-release
with:
repo: goreleaser/goreleaser
@@ -98,7 +98,7 @@ tools:
# used for triggering a release
- name: gh
version:
want: v2.87.3
want: v2.88.1
method: github-release
with:
repo: cli/cli
@@ -106,7 +106,7 @@ tools:
# used to upload test fixture cache
- name: oras
version:
want: v1.3.0
want: v1.3.1
method: github-release
with:
repo: oras-project/oras
+5
View File
@@ -23,6 +23,11 @@ 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@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
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@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
uses: github/codeql-action/autobuild@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
# ℹ️ 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@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
@@ -37,6 +37,8 @@ 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@c94ce9fb468520275223c153574b00df6fe4bcc9 #v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 #v4.0.0
with:
username: ${{ secrets.ANCHOREOSSWRITE_DH_USERNAME }}
password: ${{ secrets.ANCHOREOSSWRITE_DH_PAT }}
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 #v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 #v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -1,51 +0,0 @@
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 }}
@@ -1,67 +0,0 @@
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 }}
@@ -1,84 +0,0 @@
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' }}
@@ -1,64 +0,0 @@
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,7 +48,6 @@ 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
@@ -1,264 +0,0 @@
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
@@ -1,119 +0,0 @@
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(resp.Body)
versionBytes, err := io.ReadAll(io.LimitReader(resp.Body, 500))
if err != nil {
return nil, fmt.Errorf("failed to read latest version: %w", err)
}
-307
View File
@@ -1,307 +0,0 @@
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
@@ -1,499 +0,0 @@
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
@@ -1,92 +0,0 @@
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
@@ -1,124 +0,0 @@
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)
}
})
}
}
@@ -1,158 +0,0 @@
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
@@ -1,314 +0,0 @@
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
@@ -1,606 +0,0 @@
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)
version, err := io.ReadAll(reader) //nolint:gocritic // example code
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)
content, err := io.ReadAll(reader) //nolint:gocritic // example code
if err != nil {
return nil, nil, fmt.Errorf("unable to read apk key content at %s: %w", location.RealPath, err)
}
+50 -52
View File
@@ -1,6 +1,6 @@
module github.com/anchore/syft
go 1.25.6
go 1.25.8
require (
github.com/BurntSushi/toml v1.6.0
@@ -9,7 +9,6 @@ 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
@@ -22,7 +21,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.21
github.com/anchore/stereoscope v0.1.22
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
@@ -45,12 +44,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.1
github.com/google/go-containerregistry v0.21.2
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.4
github.com/hashicorp/go-getter v1.8.5
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/hcl/v2 v2.24.0
github.com/iancoleman/strcase v0.3.0
@@ -62,7 +61,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.3
github.com/olekukonko/tablewriter v1.1.4
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
@@ -87,24 +86,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.33.0
golang.org/x/net v0.51.0
golang.org/x/mod v0.34.0
golang.org/x/net v0.52.0
modernc.org/sqlite v1.46.1
)
require (
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth v0.18.1 // 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.58.0 // indirect
cloud.google.com/go/storage v1.60.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.3.0 // indirect
github.com/ProtonMail/go-crypto v1.4.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
@@ -137,7 +136,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.2.1+incompatible // indirect
github.com/docker/cli v29.3.0+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
@@ -160,8 +159,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.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.17.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
@@ -222,7 +221,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.23.0 // indirect
github.com/sylabs/sif/v2 v2.24.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
@@ -247,20 +246,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.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/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/xerrors v0.0.0-20231012003039-104605ab7028 // 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/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/protobuf v1.36.11 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1
@@ -270,6 +269,7 @@ 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.24.0 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
cyphar.com/go-pathrs v0.2.1 // 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/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/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,46 +291,44 @@ 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.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // 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.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // 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.16 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 // 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/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.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/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/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.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/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/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.53.0 // indirect
github.com/moby/moby/client v0.2.2 // indirect
github.com/moby/moby/api v1.54.0 // indirect
github.com/moby/moby/client v0.3.0 // 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.1.0 // indirect
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect
github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.6 // 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.5.0 // indirect
github.com/zeebo/errs v1.4.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.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
+108 -112
View File
@@ -1,5 +1,5 @@
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
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.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 v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
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.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/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/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.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=
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=
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.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/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/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.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
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/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.21 h1:rIwPDjJQdOdIvMrkNyZFkUS5dSyQPDjAwUqU0XjVF74=
github.com/anchore/stereoscope v0.1.21/go.mod h1:8652axKmO8itOcGX8gzAvHW3cVWvbxpJ4woLNMEO4a0=
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/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.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/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/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.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/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/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.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/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/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.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
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/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,12 +276,10 @@ 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.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/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/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=
@@ -294,8 +292,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-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
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/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=
@@ -345,8 +343,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.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
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/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=
@@ -377,16 +375,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.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 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/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.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
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/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=
@@ -433,8 +431,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.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
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-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=
@@ -515,8 +513,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.1 h1:sOt/o9BS2b87FnR7wxXPvRKU1XVJn2QCwOS5g8zQXlc=
github.com/google/go-containerregistry v0.21.1/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0=
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/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=
@@ -551,14 +549,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.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
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/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.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
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/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=
@@ -578,8 +576,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.4 h1:hGEd2xsuVKgwkMtPVufq73fAmZU/x65PPcqH3cb0D9A=
github.com/hashicorp/go-getter v1.8.4/go.mod h1:x27pPGSg9kzoB147QXI8d/nDvp2IgYGcwuRjpaXE9Yg=
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-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=
@@ -727,10 +725,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.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/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/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=
@@ -766,12 +764,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.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/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/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=
@@ -902,8 +900,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.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
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/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=
@@ -925,8 +923,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.23.0 h1:VWJC7iryINdIgyIK8EdxMCJkx8EDPVlvFaskgY+ruFk=
github.com/sylabs/sif/v2 v2.23.0/go.mod h1:0UJk7MMSQS2n/95yYd+CvAfK7K2mE0aWHRYYybd+ouw=
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/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=
@@ -984,8 +982,6 @@ 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=
@@ -1002,16 +998,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.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
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/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.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/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/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=
@@ -1046,8 +1042,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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
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/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=
@@ -1087,8 +1083,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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
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/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=
@@ -1133,8 +1129,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.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
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/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=
@@ -1152,8 +1148,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.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
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/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=
@@ -1166,8 +1162,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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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/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=
@@ -1242,13 +1238,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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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/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.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
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/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=
@@ -1259,13 +1255,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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
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/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.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
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=
@@ -1321,8 +1317,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.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
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/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=
@@ -1363,8 +1359,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.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
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/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=
@@ -1435,12 +1431,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-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/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/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=
@@ -1468,8 +1464,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.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
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/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)
existingSchemaBytes, err := io.ReadAll(existingFh) //nolint:gocritic // offline code generator
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)
content, err = io.ReadAll(reader) //nolint:gocritic // license scanner requires full content
if err != nil {
return nil, nil, err
}
+4 -2
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, 0600)
err := os.WriteFile(goldenFilePath, contents, 0o600)
if err != nil {
t.Fatalf("could not update golden file (%s): %+v", goldenFilePath, err)
}
@@ -50,7 +50,9 @@ func GetGoldenFileContents(t *testing.T) []byte {
}
defer f.Close()
bytes, err := io.ReadAll(f)
// 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
if err != nil {
t.Fatalf("could not read file (%s): %+v", goldenPath, err)
}
+121
View File
@@ -0,0 +1,121 @@
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
@@ -0,0 +1,295 @@
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))
}
+23 -1
View File
@@ -12,8 +12,10 @@ 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"
@@ -24,6 +26,8 @@ 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()
@@ -65,12 +69,25 @@ func CreateSBOM(ctx context.Context, src source.Source, cfg *CreateSBOMConfig) (
},
}
// setup everything we need in context: license scanner, executors, etc.
// 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.
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()
@@ -95,6 +112,11 @@ 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 inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256)
@(docker image inspect $(TOOL_IMAGE) > /dev/null 2>&1 && 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)
@python ./differ expected_verify $(VERIFY_FILE)
@python3 ./differ expected_verify $(VERIFY_FILE)
debug:
docker run -i --rm -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) bash
@@ -1,10 +1,9 @@
# syntax=docker/dockerfile:1
ARG OSXCROSS_VERSION=13.1
ARG BUILDPLATFORM=linux/amd64
FROM --platform=$BUILDPLATFORM crazymax/osxcross:${OSXCROSS_VERSION}-ubuntu AS osxcross
FROM --platform=linux/amd64 crazymax/osxcross:${OSXCROSS_VERSION}-ubuntu AS osxcross
FROM --platform=$BUILDPLATFORM ubuntu:22.04
FROM --platform=linux/amd64 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 inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256)
@(docker image inspect $(TOOL_IMAGE) > /dev/null 2>&1 && make tools-check) || (docker build --platform linux/amd64 -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)
content, err := io.ReadAll(reader) //nolint:gocritic // buffering non-seekable to seekable 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)
b, err := io.ReadAll(readerCloser) //nolint:gocritic // buffering non-seekable to ReaderAt
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(contentReader)
content, err := io.ReadAll(io.LimitReader(contentReader, 5*1024*1024))
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(rdr)
contents, err := io.ReadAll(io.LimitReader(rdr, 5*1024*1024))
if err != nil {
log.Debugf("error reading %s: %v", location.RealPath, err)
return ""
+8 -6
View File
@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
@@ -15,6 +14,7 @@ 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,17 +24,19 @@ 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(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
func parseGGUFModel(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
defer internal.CloseAndLogError(reader, reader.Path())
// 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")
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")
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)
reposB, err := io.ReadAll(reader) //nolint:gocritic // regex matching requires full buffer
if err != nil {
log.Tracef("unable to read APK repositories file %q: %+v", reader.RealPath, err)
return nil
@@ -61,6 +61,28 @@ 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{
@@ -727,6 +749,16 @@ 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]+)?)\s`)),
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+|-[_0-9a-z]+)?)`)),
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]+)?)\s`)),
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+|-[_0-9a-z]+)?)`)),
),
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\[linux\]`),
`(?m)\x00*(?P<version>[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?)\s(enterprise\s)?\[linux\]`),
Package: "arangodb",
PURL: mustPURL("pkg:generic/arangodb@version"),
CPEs: singleCPE("cpe:2.3:a:arangodb:arangodb:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
@@ -127,23 +127,83 @@ 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)
out, err := cmd.CombinedOutput()
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
}
var inspect []imageInspect
if err := json.Unmarshal(out, &inspect); err != nil {
return false, "", fmt.Errorf("unable to unmarshal image inspect: %w", err)
return "", fmt.Errorf("unable to unmarshal image inspect: %w", err)
}
if len(inspect) != 1 {
return false, "", fmt.Errorf("expected 1 image inspect, got %d", len(inspect))
return "", fmt.Errorf("expected 1 image inspect, got %d", len(inspect))
}
gotPlatform := inspect[0].Platform()
return gotPlatform == platform, gotPlatform, nil
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
}
func copyBinariesFromDockerImages(config config.BinaryFromImage, destination string) (err error) {
+1 -1
View File
@@ -151,4 +151,4 @@ while $CONTINUE_LOOP; do
done
go run ./manager write-snippet "$BINARY_FILE" --offset "$OFFSET" --length "$LENGTH" --name "$GROUP_NAME" --version "$VERSION"
go run ../internal/manager write-snippet "$BINARY_FILE" --offset "$OFFSET" --length "$LENGTH" --name "$GROUP_NAME" --version "$VERSION"
@@ -0,0 +1 @@
go1.15 Fri 2003
@@ -0,0 +1 @@
no version in this binary
@@ -1 +1 @@
go1.15 Fri 2003
go1.15
+14
View File
@@ -458,6 +458,20 @@ 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)
data, err := io.ReadAll(reader) //nolint:gocritic // custom parser requires []byte
if err != nil {
return node(nil), err
}
@@ -3,7 +3,6 @@ package githubactions
import (
"context"
"fmt"
"io"
"go.yaml.in/yaml/v3"
@@ -25,13 +24,9 @@ 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
if errs = yaml.Unmarshal(contents, &ca); errs != nil {
var errs error
if errs = yaml.NewDecoder(reader).Decode(&ca); errs != nil {
return nil, nil, fmt.Errorf("unable to parse yaml composite action file: %w", errs)
}
@@ -3,7 +3,6 @@ package githubactions
import (
"context"
"fmt"
"io"
"regexp"
"go.yaml.in/yaml/v3"
@@ -41,14 +40,10 @@ 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
if errs = yaml.Unmarshal(contents, &node); errs != nil {
var errs error
if errs = yaml.NewDecoder(reader).Decode(&node); errs != nil {
return nil, nil, fmt.Errorf("unable to parse yaml workflow file: %w", errs)
}
@@ -79,14 +74,10 @@ 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
if errs = yaml.Unmarshal(contents, &node); errs != nil {
var errs error
if errs = yaml.NewDecoder(reader).Decode(&node); errs != nil {
return nil, nil, fmt.Errorf("unable to parse yaml workflow file: %w", errs)
}
+52 -12
View File
@@ -23,6 +23,7 @@ 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"
@@ -163,7 +164,10 @@ 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, err := getModule(proxies, moduleName, moduleVersion)
urlPrefix, fsys, cleanup, err := getModule(ctx, proxies, moduleName, moduleVersion)
if cleanup != nil {
defer cleanup()
}
if err != nil {
return nil, err
}
@@ -258,7 +262,7 @@ func processCaps(s string) string {
})
}
func getModule(proxies []string, moduleName, moduleVersion string) (urlPrefix string, fsys fs.FS, err error) {
func getModule(ctx context.Context, proxies []string, moduleName, moduleVersion string) (urlPrefix string, fsys fs.FS, cleanup func(), err error) {
for _, proxy := range proxies {
u, _ := url.Parse(proxy)
if proxy == "direct" {
@@ -267,7 +271,7 @@ func getModule(proxies []string, moduleName, moduleVersion string) (urlPrefix st
}
switch u.Scheme {
case "https", "http":
urlPrefix, fsys, err = getModuleProxy(proxy, moduleName, moduleVersion)
urlPrefix, fsys, cleanup, err = getModuleProxy(ctx, proxy, moduleName, moduleVersion)
case "file":
p := filepath.Join(u.Path, moduleName, "@v", moduleVersion)
urlPrefix = path.Join("file://", p) + "/"
@@ -281,42 +285,78 @@ func getModule(proxies []string, moduleName, moduleVersion string) (urlPrefix st
return
}
func getModuleProxy(proxy string, moduleName string, moduleVersion string) (moduleURL string, out fs.FS, _ error) {
// 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) {
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, err
return "", nil, 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, err
return "", nil, nil, err
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return "", nil, fmt.Errorf("failed to get module zip: %s", resp.Status)
return "", nil, nil, fmt.Errorf("failed to get module zip: %s", resp.Status)
}
}
// read the zip
b, err := io.ReadAll(resp.Body)
// 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
if err != nil {
return "", nil, err
return "", nil, nil, fmt.Errorf("failed to create temp file for module zip: %w", err)
}
out, err = zip.NewReader(bytes.NewReader(b), resp.ContentLength)
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)
}
versionPath := findVersionPath(out, ".")
out = getSubFS(out, versionPath)
return u + "#" + versionPath + "/", out, err
return u + "#" + versionPath + "/", out, cleanup, 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()
ctx := pkgtest.Context(t)
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()
ctx := pkgtest.Context(t)
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)
contents, err := io.ReadAll(reader) //nolint:gocritic // modfile.Parse requires []byte
if err != nil {
return nil, fmt.Errorf("failed to read go module: %w", err)
}
@@ -3,7 +3,6 @@ package haskell
import (
"context"
"fmt"
"io"
"strings"
"go.yaml.in/yaml/v3"
@@ -42,16 +41,11 @@ 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.Unmarshal(bytes, &lockFile); err != nil {
if err := yaml.NewDecoder(reader).Decode(&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")
return nil, nil, fmt.Errorf("failed to parse stack.yaml.lock file: %w", err)
}
var (
@@ -3,7 +3,6 @@ package haskell
import (
"context"
"fmt"
"io"
"go.yaml.in/yaml/v3"
@@ -23,16 +22,11 @@ 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.Unmarshal(bytes, &stackFile); err != nil {
if err := yaml.NewDecoder(reader).Decode(&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")
return nil, nil, fmt.Errorf("failed to parse stack.yaml file: %w", err)
}
var pkgs []pkg.Package
@@ -52,6 +52,12 @@
"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:*:*"
],
@@ -136,6 +142,9 @@
"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:*:*"
],
@@ -2311,6 +2320,9 @@
"@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:*:*"
],
@@ -2350,6 +2362,9 @@
"@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:*:*"
],
@@ -2471,6 +2486,12 @@
"@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:*:*"
],
@@ -3070,6 +3091,9 @@
"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:*:*"
],
@@ -3273,6 +3297,9 @@
"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:*:*"
],
@@ -3319,7 +3346,6 @@
"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": [
@@ -3356,7 +3382,7 @@
"cpe:2.3:a:fibjs_project:fibjs:*:*:*:*:*:node.js:*:*"
],
"file-type": [
"cpe:2.3:a:file-type_project:file-type:*:*:*:*:*:node.js:*:*"
"cpe:2.3:a:sindresorhus:file-type:*:*:*:*:*:node.js:*:*"
],
"file-upload-with-preview": [
"cpe:2.3:a:johndatserakis:file-upload-with-preview:*:*:*:*:*:node.js:*:*"
@@ -5402,6 +5428,9 @@
"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:*:*"
@@ -5468,6 +5497,9 @@
"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:*:*"
],
@@ -5483,6 +5515,9 @@
"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:*:*"
],
@@ -5591,6 +5626,9 @@
"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:*:*"
],
@@ -5972,6 +6010,9 @@
"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:*:*:*:*:*:*:*:*"
],
@@ -6039,6 +6080,9 @@
"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:*:*:*:*:*:*:*:*"
],
@@ -6163,12 +6207,18 @@
"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:*:*:*:*:*:*:*:*"
],
@@ -6994,6 +7044,15 @@
"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:*:*"
],
@@ -9172,6 +9231,9 @@
"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:*:*"
],
@@ -9410,8 +9472,7 @@
"cpe:2.3:a:kieranoshea:calendar:*:*:*:*:*:wordpress:*:*"
],
"calendar-booking": [
"cpe:2.3:a:startbooking:scheduling_plugin:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:startbooking:scheduling_plugin_-_online_booking:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:startbooking:scheduling_plugin:*:*:*:*:*:wordpress:*:*"
],
"calendar-plugin": [
"cpe:2.3:a:calendar_plugin_project:calendar_plugin:*:*:*:*:*:wordpress:*:*"
@@ -10026,6 +10087,9 @@
"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:*:*"
],
@@ -13449,7 +13513,7 @@
"cpe:2.3:a:brijeshk89:ip_based_login:*:*:*:*:*:wordpress:*:*"
],
"ip-blacklist-cloud": [
"cpe:2.3:a:ip_blacklist_cloud_project:ip_blacklist_cloud:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:ad33lx: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)
body, _ := io.ReadAll(httpResp.Body) //nolint:gocritic // offline code generator
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)
body, _ := io.ReadAll(httpResp.Body) //nolint:gocritic // offline code generator
httpResp.Body.Close()
return fmt.Errorf("NVD API error (status %d): %s", httpResp.StatusCode, string(body))
}
@@ -24,6 +24,7 @@ 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"
@@ -62,7 +63,7 @@ type CatalogTester struct {
skipTestObservations bool
}
func Context() context.Context {
func initLicenseScanner() {
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}
@@ -72,13 +73,20 @@ func Context() context.Context {
}
licenseScanner = &scanner
})
}
return licenses.SetContextLicenseScanner(context.Background(), *licenseScanner)
// 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)
}
func NewCatalogTester() *CatalogTester {
return &CatalogTester{
context: Context(),
locationComparer: cmptest.DefaultLocationComparer,
licenseComparer: cmptest.DefaultLicenseComparer,
packageStringer: stringPackage,
@@ -272,9 +280,18 @@ 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()
pkgs, relationships, err := parser(p.context, p.resolver, p.env, p.reader)
ctx := p.ensureContext(t)
pkgs, relationships, err := parser(ctx, p.resolver, p.env, p.reader)
// only test for errors if explicitly requested
if p.wantErr != nil {
@@ -289,10 +306,11 @@ 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(p.context, resolver)
pkgs, relationships, err := cataloger.Catalog(ctx, resolver)
// this is a minimum set, the resolver may return more that just this list
for _, path := range p.expectedPathResponses {
+6 -1
View File
@@ -18,6 +18,7 @@ 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"
@@ -104,7 +105,11 @@ func newJavaArchiveParser(ctx context.Context, reader file.LocationReadCloser, d
virtualElements := strings.Split(reader.Path(), ":")
currentFilepath := virtualElements[len(virtualElements)-1]
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(currentFilepath, reader)
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)
if err != nil {
return nil, cleanupFn, fmt.Errorf("unable to process java archive: %w", err)
}
+13 -14
View File
@@ -2,7 +2,6 @@ package java
import (
"bufio"
"context"
"fmt"
"io"
"os"
@@ -30,7 +29,7 @@ import (
func TestSearchMavenForLicenses(t *testing.T) {
url := maventest.MockRepo(t, "internal/maven/testdata/maven-repo")
ctx := pkgtest.Context()
ctx := pkgtest.Context(t)
tests := []struct {
name string
@@ -72,7 +71,7 @@ func TestSearchMavenForLicenses(t *testing.T) {
require.NoError(t, err)
// setup parser
ap, cleanupFn, err := newJavaArchiveParser(context.Background(),
ap, cleanupFn, err := newJavaArchiveParser(pkgtest.Context(t),
file.LocationReadCloser{
Location: file.NewLocation(fixture.Name()),
ReadCloser: fixture,
@@ -81,17 +80,17 @@ func TestSearchMavenForLicenses(t *testing.T) {
require.NoError(t, err)
// assert licenses are discovered from upstream
_, _, _, parsedPom := ap.discoverMainPackageFromPomInfo(context.Background())
_, _, _, parsedPom := ap.discoverMainPackageFromPomInfo(pkgtest.Context(t))
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(context.Background(), parsedPom.project)
resolvedLicenses, _ := ap.maven.ResolveLicenses(pkgtest.Context(t), parsedPom.project)
assert.Equal(t, tc.expectedLicenses, toPkgLicenses(ctx, nil, resolvedLicenses))
})
}
}
func TestParseJar(t *testing.T) {
ctx := pkgtest.Context()
ctx := pkgtest.Context(t)
tests := []struct {
name string
fixture string
@@ -372,7 +371,7 @@ func TestParseJar(t *testing.T) {
UseNetwork: false,
UseMavenLocalRepository: false,
}
parser, cleanupFn, err := newJavaArchiveParser(context.Background(),
parser, cleanupFn, err := newJavaArchiveParser(pkgtest.Context(t),
file.LocationReadCloser{
Location: file.NewLocation(fixture.Name()),
ReadCloser: fixture,
@@ -662,7 +661,7 @@ func TestParseNestedJar(t *testing.T) {
require.NoError(t, err)
gap := newGenericArchiveParserAdapter(ArchiveCatalogerConfig{})
actual, _, err := gap.processJavaArchive(context.Background(), file.LocationReadCloser{
actual, _, err := gap.processJavaArchive(pkgtest.Context(t), file.LocationReadCloser{
Location: file.NewLocation(fixture.Name()),
ReadCloser: fixture,
}, nil)
@@ -1080,7 +1079,7 @@ func Test_newPackageFromMavenData(t *testing.T) {
test.expectedParent.Locations = locations
r := maven.NewResolver(nil, maven.DefaultConfig())
actualPackage := newPackageFromMavenData(context.Background(), r, test.props, test.project, test.parent, file.NewLocation(virtualPath))
actualPackage := newPackageFromMavenData(pkgtest.Context(t), r, test.props, test.project, test.parent, file.NewLocation(virtualPath))
if test.expectedPackage == nil {
require.Nil(t, actualPackage)
} else {
@@ -1120,7 +1119,7 @@ func Test_artifactIDMatchesFilename(t *testing.T) {
}
func Test_parseJavaArchive_regressions(t *testing.T) {
ctx := context.TODO()
ctx := pkgtest.Context(t)
apiAll := pkg.Package{
Name: "api-all",
Version: "2.0.0",
@@ -1499,7 +1498,7 @@ func Test_deterministicMatchingPomProperties(t *testing.T) {
fixture, err := os.Open(fixturePath)
require.NoError(t, err)
parser, cleanupFn, err := newJavaArchiveParser(context.Background(),
parser, cleanupFn, err := newJavaArchiveParser(pkgtest.Context(t),
file.LocationReadCloser{
Location: file.NewLocation(fixture.Name()),
ReadCloser: fixture,
@@ -1507,7 +1506,7 @@ func Test_deterministicMatchingPomProperties(t *testing.T) {
defer cleanupFn()
require.NoError(t, err)
groupID, artifactID, version, _ := parser.discoverMainPackageFromPomInfo(context.TODO())
groupID, artifactID, version, _ := parser.discoverMainPackageFromPomInfo(pkgtest.Context(t))
require.Equal(t, test.expected, maven.NewID(groupID, artifactID, version))
}()
}
@@ -1634,9 +1633,9 @@ func Test_jarPomPropertyResolutionDoesNotPanic(t *testing.T) {
fixture, err := os.Open(jarName)
require.NoError(t, err)
ctx := context.TODO()
ctx := pkgtest.Context(t)
// setup parser
ap, cleanupFn, err := newJavaArchiveParser(context.Background(),
ap, cleanupFn, err := newJavaArchiveParser(pkgtest.Context(t),
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)
pomContents, err := io.ReadAll(content) //nolint:gocritic // charset detection requires full buffer
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)
contents, err := io.ReadAll(contentReader) //nolint:gocritic // caching requires full buffer
if err != nil {
return nil, err
}
@@ -1,7 +1,6 @@
package java
import (
"context"
"os"
"testing"
@@ -194,7 +193,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 := context.TODO()
ctx := pkgtest.Context(t)
tests := []struct {
name string
project *pkg.JavaPomProject
@@ -265,11 +264,11 @@ func Test_parsePomXMLProject(t *testing.T) {
pom, err := maven.ParsePomXML(fixture)
require.NoError(t, err)
actual := newPomProject(context.Background(), r, fixture.Name(), pom)
actual := newPomProject(pkgtest.Context(t), r, fixture.Name(), pom)
assert.NoError(t, err)
assert.Equal(t, test.project, actual)
licenses, err := r.ResolveLicenses(context.Background(), pom)
licenses, err := r.ResolveLicenses(pkgtest.Context(t), pom)
//assert.NoError(t, err)
assert.Equal(t, test.licenses, toPkgLicenses(ctx, &jarLocation, licenses))
})
@@ -331,7 +330,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(context.Background(), r, &maven.Project{Parent: test.input}))
assert.Equal(t, test.expected, pomParent(pkgtest.Context(t), r, &maven.Project{Parent: test.input}))
})
}
}
@@ -433,7 +432,7 @@ func Test_resolveLicenses(t *testing.T) {
fr, err := ds.FileResolver(source.AllLayersScope)
require.NoError(t, err)
ctx := context.TODO()
ctx := pkgtest.Context(t)
pkgs, _, err := cat.Catalog(ctx, fr)
require.NoError(t, err)
+6 -10
View File
@@ -6,23 +6,16 @@ import (
"os"
"path/filepath"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/tmpdir"
)
func saveArchiveToTmp(archiveVirtualPath string, reader io.Reader) (string, string, func(), error) {
func saveArchiveToTmp(td *tmpdir.TempDir, archiveVirtualPath string, reader io.Reader) (string, string, func(), error) {
name := filepath.Base(archiveVirtualPath)
tempDir, err := os.MkdirTemp("", "syft-archive-contents-")
tempDir, cleanupFn, err := td.NewChild("archive-contents") //nolint:gocritic // cleanup is returned to caller, not deferred here
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")
@@ -37,6 +30,9 @@ func saveArchiveToTmp(archiveVirtualPath string, reader io.Reader) (string, stri
}
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,6 +5,7 @@ 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"
@@ -58,7 +59,11 @@ func newGenericTarWrappedJavaArchiveParser(cfg ArchiveCatalogerConfig) genericTa
}
func (gtp genericTarWrappedJavaArchiveParser) parseTarWrappedJavaArchive(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(reader.Path(), reader)
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)
// note: even on error, we should always run cleanup functions
defer cleanupFn()
if err != nil {
@@ -1,7 +1,6 @@
package java
import (
"context"
"os"
"path"
"testing"
@@ -50,7 +49,7 @@ func Test_parseTarWrappedJavaArchive(t *testing.T) {
}
gtp := newGenericTarWrappedJavaArchiveParser(ArchiveCatalogerConfig{})
actualPkgs, _, err := gtp.parseTarWrappedJavaArchive(context.Background(), nil, nil, file.LocationReadCloser{
actualPkgs, _, err := gtp.parseTarWrappedJavaArchive(pkgtest.Context(t), nil, nil, file.LocationReadCloser{
Location: file.NewLocation(test.fixture),
ReadCloser: fixture,
})
@@ -5,6 +5,7 @@ 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"
@@ -30,7 +31,11 @@ func newGenericZipWrappedJavaArchiveParser(cfg ArchiveCatalogerConfig) genericZi
}
func (gzp genericZipWrappedJavaArchiveParser) parseZipWrappedJavaArchive(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(reader.Path(), reader)
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)
// note: even on error, we should always run cleanup functions
defer cleanupFn()
if err != nil {
@@ -1,7 +1,6 @@
package java
import (
"context"
"os"
"path"
"testing"
@@ -10,6 +9,7 @@ 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(context.Background(), nil, nil, file.LocationReadCloser{
actualPkgs, _, err := gzp.parseZipWrappedJavaArchive(pkgtest.Context(t), nil, nil, file.LocationReadCloser{
Location: file.NewLocation(test.fixture),
ReadCloser: fixture,
})
+2 -16
View File
@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
@@ -257,19 +256,12 @@ 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 := dec.Decode(&license); err != nil {
if err := json.NewDecoder(resp.Body).Decode(&license); err != nil {
return "", fmt.Errorf("unable to parse license from npm registry: %w", err)
}
@@ -324,14 +316,8 @@ 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.Unmarshal(contents, &pkgJSON)
err = json.NewDecoder(contentReader).Decode(&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)
data, err := io.ReadAll(reader) //nolint:gocritic // multi-pass parse requires []byte
if err != nil {
return nil, nil, fmt.Errorf("failed to load pnpm-lock.yaml file: %w", err)
}
@@ -166,7 +166,8 @@ func findDevOnlyPkgs(yarnPkgs []yarnPackage, prodDeps, devDeps map[string]string
}
func parseYarnV1LockFile(reader io.ReadCloser) ([]yarnPackage, error) {
content, err := io.ReadAll(reader)
// 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
if err != nil {
return nil, fmt.Errorf("failed to read yarn.lock file: %w", err)
}
@@ -265,7 +266,8 @@ func (a genericYarnLockAdapter) parseYarnLock(ctx context.Context, resolver file
return nil, nil, nil
}
data, err := io.ReadAll(reader)
// 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
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)
data, err := io.ReadAll(reader) //nolint:gocritic // custom parser requires []byte
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(resolver)
pkgs, rels, err := c.dbParser.catalog(ctx, resolver)
if err != nil {
return nil, nil, fmt.Errorf("failed to catalog nix packages from database: %w", err)
}
+8 -7
View File
@@ -1,8 +1,9 @@
package nix
import (
"bufio"
"context"
"fmt"
"io"
"path"
"strconv"
"strings"
@@ -17,7 +18,7 @@ import (
const defaultSchema = 10
type dbProcessor func(config Config, dbLocation file.Location, resolver file.Resolver, catalogerName string) ([]pkg.Package, []artifact.Relationship, error)
type dbProcessor func(ctx context.Context, config Config, dbLocation file.Location, resolver file.Resolver, catalogerName string) ([]pkg.Package, []artifact.Relationship, error)
type dbCataloger struct {
config Config
@@ -45,7 +46,7 @@ type dbPackageEntry struct {
Files []string
}
func (c dbCataloger) catalog(resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
func (c dbCataloger) catalog(ctx context.Context, 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)
@@ -65,7 +66,7 @@ func (c dbCataloger) catalog(resolver file.Resolver) ([]pkg.Package, []artifact.
continue
}
newPkgs, newRelationships, err := parser(c.config, dbLoc, resolver, c.catalogerName)
newPkgs, newRelationships, err := parser(ctx, c.config, dbLoc, resolver, c.catalogerName)
if err != nil {
errs = unknown.Append(errs, dbLoc.Coordinates, err)
continue
@@ -92,13 +93,13 @@ func (c dbCataloger) selectDBParser(dbLocation file.Location, resolver file.Reso
return c.schemaProcessor[defaultSchema], 0
}
contents, err := io.ReadAll(schemaContents)
if err != nil {
scanner := bufio.NewScanner(schemaContents)
if !scanner.Scan() {
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(string(contents)))
schema, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
if err != nil {
log.WithFields("path", loc.RealPath).Tracef("failed to parse Nix database schema file, assuming %d", defaultSchema)
return c.schemaProcessor[defaultSchema], 0
+22 -11
View File
@@ -1,6 +1,7 @@
package nix
import (
"context"
"database/sql"
"fmt"
"io"
@@ -8,6 +9,7 @@ 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"
@@ -15,26 +17,33 @@ import (
var _ dbProcessor = processV10DB
func processV10DB(config Config, dbLocation file.Location, resolver file.Resolver, catalogerName string) ([]pkg.Package, []artifact.Relationship, error) {
func processV10DB(ctx context.Context, 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)
}
tempDB, err := createTempDB(dbContents)
td := tmpdir.FromContext(ctx)
if td == nil {
return nil, nil, fmt.Errorf("no temp dir factory in context")
}
tempDB, cleanupDB, err := createTempDB(td, dbContents)
if err != nil {
return nil, nil, fmt.Errorf("failed to create temporary database: %w", err)
}
defer os.RemoveAll(tempDB.Name())
// 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()
db, err := sql.Open("sqlite", tempDB.Name())
if err != nil {
return nil, nil, fmt.Errorf("failed to open database: %w", err)
}
db.SetConnMaxLifetime(0)
defer db.Close()
db.SetConnMaxLifetime(0)
packageEntries, err := extractV10DBPackages(config, db, dbLocation, resolver)
if err != nil {
@@ -232,18 +241,20 @@ func finalizeV10DBResults(db *sql.DB, packageEntries map[int]*dbPackageEntry, ca
return pkgs, relationships, nil
}
func createTempDB(content io.ReadCloser) (*os.File, error) {
tempFile, err := os.CreateTemp("", "nix-db-*.sqlite")
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
if err != nil {
return nil, err
return nil, noop, err
}
_, err = io.Copy(tempFile, content)
if err != nil {
tempFile.Close()
os.Remove(tempFile.Name())
return nil, err
cleanup()
return nil, noop, err
}
return tempFile, nil
return tempFile, cleanup, 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)
data, err := io.ReadAll(reader) //nolint:gocritic // regex matching requires full buffer
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)
data, err := io.ReadAll(reader) //nolint:gocritic // phpserialize requires []byte
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(), resolver)
actual, _, err := NewInstalledPackageCataloger().Catalog(pkgtest.Context(t), resolver)
require.NoError(t, err)
if len(actual) != 0 {
+1 -10
View File
@@ -4,10 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/anchore/syft/internal/cache"
@@ -100,13 +98,6 @@ 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 {
@@ -115,7 +106,7 @@ func getLicenseFromPypiRegistry(baseURL, packageName, version string) (string, e
} `json:"info"`
}
if err := dec.Decode(&pypiResponse); err != nil {
if err := json.NewDecoder(resp.Body).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)
contents, err := io.ReadAll(reader) //nolint:gocritic // multi-pass parse requires []byte
if err != nil {
return nil, unknown.New(reader.Location, fmt.Errorf("failed to read uv lock file: %w", err))
}
+1 -7
View File
@@ -4,7 +4,6 @@ import (
"bufio"
"context"
"encoding/json"
"io"
"path"
"path/filepath"
"strings"
@@ -174,13 +173,8 @@ 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.Unmarshal(buffer, &directURLJson); err != nil {
if err := json.NewDecoder(directURLContents).Decode(&directURLJson); err != nil {
return nil, nil, err
}
+8 -13
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,21 +22,16 @@ import (
//
//nolint:funlen
func parseRpmDB(ctx context.Context, resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
f, err := os.CreateTemp("", "rpmdb")
td := tmpdir.FromContext(ctx)
if td == nil {
return nil, nil, fmt.Errorf("no temp dir factory in context")
}
f, cleanup, err := td.NewFile("rpmdb-*")
if err != nil {
return nil, nil, fmt.Errorf("failed to create temp rpmdb file: %w", err)
}
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)
}
}()
defer cleanup()
defer f.Close()
_, 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)
_, err := io.Copy(&buff, reader) //nolint:gocritic // buffering to ReadSeeker
return bytes.NewReader(buff.Bytes()), err
}
@@ -3,7 +3,6 @@ package swift
import (
"context"
"fmt"
"io"
"strings"
"go.yaml.in/yaml/v3"
@@ -28,12 +27,8 @@ 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.Unmarshal(bytes, &podfile); err != nil {
if err := yaml.NewDecoder(reader).Decode(&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)
data, err := io.ReadAll(reader) //nolint:gocritic // regex matching requires full buffer
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)
contents, err := io.ReadAll(reader) //nolint:gocritic // hclsimple.Decode requires []byte
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)
bytes, err := io.ReadAll(r) //nolint:gocritic // reading license content for storage
if err != nil {
return "", err
}
+2 -13
View File
@@ -3,7 +3,6 @@ package snapsource
import (
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strconv"
@@ -215,13 +214,8 @@ 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.Unmarshal(body, &info); err != nil {
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return "", fmt.Errorf("failed to parse JSON response: %w", err)
}
@@ -259,13 +253,8 @@ 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.Unmarshal(body, &findResp); err != nil {
if err := json.NewDecoder(resp.Body).Decode(&findResp); err != nil {
return false, "", fmt.Errorf("failed to parse find JSON response: %w", err)
}
+44
View File
@@ -24,6 +24,50 @@ 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")