mirror of
https://github.com/anchore/syft.git
synced 2026-10-14 15:17:18 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c93394bc2 | ||
|
|
3e4e82f03e | ||
|
|
793b0a346f | ||
|
|
a0dac519db | ||
|
|
34f5e521c1 | ||
|
|
774b1e97b9 | ||
|
|
538430d65d | ||
|
|
5db3a9bf55 | ||
|
|
efc2f0012c | ||
|
|
c5c1454848 | ||
|
|
f5c765192c | ||
|
|
728feea620 | ||
|
|
45fb52dca1 | ||
|
|
45bf8b14ab | ||
|
|
9478cd974b | ||
|
|
0d9ea69a66 | ||
|
|
bee78c0b16 | ||
|
|
88bbcbe9c6 | ||
|
|
e0680eb704 | ||
|
|
16f851c5d9 | ||
|
|
d5ca1ad543 |
+1
-1
@@ -58,7 +58,7 @@ tools:
|
||||
# used to release all artifacts
|
||||
- name: goreleaser
|
||||
version:
|
||||
want: v2.12.6
|
||||
want: v2.12.7
|
||||
method: github-release
|
||||
with:
|
||||
repo: goreleaser/goreleaser
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 #v3.29.5
|
||||
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee #v3.29.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -58,7 +58,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@16140ae1a102900babc80a33c44059580f687047 #v3.29.5
|
||||
uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee #v3.29.5
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -72,4 +72,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 #v3.29.5
|
||||
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee #v3.29.5
|
||||
|
||||
@@ -19,6 +19,16 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Validate Apple notarization credentials
|
||||
run: .tool/quill submission list
|
||||
env:
|
||||
QUILL_NOTARY_ISSUER: ${{ secrets.APPLE_NOTARY_ISSUER }}
|
||||
QUILL_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
|
||||
QUILL_NOTARY_KEY: ${{ secrets.APPLE_NOTARY_KEY }}
|
||||
|
||||
- name: Check if running on main
|
||||
if: github.ref != 'refs/heads/main'
|
||||
# we are using the following flag when running `cosign blob-verify` for checksum signature verification:
|
||||
@@ -161,7 +171,7 @@ jobs:
|
||||
# for updating brew formula in anchore/homebrew-syft
|
||||
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
|
||||
|
||||
- uses: anchore/sbom-action@aa0e114b2e19480f157109b9922bda359bd98b90 #v0.20.8
|
||||
- uses: anchore/sbom-action@8e94d75ddd33f69f691467e42275782e4bfefe84 #v0.20.9
|
||||
continue-on-error: true
|
||||
with:
|
||||
file: go.mod
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
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@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8
|
||||
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 }}
|
||||
|
||||
- uses: 8398a7/action-slack@77eaa4f1c608a7d68b38af4e3f739dcd8cba273e #v3.19.0
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
fields: workflow,eventName,job
|
||||
text: Syft SPDX license list update failed
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
||||
if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }}
|
||||
@@ -116,7 +116,7 @@ Where the `formats` available are:
|
||||
- `spdx-json@2.2`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
|
||||
- `github-json`: A JSON report conforming to GitHub's dependency snapshot format.
|
||||
- `syft-table`: A columnar summary (default).
|
||||
- `template`: Lets the user specify the output format. See ["Using templates"](#using-templates) below.
|
||||
- `template`: Lets the user specify the output format. See ["Using templates"](https://github.com/anchore/syft/wiki/using-templates) below.
|
||||
|
||||
Note that flags using the @<version> can be used for earlier versions of each specification as well.
|
||||
|
||||
@@ -135,7 +135,7 @@ Note that flags using the @<version> can be used for earlier versions of each sp
|
||||
- Go (go.mod, Go binaries)
|
||||
- GitHub (workflows, actions)
|
||||
- Haskell (cabal, stack)
|
||||
- Java (jar, ear, war, par, sar, nar, native-image)
|
||||
- Java (jar, ear, war, par, sar, nar, rar, native-image)
|
||||
- JavaScript (npm, yarn)
|
||||
- Jenkins Plugins (jpi, hpi)
|
||||
- Linux kernel archives (vmlinz)
|
||||
|
||||
@@ -253,7 +253,6 @@ func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opt
|
||||
}
|
||||
|
||||
src, err := getSource(ctx, opts, userInput, stereoscope.RegistryTag)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -185,7 +185,6 @@ func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, use
|
||||
}
|
||||
|
||||
src, err := getSource(ctx, &opts.Catalog, userInput, sources...)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ func BenchmarkImagePackageCatalogers(b *testing.B) {
|
||||
// get the source object for the image
|
||||
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
|
||||
require.NoError(b, err)
|
||||
|
||||
b.Cleanup(func() {
|
||||
require.NoError(b, theSource.Close())
|
||||
})
|
||||
|
||||
@@ -38,11 +38,11 @@ func catalogFixtureImageWithConfig(t *testing.T, fixtureImageName string, cfg *s
|
||||
// get the source to build an SBOM against
|
||||
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, theSource.Close())
|
||||
})
|
||||
|
||||
// build the SBOM
|
||||
s, err := syft.CreateSBOM(context.Background(), theSource, cfg)
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -66,7 +66,7 @@ func catalogDirectory(t *testing.T, dir string, catalogerSelection ...string) (s
|
||||
func catalogDirectoryWithConfig(t *testing.T, dir string, cfg *syft.CreateSBOMConfig) (sbom.SBOM, source.Source) {
|
||||
cfg.CatalogerSelection = cfg.CatalogerSelection.WithDefaults(pkgcataloging.DirectoryTag)
|
||||
|
||||
// get the source to build an sbom against
|
||||
// get the source to build an SBOM against
|
||||
theSource, err := syft.GetSource(context.Background(), dir, syft.DefaultGetSourceConfig().WithSources("dir"))
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
|
||||
@@ -23,6 +23,7 @@ const defaultImage = "alpine:3.19"
|
||||
func main() {
|
||||
// automagically get a source.Source for arbitrary string input
|
||||
src := getSource(imageReference())
|
||||
defer src.Close()
|
||||
|
||||
// will catalog the given source and return a SBOM keeping in mind several configurable options
|
||||
sbom := getSBOM(src)
|
||||
@@ -46,7 +47,6 @@ func getSource(input string) source.Source {
|
||||
fmt.Println("detecting source type for input:", input, "...")
|
||||
|
||||
src, err := syft.GetSource(context.Background(), input, nil)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const defaultImage = "alpine:3.19"
|
||||
func main() {
|
||||
// automagically get a source.Source for arbitrary string input
|
||||
src := getSource(imageReference())
|
||||
defer src.Close()
|
||||
|
||||
// catalog the given source and return a SBOM
|
||||
sbom := getSBOM(src)
|
||||
@@ -40,7 +41,6 @@ func imageReference() string {
|
||||
|
||||
func getSource(input string) source.Source {
|
||||
src, err := syft.GetSource(context.Background(), input, nil)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const defaultImage = "alpine:3.19"
|
||||
func main() {
|
||||
// automagically get a source.Source for arbitrary string input
|
||||
src := getSource(imageReference())
|
||||
defer src.Close()
|
||||
|
||||
// catalog the given source and return a SBOM
|
||||
// let's explicitly use catalogers that are:
|
||||
@@ -44,7 +45,6 @@ func imageReference() string {
|
||||
|
||||
func getSource(input string) source.Source {
|
||||
src, err := syft.GetSource(context.Background(), input, nil)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ func main() {
|
||||
image := "alpine:3.19"
|
||||
|
||||
src, _ := syft.GetSource(context.Background(), image, syft.DefaultGetSourceConfig().WithSources("registry"))
|
||||
defer src.Close()
|
||||
|
||||
sbom, _ := syft.CreateSBOM(context.Background(), src, syft.DefaultCreateSBOMConfig())
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ require (
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||
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.11
|
||||
github.com/anchore/stereoscope v0.1.12
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/aquasecurity/go-pep440-version v0.0.1
|
||||
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
|
||||
@@ -53,12 +53,12 @@ require (
|
||||
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.2
|
||||
github.com/hashicorp/go-getter v1.8.3
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/hcl/v2 v2.24.0
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
github.com/invopop/jsonschema v0.7.0
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.8
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.9
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
|
||||
github.com/magiconair/properties v1.8.10
|
||||
@@ -92,7 +92,7 @@ require (
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
golang.org/x/mod v0.29.0
|
||||
golang.org/x/net v0.46.0
|
||||
modernc.org/sqlite v1.39.1
|
||||
modernc.org/sqlite v1.40.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -138,8 +138,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.11 h1:YP/XUNcJyMbOOPAWPkeZNCVlKKTRO2cnBTEeUW6I40Y=
|
||||
github.com/anchore/stereoscope v0.1.11/go.mod h1:G3PZlzPbxFhylj9pQwtqfVPaahuWmy/UCtv5FTIIMvg=
|
||||
github.com/anchore/stereoscope v0.1.12 h1:4T/10G7Nb98UoJBKVvAIhsAtrR63lZXxMJb/Qfw5inw=
|
||||
github.com/anchore/stereoscope v0.1.12/go.mod h1:G3PZlzPbxFhylj9pQwtqfVPaahuWmy/UCtv5FTIIMvg=
|
||||
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=
|
||||
@@ -556,8 +556,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.2 h1:CGCK+bZQLl44PYiwJweVzfpjg7bBwtuXu3AGcLiod2o=
|
||||
github.com/hashicorp/go-getter v1.8.2/go.mod h1:CUTt9x2bCtJ/sV8ihgrITL3IUE+0BE1j/e4n5P/GIM4=
|
||||
github.com/hashicorp/go-getter v1.8.3 h1:gIS+oTNv3kyYAvlUVgMR46MiG0bM0KuSON/KZEvRoRg=
|
||||
github.com/hashicorp/go-getter v1.8.3/go.mod h1:CUTt9x2bCtJ/sV8ihgrITL3IUE+0BE1j/e4n5P/GIM4=
|
||||
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=
|
||||
@@ -609,8 +609,8 @@ github.com/invopop/jsonschema v0.7.0 h1:2vgQcBz1n256N+FpX3Jq7Y17AjYt46Ig3zIWyy77
|
||||
github.com/invopop/jsonschema v0.7.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc=
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.9 h1:PQecJLK3L8ODuVyMe2223b61oRJjrKnmXAncbWTv9MY=
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.9/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
@@ -1509,8 +1509,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
|
||||
modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||
modernc.org/sqlite v1.40.0 h1:bNWEDlYhNPAUdUdBzjAvn8icAs/2gaKlj4vM+tQ6KdQ=
|
||||
modernc.org/sqlite v1.40.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package pkgtestobservation
|
||||
|
||||
import "time"
|
||||
|
||||
// Observations represents capability observations during testing
|
||||
type Observations struct {
|
||||
License bool `json:"license"`
|
||||
Relationships Relationship `json:"relationships"`
|
||||
FileListing Count `json:"file_listing"`
|
||||
FileDigests Count `json:"file_digests"`
|
||||
IntegrityHash Count `json:"integrity_hash"`
|
||||
}
|
||||
|
||||
// Relationship tracks dependency relationship observations
|
||||
type Relationship struct {
|
||||
Found bool `json:"found"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// Count tracks whether a capability was found and how many times
|
||||
type Count struct {
|
||||
Found bool `json:"found"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// Test is the root structure for test-observations.json
|
||||
type Test struct {
|
||||
Package string `json:"package"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Catalogers map[string]*Cataloger `json:"catalogers"`
|
||||
Parsers map[string]*Parser `json:"parsers"`
|
||||
}
|
||||
|
||||
// Parser captures all observations for a parser
|
||||
type Parser struct {
|
||||
MetadataTypes []string `json:"metadata_types"`
|
||||
PackageTypes []string `json:"package_types"`
|
||||
Observations Observations `json:"observations"`
|
||||
}
|
||||
|
||||
// Cataloger captures all observations for a cataloger
|
||||
type Cataloger struct {
|
||||
MetadataTypes []string `json:"metadata_types"`
|
||||
PackageTypes []string `json:"package_types"`
|
||||
Observations Observations `json:"observations"`
|
||||
}
|
||||
@@ -81,6 +81,10 @@ func Test_EnvironmentTask(t *testing.T) {
|
||||
// get the source
|
||||
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, theSource.Close())
|
||||
})
|
||||
|
||||
resolver, err := theSource.FileResolver(source.SquashedScope)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This program generates license_list.go.
|
||||
@@ -20,8 +19,7 @@ const (
|
||||
)
|
||||
|
||||
var tmp = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at {{ .Timestamp }}
|
||||
// using data from {{ .URL }}
|
||||
// This file was generated using data from {{ .URL }}
|
||||
package spdxlicense
|
||||
|
||||
const Version = {{ printf "%q" .Version }}
|
||||
@@ -78,13 +76,11 @@ func run() error {
|
||||
urlToLicense := buildURLToLicenseMap(result)
|
||||
|
||||
err = tmp.Execute(f, struct {
|
||||
Timestamp time.Time
|
||||
URL string
|
||||
Version string
|
||||
LicenseIDs map[string]string
|
||||
URLToLicense map[string]string
|
||||
}{
|
||||
Timestamp: time.Now(),
|
||||
URL: url,
|
||||
Version: result.Version,
|
||||
LicenseIDs: licenseIDs,
|
||||
|
||||
@@ -87,6 +87,9 @@ func toGithubManifests(s *sbom.SBOM) Manifests {
|
||||
}
|
||||
|
||||
name := dependencyName(p)
|
||||
if name == "" || p.PURL == "" {
|
||||
continue
|
||||
}
|
||||
manifest.Resolved[name] = DependencyNode{
|
||||
PackageURL: p.PURL,
|
||||
Metadata: toDependencyMetadata(p),
|
||||
|
||||
@@ -16,11 +16,6 @@
|
||||
"source_location": "redacted/some/path/some/path/pkg1"
|
||||
},
|
||||
"resolved": {
|
||||
"": {
|
||||
"package_url": "a-purl-2",
|
||||
"relationship": "direct",
|
||||
"scope": "runtime"
|
||||
},
|
||||
"pkg:deb/debian/package-2@2.0.1": {
|
||||
"package_url": "pkg:deb/debian/package-2@2.0.1",
|
||||
"relationship": "direct",
|
||||
|
||||
@@ -17,13 +17,6 @@
|
||||
},
|
||||
"metadata": {
|
||||
"syft:filesystem":"redacted"
|
||||
},
|
||||
"resolved": {
|
||||
"": {
|
||||
"package_url": "a-purl-1",
|
||||
"relationship": "direct",
|
||||
"scope": "runtime"
|
||||
}
|
||||
}
|
||||
},
|
||||
"user-image-input:/somefile-2.txt": {
|
||||
|
||||
@@ -29,6 +29,7 @@ func Backfill(p *pkg.Package) {
|
||||
|
||||
var cpes []cpe.CPE
|
||||
epoch := ""
|
||||
rpmmod := ""
|
||||
|
||||
for _, qualifier := range purl.Qualifiers {
|
||||
switch qualifier.Key {
|
||||
@@ -44,6 +45,8 @@ func Backfill(p *pkg.Package) {
|
||||
}
|
||||
case pkg.PURLQualifierEpoch:
|
||||
epoch = qualifier.Value
|
||||
case pkg.PURLQualifierRpmModularity:
|
||||
rpmmod = qualifier.Value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +66,10 @@ func Backfill(p *pkg.Package) {
|
||||
setJavaMetadataFromPurl(p, purl)
|
||||
}
|
||||
|
||||
if p.Type == pkg.RpmPkg {
|
||||
setRpmMetadataFromPurl(p, rpmmod)
|
||||
}
|
||||
|
||||
for _, c := range cpes {
|
||||
if slices.Contains(p.CPEs, c) {
|
||||
continue
|
||||
@@ -82,6 +89,35 @@ func setJavaMetadataFromPurl(p *pkg.Package, _ packageurl.PackageURL) {
|
||||
}
|
||||
}
|
||||
|
||||
func setRpmMetadataFromPurl(p *pkg.Package, rpmmod string) {
|
||||
if p.Type != pkg.RpmPkg {
|
||||
return
|
||||
}
|
||||
if rpmmod == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if p.Metadata == nil {
|
||||
p.Metadata = pkg.RpmDBEntry{
|
||||
ModularityLabel: &rpmmod,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch m := p.Metadata.(type) {
|
||||
case pkg.RpmDBEntry:
|
||||
if m.ModularityLabel == nil {
|
||||
m.ModularityLabel = &rpmmod
|
||||
p.Metadata = m
|
||||
}
|
||||
case pkg.RpmArchive:
|
||||
if m.ModularityLabel == nil {
|
||||
m.ModularityLabel = &rpmmod
|
||||
p.Metadata = m
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setVersionFromPurl(p *pkg.Package, purl packageurl.PackageURL, epoch string) {
|
||||
if p.Version == "" {
|
||||
p.Version = purl.Version
|
||||
|
||||
@@ -53,6 +53,21 @@ func Test_Backfill(t *testing.T) {
|
||||
Version: "1:1.12.8-26.el8",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rpm with rpmmod",
|
||||
in: pkg.Package{
|
||||
PURL: "pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7&rpmmod=httpd:2.4",
|
||||
},
|
||||
expected: pkg.Package{
|
||||
PURL: "pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7&rpmmod=httpd:2.4",
|
||||
Type: pkg.RpmPkg,
|
||||
Name: "httpd",
|
||||
Version: "2.4.37-51",
|
||||
Metadata: pkg.RpmDBEntry{
|
||||
ModularityLabel: strRef("httpd:2.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad cpe",
|
||||
in: pkg.Package{
|
||||
@@ -171,3 +186,7 @@ func Test_nameFromPurl(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func strRef(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
@@ -19,16 +19,16 @@ type Directory struct {
|
||||
indexer *directoryIndexer
|
||||
}
|
||||
|
||||
func NewFromDirectory(root string, base string, pathFilters ...PathIndexVisitor) (*Directory, error) {
|
||||
r, err := newFromDirectoryWithoutIndex(root, base, pathFilters...)
|
||||
func NewFromDirectory(root, base string, pathFilters ...PathIndexVisitor) (*Directory, error) {
|
||||
resolver, err := newFromDirectoryWithoutIndex(root, base, pathFilters...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, r.buildIndex()
|
||||
return resolver, resolver.buildIndex()
|
||||
}
|
||||
|
||||
func newFromDirectoryWithoutIndex(root string, base string, pathFilters ...PathIndexVisitor) (*Directory, error) {
|
||||
func newFromDirectoryWithoutIndex(root, base string, pathFilters ...PathIndexVisitor) (*Directory, error) {
|
||||
chroot, err := NewChrootContextFromCWD(root, base)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to interpret chroot context: %w", err)
|
||||
@@ -66,6 +66,6 @@ func (r *Directory) buildIndex() error {
|
||||
}
|
||||
|
||||
// Stringer to represent a directory path data source
|
||||
func (r Directory) String() string {
|
||||
func (r *Directory) String() string {
|
||||
return fmt.Sprintf("dir:%s", r.path)
|
||||
}
|
||||
|
||||
@@ -17,17 +17,31 @@ type File struct {
|
||||
indexer *fileIndexer
|
||||
}
|
||||
|
||||
// parent should be the symlink free absolute path to the parent directory
|
||||
// NewFromFile single file analyser
|
||||
// path is the filepath of the file we're creating content access for
|
||||
func NewFromFile(parent, path string, pathFilters ...PathIndexVisitor) (*File, error) {
|
||||
chroot, err := NewChrootContextFromCWD(parent, parent)
|
||||
func NewFromFile(path string, pathFilters ...PathIndexVisitor) (*File, error) {
|
||||
resolver, err := newFromFileWithoutIndex(path, pathFilters...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resolver, resolver.buildIndex()
|
||||
}
|
||||
|
||||
func newFromFileWithoutIndex(path string, pathFilters ...PathIndexVisitor) (*File, error) {
|
||||
absParentDir, err := absoluteSymlinkFreePathToParent(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chroot, err := NewChrootContextFromCWD(absParentDir, absParentDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to interpret chroot context: %w", err)
|
||||
}
|
||||
|
||||
cleanBase := chroot.Base()
|
||||
|
||||
file := &File{
|
||||
return &File{
|
||||
path: path,
|
||||
FiletreeResolver: FiletreeResolver{
|
||||
Chroot: *chroot,
|
||||
@@ -36,9 +50,7 @@ func NewFromFile(parent, path string, pathFilters ...PathIndexVisitor) (*File, e
|
||||
Opener: nativeOSFileOpener,
|
||||
},
|
||||
indexer: newFileIndexer(path, cleanBase, pathFilters...),
|
||||
}
|
||||
|
||||
return file, file.buildIndex()
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *File) buildIndex() error {
|
||||
@@ -58,6 +70,6 @@ func (r *File) buildIndex() error {
|
||||
}
|
||||
|
||||
// Stringer to represent a file path data source
|
||||
func (r File) String() string {
|
||||
func (r *File) String() string {
|
||||
return fmt.Sprintf("file:%s", r.path)
|
||||
}
|
||||
|
||||
@@ -1384,9 +1384,10 @@ func TestFileResolver_FilesByPath(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, parentPath)
|
||||
|
||||
resolver, err := NewFromFile(parentPath, tt.filePath)
|
||||
resolver, err := NewFromFile(tt.filePath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resolver)
|
||||
assert.Equal(t, resolver.Chroot.Base(), parentPath)
|
||||
|
||||
refs, err := resolver.FilesByPath(tt.fileByPathInput)
|
||||
require.NoError(t, err)
|
||||
@@ -1431,8 +1432,11 @@ func TestFileResolver_MultipleFilesByPath(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, parentPath)
|
||||
|
||||
resolver, err := NewFromFile(parentPath, filePath)
|
||||
resolver, err := NewFromFile(filePath)
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, resolver)
|
||||
assert.Equal(t, resolver.Chroot.Base(), parentPath)
|
||||
|
||||
refs, err := resolver.FilesByPath(tt.input...)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -1449,8 +1453,11 @@ func TestFileResolver_FilesByGlob(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, parentPath)
|
||||
|
||||
resolver, err := NewFromFile(parentPath, filePath)
|
||||
resolver, err := NewFromFile(filePath)
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, resolver)
|
||||
assert.Equal(t, resolver.Chroot.Base(), parentPath)
|
||||
|
||||
refs, err := resolver.FilesByGlob("**/*.txt")
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -1476,8 +1483,11 @@ func Test_fileResolver_FilesByMIMEType(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, parentPath)
|
||||
|
||||
resolver, err := NewFromFile(parentPath, filePath)
|
||||
resolver, err := NewFromFile(filePath)
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, resolver)
|
||||
assert.Equal(t, resolver.Chroot.Base(), parentPath)
|
||||
|
||||
locations, err := resolver.FilesByMIMEType(test.mimeType)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.expectedPaths.Size(), len(locations))
|
||||
@@ -1497,10 +1507,12 @@ func Test_fileResolver_FileContentsByLocation(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, parentPath)
|
||||
|
||||
r, err := NewFromFile(parentPath, filePath)
|
||||
resolver, err := NewFromFile(filePath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resolver)
|
||||
assert.Equal(t, resolver.Chroot.Base(), parentPath)
|
||||
|
||||
exists, existingPath, err := r.Tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")))
|
||||
exists, existingPath, err := resolver.Tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")))
|
||||
require.True(t, exists)
|
||||
require.NoError(t, err)
|
||||
require.True(t, existingPath.HasReference())
|
||||
@@ -1525,7 +1537,7 @@ func Test_fileResolver_FileContentsByLocation(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
actual, err := r.FileContentsByLocation(test.location)
|
||||
actual, err := resolver.FileContentsByLocation(test.location)
|
||||
if test.err {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -1546,8 +1558,11 @@ func TestFileResolver_AllLocations_errorOnDirRequest(t *testing.T) {
|
||||
parentPath, err := absoluteSymlinkFreePathToParent(filePath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, parentPath)
|
||||
resolver, err := NewFromFile(parentPath, filePath)
|
||||
|
||||
resolver, err := NewFromFile(filePath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resolver)
|
||||
assert.Equal(t, resolver.Chroot.Base(), parentPath)
|
||||
|
||||
var dirLoc *file.Location
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -1575,8 +1590,11 @@ func TestFileResolver_AllLocations(t *testing.T) {
|
||||
parentPath, err := absoluteSymlinkFreePathToParent(filePath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, parentPath)
|
||||
resolver, err := NewFromFile(parentPath, filePath)
|
||||
|
||||
resolver, err := NewFromFile(filePath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resolver)
|
||||
assert.Equal(t, resolver.Chroot.Base(), parentPath)
|
||||
|
||||
paths := strset.New()
|
||||
for loc := range resolver.AllLocations(context.Background()) {
|
||||
@@ -1600,8 +1618,11 @@ func Test_FileResolver_AllLocationsDoesNotLeakGoRoutine(t *testing.T) {
|
||||
parentPath, err := absoluteSymlinkFreePathToParent(filePath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, parentPath)
|
||||
resolver, err := NewFromFile(parentPath, filePath)
|
||||
|
||||
resolver, err := NewFromFile(filePath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resolver)
|
||||
assert.Equal(t, resolver.Chroot.Base(), parentPath)
|
||||
|
||||
require.NoError(t, err)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
)
|
||||
|
||||
func TestFileMetadataFromPath(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# these are generated by pkgtest helpers, no need to check them in
|
||||
**/test-fixtures/test-observations.json
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
packageurl "github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
@@ -32,6 +33,19 @@ func newPEPackage(versionResources map[string]string, f file.Location) pkg.Packa
|
||||
Metadata: newPEBinaryVersionResourcesFromMap(versionResources),
|
||||
}
|
||||
|
||||
// If this appears to be Ghostscript, emit a canonical generic purl
|
||||
// Example expected: pkg:generic/ghostscript@<version>
|
||||
prod := strings.ToLower(spaceNormalize(versionResources["ProductName"]))
|
||||
if prod == "" {
|
||||
// fall back to FileDescription if ProductName is missing
|
||||
prod = strings.ToLower(spaceNormalize(versionResources["FileDescription"]))
|
||||
}
|
||||
if p.Version != "" && strings.Contains(prod, "ghostscript") {
|
||||
// build a generic PURL for ghostscript
|
||||
purl := packageurl.NewPackageURL(packageurl.TypeGeneric, "", "ghostscript", p.Version, nil, "").ToString()
|
||||
p.PURL = purl
|
||||
}
|
||||
|
||||
p.SetID()
|
||||
|
||||
return p
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
func TestGhostscriptPEGeneratesGenericPURL(t *testing.T) {
|
||||
vr := map[string]string{
|
||||
"CompanyName": "Artifex Software, Inc.",
|
||||
"ProductName": "GPL Ghostscript",
|
||||
"FileDescription": "Ghostscript Interpreter",
|
||||
"ProductVersion": "9.54.0",
|
||||
}
|
||||
|
||||
loc := file.NewLocation("/usr/bin/gswin64c.exe")
|
||||
p := newPEPackage(vr, loc)
|
||||
|
||||
expected := "pkg:generic/ghostscript@9.54.0"
|
||||
if p.PURL != expected {
|
||||
t.Fatalf("expected purl %q, got %q", expected, p.PURL)
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,10 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
)
|
||||
|
||||
const (
|
||||
deinstallStatus string = "deinstall"
|
||||
)
|
||||
|
||||
var (
|
||||
errEndOfPackages = fmt.Errorf("no more packages to read")
|
||||
sourceRegexp = regexp.MustCompile(`(?P<name>\S+)( \((?P<version>.*)\))?`)
|
||||
@@ -112,6 +116,7 @@ type dpkgExtractedMetadata struct {
|
||||
Provides string `mapstructure:"Provides"`
|
||||
Depends string `mapstructure:"Depends"`
|
||||
PreDepends string `mapstructure:"PreDepends"` // note: original doc is Pre-Depends
|
||||
Status string `mapstructure:"Status"`
|
||||
}
|
||||
|
||||
// parseDpkgStatusEntry returns an individual Dpkg entry, or returns errEndOfPackages if there are no more packages to parse from the reader.
|
||||
@@ -134,6 +139,11 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (*pkg.DpkgDBEntry, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Skip entries which have been removed but not purged, e.g. "rc" status in dpkg -l
|
||||
if strings.Contains(raw.Status, deinstallStatus) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sourceName, sourceVersion := extractSourceVersion(raw.Source)
|
||||
if sourceVersion != "" {
|
||||
raw.SourceVersion = sourceVersion
|
||||
|
||||
@@ -237,6 +237,37 @@ func Test_parseDpkgStatus(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deinstall status packages are ignored",
|
||||
fixturePath: "test-fixtures/var/lib/dpkg/status.d/deinstall",
|
||||
expected: []pkg.DpkgDBEntry{
|
||||
{
|
||||
Package: "linux-image-6.14.0-1012-aws",
|
||||
Source: "linux-signed-aws-6.14",
|
||||
Version: "6.14.0-1012.12~24.04.1",
|
||||
Architecture: "amd64",
|
||||
InstalledSize: 15221,
|
||||
Maintainer: "Canonical Kernel Team <kernel-team@lists.ubuntu.com>",
|
||||
Description: `Signed kernel image aws
|
||||
A kernel image for aws. This version of it is signed with
|
||||
Canonical's signing key.`,
|
||||
Provides: []string{"fuse-module",
|
||||
"linux-image",
|
||||
"spl-dkms",
|
||||
"spl-modules",
|
||||
"v4l2loopback-dkms",
|
||||
"v4l2loopback-modules",
|
||||
"zfs-dkms",
|
||||
"zfs-modules"},
|
||||
Depends: []string{
|
||||
"kmod",
|
||||
"linux-base (>= 4.5ubuntu1~16.04.1)",
|
||||
"linux-modules-6.14.0-1012-aws",
|
||||
},
|
||||
Files: []pkg.DpkgFileRecord{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
Package: linux-image-6.14.0-1012-aws
|
||||
Status: install ok installed
|
||||
Priority: optional
|
||||
Section: kernel
|
||||
Installed-Size: 15221
|
||||
Maintainer: Canonical Kernel Team <kernel-team@lists.ubuntu.com>
|
||||
Architecture: amd64
|
||||
Source: linux-signed-aws-6.14
|
||||
Version: 6.14.0-1012.12~24.04.1
|
||||
Provides: fuse-module, linux-image, spl-dkms, spl-modules, v4l2loopback-dkms, v4l2loopback-modules, zfs-dkms, zfs-modules
|
||||
Depends: kmod, linux-base (>= 4.5ubuntu1~16.04.1), linux-modules-6.14.0-1012-aws
|
||||
Recommends: grub-pc | grub-efi-amd64 | grub-efi-ia32 | grub | lilo, initramfs-tools | linux-initramfs-tool
|
||||
Suggests: bpftool, linux-perf, linux-aws-6.14-doc-6.14.0 | linux-aws-6.14-source-6.14.0, linux-aws-6.14-tools, linux-headers-6.14.0-1012-aws
|
||||
Conflicts: linux-image-unsigned-6.14.0-1012-aws
|
||||
Description: Signed kernel image aws
|
||||
A kernel image for aws. This version of it is signed with
|
||||
Canonical's signing key.
|
||||
Built-Using: linux-aws-6.14 (= 6.14.0-1012.12~24.04.1)
|
||||
|
||||
Package: linux-image-6.8.0-1029-aws
|
||||
Status: deinstall ok config-files
|
||||
Priority: optional
|
||||
Section: kernel
|
||||
Installed-Size: 14591
|
||||
Maintainer: Canonical Kernel Team <kernel-team@lists.ubuntu.com>
|
||||
Architecture: amd64
|
||||
Source: linux-signed-aws
|
||||
Version: 6.8.0-1029.31
|
||||
Config-Version: 6.8.0-1029.31
|
||||
Provides: fuse-module, linux-image, spl-dkms, spl-modules, v4l2loopback-dkms, v4l2loopback-modules, zfs-dkms, zfs-modules
|
||||
Depends: kmod, linux-base (>= 4.5ubuntu1~16.04.1), linux-modules-6.8.0-1029-aws
|
||||
Recommends: grub-pc | grub-efi-amd64 | grub-efi-ia32 | grub | lilo, initramfs-tools | linux-initramfs-tool
|
||||
Suggests: fdutils, linux-aws-doc-6.8.0 | linux-aws-source-6.8.0, linux-aws-tools, linux-headers-6.8.0-1029-aws
|
||||
Conflicts: linux-image-unsigned-6.8.0-1029-aws
|
||||
Description: Signed kernel image aws
|
||||
A kernel image for aws. This version of it is signed with
|
||||
Canonical's signing key.
|
||||
Built-Using: linux-aws (= 6.8.0-1029.31)
|
||||
@@ -10,7 +10,14 @@ import (
|
||||
)
|
||||
|
||||
func (c *goBinaryCataloger) newGoBinaryPackage(dep *debug.Module, m pkg.GolangBinaryBuildinfoEntry, licenses []pkg.License, locations ...file.Location) pkg.Package {
|
||||
// Similar to syft/pkg/cataloger/golang/parse_go_mod.go logic - use original path for relative replacements
|
||||
finalPath := dep.Path
|
||||
if dep.Replace != nil {
|
||||
if strings.HasPrefix(dep.Replace.Path, ".") || strings.HasPrefix(dep.Replace.Path, "/") {
|
||||
finalPath = dep.Path
|
||||
} else {
|
||||
finalPath = dep.Replace.Path
|
||||
}
|
||||
dep = dep.Replace
|
||||
}
|
||||
|
||||
@@ -23,10 +30,10 @@ func (c *goBinaryCataloger) newGoBinaryPackage(dep *debug.Module, m pkg.GolangBi
|
||||
}
|
||||
|
||||
p := pkg.Package{
|
||||
Name: dep.Path,
|
||||
Name: finalPath,
|
||||
Version: version,
|
||||
Licenses: pkg.NewLicenseSet(licenses...),
|
||||
PURL: packageURL(dep.Path, version),
|
||||
PURL: packageURL(finalPath, version),
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Locations: file.NewLocationSet(locations...),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -54,3 +55,67 @@ func Test_packageURL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_newGoBinaryPackage_relativeReplace(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dep *debug.Module
|
||||
expectedName string
|
||||
}{
|
||||
{
|
||||
name: "relative replace with ../",
|
||||
dep: &debug.Module{
|
||||
Path: "github.com/aws/aws-sdk-go-v2",
|
||||
Version: "(devel)",
|
||||
Replace: &debug.Module{
|
||||
Path: "../../",
|
||||
Version: "(devel)",
|
||||
},
|
||||
},
|
||||
expectedName: "github.com/aws/aws-sdk-go-v2", // should use original path, not relative
|
||||
},
|
||||
{
|
||||
name: "relative replace with ./",
|
||||
dep: &debug.Module{
|
||||
Path: "github.com/example/module",
|
||||
Version: "v1.0.0",
|
||||
Replace: &debug.Module{
|
||||
Path: "./local",
|
||||
Version: "v0.0.0",
|
||||
},
|
||||
},
|
||||
expectedName: "github.com/example/module", // should use original path
|
||||
},
|
||||
{
|
||||
name: "absolute replace",
|
||||
dep: &debug.Module{
|
||||
Path: "github.com/old/module",
|
||||
Version: "v1.0.0",
|
||||
Replace: &debug.Module{
|
||||
Path: "github.com/new/module",
|
||||
Version: "v2.0.0",
|
||||
},
|
||||
},
|
||||
expectedName: "github.com/new/module", // should use replacement path
|
||||
},
|
||||
{
|
||||
name: "no replace",
|
||||
dep: &debug.Module{
|
||||
Path: "github.com/normal/module",
|
||||
Version: "v1.0.0",
|
||||
},
|
||||
expectedName: "github.com/normal/module", // should use original path
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
cataloger := &goBinaryCataloger{}
|
||||
result := cataloger.newGoBinaryPackage(test.dep, pkg.GolangBinaryBuildinfoEntry{}, nil)
|
||||
|
||||
assert.Equal(t, test.expectedName, result.Name)
|
||||
assert.Equal(t, pkg.Go, result.Language)
|
||||
assert.Equal(t, pkg.GoModulePkg, result.Type)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package cpegenerate
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
// candidateVendorsForPE returns vendor candidates for PE (BinaryPkg) packages based on common metadata hints.
|
||||
// Specifically, normalize Ghostscript binaries to vendor "artifex" when detected.
|
||||
func candidateVendorsForPE(p pkg.Package) fieldCandidateSet {
|
||||
candidates := newFieldCandidateSet()
|
||||
|
||||
meta, ok := p.Metadata.(pkg.PEBinary)
|
||||
if !ok {
|
||||
return candidates
|
||||
}
|
||||
|
||||
var company, product, fileDesc string
|
||||
for _, kv := range meta.VersionResources {
|
||||
switch strings.ToLower(kv.Key) {
|
||||
case "companyname":
|
||||
company = strings.ToLower(kv.Value)
|
||||
case "productname":
|
||||
product = strings.ToLower(kv.Value)
|
||||
case "filedescription":
|
||||
fileDesc = strings.ToLower(kv.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(product, "ghostscript") || strings.Contains(fileDesc, "ghostscript") || strings.Contains(company, "artifex") {
|
||||
candidates.addValue("artifex")
|
||||
}
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
// candidateProductsForPE returns product candidates for PE (BinaryPkg) packages based on common metadata hints.
|
||||
// Specifically, normalize Ghostscript binaries to product "ghostscript" when detected.
|
||||
func candidateProductsForPE(p pkg.Package) fieldCandidateSet {
|
||||
candidates := newFieldCandidateSet()
|
||||
|
||||
meta, ok := p.Metadata.(pkg.PEBinary)
|
||||
if !ok {
|
||||
return candidates
|
||||
}
|
||||
|
||||
var product, fileDesc string
|
||||
for _, kv := range meta.VersionResources {
|
||||
switch strings.ToLower(kv.Key) {
|
||||
case "productname":
|
||||
product = strings.ToLower(kv.Value)
|
||||
case "filedescription":
|
||||
fileDesc = strings.ToLower(kv.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(product, "ghostscript") || strings.Contains(fileDesc, "ghostscript") {
|
||||
candidates.addValue("ghostscript")
|
||||
}
|
||||
|
||||
return candidates
|
||||
}
|
||||
@@ -225,6 +225,9 @@ func candidateVendors(p pkg.Package) []string {
|
||||
vendors.union(candidateVendorsForAPK(p))
|
||||
case pkg.NpmPackage:
|
||||
vendors.union(candidateVendorsForJavascript(p))
|
||||
case pkg.PEBinary:
|
||||
// Add PE-specific vendor hints (e.g. ghostscript -> artifex)
|
||||
vendors.union(candidateVendorsForPE(p))
|
||||
case pkg.WordpressPluginEntry:
|
||||
vendors.clear()
|
||||
vendors.union(candidateVendorsForWordpressPlugin(p))
|
||||
@@ -301,6 +304,9 @@ func candidateProductSet(p pkg.Package) fieldCandidateSet {
|
||||
switch p.Metadata.(type) {
|
||||
case pkg.ApkDBEntry:
|
||||
products.union(candidateProductsForAPK(p))
|
||||
case pkg.PEBinary:
|
||||
// Add PE-specific product hints (e.g. ghostscript)
|
||||
products.union(candidateProductsForPE(p))
|
||||
case pkg.WordpressPluginEntry:
|
||||
products.clear()
|
||||
products.union(candidateProductsForWordpressPlugin(p))
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package cpegenerate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func TestGhostscriptPEGeneratesArtifexCPE(t *testing.T) {
|
||||
// construct a BinaryPkg with PE metadata resembling Ghostscript
|
||||
p := pkg.Package{
|
||||
Name: "GPL Ghostscript",
|
||||
Version: "9.54.0",
|
||||
Type: pkg.BinaryPkg,
|
||||
Metadata: pkg.PEBinary{
|
||||
VersionResources: pkg.KeyValues{
|
||||
{Key: "CompanyName", Value: "Artifex Software, Inc."},
|
||||
{Key: "ProductName", Value: "GPL Ghostscript"},
|
||||
{Key: "FileDescription", Value: "Ghostscript Interpreter"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cpes := FromPackageAttributes(p)
|
||||
if len(cpes) == 0 {
|
||||
t.Fatalf("expected at least one CPE, got none")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, c := range cpes {
|
||||
if c.Attributes.Vendor == "artifex" && c.Attributes.Product == "ghostscript" && c.Attributes.Version == p.Version {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected to find CPE with vendor 'artifex' and product 'ghostscript' for Ghostscript PE binary; got: %+v", cpes)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
// Package pkgtest provides test helpers for cataloger and parser testing,
|
||||
// including automatic observation tracking for capability documentation.
|
||||
package pkgtest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/syft/internal/capabilities/pkgtestobservation"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
var (
|
||||
globalTracker *MetadataTracker
|
||||
globalTrackerOnce sync.Once
|
||||
|
||||
// commonPackageIntegrityFields are common field names used to store integrity hashes in package metadata.
|
||||
// TODO: this is a best-effort list and may need to be expanded as new package types are added. Don't depend on this list to catch everything - it's only for test validation.
|
||||
commonPackageIntegrityFields = []string{
|
||||
"Integrity", "Checksum", "H1Digest",
|
||||
"OutputHash", "PkgHash", "ContentHash",
|
||||
"PkgHashExt", "Hash", "IntegrityHash",
|
||||
}
|
||||
)
|
||||
|
||||
// MetadataTracker collects metadata type and package type usage during test execution
|
||||
type MetadataTracker struct {
|
||||
mu sync.Mutex
|
||||
parserData map[string]map[string]map[string]bool // package -> parser -> metadata types (set)
|
||||
catalogerData map[string]map[string]bool // cataloger -> metadata types (set)
|
||||
parserPackageTypes map[string]map[string]map[string]bool // package -> parser -> package types (set)
|
||||
catalogerPackageTypes map[string]map[string]bool // cataloger -> package types (set)
|
||||
|
||||
// unified observations for the current test package
|
||||
observations *pkgtestobservation.Test
|
||||
}
|
||||
|
||||
// getTracker returns the singleton metadata tracker
|
||||
func getTracker() *MetadataTracker {
|
||||
globalTrackerOnce.Do(func() {
|
||||
globalTracker = &MetadataTracker{
|
||||
parserData: make(map[string]map[string]map[string]bool),
|
||||
catalogerData: make(map[string]map[string]bool),
|
||||
parserPackageTypes: make(map[string]map[string]map[string]bool),
|
||||
catalogerPackageTypes: make(map[string]map[string]bool),
|
||||
}
|
||||
})
|
||||
return globalTracker
|
||||
}
|
||||
|
||||
// RecordParser records a metadata type usage for a parser function
|
||||
func (t *MetadataTracker) RecordParser(packageName, parserFunction, metadataType string) {
|
||||
if packageName == "" || parserFunction == "" || metadataType == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// filter out non-metadata types
|
||||
if metadataType == "pkg.Package" || metadataType == "" {
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.parserData[packageName] == nil {
|
||||
t.parserData[packageName] = make(map[string]map[string]bool)
|
||||
}
|
||||
|
||||
if t.parserData[packageName][parserFunction] == nil {
|
||||
t.parserData[packageName][parserFunction] = make(map[string]bool)
|
||||
}
|
||||
|
||||
t.parserData[packageName][parserFunction][metadataType] = true
|
||||
}
|
||||
|
||||
// RecordCataloger records a metadata type usage for a cataloger
|
||||
func (t *MetadataTracker) RecordCataloger(catalogerName, metadataType string) {
|
||||
if catalogerName == "" || metadataType == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// filter out non-metadata types
|
||||
if metadataType == "pkg.Package" || metadataType == "" {
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.catalogerData[catalogerName] == nil {
|
||||
t.catalogerData[catalogerName] = make(map[string]bool)
|
||||
}
|
||||
|
||||
t.catalogerData[catalogerName][metadataType] = true
|
||||
}
|
||||
|
||||
// RecordParserPackageType records a package type usage for a parser function
|
||||
func (t *MetadataTracker) RecordParserPackageType(packageName, parserFunction, pkgType string) {
|
||||
if packageName == "" || parserFunction == "" || pkgType == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// filter out unknown types
|
||||
if pkgType == pkg.UnknownPkg.String() || pkgType == "" {
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.parserPackageTypes[packageName] == nil {
|
||||
t.parserPackageTypes[packageName] = make(map[string]map[string]bool)
|
||||
}
|
||||
|
||||
if t.parserPackageTypes[packageName][parserFunction] == nil {
|
||||
t.parserPackageTypes[packageName][parserFunction] = make(map[string]bool)
|
||||
}
|
||||
|
||||
t.parserPackageTypes[packageName][parserFunction][pkgType] = true
|
||||
}
|
||||
|
||||
// RecordCatalogerPackageType records a package type usage for a cataloger
|
||||
func (t *MetadataTracker) RecordCatalogerPackageType(catalogerName, pkgType string) {
|
||||
if catalogerName == "" || pkgType == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// filter out unknown types
|
||||
if pkgType == pkg.UnknownPkg.String() || pkgType == "" {
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.catalogerPackageTypes[catalogerName] == nil {
|
||||
t.catalogerPackageTypes[catalogerName] = make(map[string]bool)
|
||||
}
|
||||
|
||||
t.catalogerPackageTypes[catalogerName][pkgType] = true
|
||||
}
|
||||
|
||||
// RecordParserPackageMetadata extracts and records metadata type and package type from a package for a parser
|
||||
func (t *MetadataTracker) RecordParserPackageMetadata(packageName, parserFunction string, p pkg.Package) {
|
||||
if p.Metadata != nil {
|
||||
metadataType := getMetadataTypeName(p.Metadata)
|
||||
if metadataType != "" {
|
||||
t.RecordParser(packageName, parserFunction, metadataType)
|
||||
}
|
||||
}
|
||||
|
||||
// record package type
|
||||
t.RecordParserPackageType(packageName, parserFunction, string(p.Type))
|
||||
}
|
||||
|
||||
// RecordCatalogerPackageMetadata extracts and records metadata type and package type from a package for a cataloger
|
||||
func (t *MetadataTracker) RecordCatalogerPackageMetadata(catalogerName string, p pkg.Package) {
|
||||
if p.Metadata != nil {
|
||||
metadataType := getMetadataTypeName(p.Metadata)
|
||||
if metadataType != "" {
|
||||
t.RecordCataloger(catalogerName, metadataType)
|
||||
}
|
||||
}
|
||||
|
||||
// record package type
|
||||
t.RecordCatalogerPackageType(catalogerName, string(p.Type))
|
||||
}
|
||||
|
||||
// aggregateObservations aggregates package and relationship observations into metadata types, package types, and observations.
|
||||
// this is used by both parser and cataloger observation recording.
|
||||
func aggregateObservations(
|
||||
metadataTypes *[]string,
|
||||
packageTypes *[]string,
|
||||
obs *pkgtestobservation.Observations,
|
||||
pkgs []pkg.Package,
|
||||
relationships []artifact.Relationship,
|
||||
) {
|
||||
// aggregate observations from packages
|
||||
for _, p := range pkgs {
|
||||
// metadata types
|
||||
if p.Metadata != nil {
|
||||
metadataType := getMetadataTypeName(p.Metadata)
|
||||
if metadataType != "" && !contains(*metadataTypes, metadataType) {
|
||||
*metadataTypes = append(*metadataTypes, metadataType)
|
||||
}
|
||||
}
|
||||
|
||||
// package types
|
||||
pkgType := string(p.Type)
|
||||
if pkgType != "" && pkgType != pkg.UnknownPkg.String() && !contains(*packageTypes, pkgType) {
|
||||
*packageTypes = append(*packageTypes, pkgType)
|
||||
}
|
||||
|
||||
// license observation
|
||||
if !p.Licenses.Empty() {
|
||||
obs.License = true
|
||||
}
|
||||
|
||||
// file listing observation
|
||||
if fileOwner, ok := p.Metadata.(pkg.FileOwner); ok {
|
||||
files := fileOwner.OwnedFiles()
|
||||
if len(files) > 0 {
|
||||
obs.FileListing.Found = true
|
||||
obs.FileListing.Count += len(files)
|
||||
}
|
||||
}
|
||||
|
||||
// file digests observation
|
||||
if hasFileDigests(p.Metadata) {
|
||||
obs.FileDigests.Found = true
|
||||
obs.FileDigests.Count++
|
||||
}
|
||||
|
||||
// integrity hash observation
|
||||
if hasIntegrityHash(p.Metadata) {
|
||||
obs.IntegrityHash.Found = true
|
||||
obs.IntegrityHash.Count++
|
||||
}
|
||||
}
|
||||
|
||||
// relationship observations
|
||||
depCount := countDependencyRelationships(relationships)
|
||||
if depCount > 0 {
|
||||
obs.Relationships.Found = true
|
||||
obs.Relationships.Count = depCount
|
||||
}
|
||||
|
||||
// sort arrays for consistency
|
||||
sort.Strings(*metadataTypes)
|
||||
sort.Strings(*packageTypes)
|
||||
}
|
||||
|
||||
// ensureObservationsInitialized ensures t.observations is initialized and package name is set.
|
||||
// must be called with t.mu locked.
|
||||
func (t *MetadataTracker) ensureObservationsInitialized(packageName string) {
|
||||
if t.observations == nil {
|
||||
t.observations = &pkgtestobservation.Test{
|
||||
Package: packageName,
|
||||
Catalogers: make(map[string]*pkgtestobservation.Cataloger),
|
||||
Parsers: make(map[string]*pkgtestobservation.Parser),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// update package name if not set (for the first test) or if it matches (for subsequent tests in same package)
|
||||
if t.observations.Package == "" || t.observations.Package == packageName {
|
||||
t.observations.Package = packageName
|
||||
}
|
||||
}
|
||||
|
||||
// getOrCreateParser gets an existing parser observation or creates a new one.
|
||||
// must be called with t.mu locked.
|
||||
func (t *MetadataTracker) getOrCreateParser(parserFunction string) *pkgtestobservation.Parser {
|
||||
if t.observations.Parsers[parserFunction] == nil {
|
||||
t.observations.Parsers[parserFunction] = &pkgtestobservation.Parser{
|
||||
MetadataTypes: []string{},
|
||||
PackageTypes: []string{},
|
||||
Observations: pkgtestobservation.Observations{},
|
||||
}
|
||||
}
|
||||
return t.observations.Parsers[parserFunction]
|
||||
}
|
||||
|
||||
// getOrCreateCataloger gets an existing cataloger observation or creates a new one.
|
||||
// must be called with t.mu locked.
|
||||
func (t *MetadataTracker) getOrCreateCataloger(catalogerName string) *pkgtestobservation.Cataloger {
|
||||
if t.observations.Catalogers[catalogerName] == nil {
|
||||
t.observations.Catalogers[catalogerName] = &pkgtestobservation.Cataloger{
|
||||
MetadataTypes: []string{},
|
||||
PackageTypes: []string{},
|
||||
Observations: pkgtestobservation.Observations{},
|
||||
}
|
||||
}
|
||||
return t.observations.Catalogers[catalogerName]
|
||||
}
|
||||
|
||||
// RecordParserObservations records comprehensive observations for a parser.
|
||||
func (t *MetadataTracker) RecordParserObservations(
|
||||
packageName, parserFunction string,
|
||||
pkgs []pkg.Package,
|
||||
relationships []artifact.Relationship,
|
||||
) {
|
||||
if packageName == "" || parserFunction == "" {
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
t.ensureObservationsInitialized(packageName)
|
||||
parser := t.getOrCreateParser(parserFunction)
|
||||
aggregateObservations(&parser.MetadataTypes, &parser.PackageTypes, &parser.Observations, pkgs, relationships)
|
||||
}
|
||||
|
||||
// RecordCatalogerObservations records comprehensive observations for a cataloger.
|
||||
func (t *MetadataTracker) RecordCatalogerObservations(
|
||||
packageName, catalogerName string,
|
||||
pkgs []pkg.Package,
|
||||
relationships []artifact.Relationship,
|
||||
) {
|
||||
if packageName == "" || catalogerName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
t.ensureObservationsInitialized(packageName)
|
||||
cataloger := t.getOrCreateCataloger(catalogerName)
|
||||
aggregateObservations(&cataloger.MetadataTypes, &cataloger.PackageTypes, &cataloger.Observations, pkgs, relationships)
|
||||
}
|
||||
|
||||
// ===== Metadata Type and Capability Detection =====
|
||||
// These functions use reflection to inspect package metadata and detect capabilities.
|
||||
// They are best-effort and may not catch all cases.
|
||||
|
||||
// getMetadataTypeName returns the fully qualified type name of metadata (e.g., "pkg.ApkDBEntry").
|
||||
// extracts just the last package path segment to keep names concise.
|
||||
func getMetadataTypeName(metadata interface{}) string {
|
||||
if metadata == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
t := reflect.TypeOf(metadata)
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// handle pointers
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
// return pkg path + type name (e.g., "pkg.ApkDBEntry")
|
||||
if t.PkgPath() != "" {
|
||||
// extract just "pkg" from "github.com/anchore/syft/syft/pkg"
|
||||
pkgPath := lastPathSegment(t.PkgPath())
|
||||
return pkgPath + "." + t.Name()
|
||||
}
|
||||
|
||||
return t.Name()
|
||||
}
|
||||
|
||||
// lastPathSegment extracts the last segment from a package path.
|
||||
// for example: "github.com/anchore/syft/syft/pkg" -> "pkg"
|
||||
func lastPathSegment(path string) string {
|
||||
for i := len(path) - 1; i >= 0; i-- {
|
||||
if path[i] == '/' {
|
||||
return path[i+1:]
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// hasIntegrityHash checks if metadata contains an integrity hash field.
|
||||
// note: this uses a best-effort approach checking common field names.
|
||||
// DO NOT depend on these values in auto-generated capabilities definitions - use for test validation only.
|
||||
func hasIntegrityHash(metadata interface{}) bool {
|
||||
v := dereferenceToStruct(metadata)
|
||||
if !v.IsValid() || v.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, fieldName := range commonPackageIntegrityFields {
|
||||
if hasPopulatedStringField(v, fieldName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasFileDigests checks if metadata contains file records with digests.
|
||||
// note: uses a best-effort approach for detection.
|
||||
// DO NOT depend on these values in auto-generated capabilities definitions - use for test validation only.
|
||||
func hasFileDigests(metadata interface{}) bool {
|
||||
v := dereferenceToStruct(metadata)
|
||||
if !v.IsValid() || v.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
|
||||
filesField := v.FieldByName("Files")
|
||||
if !filesField.IsValid() || filesField.Kind() != reflect.Slice {
|
||||
return false
|
||||
}
|
||||
|
||||
// check if any file record has a Digest field populated
|
||||
for i := 0; i < filesField.Len(); i++ {
|
||||
if hasPopulatedDigest(filesField.Index(i)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// dereferenceToStruct handles pointer dereferencing and returns the underlying value.
|
||||
// returns an invalid value if the input is nil or not convertible to a struct.
|
||||
func dereferenceToStruct(v interface{}) reflect.Value {
|
||||
if v == nil {
|
||||
return reflect.Value{}
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(v)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
if val.IsNil() {
|
||||
return reflect.Value{}
|
||||
}
|
||||
val = val.Elem()
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// hasPopulatedStringField checks if a struct has a non-empty string field with the given name.
|
||||
func hasPopulatedStringField(v reflect.Value, fieldName string) bool {
|
||||
field := v.FieldByName(fieldName)
|
||||
return field.IsValid() && field.Kind() == reflect.String && field.String() != ""
|
||||
}
|
||||
|
||||
// hasPopulatedDigest checks if a file record has a populated Digest field.
|
||||
func hasPopulatedDigest(fileRecord reflect.Value) bool {
|
||||
fileRecord = dereferenceToStruct(fileRecord.Interface())
|
||||
if !fileRecord.IsValid() || fileRecord.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
|
||||
digestField := fileRecord.FieldByName("Digest")
|
||||
if !digestField.IsValid() {
|
||||
return false
|
||||
}
|
||||
|
||||
// check if digest is a pointer and not nil, or a non-zero value
|
||||
switch digestField.Kind() {
|
||||
case reflect.Ptr:
|
||||
return !digestField.IsNil()
|
||||
case reflect.String:
|
||||
return digestField.String() != ""
|
||||
case reflect.Struct:
|
||||
return !digestField.IsZero()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ===== Utility Functions =====
|
||||
|
||||
// countDependencyRelationships counts the number of dependency relationships.
|
||||
func countDependencyRelationships(relationships []artifact.Relationship) int {
|
||||
count := 0
|
||||
for _, rel := range relationships {
|
||||
if rel.Type == artifact.DependencyOfRelationship {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// contains checks if a string slice contains a specific string.
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ===== Result Writing =====
|
||||
|
||||
// WriteResults writes the collected observation data to test-fixtures/test-observations.json.
|
||||
func (t *MetadataTracker) WriteResults() error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.observations == nil {
|
||||
// no data to write
|
||||
return nil
|
||||
}
|
||||
|
||||
// create output directory
|
||||
outDir := "test-fixtures"
|
||||
if err := os.MkdirAll(outDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write unified test-observations.json
|
||||
t.observations.UpdatedAt = time.Now().UTC()
|
||||
|
||||
filename := filepath.Join(outDir, "test-observations.json")
|
||||
return writeJSONFile(filename, t.observations)
|
||||
}
|
||||
|
||||
// writeJSONFile writes data as pretty-printed JSON to the specified path.
|
||||
func writeJSONFile(path string, data interface{}) error {
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(data)
|
||||
}
|
||||
|
||||
// WriteResultsIfEnabled writes results if tracking is enabled.
|
||||
// this is typically called via t.Cleanup() in tests.
|
||||
func WriteResultsIfEnabled() error {
|
||||
tracker := getTracker()
|
||||
return tracker.WriteResults()
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// Package pkgtest provides test helpers for cataloger and parser testing,
|
||||
// including resolver decorators that track file access patterns.
|
||||
package pkgtest
|
||||
|
||||
import (
|
||||
@@ -13,29 +15,36 @@ import (
|
||||
|
||||
var _ file.Resolver = (*ObservingResolver)(nil)
|
||||
|
||||
// ObservingResolver wraps a file.Resolver to observe and track all file access patterns.
|
||||
// it records what paths were queried, what was returned, and what file contents were read.
|
||||
// this is useful for validating that catalogers use appropriate glob patterns and don't over-read files.
|
||||
type ObservingResolver struct {
|
||||
decorated file.Resolver
|
||||
pathQueries map[string][]string
|
||||
pathResponses []file.Location
|
||||
contentQueries []file.Location
|
||||
emptyPathResponses map[string][]string
|
||||
pathQueries map[string][]string // method name -> list of query patterns
|
||||
pathResponses []file.Location // all locations successfully returned
|
||||
contentQueries []file.Location // all locations whose content was read
|
||||
emptyPathResponses map[string][]string // method name -> paths that returned empty results
|
||||
}
|
||||
|
||||
// NewObservingResolver creates a new ObservingResolver that wraps the given resolver.
|
||||
func NewObservingResolver(resolver file.Resolver) *ObservingResolver {
|
||||
return &ObservingResolver{
|
||||
decorated: resolver,
|
||||
pathResponses: make([]file.Location, 0),
|
||||
emptyPathResponses: make(map[string][]string),
|
||||
pathQueries: make(map[string][]string),
|
||||
pathResponses: make([]file.Location, 0),
|
||||
contentQueries: make([]file.Location, 0),
|
||||
emptyPathResponses: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
// testing helpers...
|
||||
// ===== Test Assertion Helpers =====
|
||||
// these methods are used by tests to validate expected file access patterns.
|
||||
|
||||
// ObservedPathQuery checks if a specific path pattern was queried.
|
||||
func (r *ObservingResolver) ObservedPathQuery(input string) bool {
|
||||
for _, vs := range r.pathQueries {
|
||||
for _, v := range vs {
|
||||
if v == input {
|
||||
for _, queries := range r.pathQueries {
|
||||
for _, query := range queries {
|
||||
if query == input {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -43,6 +52,7 @@ func (r *ObservingResolver) ObservedPathQuery(input string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ObservedPathResponses checks if a specific path was returned in any response.
|
||||
func (r *ObservingResolver) ObservedPathResponses(path string) bool {
|
||||
for _, loc := range r.pathResponses {
|
||||
if loc.RealPath == path {
|
||||
@@ -52,6 +62,7 @@ func (r *ObservingResolver) ObservedPathResponses(path string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ObservedContentQueries checks if a specific file's content was read.
|
||||
func (r *ObservingResolver) ObservedContentQueries(path string) bool {
|
||||
for _, loc := range r.contentQueries {
|
||||
if loc.RealPath == path {
|
||||
@@ -61,6 +72,7 @@ func (r *ObservingResolver) ObservedContentQueries(path string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// AllContentQueries returns a deduplicated list of all file paths whose content was read.
|
||||
func (r *ObservingResolver) AllContentQueries() []string {
|
||||
observed := strset.New()
|
||||
for _, loc := range r.contentQueries {
|
||||
@@ -69,155 +81,166 @@ func (r *ObservingResolver) AllContentQueries() []string {
|
||||
return observed.List()
|
||||
}
|
||||
|
||||
// AllPathQueries returns all path query patterns grouped by method name.
|
||||
func (r *ObservingResolver) AllPathQueries() map[string][]string {
|
||||
return r.pathQueries
|
||||
}
|
||||
|
||||
// PruneUnfulfilledPathResponses removes specified paths from the unfulfilled requests tracking.
|
||||
// ignore maps method names to paths that should be ignored for that method.
|
||||
// ignorePaths lists paths that should be ignored for all methods.
|
||||
func (r *ObservingResolver) PruneUnfulfilledPathResponses(ignore map[string][]string, ignorePaths ...string) {
|
||||
if ignore == nil {
|
||||
return
|
||||
}
|
||||
// remove any paths that were ignored for specific calls
|
||||
for k, v := range ignore {
|
||||
results := r.emptyPathResponses[k]
|
||||
for _, ig := range v {
|
||||
for i, result := range results {
|
||||
if result == ig {
|
||||
results = append(results[:i], results[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(results) > 0 {
|
||||
r.emptyPathResponses[k] = results
|
||||
} else {
|
||||
delete(r.emptyPathResponses, k)
|
||||
// remove paths ignored for specific methods
|
||||
for methodName, pathsToIgnore := range ignore {
|
||||
r.emptyPathResponses[methodName] = removeStrings(r.emptyPathResponses[methodName], pathsToIgnore)
|
||||
if len(r.emptyPathResponses[methodName]) == 0 {
|
||||
delete(r.emptyPathResponses, methodName)
|
||||
}
|
||||
}
|
||||
|
||||
// remove any paths that were ignored for all calls
|
||||
for _, ig := range ignorePaths {
|
||||
for k, v := range r.emptyPathResponses {
|
||||
for i, result := range v {
|
||||
if result == ig {
|
||||
v = append(v[:i], v[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(v) > 0 {
|
||||
r.emptyPathResponses[k] = v
|
||||
} else {
|
||||
delete(r.emptyPathResponses, k)
|
||||
// remove paths ignored for all methods
|
||||
if len(ignorePaths) > 0 {
|
||||
for methodName := range r.emptyPathResponses {
|
||||
r.emptyPathResponses[methodName] = removeStrings(r.emptyPathResponses[methodName], ignorePaths)
|
||||
if len(r.emptyPathResponses[methodName]) == 0 {
|
||||
delete(r.emptyPathResponses, methodName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HasUnfulfilledPathRequests returns true if there are any paths that were queried but returned empty.
|
||||
func (r *ObservingResolver) HasUnfulfilledPathRequests() bool {
|
||||
return len(r.emptyPathResponses) > 0
|
||||
}
|
||||
|
||||
// PrettyUnfulfilledPathRequests returns a formatted string of all unfulfilled path requests.
|
||||
func (r *ObservingResolver) PrettyUnfulfilledPathRequests() string {
|
||||
var res string
|
||||
var keys []string
|
||||
if len(r.emptyPathResponses) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for k := range r.emptyPathResponses {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
var result string
|
||||
for _, k := range keys {
|
||||
res += fmt.Sprintf(" %s: %+v\n", k, r.emptyPathResponses[k])
|
||||
result += fmt.Sprintf(" %s: %+v\n", k, r.emptyPathResponses[k])
|
||||
}
|
||||
return res
|
||||
return result
|
||||
}
|
||||
|
||||
// For the file path resolver...
|
||||
// removeStrings removes all occurrences of toRemove from slice.
|
||||
func removeStrings(slice []string, toRemove []string) []string {
|
||||
if len(toRemove) == 0 {
|
||||
return slice
|
||||
}
|
||||
|
||||
func (r *ObservingResolver) addPathQuery(name string, input ...string) {
|
||||
r.pathQueries[name] = append(r.pathQueries[name], input...)
|
||||
// create a set for O(1) lookup
|
||||
removeSet := make(map[string]bool)
|
||||
for _, s := range toRemove {
|
||||
removeSet[s] = true
|
||||
}
|
||||
|
||||
// filter the slice
|
||||
result := make([]string, 0, len(slice))
|
||||
for _, s := range slice {
|
||||
if !removeSet[s] {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *ObservingResolver) addPathResponse(locs ...file.Location) {
|
||||
// ===== Internal Tracking Helpers =====
|
||||
|
||||
// recordQuery records a path query for a given method.
|
||||
func (r *ObservingResolver) recordQuery(methodName string, queries ...string) {
|
||||
r.pathQueries[methodName] = append(r.pathQueries[methodName], queries...)
|
||||
}
|
||||
|
||||
// recordResponses records successful path responses and tracks any unfulfilled queries.
|
||||
func (r *ObservingResolver) recordResponses(methodName string, locs []file.Location, queriedPaths ...string) {
|
||||
r.pathResponses = append(r.pathResponses, locs...)
|
||||
}
|
||||
|
||||
func (r *ObservingResolver) addEmptyPathResponse(name string, locs []file.Location, paths ...string) {
|
||||
if len(locs) == 0 {
|
||||
results := r.emptyPathResponses[name]
|
||||
results = append(results, paths...)
|
||||
r.emptyPathResponses[name] = results
|
||||
// track paths that returned no results
|
||||
if len(locs) == 0 && len(queriedPaths) > 0 {
|
||||
r.emptyPathResponses[methodName] = append(r.emptyPathResponses[methodName], queriedPaths...)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== file.Resolver Implementation =====
|
||||
// these methods delegate to the wrapped resolver while recording observations.
|
||||
|
||||
// FilesByPath returns files matching the given paths.
|
||||
func (r *ObservingResolver) FilesByPath(paths ...string) ([]file.Location, error) {
|
||||
name := "FilesByPath"
|
||||
r.addPathQuery(name, paths...)
|
||||
const methodName = "FilesByPath"
|
||||
r.recordQuery(methodName, paths...)
|
||||
|
||||
locs, err := r.decorated.FilesByPath(paths...)
|
||||
r.recordResponses(methodName, locs, paths...)
|
||||
|
||||
r.addPathResponse(locs...)
|
||||
r.addEmptyPathResponse(name, locs, paths...)
|
||||
return locs, err
|
||||
}
|
||||
|
||||
// FilesByGlob returns files matching the given glob patterns.
|
||||
func (r *ObservingResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
|
||||
name := "FilesByGlob"
|
||||
r.addPathQuery(name, patterns...)
|
||||
const methodName = "FilesByGlob"
|
||||
r.recordQuery(methodName, patterns...)
|
||||
|
||||
locs, err := r.decorated.FilesByGlob(patterns...)
|
||||
r.recordResponses(methodName, locs, patterns...)
|
||||
|
||||
r.addPathResponse(locs...)
|
||||
r.addEmptyPathResponse(name, locs, patterns...)
|
||||
return locs, err
|
||||
}
|
||||
|
||||
// FilesByMIMEType returns files matching the given MIME types.
|
||||
func (r *ObservingResolver) FilesByMIMEType(types ...string) ([]file.Location, error) {
|
||||
name := "FilesByMIMEType"
|
||||
r.addPathQuery(name, types...)
|
||||
const methodName = "FilesByMIMEType"
|
||||
r.recordQuery(methodName, types...)
|
||||
|
||||
locs, err := r.decorated.FilesByMIMEType(types...)
|
||||
r.recordResponses(methodName, locs, types...)
|
||||
|
||||
r.addPathResponse(locs...)
|
||||
r.addEmptyPathResponse(name, locs, types...)
|
||||
return locs, err
|
||||
}
|
||||
|
||||
func (r *ObservingResolver) RelativeFileByPath(l file.Location, path string) *file.Location {
|
||||
name := "RelativeFileByPath"
|
||||
r.addPathQuery(name, path)
|
||||
// RelativeFileByPath returns a file at a path relative to the given location.
|
||||
func (r *ObservingResolver) RelativeFileByPath(location file.Location, path string) *file.Location {
|
||||
const methodName = "RelativeFileByPath"
|
||||
r.recordQuery(methodName, path)
|
||||
|
||||
loc := r.decorated.RelativeFileByPath(l, path)
|
||||
loc := r.decorated.RelativeFileByPath(location, path)
|
||||
|
||||
if loc != nil {
|
||||
r.addPathResponse(*loc)
|
||||
r.pathResponses = append(r.pathResponses, *loc)
|
||||
} else {
|
||||
results := r.emptyPathResponses[name]
|
||||
results = append(results, path)
|
||||
r.emptyPathResponses[name] = results
|
||||
r.emptyPathResponses[methodName] = append(r.emptyPathResponses[methodName], path)
|
||||
}
|
||||
|
||||
return loc
|
||||
}
|
||||
|
||||
// For the content resolver methods...
|
||||
|
||||
// FileContentsByLocation returns a reader for the contents of the file at the given location.
|
||||
func (r *ObservingResolver) FileContentsByLocation(location file.Location) (io.ReadCloser, error) {
|
||||
r.contentQueries = append(r.contentQueries, location)
|
||||
reader, err := r.decorated.FileContentsByLocation(location)
|
||||
return reader, err
|
||||
return r.decorated.FileContentsByLocation(location)
|
||||
}
|
||||
|
||||
// For the remaining resolver methods...
|
||||
|
||||
// AllLocations returns all file locations known to the resolver.
|
||||
func (r *ObservingResolver) AllLocations(ctx context.Context) <-chan file.Location {
|
||||
return r.decorated.AllLocations(ctx)
|
||||
}
|
||||
|
||||
func (r *ObservingResolver) HasPath(s string) bool {
|
||||
return r.decorated.HasPath(s)
|
||||
// HasPath returns true if the resolver knows about the given path.
|
||||
func (r *ObservingResolver) HasPath(path string) bool {
|
||||
return r.decorated.HasPath(path)
|
||||
}
|
||||
|
||||
// FileMetadataByLocation returns metadata for the file at the given location.
|
||||
func (r *ObservingResolver) FileMetadataByLocation(location file.Location) (file.Metadata, error) {
|
||||
return r.decorated.FileMetadataByLocation(location)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -56,6 +58,7 @@ type CatalogTester struct {
|
||||
packageStringer func(pkg.Package) string
|
||||
customAssertions []func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship)
|
||||
context context.Context
|
||||
skipTestObservations bool
|
||||
}
|
||||
|
||||
func Context() context.Context {
|
||||
@@ -260,13 +263,23 @@ func (p *CatalogTester) IgnoreUnfulfilledPathResponses(paths ...string) *Catalog
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *CatalogTester) WithoutTestObserver() *CatalogTester {
|
||||
p.skipTestObservations = true
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *CatalogTester) TestParser(t *testing.T, parser generic.Parser) {
|
||||
t.Helper()
|
||||
pkgs, relationships, err := parser(p.context, p.resolver, p.env, p.reader)
|
||||
|
||||
// only test for errors if explicitly requested
|
||||
if p.wantErr != nil {
|
||||
p.wantErr(t, err)
|
||||
}
|
||||
|
||||
// track metadata types for cataloger discovery
|
||||
p.trackParserMetadata(t, parser, pkgs, relationships)
|
||||
|
||||
p.assertPkgs(t, pkgs, relationships)
|
||||
}
|
||||
|
||||
@@ -292,6 +305,9 @@ func (p *CatalogTester) TestCataloger(t *testing.T, cataloger pkg.Cataloger) {
|
||||
p.wantErr(t, err)
|
||||
}
|
||||
|
||||
// track metadata types for cataloger discovery
|
||||
p.trackCatalogerMetadata(t, cataloger, pkgs, relationships)
|
||||
|
||||
if p.assertResultExpectations {
|
||||
p.assertPkgs(t, pkgs, relationships)
|
||||
}
|
||||
@@ -458,3 +474,163 @@ func stringPackage(p pkg.Package) string {
|
||||
|
||||
return fmt.Sprintf("%s @ %s (%s)", p.Name, p.Version, loc)
|
||||
}
|
||||
|
||||
// getFunctionName extracts the function name from a function pointer using reflection
|
||||
func getFunctionName(fn interface{}) string {
|
||||
// get the function pointer
|
||||
ptr := reflect.ValueOf(fn).Pointer()
|
||||
|
||||
// get the function details
|
||||
funcForPC := runtime.FuncForPC(ptr)
|
||||
if funcForPC == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
fullName := funcForPC.Name()
|
||||
|
||||
// extract just the function name from the full path
|
||||
// e.g., "github.com/anchore/syft/syft/pkg/cataloger/python.parseRequirementsTxt"
|
||||
// -> "parseRequirementsTxt"
|
||||
parts := strings.Split(fullName, ".")
|
||||
if len(parts) > 0 {
|
||||
name := parts[len(parts)-1]
|
||||
// strip the -fm suffix that Go's reflection adds for methods
|
||||
// e.g., "parsePackageLock-fm" -> "parsePackageLock"
|
||||
return strings.TrimSuffix(name, "-fm")
|
||||
}
|
||||
|
||||
return fullName
|
||||
}
|
||||
|
||||
// getCatalogerName extracts the cataloger name from the test context or cataloger name
|
||||
func getCatalogerName(_ *testing.T, cataloger pkg.Cataloger) string {
|
||||
// use the cataloger's name method if available
|
||||
return cataloger.Name()
|
||||
}
|
||||
|
||||
// getPackagePath extracts the package path from a function name
|
||||
// e.g., "github.com/anchore/syft/syft/pkg/cataloger/python.parseRequirementsTxt" -> "python"
|
||||
func getPackagePath(fn interface{}) string {
|
||||
ptr := reflect.ValueOf(fn).Pointer()
|
||||
funcForPC := runtime.FuncForPC(ptr)
|
||||
if funcForPC == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
fullName := funcForPC.Name()
|
||||
|
||||
// extract package name from path
|
||||
// e.g., "github.com/anchore/syft/syft/pkg/cataloger/python.parseRequirementsTxt"
|
||||
// -> "python"
|
||||
if strings.Contains(fullName, "/cataloger/") {
|
||||
parts := strings.Split(fullName, "/cataloger/")
|
||||
if len(parts) > 1 {
|
||||
// get the next segment after "/cataloger/"
|
||||
remaining := parts[1]
|
||||
// split by "." to get package name
|
||||
pkgParts := strings.Split(remaining, ".")
|
||||
if len(pkgParts) > 0 {
|
||||
return pkgParts[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// getPackagePathFromCataloger extracts the package path from the caller's file path
|
||||
// For generic catalogers, the cataloger type is from the generic package, but we need
|
||||
// the package where the test is defined (e.g., rust, python, etc.)
|
||||
func getPackagePathFromCataloger(_ pkg.Cataloger) string {
|
||||
// walk up the call stack to find the test file
|
||||
// we're looking for a file in the cataloger directory structure
|
||||
for i := 0; i < 10; i++ {
|
||||
_, file, _, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
// extract package name from file path
|
||||
// e.g., "/Users/.../syft/pkg/cataloger/rust/cataloger_test.go" -> "rust"
|
||||
if strings.Contains(file, "/cataloger/") {
|
||||
parts := strings.Split(file, "/cataloger/")
|
||||
if len(parts) > 1 {
|
||||
// get the next segment after "/cataloger/"
|
||||
remaining := parts[1]
|
||||
// split by "/" to get package name
|
||||
pkgParts := strings.Split(remaining, "/")
|
||||
if len(pkgParts) > 0 && pkgParts[0] != "internal" {
|
||||
return pkgParts[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// trackParserMetadata records metadata types for a parser function
|
||||
func (p *CatalogTester) trackParserMetadata(t *testing.T, parser generic.Parser, pkgs []pkg.Package, relationships []artifact.Relationship) {
|
||||
if p.skipTestObservations {
|
||||
return
|
||||
}
|
||||
|
||||
parserName := getFunctionName(parser)
|
||||
if parserName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// try to infer package name from function path
|
||||
packageName := getPackagePath(parser)
|
||||
if packageName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
tracker := getTracker()
|
||||
|
||||
// old tracking (still used by metadata discovery)
|
||||
for _, pkg := range pkgs {
|
||||
tracker.RecordParserPackageMetadata(packageName, parserName, pkg)
|
||||
}
|
||||
|
||||
// new unified observations with capability tracking
|
||||
tracker.RecordParserObservations(packageName, parserName, pkgs, relationships)
|
||||
|
||||
// ensure results are written when tests complete
|
||||
t.Cleanup(func() {
|
||||
_ = WriteResultsIfEnabled()
|
||||
})
|
||||
}
|
||||
|
||||
// trackCatalogerMetadata records metadata types for a cataloger
|
||||
func (p *CatalogTester) trackCatalogerMetadata(t *testing.T, cataloger pkg.Cataloger, pkgs []pkg.Package, relationships []artifact.Relationship) {
|
||||
if p.skipTestObservations {
|
||||
return
|
||||
}
|
||||
|
||||
catalogerName := getCatalogerName(t, cataloger)
|
||||
if catalogerName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// try to infer package name from cataloger type
|
||||
packageName := getPackagePathFromCataloger(cataloger)
|
||||
if packageName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
tracker := getTracker()
|
||||
|
||||
// old tracking (still used by metadata discovery)
|
||||
for _, pkg := range pkgs {
|
||||
tracker.RecordCatalogerPackageMetadata(catalogerName, pkg)
|
||||
}
|
||||
|
||||
// new unified observations with capability tracking
|
||||
tracker.RecordCatalogerObservations(packageName, catalogerName, pkgs, relationships)
|
||||
|
||||
// ensure results are written when tests complete
|
||||
t.Cleanup(func() {
|
||||
_ = WriteResultsIfEnabled()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ func (a archiveFilename) extension() string {
|
||||
|
||||
func (a archiveFilename) pkgType() pkg.Type {
|
||||
switch strings.ToLower(a.extension()) {
|
||||
case "jar", "war", "ear", "lpkg", "par", "sar", "nar", "kar":
|
||||
case "jar", "war", "ear", "lpkg", "par", "sar", "nar", "kar", "rar":
|
||||
return pkg.JavaPkg
|
||||
case "jpi", "hpi":
|
||||
return pkg.JenkinsPluginPkg
|
||||
|
||||
@@ -187,6 +187,13 @@ func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
|
||||
name: "gradle-build-cache",
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
{
|
||||
filename: "pkg-extra-field-maven-4.3.2-rc1.rar",
|
||||
version: "4.3.2-rc1",
|
||||
extension: "rar",
|
||||
name: "pkg-extra-field-maven",
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -37,6 +37,7 @@ var archiveFormatGlobs = []string{
|
||||
"**/*.jpi",
|
||||
"**/*.hpi",
|
||||
"**/*.kar",
|
||||
"**/*.far",
|
||||
"**/*.lpkg", // Zip-compressed package used to deploy applications
|
||||
// (aka plugins) to Liferay Portal server. Those files contains .JAR(s) and a .PROPERTIES file, the latter
|
||||
// has information about the application and installation requirements.
|
||||
@@ -45,6 +46,7 @@ var archiveFormatGlobs = []string{
|
||||
// LifeRay makes it pretty cumbersome to make a such plugins; their docs are
|
||||
// out of date, and they charge for their IDE. If you find an example
|
||||
// project that we can build in CI feel free to include it
|
||||
"**/*.rar", // Java Resource Adapter Archive
|
||||
}
|
||||
|
||||
// javaArchiveHashes are all the current hash algorithms used to calculate archive digests
|
||||
|
||||
@@ -29,7 +29,9 @@ func Test_ArchiveCataloger_Globs(t *testing.T) {
|
||||
"java-archives/example.kar",
|
||||
"java-archives/example.jpi",
|
||||
"java-archives/example.hpi",
|
||||
"java-archives/example.far",
|
||||
"java-archives/example.lpkg",
|
||||
"java-archives/example.rar",
|
||||
"archives/example.zip",
|
||||
"archives/example.tar",
|
||||
"archives/example.tar.gz",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
example archive
|
||||
@@ -0,0 +1 @@
|
||||
example archive
|
||||
@@ -284,7 +284,11 @@ func TestSearchYarnForLicenses(t *testing.T) {
|
||||
}
|
||||
tc.config.NPMBaseURL = url
|
||||
adapter := newGenericYarnLockAdapter(tc.config)
|
||||
pkgtest.TestFileParser(t, fixture, adapter.parseYarnLock, tc.expectedPackages, nil)
|
||||
pkgtest.NewCatalogTester().
|
||||
FromFile(t, fixture).
|
||||
Expects(tc.expectedPackages, nil).
|
||||
WithoutTestObserver(). // this is an online test, thus not the default configuration
|
||||
TestParser(t, adapter.parseYarnLock)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
func Test_noSQLiteDriverError(t *testing.T) {
|
||||
// this test package does must not import the sqlite library
|
||||
file := "../test-fixtures/Packages"
|
||||
resolver, err := fileresolver.NewFromFile(file, file)
|
||||
resolver, err := fileresolver.NewFromFile(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
cataloger := redhat.NewDBCataloger()
|
||||
|
||||
@@ -7,6 +7,10 @@ import (
|
||||
// Type represents a Package Type for or within a language ecosystem (there may be multiple package types within a language ecosystem)
|
||||
type Type string
|
||||
|
||||
func (t Type) String() string {
|
||||
return string(t)
|
||||
}
|
||||
|
||||
const (
|
||||
// the full set of supported packages
|
||||
UnknownPkg Type = "UnknownPackage"
|
||||
|
||||
@@ -18,6 +18,9 @@ const (
|
||||
// PURLQualifierUpstream this qualifier is not in the pURL spec, but is used by grype to perform indirect matching based on source information
|
||||
PURLQualifierUpstream = "upstream"
|
||||
|
||||
// PURLQualifierRpmModularity this qualifier is not in the pURL spec, but is used to specify RPM modularity information
|
||||
PURLQualifierRpmModularity = "rpmmod"
|
||||
|
||||
purlCargoPkgType = "cargo"
|
||||
purlGradlePkgType = "gradle"
|
||||
)
|
||||
|
||||
@@ -30,24 +30,21 @@ type Config struct {
|
||||
type directorySource struct {
|
||||
id artifact.ID
|
||||
config Config
|
||||
resolver *fileresolver.Directory
|
||||
resolver file.Resolver
|
||||
mutex *sync.Mutex
|
||||
}
|
||||
|
||||
func NewFromPath(path string) (source.Source, error) {
|
||||
cfg := Config{
|
||||
Path: path,
|
||||
}
|
||||
return New(cfg)
|
||||
return New(Config{Path: path})
|
||||
}
|
||||
|
||||
func New(cfg Config) (source.Source, error) {
|
||||
fi, err := os.Stat(cfg.Path)
|
||||
fileMeta, err := os.Stat(cfg.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err)
|
||||
}
|
||||
|
||||
if !fi.IsDir() {
|
||||
if !fileMeta.IsDir() {
|
||||
return nil, fmt.Errorf("given path is not a directory: %q", cfg.Path)
|
||||
}
|
||||
|
||||
@@ -58,53 +55,6 @@ func New(cfg Config) (source.Source, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// deriveIDFromDirectory generates an artifact ID from the given directory config. If an alias is provided, then
|
||||
// the artifact ID is derived exclusively from the alias name and version. Otherwise, the artifact ID is derived
|
||||
// from the path provided with an attempt to prune a prefix if a base is given. Since the contents of the directory
|
||||
// are not considered, there is no semantic meaning to the artifact ID -- this is why the alias is preferred without
|
||||
// consideration for the path.
|
||||
func deriveIDFromDirectory(cfg Config) artifact.ID {
|
||||
var info string
|
||||
if !cfg.Alias.IsEmpty() {
|
||||
// don't use any of the path information -- instead use the alias name and version as the artifact ID.
|
||||
// why? this allows the user to set a dependable stable value for the artifact ID in case the
|
||||
// scanning root changes (e.g. a user scans a directory, then moves it to a new location and scans again).
|
||||
info = fmt.Sprintf("%s@%s", cfg.Alias.Name, cfg.Alias.Version)
|
||||
} else {
|
||||
log.Warn("no explicit name and version provided for directory source, deriving artifact ID from the given path (which is not ideal)")
|
||||
info = cleanDirPath(cfg.Path, cfg.Base)
|
||||
}
|
||||
|
||||
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(filepath.Clean(info)).String())
|
||||
}
|
||||
|
||||
func cleanDirPath(path, base string) string {
|
||||
if path == base {
|
||||
return path
|
||||
}
|
||||
|
||||
if base != "" {
|
||||
cleanRoot, rootErr := fileresolver.NormalizeRootDirectory(path)
|
||||
cleanBase, baseErr := fileresolver.NormalizeBaseDirectory(base)
|
||||
|
||||
if rootErr == nil && baseErr == nil {
|
||||
// allows for normalizing inputs:
|
||||
// cleanRoot: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001/some/path
|
||||
// cleanBase: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001
|
||||
// normalized: some/path
|
||||
|
||||
relPath, err := filepath.Rel(cleanBase, cleanRoot)
|
||||
if err == nil {
|
||||
path = relPath
|
||||
}
|
||||
// this is odd, but this means we can't use base
|
||||
}
|
||||
// if the base is not a valid chroot, then just use the path as-is
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
func (s directorySource) ID() artifact.ID {
|
||||
return s.id
|
||||
}
|
||||
@@ -118,9 +68,11 @@ func (s directorySource) Describe() source.Description {
|
||||
if a.Name != "" {
|
||||
name = a.Name
|
||||
}
|
||||
|
||||
if a.Version != "" {
|
||||
version = a.Version
|
||||
}
|
||||
|
||||
if a.Supplier != "" {
|
||||
supplier = a.Supplier
|
||||
}
|
||||
@@ -141,29 +93,31 @@ func (s *directorySource) FileResolver(_ source.Scope) (file.Resolver, error) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if s.resolver == nil {
|
||||
exclusionFunctions, err := GetDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// this should be the only file resolver that might have overlap with where files are cached
|
||||
exclusionFunctions = append(exclusionFunctions, excludeCachePathVisitors()...)
|
||||
|
||||
res, err := fileresolver.NewFromDirectory(s.config.Path, s.config.Base, exclusionFunctions...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create directory resolver: %w", err)
|
||||
}
|
||||
|
||||
s.resolver = res
|
||||
if s.resolver != nil {
|
||||
return s.resolver, nil
|
||||
}
|
||||
|
||||
exclusionFunctions, err := GetDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// this should be the only file resolver that might have overlap with where files are cached
|
||||
exclusionFunctions = append(exclusionFunctions, excludeCachePathVisitors()...)
|
||||
|
||||
res, err := fileresolver.NewFromDirectory(s.config.Path, s.config.Base, exclusionFunctions...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create directory resolver: %w", err)
|
||||
}
|
||||
|
||||
s.resolver = res
|
||||
return s.resolver, nil
|
||||
}
|
||||
|
||||
func (s *directorySource) Close() error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
s.resolver = nil
|
||||
return nil
|
||||
}
|
||||
@@ -221,3 +175,50 @@ func GetDirectoryExclusionFunctions(root string, exclusions []string) ([]fileres
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// deriveIDFromDirectory generates an artifact ID from the given directory config. If an alias is provided, then
|
||||
// the artifact ID is derived exclusively from the alias name and version. Otherwise, the artifact ID is derived
|
||||
// from the path provided with an attempt to prune a prefix if a base is given. Since the contents of the directory
|
||||
// are not considered, there is no semantic meaning to the artifact ID -- this is why the alias is preferred without
|
||||
// consideration for the path.
|
||||
func deriveIDFromDirectory(cfg Config) artifact.ID {
|
||||
var info string
|
||||
if !cfg.Alias.IsEmpty() {
|
||||
// don't use any of the path information -- instead use the alias name and version as the artifact ID.
|
||||
// why? this allows the user to set a dependable stable value for the artifact ID in case the
|
||||
// scanning root changes (e.g. a user scans a directory, then moves it to a new location and scans again).
|
||||
info = fmt.Sprintf("%s@%s", cfg.Alias.Name, cfg.Alias.Version)
|
||||
} else {
|
||||
log.Warn("no explicit name and version provided for directory source, deriving artifact ID from the given path (which is not ideal)")
|
||||
info = cleanDirPath(cfg.Path, cfg.Base)
|
||||
}
|
||||
|
||||
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(filepath.Clean(info)).String())
|
||||
}
|
||||
|
||||
func cleanDirPath(path, base string) string {
|
||||
if path == base {
|
||||
return path
|
||||
}
|
||||
|
||||
if base != "" {
|
||||
cleanRoot, rootErr := fileresolver.NormalizeRootDirectory(path)
|
||||
cleanBase, baseErr := fileresolver.NormalizeBaseDirectory(base)
|
||||
|
||||
if rootErr == nil && baseErr == nil {
|
||||
// allows for normalizing inputs:
|
||||
// cleanRoot: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001/some/path
|
||||
// cleanBase: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001
|
||||
// normalized: some/path
|
||||
|
||||
relPath, err := filepath.Rel(cleanBase, cleanRoot)
|
||||
if err == nil {
|
||||
path = relPath
|
||||
}
|
||||
// this is odd, but this means we can't use base
|
||||
}
|
||||
// if the base is not a valid chroot, then just use the path as-is
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
@@ -50,7 +49,13 @@ func NewFromPath(path string) (source.Source, error) {
|
||||
}
|
||||
|
||||
func New(cfg Config) (source.Source, error) {
|
||||
fileMeta, err := os.Stat(cfg.Path)
|
||||
f, err := os.Open(cfg.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open file=%q: %w", cfg.Path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fileMeta, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err)
|
||||
}
|
||||
@@ -59,33 +64,19 @@ func New(cfg Config) (source.Source, error) {
|
||||
return nil, fmt.Errorf("given path is a directory: %q", cfg.Path)
|
||||
}
|
||||
|
||||
analysisPath, cleanupFn, err := fileAnalysisPath(cfg.Path, cfg.SkipExtractArchive)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to extract file analysis path=%q: %w", cfg.Path, err)
|
||||
}
|
||||
|
||||
var digests []file.Digest
|
||||
if len(cfg.DigestAlgorithms) > 0 {
|
||||
fh, err := os.Open(cfg.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open file=%q: %w", cfg.Path, err)
|
||||
}
|
||||
|
||||
defer fh.Close()
|
||||
|
||||
digests, err = intFile.NewDigestsFromFile(context.TODO(), fh, cfg.DigestAlgorithms)
|
||||
digests, err = intFile.NewDigestsFromFile(context.TODO(), f, cfg.DigestAlgorithms)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to calculate digests for file=%q: %w", cfg.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
fh, err := os.Open(cfg.Path)
|
||||
analysisPath, cleanupFn, err := fileAnalysisPath(cfg.Path, cfg.SkipExtractArchive)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open file=%q: %w", cfg.Path, err)
|
||||
return nil, fmt.Errorf("unable to extract file analysis path=%q: %w", cfg.Path, err)
|
||||
}
|
||||
|
||||
defer fh.Close()
|
||||
|
||||
id, versionDigest := deriveIDFromFile(cfg)
|
||||
|
||||
return &fileSource{
|
||||
@@ -96,26 +87,10 @@ func New(cfg Config) (source.Source, error) {
|
||||
analysisPath: analysisPath,
|
||||
digestForVersion: versionDigest,
|
||||
digests: digests,
|
||||
mimeType: stereoFile.MIMEType(fh),
|
||||
mimeType: stereoFile.MIMEType(f),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// deriveIDFromFile derives an artifact ID from the contents of a file. If an alias is provided, it will be included
|
||||
// in the ID derivation (along with contents). This way if the user scans the same item but is considered to be
|
||||
// logically different, then ID will express that.
|
||||
func deriveIDFromFile(cfg Config) (artifact.ID, string) {
|
||||
d := digestOfFileContents(cfg.Path)
|
||||
info := d
|
||||
|
||||
if !cfg.Alias.IsEmpty() {
|
||||
// if the user provided an alias, we want to consider that in the artifact ID. This way if the user
|
||||
// scans the same item but is considered to be logically different, then ID will express that.
|
||||
info += fmt.Sprintf(":%s@%s", cfg.Alias.Name, cfg.Alias.Version)
|
||||
}
|
||||
|
||||
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(info).String()), d
|
||||
}
|
||||
|
||||
func (s fileSource) ID() artifact.ID {
|
||||
return s.id
|
||||
}
|
||||
@@ -168,52 +143,56 @@ func (s fileSource) FileResolver(_ source.Scope) (file.Resolver, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to stat path=%q: %w", s.analysisPath, err)
|
||||
}
|
||||
isArchiveAnalysis := fi.IsDir()
|
||||
|
||||
absParentDir, err := absoluteSymlinkFreePathToParent(s.analysisPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isArchiveAnalysis {
|
||||
if isArchiveAnalysis := fi.IsDir(); isArchiveAnalysis {
|
||||
// this is an analysis of an archive file... we should scan the directory where the archive contents
|
||||
res, err := fileresolver.NewFromDirectory(s.analysisPath, "", exclusionFunctions...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create directory resolver: %w", err)
|
||||
}
|
||||
|
||||
s.resolver = res
|
||||
return s.resolver, nil
|
||||
}
|
||||
|
||||
// This is analysis of a single file. Use file indexer.
|
||||
res, err := fileresolver.NewFromFile(absParentDir, s.analysisPath, exclusionFunctions...)
|
||||
res, err := fileresolver.NewFromFile(s.analysisPath, exclusionFunctions...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create file resolver: %w", err)
|
||||
}
|
||||
|
||||
s.resolver = res
|
||||
return s.resolver, nil
|
||||
}
|
||||
|
||||
func absoluteSymlinkFreePathToParent(path string) (string, error) {
|
||||
absAnalysisPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to get absolute path for analysis path=%q: %w", path, err)
|
||||
}
|
||||
dereferencedAbsAnalysisPath, err := filepath.EvalSymlinks(absAnalysisPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to get absolute path for analysis path=%q: %w", path, err)
|
||||
}
|
||||
return filepath.Dir(dereferencedAbsAnalysisPath), nil
|
||||
}
|
||||
|
||||
func (s *fileSource) Close() error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if s.closer == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.resolver = nil
|
||||
return s.closer()
|
||||
}
|
||||
|
||||
// deriveIDFromFile derives an artifact ID from the contents of a file. If an alias is provided, it will be included
|
||||
// in the ID derivation (along with contents). This way if the user scans the same item but is considered to be
|
||||
// logically different, then ID will express that.
|
||||
func deriveIDFromFile(cfg Config) (artifact.ID, string) {
|
||||
d := digestOfFileContents(cfg.Path)
|
||||
info := d
|
||||
|
||||
if !cfg.Alias.IsEmpty() {
|
||||
// if the user provided an alias, we want to consider that in the artifact ID. This way if the user
|
||||
// scans the same item but is considered to be logically different, then ID will express that.
|
||||
info += fmt.Sprintf(":%s@%s", cfg.Alias.Name, cfg.Alias.Version)
|
||||
}
|
||||
|
||||
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(info).String()), d
|
||||
}
|
||||
|
||||
// fileAnalysisPath returns the path given, or in the case the path is an archive, the location where the archive
|
||||
// contents have been made available. A cleanup function is provided for any temp files created (if any).
|
||||
// Users can disable unpacking archives, allowing individual cataloguers to extract them instead (where
|
||||
@@ -253,15 +232,17 @@ func fileAnalysisPath(path string, skipExtractArchive bool) (string, func() erro
|
||||
}
|
||||
|
||||
func digestOfFileContents(path string) string {
|
||||
file, err := os.Open(path)
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return digest.SHA256.FromString(path).String()
|
||||
}
|
||||
defer file.Close()
|
||||
di, err := digest.SHA256.FromReader(file)
|
||||
defer f.Close()
|
||||
|
||||
di, err := digest.SHA256.FromReader(f)
|
||||
if err != nil {
|
||||
return digest.SHA256.FromString(path).String()
|
||||
}
|
||||
|
||||
return di.String()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user