mirror of
https://github.com/anchore/syft.git
synced 2026-10-26 12:02:23 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d5ff0fd8e | ||
|
|
c5e3b631ac | ||
|
|
05b9d35ee6 | ||
|
|
555376e250 | ||
|
|
fee878028a | ||
|
|
5370daf027 | ||
|
|
8d838b18a8 | ||
|
|
bf2d5ed87e | ||
|
|
a350e5889b | ||
|
|
e4393e7a3d | ||
|
|
26d1975870 | ||
|
|
babb09b3a4 | ||
|
|
b207bc8ee2 | ||
|
|
5b5fa7ec90 | ||
|
|
6d730d24dd | ||
|
|
b22fd987db | ||
|
|
3b4d4b186f | ||
|
|
407769e88c | ||
|
|
1416e3cb7a | ||
|
|
0ccfee03f1 | ||
|
|
bb1facbf81 | ||
|
|
ae7cd6bbb7 | ||
|
|
7bcdafe745 | ||
|
|
4576c081b9 | ||
|
|
d5779a9822 | ||
|
|
9ec3ad58c8 | ||
|
|
176dfdd9c1 | ||
|
|
0f6288881b | ||
|
|
6a49717314 | ||
|
|
06641cfda2 | ||
|
|
a5537943fa | ||
|
|
0030880e74 | ||
|
|
fc8b431ea6 | ||
|
|
bb70b0b43e | ||
|
|
ee0a02621a | ||
|
|
33c27c4f3d | ||
|
|
133d180eec |
@@ -1,4 +1,4 @@
|
||||
name: 'Acceptance'
|
||||
name: "Acceptance"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
@@ -16,7 +16,6 @@ jobs:
|
||||
Build-Snapshot-Artifacts:
|
||||
runs-on: macos-latest # We're creating these snapshot builds on macOS to be consistent with our release workflow's build process, which also takes place on macOS (due to code signing requirements).
|
||||
steps:
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
@@ -25,7 +24,7 @@ jobs:
|
||||
|
||||
- name: Restore bootstrap cache
|
||||
id: cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -75,11 +74,10 @@ jobs:
|
||||
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
Acceptance-Linux:
|
||||
needs: [ Build-Snapshot-Artifacts ]
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
# come Nov 30 2020 ubuntu-latest will be ubuntu-20.04, until then it needs to be explicitly referenced due to python 3.7 specific features being used
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
@@ -101,10 +99,9 @@ jobs:
|
||||
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
Acceptance-Mac:
|
||||
needs: [ Build-Snapshot-Artifacts ]
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
@@ -126,11 +123,10 @@ jobs:
|
||||
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
Inline-Compare:
|
||||
needs: [ Build-Snapshot-Artifacts ]
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
# come Nov 30 2020 ubuntu-latest will be ubuntu-20.04, until then it needs to be explicitly referenced due to python 3.7 specific features being used
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Fingerprint inline-compare sources
|
||||
@@ -138,7 +134,7 @@ jobs:
|
||||
|
||||
- name: Restore inline reports cache
|
||||
id: cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/test/inline-compare/inline-reports
|
||||
key: inline-reports-${{ hashFiles('**/inline-compare.fingerprint') }}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
name: 'Release'
|
||||
name: "Release"
|
||||
on:
|
||||
push:
|
||||
# take no actions on push to any branch...
|
||||
branches-ignore:
|
||||
- '**'
|
||||
- "**"
|
||||
# ... only act on release tags
|
||||
tags:
|
||||
- 'v*'
|
||||
- "v*"
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.14.x"
|
||||
@@ -15,7 +15,6 @@ jobs:
|
||||
wait-for-checks:
|
||||
runs-on: ubuntu-latest # This OS choice is arbitrary. None of the steps in this job are specific to either Linux or macOS.
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# we don't want to release commits that have been pushed and tagged, but not necessarily merged onto main
|
||||
@@ -81,10 +80,9 @@ jobs:
|
||||
false
|
||||
|
||||
release:
|
||||
needs: [ wait-for-checks ]
|
||||
needs: [wait-for-checks]
|
||||
runs-on: macos-latest # Due to our code signing process, it's vital that we run our release steps on macOS.
|
||||
steps:
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
@@ -96,7 +94,7 @@ jobs:
|
||||
# We are expecting this cache to have been created during the "Build-Snapshot-Artifacts" job in the "Acceptance" workflow.
|
||||
- name: Restore bootstrap cache
|
||||
id: cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
name: 'Static Analysis + Unit + Integration'
|
||||
name: "Static Analysis + Unit + Integration"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
pull_request:
|
||||
jobs:
|
||||
|
||||
Static-Analysis:
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -12,7 +11,6 @@ jobs:
|
||||
platform: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
@@ -21,7 +19,7 @@ jobs:
|
||||
|
||||
- name: Restore bootstrap cache
|
||||
id: bootstrap-cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -49,7 +47,6 @@ jobs:
|
||||
platform: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
@@ -58,7 +55,7 @@ jobs:
|
||||
|
||||
- name: Restore bootstrap cache
|
||||
id: bootstrap-cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -80,7 +77,7 @@ jobs:
|
||||
|
||||
- name: Restore Java test-fixture cache
|
||||
id: unit-java-cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: syft/cataloger/java/test-fixtures/java-builds/packages
|
||||
key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/cataloger/java/test-fixtures/java-builds/packages.fingerprint' ) }}
|
||||
@@ -95,7 +92,7 @@ jobs:
|
||||
run: make integration-fingerprint
|
||||
|
||||
- name: Restore integration test cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/integration/test-fixtures/cache
|
||||
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('integration/test-fixtures/cache.fingerprint') }}
|
||||
|
||||
@@ -19,6 +19,7 @@ COVERAGE_THRESHOLD := 68
|
||||
# CI cache busting values; change these if you want CI to not use previous stored cache
|
||||
COMPARE_CACHE_BUSTER="f7e689d76a9"
|
||||
INTEGRATION_CACHE_BUSTER="789bacdf"
|
||||
BOOTSTRAP_CACHE="789bacdf"
|
||||
|
||||
## Build variables
|
||||
DISTDIR=./dist
|
||||
|
||||
+2
-11
@@ -158,22 +158,13 @@ func doImport(src source.Source, s source.Metadata, catalog *pkg.Catalog, d *dis
|
||||
}
|
||||
}
|
||||
|
||||
var scheme string
|
||||
var hostname = appConfig.Anchore.Host
|
||||
urlFields := strings.Split(hostname, "://")
|
||||
if len(urlFields) > 1 {
|
||||
scheme = urlFields[0]
|
||||
hostname = urlFields[1]
|
||||
}
|
||||
|
||||
c, err := anchore.NewClient(anchore.Configuration{
|
||||
Hostname: hostname,
|
||||
BaseURL: appConfig.Anchore.Host,
|
||||
Username: appConfig.Anchore.Username,
|
||||
Password: appConfig.Anchore.Password,
|
||||
Scheme: scheme,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create anchore client: %+v", err)
|
||||
return fmt.Errorf("unable to upload results: %w", err)
|
||||
}
|
||||
|
||||
importCfg := anchore.ImportConfig{
|
||||
|
||||
@@ -5,11 +5,11 @@ go 1.14
|
||||
require (
|
||||
github.com/adrg/xdg v0.2.1
|
||||
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921
|
||||
github.com/anchore/client-go v0.0.0-20201216213038-a486b838e238
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf
|
||||
github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||
github.com/anchore/stereoscope v0.0.0-20210104203718-4c1d1bd9a255
|
||||
github.com/anchore/stereoscope v0.0.0-20210201165248-e94c52b4052d
|
||||
github.com/antihax/optional v1.0.0
|
||||
github.com/bmatcuk/doublestar/v2 v2.0.4
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
|
||||
|
||||
@@ -126,16 +126,16 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/anchore/client-go v0.0.0-20201216213038-a486b838e238 h1:/iI+1cj1a27ow0wj378pPJIm8sCSy6I21Tz6oLbLDQY=
|
||||
github.com/anchore/client-go v0.0.0-20201216213038-a486b838e238/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12 h1:xbeIbn5F52JVx3RUIajxCj8b0y+9lywspql4sFhcxWQ=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12/go.mod h1:juoyWXIj7sJ1IDl4E/KIfyLtovbs5XQVSIdaQifFQT8=
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8=
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ=
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods=
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
||||
github.com/anchore/stereoscope v0.0.0-20210104203718-4c1d1bd9a255 h1:Ng7BDr9PQTCztANogjfEdEjjWUylhlPyZPhtarIGo00=
|
||||
github.com/anchore/stereoscope v0.0.0-20210104203718-4c1d1bd9a255/go.mod h1:BMdPL0QEIYfpjQ3M7sHYZvuh6+vcomqF3TMHL8gr6Vw=
|
||||
github.com/anchore/stereoscope v0.0.0-20210201165248-e94c52b4052d h1:2hv5NOZ0fD8tPk1UdGiW9PHxmjBmBLL+sFlhLXjjKgo=
|
||||
github.com/anchore/stereoscope v0.0.0-20210201165248-e94c52b4052d/go.mod h1:lhSEYyGLXTXMIFHAz7Ls/MNQ5EjYd5ziLxovKZp1xOs=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
@@ -845,6 +845,7 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
|
||||
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
|
||||
github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/vbatts/tar-split v0.11.1/go.mod h1:LEuURwDEiWjRjwu46yU3KVGuUdVv/dcnpcEPSzR8z6g=
|
||||
github.com/vdemeester/k8s-pkg-credentialprovider v1.17.4/go.mod h1:inCTmtUdr5KJbreVojo06krnTgaeAz/Z7lynpPk/Q2c=
|
||||
github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU=
|
||||
github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d h1:KOxOL6qpmqwoPloNwi+CEgc1ayjHNOFNrvoOmeDOjDg=
|
||||
|
||||
@@ -2,7 +2,11 @@ package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal"
|
||||
@@ -10,11 +14,10 @@ import (
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
Hostname string
|
||||
BaseURL string
|
||||
Username string
|
||||
Password string
|
||||
UserAgent string
|
||||
Scheme string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
@@ -29,16 +32,16 @@ func NewClient(cfg Configuration) (*Client, error) {
|
||||
cfg.UserAgent = fmt.Sprintf("%s / %s %s", internal.ApplicationName, versionInfo.Version, versionInfo.Platform)
|
||||
}
|
||||
|
||||
if cfg.Scheme == "" {
|
||||
cfg.Scheme = "https"
|
||||
baseURL, err := prepareBaseURLForClient(cfg.BaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create client: %w", err)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
config: cfg,
|
||||
client: external.NewAPIClient(&external.Configuration{
|
||||
Host: cfg.Hostname,
|
||||
BasePath: baseURL,
|
||||
UserAgent: cfg.UserAgent,
|
||||
Scheme: cfg.Scheme,
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
@@ -56,3 +59,57 @@ func (c *Client) newRequestContext(parentContext context.Context) context.Contex
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
var ErrInvalidBaseURLInput = errors.New("invalid base URL input")
|
||||
|
||||
func prepareBaseURLForClient(baseURL string) (string, error) {
|
||||
if err := checkBaseURLInput(baseURL); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
scheme, urlWithoutScheme := splitSchemeFromURL(baseURL)
|
||||
|
||||
if scheme == "" {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
urlWithoutScheme = path.Clean(urlWithoutScheme)
|
||||
|
||||
const requiredSuffix = "v1"
|
||||
if path.Base(urlWithoutScheme) != requiredSuffix {
|
||||
urlWithoutScheme = path.Join(urlWithoutScheme, requiredSuffix)
|
||||
}
|
||||
|
||||
preparedBaseURL := scheme + "://" + urlWithoutScheme
|
||||
return preparedBaseURL, nil
|
||||
}
|
||||
|
||||
func checkBaseURLInput(url string) error {
|
||||
if url == "" {
|
||||
return ErrInvalidBaseURLInput
|
||||
}
|
||||
|
||||
firstCharacter := rune(url[0])
|
||||
if !(unicode.IsLetter(firstCharacter)) {
|
||||
return ErrInvalidBaseURLInput
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitSchemeFromURL(url string) (scheme, urlWithoutScheme string) {
|
||||
if hasScheme(url) {
|
||||
urlParts := strings.SplitN(url, "://", 2)
|
||||
scheme = urlParts[0]
|
||||
urlWithoutScheme = urlParts[1]
|
||||
return
|
||||
}
|
||||
|
||||
return "", url
|
||||
}
|
||||
|
||||
func hasScheme(url string) bool {
|
||||
parts := strings.Split(url, "://")
|
||||
|
||||
return len(parts) > 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
package anchore
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestHasScheme(t *testing.T) {
|
||||
cases := []struct {
|
||||
url string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
url: "http://localhost",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
url: "https://anchore.com:8443",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
url: "google.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
url: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.url, func(t *testing.T) {
|
||||
result := hasScheme(testCase.url)
|
||||
|
||||
if testCase.expected != result {
|
||||
t.Errorf("expected %t but got %t", testCase.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareBaseURLForClient(t *testing.T) {
|
||||
cases := []struct {
|
||||
inputURL string
|
||||
expectedURL string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
inputURL: "",
|
||||
expectedURL: "",
|
||||
expectedErr: ErrInvalidBaseURLInput,
|
||||
},
|
||||
{
|
||||
inputURL: "localhost",
|
||||
expectedURL: "http://localhost/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "https://localhost",
|
||||
expectedURL: "https://localhost/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "https://localhost/",
|
||||
expectedURL: "https://localhost/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "https://localhost/v1/",
|
||||
expectedURL: "https://localhost/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "https://localhost/v1//",
|
||||
expectedURL: "https://localhost/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "http://something.com/platform/v1/services/anchore",
|
||||
expectedURL: "http://something.com/platform/v1/services/anchore/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "my-host:8228",
|
||||
expectedURL: "http://my-host:8228/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "v1/v1",
|
||||
expectedURL: "http://v1/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "/v1",
|
||||
expectedURL: "",
|
||||
expectedErr: ErrInvalidBaseURLInput,
|
||||
},
|
||||
{
|
||||
inputURL: "/imports/images",
|
||||
expectedURL: "",
|
||||
expectedErr: ErrInvalidBaseURLInput,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.inputURL, func(t *testing.T) {
|
||||
resultURL, err := prepareBaseURLForClient(testCase.inputURL)
|
||||
if err != testCase.expectedErr {
|
||||
t.Errorf("expected err to be '%v' but got '%v'", testCase.expectedErr, err)
|
||||
}
|
||||
|
||||
if resultURL != testCase.expectedURL {
|
||||
t.Errorf("expected URL to be '%v' but got '%v'", testCase.expectedURL, resultURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckBaseURLInput(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
input: "",
|
||||
expected: ErrInvalidBaseURLInput,
|
||||
},
|
||||
{
|
||||
input: "x",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
input: "localhost:8000",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
input: ":80",
|
||||
expected: ErrInvalidBaseURLInput,
|
||||
},
|
||||
{
|
||||
input: "/v1",
|
||||
expected: ErrInvalidBaseURLInput,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.input, func(t *testing.T) {
|
||||
resultErr := checkBaseURLInput(testCase.input)
|
||||
|
||||
if testCase.expected != resultErr {
|
||||
t.Errorf("expected err to be '%v' but got '%v'", testCase.expected, resultErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSchemeFromURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
expectedScheme string
|
||||
expectedURLWithoutScheme string
|
||||
}{
|
||||
{
|
||||
input: "",
|
||||
expectedScheme: "",
|
||||
expectedURLWithoutScheme: "",
|
||||
},
|
||||
{
|
||||
input: "localhost",
|
||||
expectedScheme: "",
|
||||
expectedURLWithoutScheme: "localhost",
|
||||
},
|
||||
{
|
||||
input: "https://anchore.com/path",
|
||||
expectedScheme: "https",
|
||||
expectedURLWithoutScheme: "anchore.com/path",
|
||||
},
|
||||
{
|
||||
input: "tcp://host:1234",
|
||||
expectedScheme: "tcp",
|
||||
expectedURLWithoutScheme: "host:1234",
|
||||
},
|
||||
{
|
||||
input: "/hello",
|
||||
expectedScheme: "",
|
||||
expectedURLWithoutScheme: "/hello",
|
||||
},
|
||||
{
|
||||
input: "://host",
|
||||
expectedScheme: "",
|
||||
expectedURLWithoutScheme: "host",
|
||||
},
|
||||
{
|
||||
input: "http//localhost",
|
||||
expectedScheme: "",
|
||||
expectedURLWithoutScheme: "http//localhost",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.input, func(t *testing.T) {
|
||||
resultScheme, resultURLWithoutScheme := splitSchemeFromURL(testCase.input)
|
||||
|
||||
if testCase.expectedScheme != resultScheme {
|
||||
t.Errorf("expected scheme to be '%s' but got '%s'", testCase.expectedScheme, resultScheme)
|
||||
}
|
||||
|
||||
if testCase.expectedURLWithoutScheme != resultURLWithoutScheme {
|
||||
t.Errorf("expected urlWithoutScheme to be '%s' but got '%s'", testCase.expectedURLWithoutScheme, resultURLWithoutScheme)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func importProgress(source string) (*progress.Stage, *progress.Manual) {
|
||||
|
||||
// nolint:funlen
|
||||
func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
||||
stage, prog := importProgress(c.config.Hostname)
|
||||
stage, prog := importProgress(c.config.BaseURL)
|
||||
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestPackageSbomToModel(t *testing.T) {
|
||||
FoundBy: "foundBy",
|
||||
Locations: []source.Location{
|
||||
{
|
||||
Path: "path",
|
||||
RealPath: "path",
|
||||
FileSystemID: "layerID",
|
||||
},
|
||||
},
|
||||
@@ -107,6 +107,10 @@ func TestPackageSbomToModel(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, d := range deep.Equal(actualDoc, expectedDoc) {
|
||||
if strings.HasSuffix(d, "<nil slice> != []") {
|
||||
// do not consider nil vs empty collection semantics as a "difference"
|
||||
continue
|
||||
}
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
}
|
||||
@@ -139,7 +143,7 @@ func TestPackageSbomImport(t *testing.T) {
|
||||
FoundBy: "foundBy",
|
||||
Locations: []source.Location{
|
||||
{
|
||||
Path: "path",
|
||||
RealPath: "path",
|
||||
FileSystemID: "layerID",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,5 +6,5 @@ const (
|
||||
|
||||
// JSONSchemaVersion is the current schema version output by the JSON presenter
|
||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||
JSONSchemaVersion = "1.0.1"
|
||||
JSONSchemaVersion = "1.0.2"
|
||||
)
|
||||
|
||||
@@ -23,7 +23,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/anchore/syft/internal/logger"
|
||||
|
||||
@@ -102,6 +104,8 @@ func OutputToEphemeralTUI(workerErrs <-chan error, subscription *partybus.Subscr
|
||||
ctx := context.Background()
|
||||
syftUIHandler := ui.NewHandler()
|
||||
|
||||
signals := interruptingSignals()
|
||||
|
||||
eventLoop:
|
||||
for {
|
||||
select {
|
||||
@@ -154,8 +158,23 @@ eventLoop:
|
||||
log.Errorf("cancelled (%+v)", err)
|
||||
}
|
||||
break eventLoop
|
||||
case <-signals:
|
||||
break eventLoop
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func interruptingSignals() chan os.Signal {
|
||||
c := make(chan os.Signal, 1) // Note: A buffered channel is recommended for this; see https://golang.org/pkg/os/signal/#Notify
|
||||
|
||||
interruptions := []os.Signal{
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
}
|
||||
|
||||
signal.Notify(c, interruptions...)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -0,0 +1,718 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/Document",
|
||||
"definitions": {
|
||||
"ApkFileRecord": {
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"ownerUid": {
|
||||
"type": "string"
|
||||
},
|
||||
"ownerGid": {
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"type": "string"
|
||||
},
|
||||
"checksum": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"ApkMetadata": {
|
||||
"required": [
|
||||
"package",
|
||||
"originPackage",
|
||||
"maintainer",
|
||||
"version",
|
||||
"license",
|
||||
"architecture",
|
||||
"url",
|
||||
"description",
|
||||
"size",
|
||||
"installedSize",
|
||||
"pullDependencies",
|
||||
"pullChecksum",
|
||||
"gitCommitOfApkPort",
|
||||
"files"
|
||||
],
|
||||
"properties": {
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"originPackage": {
|
||||
"type": "string"
|
||||
},
|
||||
"maintainer": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"license": {
|
||||
"type": "string"
|
||||
},
|
||||
"architecture": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"installedSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pullDependencies": {
|
||||
"type": "string"
|
||||
},
|
||||
"pullChecksum": {
|
||||
"type": "string"
|
||||
},
|
||||
"gitCommitOfApkPort": {
|
||||
"type": "string"
|
||||
},
|
||||
"files": {
|
||||
"items": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/ApkFileRecord"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"Descriptor": {
|
||||
"required": [
|
||||
"name",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"Distribution": {
|
||||
"required": [
|
||||
"name",
|
||||
"version",
|
||||
"idLike"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"idLike": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"Document": {
|
||||
"required": [
|
||||
"artifacts",
|
||||
"source",
|
||||
"distro",
|
||||
"descriptor",
|
||||
"schema",
|
||||
"artifactRelationships"
|
||||
],
|
||||
"properties": {
|
||||
"artifacts": {
|
||||
"items": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/Package"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"source": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/Source"
|
||||
},
|
||||
"distro": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/Distribution"
|
||||
},
|
||||
"descriptor": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/Descriptor"
|
||||
},
|
||||
"schema": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/Schema"
|
||||
},
|
||||
"artifactRelationships": {
|
||||
"items": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/Relationship"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"DpkgFileRecord": {
|
||||
"required": [
|
||||
"path",
|
||||
"md5"
|
||||
],
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"md5": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"DpkgMetadata": {
|
||||
"required": [
|
||||
"package",
|
||||
"source",
|
||||
"version",
|
||||
"sourceVersion",
|
||||
"architecture",
|
||||
"maintainer",
|
||||
"installedSize",
|
||||
"files"
|
||||
],
|
||||
"properties": {
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"architecture": {
|
||||
"type": "string"
|
||||
},
|
||||
"maintainer": {
|
||||
"type": "string"
|
||||
},
|
||||
"installedSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"files": {
|
||||
"items": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/DpkgFileRecord"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"GemMetadata": {
|
||||
"required": [
|
||||
"name",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"files": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"authors": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"licenses": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"JavaManifest": {
|
||||
"properties": {
|
||||
"main": {
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"namedSections": {
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"JavaMetadata": {
|
||||
"required": [
|
||||
"virtualPath"
|
||||
],
|
||||
"properties": {
|
||||
"virtualPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"manifest": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/JavaManifest"
|
||||
},
|
||||
"pomProperties": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/PomProperties"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"Location": {
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"layerID": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"NpmPackageJSONMetadata": {
|
||||
"required": [
|
||||
"author",
|
||||
"licenses",
|
||||
"homepage",
|
||||
"description",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"files": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"author": {
|
||||
"type": "string"
|
||||
},
|
||||
"licenses": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"Package": {
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"version",
|
||||
"type",
|
||||
"foundBy",
|
||||
"locations",
|
||||
"licenses",
|
||||
"language",
|
||||
"cpes",
|
||||
"purl",
|
||||
"metadataType",
|
||||
"metadata"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"foundBy": {
|
||||
"type": "string"
|
||||
},
|
||||
"locations": {
|
||||
"items": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/Location"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"licenses": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"language": {
|
||||
"type": "string"
|
||||
},
|
||||
"cpes": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"purl": {
|
||||
"type": "string"
|
||||
},
|
||||
"metadataType": {
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/ApkMetadata"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/DpkgMetadata"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/GemMetadata"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/JavaMetadata"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/NpmPackageJSONMetadata"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/PythonPackageMetadata"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/RpmdbMetadata"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"PomProperties": {
|
||||
"required": [
|
||||
"path",
|
||||
"name",
|
||||
"groupId",
|
||||
"artifactId",
|
||||
"version",
|
||||
"extraFields"
|
||||
],
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"groupId": {
|
||||
"type": "string"
|
||||
},
|
||||
"artifactId": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"extraFields": {
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"PythonFileDigest": {
|
||||
"required": [
|
||||
"algorithm",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"algorithm": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"PythonFileRecord": {
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"digest": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/PythonFileDigest"
|
||||
},
|
||||
"size": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"PythonPackageMetadata": {
|
||||
"required": [
|
||||
"name",
|
||||
"version",
|
||||
"license",
|
||||
"author",
|
||||
"authorEmail",
|
||||
"platform",
|
||||
"sitePackagesRootPath"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"license": {
|
||||
"type": "string"
|
||||
},
|
||||
"author": {
|
||||
"type": "string"
|
||||
},
|
||||
"authorEmail": {
|
||||
"type": "string"
|
||||
},
|
||||
"platform": {
|
||||
"type": "string"
|
||||
},
|
||||
"files": {
|
||||
"items": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/PythonFileRecord"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"sitePackagesRootPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"topLevelPackages": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"Relationship": {
|
||||
"required": [
|
||||
"parent",
|
||||
"child",
|
||||
"type",
|
||||
"metadata"
|
||||
],
|
||||
"properties": {
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"child": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"RpmdbFileRecord": {
|
||||
"required": [
|
||||
"path",
|
||||
"mode",
|
||||
"size",
|
||||
"sha256"
|
||||
],
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "integer"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"RpmdbMetadata": {
|
||||
"required": [
|
||||
"name",
|
||||
"version",
|
||||
"epoch",
|
||||
"architecture",
|
||||
"release",
|
||||
"sourceRpm",
|
||||
"size",
|
||||
"license",
|
||||
"vendor",
|
||||
"files"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"epoch": {
|
||||
"type": "integer"
|
||||
},
|
||||
"architecture": {
|
||||
"type": "string"
|
||||
},
|
||||
"release": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceRpm": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"license": {
|
||||
"type": "string"
|
||||
},
|
||||
"vendor": {
|
||||
"type": "string"
|
||||
},
|
||||
"files": {
|
||||
"items": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/RpmdbFileRecord"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"Schema": {
|
||||
"required": [
|
||||
"version",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"Source": {
|
||||
"required": [
|
||||
"type",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,13 @@ package apkdb
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/cataloger/common"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
// NewApkdbCataloger returns a new Alpine DB cataloger object.
|
||||
func NewApkdbCataloger() *common.GenericCataloger {
|
||||
globParsers := map[string]common.ParserFn{
|
||||
"**/lib/apk/db/installed": parseApkDB,
|
||||
pkg.ApkDbGlob: parseApkDB,
|
||||
}
|
||||
|
||||
return common.NewGenericCataloger(nil, globParsers, "apkdb-cataloger")
|
||||
|
||||
@@ -39,6 +39,7 @@ func newMonitor() (*progress.Manual, *progress.Manual) {
|
||||
// request.
|
||||
func Catalog(resolver source.Resolver, theDistro *distro.Distro, catalogers ...Cataloger) (*pkg.Catalog, error) {
|
||||
catalog := pkg.NewCatalog()
|
||||
|
||||
filesProcessed, packagesDiscovered := newMonitor()
|
||||
|
||||
// perform analysis, accumulating errors for each failed analysis
|
||||
|
||||
@@ -101,7 +101,7 @@ func (c *GenericCataloger) catalog(contents map[source.Location]io.ReadCloser) (
|
||||
continue
|
||||
}
|
||||
|
||||
entries, err := parser(location.Path, content)
|
||||
entries, err := parser(location.RealPath, content)
|
||||
if err != nil {
|
||||
// TODO: should we fail? or only log?
|
||||
log.Warnf("cataloger '%s' failed to parse entries (location=%+v): %+v", c.upstreamCataloger, location, err)
|
||||
|
||||
@@ -21,6 +21,10 @@ func newTestResolver() *testResolverMock {
|
||||
}
|
||||
}
|
||||
|
||||
func (r testResolverMock) HasPath(path string) bool {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (r *testResolverMock) FileContentsByLocation(_ source.Location) (io.ReadCloser, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
@@ -97,7 +101,7 @@ func TestGenericCataloger(t *testing.T) {
|
||||
|
||||
for _, p := range actualPkgs {
|
||||
ref := p.Locations[0]
|
||||
exP, ok := expectedPkgs[ref.Path]
|
||||
exP, ok := expectedPkgs[ref.RealPath]
|
||||
if !ok {
|
||||
t.Errorf("missing expected pkg: ref=%+v", ref)
|
||||
continue
|
||||
|
||||
@@ -14,9 +14,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
dpkgStatusGlob = "**/var/lib/dpkg/status"
|
||||
md5sumsExt = ".md5sums"
|
||||
docsPath = "/usr/share/doc"
|
||||
md5sumsExt = ".md5sums"
|
||||
docsPath = "/usr/share/doc"
|
||||
)
|
||||
|
||||
type Cataloger struct{}
|
||||
@@ -34,7 +33,7 @@ func (c *Cataloger) Name() string {
|
||||
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing dpkg support files.
|
||||
// nolint:funlen
|
||||
func (c *Cataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) {
|
||||
dbFileMatches, err := resolver.FilesByGlob(dpkgStatusGlob)
|
||||
dbFileMatches, err := resolver.FilesByGlob(pkg.DpkgDbGlob)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find dpkg status files's by glob: %w", err)
|
||||
}
|
||||
@@ -48,7 +47,7 @@ func (c *Cataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) {
|
||||
|
||||
pkgs, err = parseDpkgStatus(dbContents)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to catalog dpkg package=%+v: %w", dbLocation.Path, err)
|
||||
return nil, fmt.Errorf("unable to catalog dpkg package=%+v: %w", dbLocation.RealPath, err)
|
||||
}
|
||||
|
||||
md5ContentsByName, md5RefsByName, err := fetchMd5Contents(resolver, dbLocation, pkgs)
|
||||
@@ -56,7 +55,7 @@ func (c *Cataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) {
|
||||
return nil, fmt.Errorf("unable to find dpkg md5 contents: %w", err)
|
||||
}
|
||||
|
||||
copyrightContentsByName, copyrightRefsByName, err := fetchCopyrightContents(resolver, dbLocation, pkgs)
|
||||
copyrightContentsByName, copyrightLocationByName, err := fetchCopyrightContents(resolver, dbLocation, pkgs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to find dpkg copyright contents: %w", err)
|
||||
}
|
||||
@@ -90,7 +89,7 @@ func (c *Cataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) {
|
||||
p.Licenses = parseLicensesFromCopyright(copyrightReader)
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
if ref, ok := copyrightRefsByName[p.Name]; ok {
|
||||
if ref, ok := copyrightLocationByName[p.Name]; ok {
|
||||
p.Locations = append(p.Locations, ref)
|
||||
}
|
||||
}
|
||||
@@ -104,20 +103,17 @@ func fetchMd5Contents(resolver source.Resolver, dbLocation source.Location, pkgs
|
||||
|
||||
var md5FileMatches []source.Location
|
||||
var nameByRef = make(map[source.Location]string)
|
||||
parentPath := filepath.Dir(dbLocation.Path)
|
||||
parentPath := filepath.Dir(dbLocation.RealPath)
|
||||
|
||||
for _, p := range pkgs {
|
||||
// look for /var/lib/dpkg/info/NAME:ARCH.md5sums
|
||||
name := md5Key(p)
|
||||
md5sumPath := path.Join(parentPath, "info", name+md5sumsExt)
|
||||
md5SumLocation := resolver.RelativeFileByPath(dbLocation, md5sumPath)
|
||||
md5SumLocation := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", name+md5sumsExt))
|
||||
|
||||
if md5SumLocation == nil {
|
||||
// the most specific key did not work, fallback to just the name
|
||||
// look for /var/lib/dpkg/info/NAME.md5sums
|
||||
name := p.Name
|
||||
md5sumPath := path.Join(parentPath, "info", name+md5sumsExt)
|
||||
md5SumLocation = resolver.RelativeFileByPath(dbLocation, md5sumPath)
|
||||
md5SumLocation = resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", p.Name+md5sumsExt))
|
||||
}
|
||||
// we should have at least one reference
|
||||
if md5SumLocation != nil {
|
||||
@@ -134,14 +130,14 @@ func fetchMd5Contents(resolver source.Resolver, dbLocation source.Location, pkgs
|
||||
|
||||
// organize content results and refs by a combination of name and architecture
|
||||
var contentsByName = make(map[string]io.Reader)
|
||||
var refsByName = make(map[string]source.Location)
|
||||
var locationByName = make(map[string]source.Location)
|
||||
for location, contents := range md5ContentsByLocation {
|
||||
name := nameByRef[location]
|
||||
contentsByName[name] = contents
|
||||
refsByName[name] = location
|
||||
locationByName[name] = location
|
||||
}
|
||||
|
||||
return contentsByName, refsByName, nil
|
||||
return contentsByName, locationByName, nil
|
||||
}
|
||||
|
||||
func fetchCopyrightContents(resolver source.Resolver, dbLocation source.Location, pkgs []pkg.Package) (map[string]io.Reader, map[string]source.Location, error) {
|
||||
|
||||
@@ -79,7 +79,7 @@ func TestDpkgCataloger(t *testing.T) {
|
||||
// we will test the sources separately
|
||||
var sourcesList = make([]string, len(a.Locations))
|
||||
for i, s := range a.Locations {
|
||||
sourcesList[i] = s.Path
|
||||
sourcesList[i] = s.RealPath
|
||||
}
|
||||
a.Locations = nil
|
||||
|
||||
|
||||
@@ -178,36 +178,40 @@ func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([
|
||||
return nil, fmt.Errorf("failed to parse pom.properties (%s): %w", j.virtualPath, err)
|
||||
}
|
||||
|
||||
if propsObj != nil {
|
||||
if propsObj.Version != "" && propsObj.ArtifactID != "" {
|
||||
// TODO: if there is no parentPkg (no java manifest) one of these poms could be the parent. We should discover the right parent and attach the correct info accordingly to each discovered package
|
||||
if propsObj == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// keep the artifact name within the virtual path if this package does not match the parent package
|
||||
vPathSuffix := ""
|
||||
if !strings.HasPrefix(propsObj.ArtifactID, parentPkg.Name) {
|
||||
vPathSuffix += ":" + propsObj.ArtifactID
|
||||
}
|
||||
virtualPath := j.virtualPath + vPathSuffix
|
||||
if propsObj.Version != "" && propsObj.ArtifactID != "" {
|
||||
// TODO: if there is no parentPkg (no java manifest) one of these poms could be the parent. We should discover the right parent and attach the correct info accordingly to each discovered package
|
||||
|
||||
// discovered props = new package
|
||||
p := pkg.Package{
|
||||
Name: propsObj.ArtifactID,
|
||||
Version: propsObj.Version,
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JavaPkg,
|
||||
MetadataType: pkg.JavaMetadataType,
|
||||
Metadata: pkg.JavaMetadata{
|
||||
VirtualPath: virtualPath,
|
||||
PomProperties: propsObj,
|
||||
Parent: parentPkg,
|
||||
},
|
||||
}
|
||||
// keep the artifact name within the virtual path if this package does not match the parent package
|
||||
vPathSuffix := ""
|
||||
if parentPkg != nil && !strings.HasPrefix(propsObj.ArtifactID, parentPkg.Name) {
|
||||
vPathSuffix += ":" + propsObj.ArtifactID
|
||||
}
|
||||
virtualPath := j.virtualPath + vPathSuffix
|
||||
|
||||
pkgKey := uniquePkgKey(&p)
|
||||
// discovered props = new package
|
||||
p := pkg.Package{
|
||||
Name: propsObj.ArtifactID,
|
||||
Version: propsObj.Version,
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JavaPkg,
|
||||
MetadataType: pkg.JavaMetadataType,
|
||||
Metadata: pkg.JavaMetadata{
|
||||
VirtualPath: virtualPath,
|
||||
PomProperties: propsObj,
|
||||
Parent: parentPkg,
|
||||
},
|
||||
}
|
||||
|
||||
// the name/version pair matches...
|
||||
matchesParentPkg := pkgKey == parentKey
|
||||
pkgKey := uniquePkgKey(&p)
|
||||
|
||||
// the name/version pair matches...
|
||||
matchesParentPkg := pkgKey == parentKey
|
||||
|
||||
if parentPkg != nil {
|
||||
// the virtual path matches...
|
||||
matchesParentPkg = matchesParentPkg || parentPkg.Metadata.(pkg.JavaMetadata).VirtualPath == virtualPath
|
||||
|
||||
@@ -231,11 +235,13 @@ func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([
|
||||
parentMetadata.PomProperties = propsObj
|
||||
parentPkg.Metadata = parentMetadata
|
||||
}
|
||||
} else if !j.discoveredPkgs.Contains(pkgKey) {
|
||||
// only keep packages we haven't seen yet (and are not related to the parent package)
|
||||
pkgs = append(pkgs, p)
|
||||
}
|
||||
}
|
||||
|
||||
if !matchesParentPkg && !j.discoveredPkgs.Contains(pkgKey) {
|
||||
// only keep packages we haven't seen yet (and are not related to the parent package)
|
||||
pkgs = append(pkgs, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
return pkgs, nil
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"io"
|
||||
"regexp"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
@@ -172,6 +174,11 @@ func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||
return nil, fmt.Errorf("failed to parse package.json file: %w", err)
|
||||
}
|
||||
|
||||
if !p.hasNameAndVersionValues() {
|
||||
log.Debug("encountered package.json file without a name and/or version field, ignoring this file")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
licenses, err := licensesFromJSON(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse package.json file: %w", err)
|
||||
@@ -195,3 +202,7 @@ func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||
|
||||
return packages, nil
|
||||
}
|
||||
|
||||
func (p PackageJSON) hasNameAndVersionValues() bool {
|
||||
return p.Name != "" && p.Version != ""
|
||||
}
|
||||
|
||||
@@ -142,3 +142,20 @@ func TestParsePackageJSON(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePackageJSON_Partial(t *testing.T) { // see https://github.com/anchore/syft/issues/311
|
||||
const fixtureFile = "test-fixtures/pkg-json/package-partial.json"
|
||||
fixture, err := os.Open(fixtureFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open fixture: %+v", err)
|
||||
}
|
||||
|
||||
actual, err := parsePackageJSON("", fixture)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse package-lock.json: %+v", err)
|
||||
}
|
||||
|
||||
if actualCount := len(actual); actualCount != 0 {
|
||||
t.Errorf("no packages should've been returned (but got %d packages)", actualCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"sideEffects": false,
|
||||
"module": "../../esm/fp/isSaturday/index.js",
|
||||
"typings": "../../typings.d.ts"
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func (c *PackageCataloger) Catalog(resolver source.Resolver) ([]pkg.Package, err
|
||||
for _, entry := range entries {
|
||||
p, err := c.catalogEggOrWheel(entry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to catalog python package=%+v: %w", entry.Metadata.Location.Path, err)
|
||||
return nil, fmt.Errorf("unable to catalog python package=%+v: %w", entry.Metadata.Location.RealPath, err)
|
||||
}
|
||||
if p != nil {
|
||||
packages = append(packages, *p)
|
||||
@@ -118,7 +118,7 @@ func (c *PackageCataloger) catalogEggOrWheel(entry *packageEntry) (*pkg.Package,
|
||||
func (c *PackageCataloger) assembleEggOrWheelMetadata(entry *packageEntry) (*pkg.PythonPackageMetadata, []source.Location, error) {
|
||||
var sources = []source.Location{entry.Metadata.Location}
|
||||
|
||||
metadata, err := parseWheelOrEggMetadata(entry.Metadata.Location.Path, entry.Metadata.Contents)
|
||||
metadata, err := parseWheelOrEggMetadata(entry.Metadata.Location.RealPath, entry.Metadata.Contents)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ func newPackageEntry(resolver source.FileResolver, metadataLocation source.Locat
|
||||
// to reconcile the RECORD path to the same layer (or a lower layer). The same is true with the top_level.txt file.
|
||||
|
||||
// lets find the RECORD file relative to the directory where the METADATA file resides (in path AND layer structure)
|
||||
recordPath := filepath.Join(filepath.Dir(metadataLocation.Path), "RECORD")
|
||||
recordPath := filepath.Join(filepath.Dir(metadataLocation.RealPath), "RECORD")
|
||||
recordLocation := resolver.RelativeFileByPath(metadataLocation, recordPath)
|
||||
|
||||
// a top_level.txt file specifies the python top-level packages (provided by this python package) installed into site-packages
|
||||
parentDir := filepath.Dir(metadataLocation.Path)
|
||||
parentDir := filepath.Dir(metadataLocation.RealPath)
|
||||
topLevelPath := filepath.Join(parentDir, "top_level.txt")
|
||||
topLevelLocation := resolver.RelativeFileByPath(metadataLocation, topLevelPath)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
@@ -56,7 +57,7 @@ func parseWheelOrEggMetadata(path string, reader io.Reader) (pkg.PythonPackageMe
|
||||
|
||||
fields[key] = val
|
||||
} else {
|
||||
return pkg.PythonPackageMetadata{}, fmt.Errorf("cannot parse field from line: '%s'", line)
|
||||
log.Warnf("cannot parse field from path: %q from line: %q", path, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,3 +117,37 @@ func TestDetermineSitePackagesRootPath(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseWheelEggMetadataInvalid(t *testing.T) {
|
||||
tests := []struct {
|
||||
Fixture string
|
||||
ExpectedMetadata pkg.PythonPackageMetadata
|
||||
}{
|
||||
{
|
||||
Fixture: "test-fixtures/egg-info/PKG-INFO-INVALID",
|
||||
ExpectedMetadata: pkg.PythonPackageMetadata{
|
||||
Name: "mxnet",
|
||||
Version: "1.8.0",
|
||||
SitePackagesRootPath: "test-fixtures",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Fixture, func(t *testing.T) {
|
||||
fixture, err := os.Open(test.Fixture)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open fixture: %+v", err)
|
||||
}
|
||||
|
||||
actual, err := parseWheelOrEggMetadata(test.Fixture, fixture)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse: %+v", err)
|
||||
}
|
||||
|
||||
for _, d := range deep.Equal(actual, test.ExpectedMetadata) {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
Metadata-Version: 1.1
|
||||
Name: mxnet
|
||||
Version: 1.8.0
|
||||
Summary: <!--- Licensed to the Apache Software Foundation (ASF) under one -->
|
||||
<!--- or more contributor license agreements. See the NOTICE file -->
|
||||
<!--- distributed with this work for additional information -->
|
||||
<!--- regarding copyright ownership. The ASF licenses this file -->
|
||||
<!--- to you under the Apache License, Version 2.0 (the -->
|
||||
<!--- "License"); you may not use this file except in compliance -->
|
||||
<!--- with the License. You may obtain a copy of the License at -->
|
||||
|
||||
<!--- http://www.apache.org/licenses/LICENSE-2.0 -->
|
||||
|
||||
<!--- Unless required by applicable law or agreed to in writing, -->
|
||||
<!--- software distributed under the License is distributed on an -->
|
||||
<!--- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -->
|
||||
<!--- KIND, either express or implied. See the License for the -->
|
||||
<!--- specific language governing permissions and limitations -->
|
||||
<!--- under the License. -->
|
||||
|
||||
MXNet Python Package
|
||||
====================
|
||||
This directory and nested files contain MXNet Python package and language binding.
|
||||
|
||||
## Installation
|
||||
To install MXNet Python package, visit MXNet [Install Instruction](https://mxnet.apache.org/get_started)
|
||||
|
||||
|
||||
## Running the unit tests
|
||||
|
||||
For running unit tests, you will need the [nose PyPi package](https://pypi.python.org/pypi/nose). To install:
|
||||
```bash
|
||||
pip install --upgrade nose
|
||||
```
|
||||
|
||||
Once ```nose``` is installed, run the following from MXNet root directory (please make sure the installation path of ```nosetests``` is included in your ```$PATH``` environment variable):
|
||||
```
|
||||
nosetests tests/python/unittest
|
||||
nosetests tests/python/train
|
||||
|
||||
```
|
||||
|
||||
Home-page: https://github.com/apache/incubator-mxnet
|
||||
Author: UNKNOWN
|
||||
Author-email: UNKNOWN
|
||||
License: UNKNOWN
|
||||
Description: UNKNOWN
|
||||
Platform: UNKNOWN
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Intended Audience :: Education
|
||||
Classifier: Intended Audience :: Science/Research
|
||||
Classifier: License :: OSI Approved :: Apache Software License
|
||||
Classifier: Programming Language :: C++
|
||||
Classifier: Programming Language :: Cython
|
||||
Classifier: Programming Language :: Other
|
||||
Classifier: Programming Language :: Perl
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Topic :: Scientific/Engineering
|
||||
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
||||
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
||||
Classifier: Topic :: Software Development
|
||||
Classifier: Topic :: Software Development :: Libraries
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
@@ -10,10 +10,7 @@ import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
const (
|
||||
packagesGlob = "**/var/lib/rpm/Packages"
|
||||
catalogerName = "rpmdb-cataloger"
|
||||
)
|
||||
const catalogerName = "rpmdb-cataloger"
|
||||
|
||||
type Cataloger struct{}
|
||||
|
||||
@@ -29,7 +26,7 @@ func (c *Cataloger) Name() string {
|
||||
|
||||
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing rpm db installation.
|
||||
func (c *Cataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) {
|
||||
fileMatches, err := resolver.FilesByGlob(packagesGlob)
|
||||
fileMatches, err := resolver.FilesByGlob(pkg.RpmDbGlob)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find rpmdb's by glob: %w", err)
|
||||
}
|
||||
@@ -43,7 +40,7 @@ func (c *Cataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) {
|
||||
|
||||
pkgs, err = parseRpmDB(resolver, location, dbContentReader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to catalog rpmdb package=%+v: %w", location.Path, err)
|
||||
return nil, fmt.Errorf("unable to catalog rpmdb package=%+v: %w", location.RealPath, err)
|
||||
}
|
||||
}
|
||||
return pkgs, nil
|
||||
|
||||
@@ -45,11 +45,6 @@ func parseRpmDB(resolver source.FileResolver, dbLocation source.Location, reader
|
||||
allPkgs := make([]pkg.Package, 0)
|
||||
|
||||
for _, entry := range pkgList {
|
||||
records, err := extractRpmdbFileRecords(resolver, entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := pkg.Package{
|
||||
Name: entry.Name,
|
||||
Version: fmt.Sprintf("%s-%s", entry.Version, entry.Release), // this is what engine does, instead of fmt.Sprintf("%d:%s-%s.%s", entry.Epoch, entry.Version, entry.Release, entry.Arch)
|
||||
@@ -67,7 +62,7 @@ func parseRpmDB(resolver source.FileResolver, dbLocation source.Location, reader
|
||||
Vendor: entry.Vendor,
|
||||
License: entry.License,
|
||||
Size: entry.Size,
|
||||
Files: records,
|
||||
Files: extractRpmdbFileRecords(resolver, entry),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -77,25 +72,19 @@ func parseRpmDB(resolver source.FileResolver, dbLocation source.Location, reader
|
||||
return allPkgs, nil
|
||||
}
|
||||
|
||||
func extractRpmdbFileRecords(resolver source.FileResolver, entry *rpmdb.PackageInfo) ([]pkg.RpmdbFileRecord, error) {
|
||||
func extractRpmdbFileRecords(resolver source.FileResolver, entry *rpmdb.PackageInfo) []pkg.RpmdbFileRecord {
|
||||
var records = make([]pkg.RpmdbFileRecord, 0)
|
||||
|
||||
for _, record := range entry.Files {
|
||||
refs, err := resolver.FilesByPath(record.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve path=%+v: %w", record.Path, err)
|
||||
}
|
||||
//only persist RPMDB file records which exist in the image/directory, otherwise ignore them
|
||||
if len(refs) == 0 {
|
||||
continue
|
||||
if resolver.HasPath(record.Path) {
|
||||
records = append(records, pkg.RpmdbFileRecord{
|
||||
Path: record.Path,
|
||||
Mode: pkg.RpmdbFileMode(record.Mode),
|
||||
Size: int(record.Size),
|
||||
SHA256: record.SHA256,
|
||||
})
|
||||
}
|
||||
|
||||
records = append(records, pkg.RpmdbFileRecord{
|
||||
Path: record.Path,
|
||||
Mode: pkg.RpmdbFileMode(record.Mode),
|
||||
Size: int(record.Size),
|
||||
SHA256: record.SHA256,
|
||||
})
|
||||
}
|
||||
return records, nil
|
||||
return records
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ func newTestFileResolver(ignorePaths bool) *rpmdbTestFileResolverMock {
|
||||
}
|
||||
}
|
||||
|
||||
func (r rpmdbTestFileResolverMock) HasPath(path string) bool {
|
||||
return !r.ignorePaths
|
||||
}
|
||||
|
||||
func (r *rpmdbTestFileResolverMock) FilesByPath(paths ...string) ([]source.Location, error) {
|
||||
if r.ignorePaths {
|
||||
// act as if no paths exist
|
||||
|
||||
@@ -19,7 +19,7 @@ func NewGemFileLockCataloger() *common.GenericCataloger {
|
||||
// NewGemSpecCataloger returns a new Bundler cataloger object tailored for detecting installations of gems (e.g. Gemspec).
|
||||
func NewGemSpecCataloger() *common.GenericCataloger {
|
||||
globParsers := map[string]common.ParserFn{
|
||||
"**/specifications/*.gemspec": parseGemSpecEntries,
|
||||
"**/specifications/**/*.gemspec": parseGemSpecEntries,
|
||||
}
|
||||
|
||||
return common.NewGenericCataloger(nil, globParsers, "ruby-gemspec-cataloger")
|
||||
|
||||
@@ -62,7 +62,7 @@ identifyLoop:
|
||||
|
||||
content, err := ioutil.ReadAll(contentReader)
|
||||
if err != nil {
|
||||
log.Errorf("unable to read %q: %+v", location.Path, err)
|
||||
log.Errorf("unable to read %q: %+v", location.RealPath, err)
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/package-url/packageurl-go"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
const ApkDbGlob = "**/lib/apk/db/installed"
|
||||
|
||||
var _ fileOwner = (*ApkMetadata)(nil)
|
||||
|
||||
// ApkMetadata represents all captured data for a Alpine DB package entry.
|
||||
// See the following sources for more information:
|
||||
// - https://wiki.alpinelinux.org/wiki/Apk_spec
|
||||
@@ -53,3 +60,15 @@ func (m ApkMetadata) PackageURL() string {
|
||||
"")
|
||||
return pURL.ToString()
|
||||
}
|
||||
|
||||
func (m ApkMetadata) ownedFiles() (result []string) {
|
||||
s := strset.New()
|
||||
for _, f := range m.Files {
|
||||
if f.Path != "" {
|
||||
s.Add(f.Path)
|
||||
}
|
||||
}
|
||||
result = s.List()
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
func TestApkMetadata_pURL(t *testing.T) {
|
||||
@@ -31,3 +34,45 @@ func TestApkMetadata_pURL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApkMetadata_fileOwner(t *testing.T) {
|
||||
tests := []struct {
|
||||
metadata ApkMetadata
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
metadata: ApkMetadata{
|
||||
Files: []ApkFileRecord{
|
||||
{Path: "/somewhere"},
|
||||
{Path: "/else"},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"/else",
|
||||
"/somewhere",
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: ApkMetadata{
|
||||
Files: []ApkFileRecord{
|
||||
{Path: "/somewhere"},
|
||||
{Path: ""},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"/somewhere",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(strings.Join(test.expected, ","), func(t *testing.T) {
|
||||
var i interface{}
|
||||
i = test.metadata
|
||||
actual := i.(fileOwner).ownedFiles()
|
||||
for _, d := range deep.Equal(test.expected, actual) {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+90
-35
@@ -4,27 +4,29 @@ import (
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
var nextPackageID int64
|
||||
var globsForbiddenFromBeingOwned = []string{
|
||||
ApkDbGlob,
|
||||
DpkgDbGlob,
|
||||
RpmDbGlob,
|
||||
}
|
||||
|
||||
// Catalog represents a collection of Packages.
|
||||
type Catalog struct {
|
||||
byID map[ID]*Package
|
||||
byType map[Type][]*Package
|
||||
byFile map[source.Location][]*Package
|
||||
lock sync.RWMutex
|
||||
byID map[ID]*Package
|
||||
idsByType map[Type][]ID
|
||||
idsByPath map[string][]ID // note: this is real path or virtual path
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewCatalog returns a new empty Catalog
|
||||
func NewCatalog(pkgs ...Package) *Catalog {
|
||||
catalog := Catalog{
|
||||
byID: make(map[ID]*Package),
|
||||
byType: make(map[Type][]*Package),
|
||||
byFile: make(map[source.Location][]*Package),
|
||||
byID: make(map[ID]*Package),
|
||||
idsByType: make(map[Type][]ID),
|
||||
idsByPath: make(map[string][]ID),
|
||||
}
|
||||
|
||||
for _, p := range pkgs {
|
||||
@@ -41,52 +43,96 @@ func (c *Catalog) PackageCount() int {
|
||||
|
||||
// Package returns the package with the given ID.
|
||||
func (c *Catalog) Package(id ID) *Package {
|
||||
return c.byID[id]
|
||||
v, exists := c.byID[id]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// PackagesByFile returns all packages that were discovered from the given source file reference.
|
||||
func (c *Catalog) PackagesByFile(location source.Location) []*Package {
|
||||
return c.byFile[location]
|
||||
// PackagesByPath returns all packages that were discovered from the given path.
|
||||
func (c *Catalog) PackagesByPath(path string) []*Package {
|
||||
return c.Packages(c.idsByPath[path])
|
||||
}
|
||||
|
||||
// Packages returns all packages for the given ID.
|
||||
func (c *Catalog) Packages(ids []ID) (result []*Package) {
|
||||
for _, i := range ids {
|
||||
p, exists := c.byID[i]
|
||||
if exists {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Add a package to the Catalog.
|
||||
func (c *Catalog) Add(p Package) {
|
||||
if p.id != 0 {
|
||||
log.Errorf("package already added to catalog: %s", p)
|
||||
return
|
||||
}
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
p.id = ID(nextPackageID)
|
||||
nextPackageID++
|
||||
_, exists := c.byID[p.ID]
|
||||
if exists {
|
||||
log.Errorf("package ID already exists in the catalog : id=%+v %+v", p.ID, p)
|
||||
return
|
||||
}
|
||||
|
||||
if p.ID == "" {
|
||||
p.ID = newID()
|
||||
}
|
||||
|
||||
// store by package ID
|
||||
c.byID[p.id] = &p
|
||||
c.byID[p.ID] = &p
|
||||
|
||||
// store by package type
|
||||
_, ok := c.byType[p.Type]
|
||||
if !ok {
|
||||
c.byType[p.Type] = make([]*Package, 0)
|
||||
}
|
||||
c.byType[p.Type] = append(c.byType[p.Type], &p)
|
||||
c.idsByType[p.Type] = append(c.idsByType[p.Type], p.ID)
|
||||
|
||||
// store by file references
|
||||
for _, s := range p.Locations {
|
||||
_, ok := c.byFile[s]
|
||||
if !ok {
|
||||
c.byFile[s] = make([]*Package, 0)
|
||||
// store by file location paths
|
||||
for _, l := range p.Locations {
|
||||
if l.RealPath != "" {
|
||||
c.idsByPath[l.RealPath] = append(c.idsByPath[l.RealPath], p.ID)
|
||||
}
|
||||
if l.VirtualPath != "" {
|
||||
c.idsByPath[l.VirtualPath] = append(c.idsByPath[l.VirtualPath], p.ID)
|
||||
}
|
||||
c.byFile[s] = append(c.byFile[s], &p)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Catalog) Remove(id ID) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
_, exists := c.byID[id]
|
||||
if !exists {
|
||||
log.Errorf("package ID does not exist in the catalog : id=%+v", id)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove all index references to this package ID
|
||||
for t, ids := range c.idsByType {
|
||||
c.idsByType[t] = removeID(id, ids)
|
||||
if len(c.idsByType[t]) == 0 {
|
||||
delete(c.idsByType, t)
|
||||
}
|
||||
}
|
||||
|
||||
for p, ids := range c.idsByPath {
|
||||
c.idsByPath[p] = removeID(id, ids)
|
||||
if len(c.idsByPath[p]) == 0 {
|
||||
delete(c.idsByPath, p)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove package
|
||||
delete(c.byID, id)
|
||||
}
|
||||
|
||||
// Enumerate all packages for the given type(s), enumerating all packages if no type is specified.
|
||||
func (c *Catalog) Enumerate(types ...Type) <-chan *Package {
|
||||
channel := make(chan *Package)
|
||||
go func() {
|
||||
defer close(channel)
|
||||
for ty, packages := range c.byType {
|
||||
for ty, ids := range c.idsByType {
|
||||
if len(types) != 0 {
|
||||
found := false
|
||||
typeCheck:
|
||||
@@ -100,8 +146,8 @@ func (c *Catalog) Enumerate(types ...Type) <-chan *Package {
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, p := range packages {
|
||||
channel <- p
|
||||
for _, id := range ids {
|
||||
channel <- c.Package(id)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -128,3 +174,12 @@ func (c *Catalog) Sorted(types ...Type) []*Package {
|
||||
|
||||
return pkgs
|
||||
}
|
||||
|
||||
func removeID(id ID, target []ID) (result []ID) {
|
||||
for _, value := range target {
|
||||
if value != id {
|
||||
result = append(result, value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
var catalogAddAndRemoveTestPkgs = []Package{
|
||||
{
|
||||
ID: "my-id",
|
||||
Locations: []source.Location{
|
||||
{
|
||||
RealPath: "/a/path",
|
||||
VirtualPath: "/another/path",
|
||||
},
|
||||
{
|
||||
RealPath: "/b/path",
|
||||
VirtualPath: "/bee/path",
|
||||
},
|
||||
},
|
||||
Type: RpmPkg,
|
||||
},
|
||||
{
|
||||
ID: "my-other-id",
|
||||
Locations: []source.Location{
|
||||
{
|
||||
RealPath: "/c/path",
|
||||
VirtualPath: "/another/path",
|
||||
},
|
||||
{
|
||||
RealPath: "/d/path",
|
||||
VirtualPath: "/another/path",
|
||||
},
|
||||
},
|
||||
Type: NpmPkg,
|
||||
},
|
||||
}
|
||||
|
||||
type expectedIndexes struct {
|
||||
byType map[Type]*strset.Set
|
||||
byPath map[string]*strset.Set
|
||||
}
|
||||
|
||||
func TestCatalogAddPopulatesIndex(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkgs []Package
|
||||
expectedIndexes expectedIndexes
|
||||
}{
|
||||
{
|
||||
name: "vanilla-add",
|
||||
pkgs: catalogAddAndRemoveTestPkgs,
|
||||
expectedIndexes: expectedIndexes{
|
||||
byType: map[Type]*strset.Set{
|
||||
RpmPkg: strset.New("my-id"),
|
||||
NpmPkg: strset.New("my-other-id"),
|
||||
},
|
||||
byPath: map[string]*strset.Set{
|
||||
"/another/path": strset.New("my-id", "my-other-id"),
|
||||
"/a/path": strset.New("my-id"),
|
||||
"/b/path": strset.New("my-id"),
|
||||
"/bee/path": strset.New("my-id"),
|
||||
"/c/path": strset.New("my-other-id"),
|
||||
"/d/path": strset.New("my-other-id"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := NewCatalog(test.pkgs...)
|
||||
|
||||
assertIndexes(t, c, test.expectedIndexes)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalogRemove(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkgs []Package
|
||||
removeId ID
|
||||
expectedIndexes expectedIndexes
|
||||
}{
|
||||
{
|
||||
name: "vanilla-add",
|
||||
removeId: "my-other-id",
|
||||
pkgs: catalogAddAndRemoveTestPkgs,
|
||||
expectedIndexes: expectedIndexes{
|
||||
byType: map[Type]*strset.Set{
|
||||
RpmPkg: strset.New("my-id"),
|
||||
},
|
||||
byPath: map[string]*strset.Set{
|
||||
"/another/path": strset.New("my-id"),
|
||||
"/a/path": strset.New("my-id"),
|
||||
"/b/path": strset.New("my-id"),
|
||||
"/bee/path": strset.New("my-id"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := NewCatalog(test.pkgs...)
|
||||
c.Remove(test.removeId)
|
||||
|
||||
assertIndexes(t, c, test.expectedIndexes)
|
||||
|
||||
if c.Package(test.removeId) != nil {
|
||||
t.Errorf("expected package to be removed, but was found!")
|
||||
}
|
||||
|
||||
if c.PackageCount() != len(test.pkgs)-1 {
|
||||
t.Errorf("expected count to be affected but was not")
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertIndexes(t *testing.T, c *Catalog, expectedIndexes expectedIndexes) {
|
||||
// assert path index
|
||||
if len(c.idsByPath) != len(expectedIndexes.byPath) {
|
||||
t.Errorf("unexpected path index length: %d != %d", len(c.idsByPath), len(expectedIndexes.byPath))
|
||||
}
|
||||
for path, expectedIds := range expectedIndexes.byPath {
|
||||
actualIds := strset.New()
|
||||
for _, p := range c.PackagesByPath(path) {
|
||||
actualIds.Add(string(p.ID))
|
||||
}
|
||||
|
||||
if !expectedIds.IsEqual(actualIds) {
|
||||
t.Errorf("mismatched IDs for path=%q : %+v", path, strset.SymmetricDifference(actualIds, expectedIds))
|
||||
}
|
||||
}
|
||||
|
||||
// assert type index
|
||||
if len(c.idsByType) != len(expectedIndexes.byType) {
|
||||
t.Errorf("unexpected type index length: %d != %d", len(c.idsByType), len(expectedIndexes.byType))
|
||||
}
|
||||
for ty, expectedIds := range expectedIndexes.byType {
|
||||
actualIds := strset.New()
|
||||
for p := range c.Enumerate(ty) {
|
||||
actualIds.Add(string(p.ID))
|
||||
}
|
||||
|
||||
if !expectedIds.IsEqual(actualIds) {
|
||||
t.Errorf("mismatched IDs for type=%q : %+v", ty, strset.SymmetricDifference(actualIds, expectedIds))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/package-url/packageurl-go"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
const DpkgDbGlob = "**/var/lib/dpkg/status"
|
||||
|
||||
var _ fileOwner = (*DpkgMetadata)(nil)
|
||||
|
||||
// DpkgMetadata represents all captured data for a Debian package DB entry; available fields are described
|
||||
// at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section.
|
||||
type DpkgMetadata struct {
|
||||
@@ -44,3 +51,15 @@ func (m DpkgMetadata) PackageURL(d *distro.Distro) string {
|
||||
"")
|
||||
return pURL.ToString()
|
||||
}
|
||||
|
||||
func (m DpkgMetadata) ownedFiles() (result []string) {
|
||||
s := strset.New()
|
||||
for _, f := range m.Files {
|
||||
if f.Path != "" {
|
||||
s.Add(f.Path)
|
||||
}
|
||||
}
|
||||
result = s.List()
|
||||
sort.Strings(result)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
@@ -50,3 +53,45 @@ func TestDpkgMetadata_pURL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDpkgMetadata_fileOwner(t *testing.T) {
|
||||
tests := []struct {
|
||||
metadata DpkgMetadata
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
metadata: DpkgMetadata{
|
||||
Files: []DpkgFileRecord{
|
||||
{Path: "/somewhere"},
|
||||
{Path: "/else"},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"/else",
|
||||
"/somewhere",
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: DpkgMetadata{
|
||||
Files: []DpkgFileRecord{
|
||||
{Path: "/somewhere"},
|
||||
{Path: ""},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"/somewhere",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(strings.Join(test.expected, ","), func(t *testing.T) {
|
||||
var i interface{}
|
||||
i = test.metadata
|
||||
actual := i.(fileOwner).ownedFiles()
|
||||
for _, d := range deep.Equal(test.expected, actual) {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package pkg
|
||||
|
||||
type fileOwner interface {
|
||||
ownedFiles() []string
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ID represents a unique value for each package added to a package catalog.
|
||||
type ID string
|
||||
|
||||
func newID() ID {
|
||||
return ID(uuid.New().String())
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/bmatcuk/doublestar/v2"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
type ownershipByFilesMetadata struct {
|
||||
Files []string `json:"files"`
|
||||
}
|
||||
|
||||
func ownershipByFilesRelationships(catalog *Catalog) []Relationship {
|
||||
var relationships = findOwnershipByFilesRelationships(catalog)
|
||||
|
||||
var edges []Relationship
|
||||
for parent, children := range relationships {
|
||||
for child, files := range children {
|
||||
edges = append(edges, Relationship{
|
||||
Parent: parent,
|
||||
Child: child,
|
||||
Type: OwnershipByFileOverlapRelationship,
|
||||
Metadata: ownershipByFilesMetadata{
|
||||
Files: files.List(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return edges
|
||||
}
|
||||
|
||||
// findOwnershipByFilesRelationships find overlaps in file ownership with a file that defines another package. Specifically, a .Location.Path of
|
||||
// a package is found to be owned by another (from the owner's .Metadata.Files[]).
|
||||
func findOwnershipByFilesRelationships(catalog *Catalog) map[ID]map[ID]*strset.Set {
|
||||
var relationships = make(map[ID]map[ID]*strset.Set)
|
||||
|
||||
for _, candidateOwnerPkg := range catalog.Sorted() {
|
||||
if candidateOwnerPkg.Metadata == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// check to see if this is a file owner
|
||||
pkgFileOwner, ok := candidateOwnerPkg.Metadata.(fileOwner)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, ownedFilePath := range pkgFileOwner.ownedFiles() {
|
||||
if matchesAny(ownedFilePath, globsForbiddenFromBeingOwned) {
|
||||
// we skip over known exceptions to file ownership, such as the RPM package owning
|
||||
// the RPM DB path, otherwise the RPM package would "own" all RPMs, which is not intended
|
||||
continue
|
||||
}
|
||||
|
||||
// look for package(s) in the catalog that may be owned by this package and mark the relationship
|
||||
for _, subPackage := range catalog.PackagesByPath(ownedFilePath) {
|
||||
if subPackage.ID == candidateOwnerPkg.ID {
|
||||
continue
|
||||
}
|
||||
if _, exists := relationships[candidateOwnerPkg.ID]; !exists {
|
||||
relationships[candidateOwnerPkg.ID] = make(map[ID]*strset.Set)
|
||||
}
|
||||
|
||||
if _, exists := relationships[candidateOwnerPkg.ID][subPackage.ID]; !exists {
|
||||
relationships[candidateOwnerPkg.ID][subPackage.ID] = strset.New()
|
||||
}
|
||||
relationships[candidateOwnerPkg.ID][subPackage.ID].Add(ownedFilePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return relationships
|
||||
}
|
||||
|
||||
func matchesAny(s string, globs []string) bool {
|
||||
for _, g := range globs {
|
||||
matches, err := doublestar.Match(g, s)
|
||||
if err != nil {
|
||||
log.Errorf("failed to match glob=%q : %+v", g, err)
|
||||
}
|
||||
if matches {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
func TestOwnershipByFilesRelationship(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkgs []Package
|
||||
expectedRelations []Relationship
|
||||
}{
|
||||
{
|
||||
name: "owns-by-real-path",
|
||||
pkgs: []Package{
|
||||
{
|
||||
ID: "parent",
|
||||
Locations: []source.Location{
|
||||
{
|
||||
RealPath: "/a/path",
|
||||
VirtualPath: "/another/path",
|
||||
},
|
||||
{
|
||||
RealPath: "/b/path",
|
||||
VirtualPath: "/bee/path",
|
||||
},
|
||||
},
|
||||
Type: RpmPkg,
|
||||
MetadataType: RpmdbMetadataType,
|
||||
Metadata: RpmdbMetadata{
|
||||
Files: []RpmdbFileRecord{
|
||||
{Path: "/owning/path/1"},
|
||||
{Path: "/owning/path/2"},
|
||||
{Path: "/d/path"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "child",
|
||||
Locations: []source.Location{
|
||||
{
|
||||
RealPath: "/c/path",
|
||||
VirtualPath: "/another/path",
|
||||
},
|
||||
{
|
||||
RealPath: "/d/path",
|
||||
VirtualPath: "/another/path",
|
||||
},
|
||||
},
|
||||
Type: NpmPkg,
|
||||
},
|
||||
},
|
||||
expectedRelations: []Relationship{
|
||||
{
|
||||
Parent: "parent",
|
||||
Child: "child",
|
||||
Type: OwnershipByFileOverlapRelationship,
|
||||
Metadata: ownershipByFilesMetadata{
|
||||
Files: []string{
|
||||
"/d/path",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owns-by-virtual-path",
|
||||
pkgs: []Package{
|
||||
{
|
||||
ID: "parent",
|
||||
Locations: []source.Location{
|
||||
{
|
||||
RealPath: "/a/path",
|
||||
VirtualPath: "/some/other/path",
|
||||
},
|
||||
{
|
||||
RealPath: "/b/path",
|
||||
VirtualPath: "/bee/path",
|
||||
},
|
||||
},
|
||||
Type: RpmPkg,
|
||||
MetadataType: RpmdbMetadataType,
|
||||
Metadata: RpmdbMetadata{
|
||||
Files: []RpmdbFileRecord{
|
||||
{Path: "/owning/path/1"},
|
||||
{Path: "/owning/path/2"},
|
||||
{Path: "/another/path"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "child",
|
||||
Locations: []source.Location{
|
||||
{
|
||||
RealPath: "/c/path",
|
||||
VirtualPath: "/another/path",
|
||||
},
|
||||
{
|
||||
RealPath: "/d/path",
|
||||
VirtualPath: "",
|
||||
},
|
||||
},
|
||||
Type: NpmPkg,
|
||||
},
|
||||
},
|
||||
expectedRelations: []Relationship{
|
||||
{
|
||||
Parent: "parent",
|
||||
Child: "child",
|
||||
Type: OwnershipByFileOverlapRelationship,
|
||||
Metadata: ownershipByFilesMetadata{
|
||||
Files: []string{
|
||||
"/another/path",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ignore-empty-path",
|
||||
pkgs: []Package{
|
||||
{
|
||||
ID: "parent",
|
||||
Locations: []source.Location{
|
||||
{
|
||||
RealPath: "/a/path",
|
||||
VirtualPath: "/some/other/path",
|
||||
},
|
||||
{
|
||||
RealPath: "/b/path",
|
||||
VirtualPath: "/bee/path",
|
||||
},
|
||||
},
|
||||
Type: RpmPkg,
|
||||
MetadataType: RpmdbMetadataType,
|
||||
Metadata: RpmdbMetadata{
|
||||
Files: []RpmdbFileRecord{
|
||||
{Path: "/owning/path/1"},
|
||||
{Path: "/owning/path/2"},
|
||||
{Path: ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "child",
|
||||
Locations: []source.Location{
|
||||
{
|
||||
RealPath: "/c/path",
|
||||
VirtualPath: "/another/path",
|
||||
},
|
||||
{
|
||||
RealPath: "/d/path",
|
||||
VirtualPath: "",
|
||||
},
|
||||
},
|
||||
Type: NpmPkg,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := NewCatalog(test.pkgs...)
|
||||
relationships := ownershipByFilesRelationships(c)
|
||||
|
||||
for _, d := range deep.Equal(test.expectedRelations, relationships) {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+1
-9
@@ -9,12 +9,9 @@ import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// ID represents a unique value for each package added to a package catalog.
|
||||
type ID int64
|
||||
|
||||
// Package represents an application or library that has been bundled into a distributable format.
|
||||
type Package struct {
|
||||
id ID // uniquely identifies a package, set by the cataloger
|
||||
ID ID // uniquely identifies a package, set by the cataloger
|
||||
Name string // the package name
|
||||
Version string // the version of the package
|
||||
FoundBy string // the specific cataloger that discovered this package
|
||||
@@ -29,11 +26,6 @@ type Package struct {
|
||||
Metadata interface{} // additional data found while parsing the package source
|
||||
}
|
||||
|
||||
// ID returns the package ID, which is unique relative to a package catalog.
|
||||
func (p Package) ID() ID {
|
||||
return p.id
|
||||
}
|
||||
|
||||
// Stringer to represent a package.
|
||||
func (p Package) String() string {
|
||||
return fmt.Sprintf("Pkg(type=%s, name=%s, version=%s)", p.Type, p.Name, p.Version)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
var _ fileOwner = (*PythonPackageMetadata)(nil)
|
||||
|
||||
// PythonFileDigest represents the file metadata for a single file attributed to a python package.
|
||||
type PythonFileDigest struct {
|
||||
Algorithm string `json:"algorithm"`
|
||||
@@ -25,3 +33,15 @@ type PythonPackageMetadata struct {
|
||||
SitePackagesRootPath string `json:"sitePackagesRootPath"`
|
||||
TopLevelPackages []string `json:"topLevelPackages,omitempty"`
|
||||
}
|
||||
|
||||
func (m PythonPackageMetadata) ownedFiles() (result []string) {
|
||||
s := strset.New()
|
||||
for _, f := range m.Files {
|
||||
if f.Path != "" {
|
||||
s.Add(f.Path)
|
||||
}
|
||||
}
|
||||
result = s.List()
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
func TestPythonMetadata_fileOwner(t *testing.T) {
|
||||
tests := []struct {
|
||||
metadata PythonPackageMetadata
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
metadata: PythonPackageMetadata{
|
||||
Files: []PythonFileRecord{
|
||||
{Path: "/somewhere"},
|
||||
{Path: "/else"},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"/else",
|
||||
"/somewhere",
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: PythonPackageMetadata{
|
||||
Files: []PythonFileRecord{
|
||||
{Path: "/somewhere"},
|
||||
{Path: ""},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"/somewhere",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(strings.Join(test.expected, ","), func(t *testing.T) {
|
||||
var i interface{}
|
||||
i = test.metadata
|
||||
actual := i.(fileOwner).ownedFiles()
|
||||
for _, d := range deep.Equal(test.expected, actual) {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package pkg
|
||||
|
||||
const (
|
||||
// OwnershipByFileOverlapRelationship indicates that the parent package owns the child package made evident by the set of provided files
|
||||
OwnershipByFileOverlapRelationship RelationshipType = "ownership-by-file-overlap"
|
||||
)
|
||||
|
||||
type RelationshipType string
|
||||
|
||||
type Relationship struct {
|
||||
Parent ID
|
||||
Child ID
|
||||
Type RelationshipType
|
||||
Metadata interface{}
|
||||
}
|
||||
|
||||
// TODO: as more relationships are added, this function signature will probably accommodate selection
|
||||
func NewRelationships(catalog *Catalog) []Relationship {
|
||||
return ownershipByFilesRelationships(catalog)
|
||||
}
|
||||
@@ -2,11 +2,18 @@ package pkg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/package-url/packageurl-go"
|
||||
)
|
||||
|
||||
const RpmDbGlob = "**/var/lib/rpm/Packages"
|
||||
|
||||
var _ fileOwner = (*RpmdbMetadata)(nil)
|
||||
|
||||
// RpmdbMetadata represents all captured data for a RPM DB package entry.
|
||||
type RpmdbMetadata struct {
|
||||
Name string `json:"name"`
|
||||
@@ -52,3 +59,15 @@ func (m RpmdbMetadata) PackageURL(d *distro.Distro) string {
|
||||
"")
|
||||
return pURL.ToString()
|
||||
}
|
||||
|
||||
func (m RpmdbMetadata) ownedFiles() (result []string) {
|
||||
s := strset.New()
|
||||
for _, f := range m.Files {
|
||||
if f.Path != "" {
|
||||
s.Add(f.Path)
|
||||
}
|
||||
}
|
||||
result = s.List()
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
@@ -52,3 +55,45 @@ func TestRpmMetadata_pURL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRpmMetadata_fileOwner(t *testing.T) {
|
||||
tests := []struct {
|
||||
metadata RpmdbMetadata
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
metadata: RpmdbMetadata{
|
||||
Files: []RpmdbFileRecord{
|
||||
{Path: "/somewhere"},
|
||||
{Path: "/else"},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"/else",
|
||||
"/somewhere",
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: RpmdbMetadata{
|
||||
Files: []RpmdbFileRecord{
|
||||
{Path: "/somewhere"},
|
||||
{Path: ""},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"/somewhere",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(strings.Join(test.expected, ","), func(t *testing.T) {
|
||||
var i interface{}
|
||||
i = test.metadata
|
||||
actual := i.(fileOwner).ownedFiles()
|
||||
for _, d := range deep.Equal(test.expected, actual) {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestCycloneDxDirsPresenter(t *testing.T) {
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Locations: []source.Location{
|
||||
{Path: "/some/path/pkg1"},
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package1",
|
||||
@@ -44,7 +44,7 @@ func TestCycloneDxDirsPresenter(t *testing.T) {
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
Locations: []source.Location{
|
||||
{Path: "/some/path/pkg1"},
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
Licenses: []string{
|
||||
"MIT",
|
||||
@@ -104,7 +104,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
|
||||
Name: "package1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(*ref1, img),
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
Type: pkg.RpmPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
@@ -114,7 +114,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
|
||||
Name: "package2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(*ref2, img),
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
Type: pkg.RpmPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
|
||||
@@ -12,11 +12,12 @@ import (
|
||||
|
||||
// Document represents the syft cataloging findings as a JSON document
|
||||
type Document struct {
|
||||
Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
|
||||
Source Source `json:"source"` // Source represents the original object that was cataloged
|
||||
Distro Distribution `json:"distro"` // Distro represents the Linux distribution that was detected from the source
|
||||
Descriptor Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
|
||||
Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
|
||||
Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
|
||||
Source Source `json:"source"` // Source represents the original object that was cataloged
|
||||
Distro Distribution `json:"distro"` // Distro represents the Linux distribution that was detected from the source
|
||||
Descriptor Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
|
||||
Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
|
||||
ArtifactRelationships []Relationship `json:"artifactRelationships"`
|
||||
}
|
||||
|
||||
// NewDocument creates and populates a new JSON document struct from the given cataloging results.
|
||||
@@ -38,6 +39,7 @@ func NewDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d *distro.Di
|
||||
Version: internal.JSONSchemaVersion,
|
||||
URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion),
|
||||
},
|
||||
ArtifactRelationships: newRelationships(pkg.NewRelationships(catalog)),
|
||||
}
|
||||
|
||||
for _, p := range catalog.Sorted() {
|
||||
|
||||
@@ -16,6 +16,7 @@ type Package struct {
|
||||
|
||||
// packageBasicMetadata contains non-ambiguous values (type-wise) from pkg.Package.
|
||||
type packageBasicMetadata struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
@@ -60,6 +61,7 @@ func NewPackage(p *pkg.Package) (Package, error) {
|
||||
|
||||
return Package{
|
||||
packageBasicMetadata: packageBasicMetadata{
|
||||
ID: string(p.ID),
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
Type: string(p.Type),
|
||||
@@ -89,6 +91,7 @@ func (a Package) ToPackage() (pkg.Package, error) {
|
||||
}
|
||||
return pkg.Package{
|
||||
// does not include found-by and locations
|
||||
ID: pkg.ID(a.ID),
|
||||
Name: a.Name,
|
||||
Version: a.Version,
|
||||
FoundBy: a.FoundBy,
|
||||
|
||||
@@ -31,12 +31,13 @@ func TestJsonDirsPresenter(t *testing.T) {
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-1-id",
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Locations: []source.Location{
|
||||
{Path: "/some/path/pkg1"},
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
Language: pkg.Python,
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
@@ -51,12 +52,13 @@ func TestJsonDirsPresenter(t *testing.T) {
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-2-id",
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
Locations: []source.Location{
|
||||
{Path: "/some/path/pkg1"},
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
@@ -113,10 +115,11 @@ func TestJsonImgsPresenter(t *testing.T) {
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-1-id",
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(*ref1, img),
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
@@ -133,10 +136,11 @@ func TestJsonImgsPresenter(t *testing.T) {
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-2-id",
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(*ref2, img),
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package json
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
|
||||
type Relationship struct {
|
||||
Parent string `json:"parent"`
|
||||
Child string `json:"child"`
|
||||
Type string `json:"type"`
|
||||
Metadata interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
func newRelationships(relationships []pkg.Relationship) []Relationship {
|
||||
result := make([]Relationship, len(relationships))
|
||||
for i, r := range relationships {
|
||||
result[i] = Relationship{
|
||||
Parent: string(r.Parent),
|
||||
Child: string(r.Child),
|
||||
Type: string(r.Type),
|
||||
Metadata: r.Metadata,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "package-1-id",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
"type": "python",
|
||||
@@ -30,6 +31,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "package-2-id",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"type": "deb",
|
||||
@@ -72,7 +74,8 @@
|
||||
"version": "[not provided]"
|
||||
},
|
||||
"schema": {
|
||||
"version": "1.0.1",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.1.json"
|
||||
}
|
||||
"version": "1.0.2",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.2.json"
|
||||
},
|
||||
"artifactRelationships": []
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "package-1-id",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
"type": "python",
|
||||
@@ -31,6 +32,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "package-2-id",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"type": "deb",
|
||||
@@ -103,7 +105,8 @@
|
||||
"version": "[not provided]"
|
||||
},
|
||||
"schema": {
|
||||
"version": "1.0.1",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.1.json"
|
||||
}
|
||||
"version": "1.0.2",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.2.json"
|
||||
},
|
||||
"artifactRelationships": []
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestTablePresenter(t *testing.T) {
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(*ref1, img),
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
@@ -43,7 +43,7 @@ func TestTablePresenter(t *testing.T) {
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(*ref2, img),
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
|
||||
@@ -80,7 +80,7 @@ func TestTextImgPresenter(t *testing.T) {
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(*ref1, img),
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
FoundBy: "dpkg",
|
||||
Type: pkg.DebPkg,
|
||||
@@ -89,7 +89,7 @@ func TestTextImgPresenter(t *testing.T) {
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(*ref2, img),
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
FoundBy: "dpkg",
|
||||
Metadata: PackageInfo{Name: "package-2", Version: "1.0.2"},
|
||||
|
||||
@@ -2,15 +2,15 @@ package source
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
var _ Resolver = (*AllLayersResolver)(nil)
|
||||
@@ -37,6 +37,18 @@ func NewAllLayersResolver(img *image.Image) (*AllLayersResolver, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HasPath indicates if the given path exists in the underlying source.
|
||||
func (r *AllLayersResolver) HasPath(path string) bool {
|
||||
p := file.Path(path)
|
||||
for _, layerIdx := range r.layers {
|
||||
tree := r.img.Layers[layerIdx].Tree
|
||||
if tree.HasPath(p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *AllLayersResolver) fileByRef(ref file.Reference, uniqueFileIDs file.ReferenceSet, layerIdx int) ([]file.Reference, error) {
|
||||
uniqueFiles := make([]file.Reference, 0)
|
||||
|
||||
@@ -102,7 +114,7 @@ func (r *AllLayersResolver) FilesByPath(paths ...string) ([]Location, error) {
|
||||
return nil, err
|
||||
}
|
||||
for _, result := range results {
|
||||
uniqueLocations = append(uniqueLocations, NewLocationFromImage(result, r.img))
|
||||
uniqueLocations = append(uniqueLocations, NewLocationFromImage(path, result, r.img))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +153,7 @@ func (r *AllLayersResolver) FilesByGlob(patterns ...string) ([]Location, error)
|
||||
return nil, err
|
||||
}
|
||||
for _, refResult := range refResults {
|
||||
uniqueLocations = append(uniqueLocations, NewLocationFromImage(refResult, r.img))
|
||||
uniqueLocations = append(uniqueLocations, NewLocationFromImage(string(result.MatchPath), refResult, r.img))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,7 +179,7 @@ func (r *AllLayersResolver) RelativeFileByPath(location Location, path string) *
|
||||
return nil
|
||||
}
|
||||
|
||||
relativeLocation := NewLocationFromImage(*relativeRef, r.img)
|
||||
relativeLocation := NewLocationFromImage(path, *relativeRef, r.img)
|
||||
|
||||
return &relativeLocation
|
||||
}
|
||||
@@ -188,11 +200,11 @@ type multiContentFetcher func(refs ...file.Reference) (map[file.Reference]io.Rea
|
||||
|
||||
func mapLocationRefs(callback multiContentFetcher, locations []Location) (map[Location]io.ReadCloser, error) {
|
||||
var fileRefs = make([]file.Reference, len(locations))
|
||||
var locationByRefs = make(map[file.Reference]Location)
|
||||
var locationByRefs = make(map[file.Reference][]Location)
|
||||
var results = make(map[Location]io.ReadCloser)
|
||||
|
||||
for i, location := range locations {
|
||||
locationByRefs[location.ref] = location
|
||||
locationByRefs[location.ref] = append(locationByRefs[location.ref], location)
|
||||
fileRefs[i] = location.ref
|
||||
}
|
||||
|
||||
@@ -201,8 +213,26 @@ func mapLocationRefs(callback multiContentFetcher, locations []Location) (map[Lo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: this is not tested, we need a test case that covers a mapLocationRefs which has multiple Locations with the same reference in the request. The io.Reader should be copied.
|
||||
for ref, content := range contentsByRef {
|
||||
results[locationByRefs[ref]] = content
|
||||
mappedLocations := locationByRefs[ref]
|
||||
switch {
|
||||
case len(mappedLocations) > 1:
|
||||
// TODO: fixme... this can lead to lots of unexpected memory usage in unusual circumstances (cache is not leveraged for large files).
|
||||
// stereoscope wont duplicate content requests if the caller asks for the same file multiple times... thats up to the caller
|
||||
contentsBytes, err := ioutil.ReadAll(content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read ref=%+v :%w", ref, err)
|
||||
}
|
||||
for _, loc := range mappedLocations {
|
||||
results[loc] = ioutil.NopCloser(bytes.NewReader(contentsBytes))
|
||||
}
|
||||
|
||||
case len(mappedLocations) == 1:
|
||||
results[locationByRefs[ref][0]] = content
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected ref-location count=%d for ref=%v", len(mappedLocations), ref)
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -13,9 +13,10 @@ type resolution struct {
|
||||
|
||||
func TestAllLayersResolver_FilesByPath(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
linkPath string
|
||||
resolutions []resolution
|
||||
name string
|
||||
linkPath string
|
||||
resolutions []resolution
|
||||
forcePositiveHasPath bool
|
||||
}{
|
||||
{
|
||||
name: "link with previous data",
|
||||
@@ -66,14 +67,17 @@ func TestAllLayersResolver_FilesByPath(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dead link",
|
||||
linkPath: "/link-dead",
|
||||
resolutions: []resolution{},
|
||||
name: "dead link",
|
||||
linkPath: "/link-dead",
|
||||
resolutions: []resolution{},
|
||||
forcePositiveHasPath: true,
|
||||
},
|
||||
{
|
||||
name: "ignore directories",
|
||||
linkPath: "/bin",
|
||||
resolutions: []resolution{},
|
||||
// directories don't resolve BUT do exist
|
||||
forcePositiveHasPath: true,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
@@ -86,6 +90,17 @@ func TestAllLayersResolver_FilesByPath(t *testing.T) {
|
||||
t.Fatalf("could not create resolver: %+v", err)
|
||||
}
|
||||
|
||||
hasPath := resolver.HasPath(c.linkPath)
|
||||
if !c.forcePositiveHasPath {
|
||||
if len(c.resolutions) > 0 && !hasPath {
|
||||
t.Errorf("expected HasPath() to indicate existance, but did not")
|
||||
} else if len(c.resolutions) == 0 && hasPath {
|
||||
t.Errorf("expeced HasPath() to NOT indicate existance, but does")
|
||||
}
|
||||
} else if !hasPath {
|
||||
t.Errorf("expected HasPath() to indicate existance, but did not (force path)")
|
||||
}
|
||||
|
||||
refs, err := resolver.FilesByPath(c.linkPath)
|
||||
if err != nil {
|
||||
t.Fatalf("could not use resolver: %+v", err)
|
||||
@@ -98,8 +113,12 @@ func TestAllLayersResolver_FilesByPath(t *testing.T) {
|
||||
for idx, actual := range refs {
|
||||
expected := c.resolutions[idx]
|
||||
|
||||
if actual.Path != expected.path {
|
||||
t.Errorf("bad resolve path: '%s'!='%s'", actual.Path, expected.path)
|
||||
if string(actual.ref.RealPath) != expected.path {
|
||||
t.Errorf("bad resolve path: '%s'!='%s'", string(actual.ref.RealPath), expected.path)
|
||||
}
|
||||
|
||||
if expected.path != "" && string(actual.ref.RealPath) != actual.RealPath {
|
||||
t.Errorf("we should always prefer real paths over ones with links")
|
||||
}
|
||||
|
||||
entry, err := img.FileCatalog.Get(actual.ref)
|
||||
@@ -202,8 +221,12 @@ func TestAllLayersResolver_FilesByGlob(t *testing.T) {
|
||||
for idx, actual := range refs {
|
||||
expected := c.resolutions[idx]
|
||||
|
||||
if actual.Path != expected.path {
|
||||
t.Errorf("bad resolve path: '%s'!='%s'", actual.Path, expected.path)
|
||||
if string(actual.ref.RealPath) != expected.path {
|
||||
t.Errorf("bad resolve path: '%s'!='%s'", string(actual.ref.RealPath), expected.path)
|
||||
}
|
||||
|
||||
if expected.path != "" && string(actual.ref.RealPath) != actual.RealPath {
|
||||
t.Errorf("we should always prefer real paths over ones with links")
|
||||
}
|
||||
|
||||
entry, err := img.FileCatalog.Get(actual.ref)
|
||||
|
||||
@@ -54,20 +54,20 @@ func TestContentRequester(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, entry := range data {
|
||||
if expected, ok := test.expectedContents[entry.Location.Path]; ok {
|
||||
if expected, ok := test.expectedContents[entry.Location.RealPath]; ok {
|
||||
actualBytes, err := ioutil.ReadAll(entry.Contents)
|
||||
if err != nil {
|
||||
t.Fatalf("could not read %q: %+v", entry.Location.Path, err)
|
||||
t.Fatalf("could not read %q: %+v", entry.Location.RealPath, err)
|
||||
}
|
||||
for expected != string(actualBytes) {
|
||||
t.Errorf("mismatched contents for %q", entry.Location.Path)
|
||||
t.Errorf("mismatched contents for %q", entry.Location.RealPath)
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(expected, string(actualBytes), true)
|
||||
t.Errorf("diff: %s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
continue
|
||||
}
|
||||
t.Errorf("could not find %q", entry.Location.Path)
|
||||
t.Errorf("could not find %q", entry.Location.RealPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,27 +19,37 @@ type DirectoryResolver struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func (r DirectoryResolver) requestPath(userPath string) string {
|
||||
fullPath := userPath
|
||||
if filepath.IsAbs(fullPath) {
|
||||
// a path relative to root should be prefixed with the resolvers directory path, otherwise it should be left as is
|
||||
fullPath = path.Join(r.Path, fullPath)
|
||||
}
|
||||
return fullPath
|
||||
}
|
||||
|
||||
// HasPath indicates if the given path exists in the underlying source.
|
||||
func (r *DirectoryResolver) HasPath(userPath string) bool {
|
||||
_, err := os.Stat(r.requestPath(userPath))
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
// Stringer to represent a directory path data source
|
||||
func (s DirectoryResolver) String() string {
|
||||
return fmt.Sprintf("dir:%s", s.Path)
|
||||
func (r DirectoryResolver) String() string {
|
||||
return fmt.Sprintf("dir:%s", r.Path)
|
||||
}
|
||||
|
||||
// FilesByPath returns all file.References that match the given paths from the directory.
|
||||
func (s DirectoryResolver) FilesByPath(userPaths ...string) ([]Location, error) {
|
||||
func (r DirectoryResolver) FilesByPath(userPaths ...string) ([]Location, error) {
|
||||
var references = make([]Location, 0)
|
||||
|
||||
for _, userPath := range userPaths {
|
||||
userStrPath := userPath
|
||||
|
||||
if filepath.IsAbs(userStrPath) {
|
||||
// a path relative to root should be prefixed with the resolvers directory path, otherwise it should be left as is
|
||||
userStrPath = path.Join(s.Path, userStrPath)
|
||||
}
|
||||
userStrPath := r.requestPath(userPath)
|
||||
fileMeta, err := os.Stat(userStrPath)
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
log.Errorf("path (%s) is not valid: %v", userStrPath, err)
|
||||
log.Errorf("path (%r) is not valid: %v", userStrPath, err)
|
||||
}
|
||||
|
||||
// don't consider directories
|
||||
@@ -54,11 +64,11 @@ func (s DirectoryResolver) FilesByPath(userPaths ...string) ([]Location, error)
|
||||
}
|
||||
|
||||
// FilesByGlob returns all file.References that match the given path glob pattern from any layer in the image.
|
||||
func (s DirectoryResolver) FilesByGlob(patterns ...string) ([]Location, error) {
|
||||
func (r DirectoryResolver) FilesByGlob(patterns ...string) ([]Location, error) {
|
||||
result := make([]Location, 0)
|
||||
|
||||
for _, pattern := range patterns {
|
||||
pathPattern := path.Join(s.Path, pattern)
|
||||
pathPattern := path.Join(r.Path, pattern)
|
||||
pathMatches, err := doublestar.Glob(pathPattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -84,8 +94,8 @@ func (s DirectoryResolver) FilesByGlob(patterns ...string) ([]Location, error) {
|
||||
// RelativeFileByPath fetches a single file at the given path relative to the layer squash of the given reference.
|
||||
// This is helpful when attempting to find a file that is in the same layer or lower as another file. For the
|
||||
// DirectoryResolver, this is a simple path lookup.
|
||||
func (s *DirectoryResolver) RelativeFileByPath(_ Location, path string) *Location {
|
||||
paths, err := s.FilesByPath(path)
|
||||
func (r *DirectoryResolver) RelativeFileByPath(_ Location, path string) *Location {
|
||||
paths, err := r.FilesByPath(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -97,16 +107,16 @@ func (s *DirectoryResolver) RelativeFileByPath(_ Location, path string) *Locatio
|
||||
}
|
||||
|
||||
// MultipleFileContentsByLocation returns the file contents for all file.References relative a directory.
|
||||
func (s DirectoryResolver) MultipleFileContentsByLocation(locations []Location) (map[Location]io.ReadCloser, error) {
|
||||
func (r DirectoryResolver) MultipleFileContentsByLocation(locations []Location) (map[Location]io.ReadCloser, error) {
|
||||
refContents := make(map[Location]io.ReadCloser)
|
||||
for _, location := range locations {
|
||||
refContents[location] = file.NewDeferredReadCloser(location.Path)
|
||||
refContents[location] = file.NewDeferredReadCloser(location.RealPath)
|
||||
}
|
||||
return refContents, nil
|
||||
}
|
||||
|
||||
// FileContentsByLocation fetches file contents for a single file reference relative to a directory.
|
||||
// If the path does not exist an error is returned.
|
||||
func (s DirectoryResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) {
|
||||
return file.NewDeferredReadCloser(location.Path), nil
|
||||
func (r DirectoryResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) {
|
||||
return file.NewDeferredReadCloser(location.RealPath), nil
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@ import (
|
||||
|
||||
func TestDirectoryResolver_FilesByPath(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
root string
|
||||
input string
|
||||
expected string
|
||||
refCount int
|
||||
name string
|
||||
root string
|
||||
input string
|
||||
expected string
|
||||
refCount int
|
||||
forcePositiveHasPath bool
|
||||
}{
|
||||
{
|
||||
name: "finds a file (relative)",
|
||||
@@ -47,15 +48,28 @@ func TestDirectoryResolver_FilesByPath(t *testing.T) {
|
||||
refCount: 1,
|
||||
},
|
||||
{
|
||||
name: "directories ignored",
|
||||
root: "./test-fixtures/",
|
||||
input: "/image-symlinks",
|
||||
refCount: 0,
|
||||
name: "directories ignored",
|
||||
root: "./test-fixtures/",
|
||||
input: "/image-symlinks",
|
||||
refCount: 0,
|
||||
forcePositiveHasPath: true,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
resolver := DirectoryResolver{c.root}
|
||||
|
||||
hasPath := resolver.HasPath(c.input)
|
||||
if !c.forcePositiveHasPath {
|
||||
if c.refCount != 0 && !hasPath {
|
||||
t.Errorf("expected HasPath() to indicate existance, but did not")
|
||||
} else if c.refCount == 0 && hasPath {
|
||||
t.Errorf("expeced HasPath() to NOT indicate existance, but does")
|
||||
}
|
||||
} else if !hasPath {
|
||||
t.Errorf("expected HasPath() to indicate existance, but did not (force path)")
|
||||
}
|
||||
|
||||
refs, err := resolver.FilesByPath(c.input)
|
||||
if err != nil {
|
||||
t.Fatalf("could not use resolver: %+v, %+v", err, refs)
|
||||
@@ -66,8 +80,8 @@ func TestDirectoryResolver_FilesByPath(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, actual := range refs {
|
||||
if actual.Path != c.expected {
|
||||
t.Errorf("bad resolve path: '%s'!='%s'", actual.Path, c.expected)
|
||||
if actual.RealPath != c.expected {
|
||||
t.Errorf("bad resolve path: '%s'!='%s'", actual.RealPath, c.expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,9 +4,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
)
|
||||
|
||||
@@ -25,6 +24,11 @@ func NewImageSquashResolver(img *image.Image) (*ImageSquashResolver, error) {
|
||||
return &ImageSquashResolver{img: img}, nil
|
||||
}
|
||||
|
||||
// HasPath indicates if the given path exists in the underlying source.
|
||||
func (r *ImageSquashResolver) HasPath(path string) bool {
|
||||
return r.img.SquashedTree().HasPath(file.Path(path))
|
||||
}
|
||||
|
||||
// FilesByPath returns all file.References that match the given paths within the squashed representation of the image.
|
||||
func (r *ImageSquashResolver) FilesByPath(paths ...string) ([]Location, error) {
|
||||
uniqueFileIDs := file.NewFileReferenceSet()
|
||||
@@ -62,7 +66,7 @@ func (r *ImageSquashResolver) FilesByPath(paths ...string) ([]Location, error) {
|
||||
|
||||
if resolvedRef != nil && !uniqueFileIDs.Contains(*resolvedRef) {
|
||||
uniqueFileIDs.Add(*resolvedRef)
|
||||
uniqueLocations = append(uniqueLocations, NewLocationFromImage(*resolvedRef, r.img))
|
||||
uniqueLocations = append(uniqueLocations, NewLocationFromImage(path, *resolvedRef, r.img))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,11 @@ import (
|
||||
|
||||
func TestImageSquashResolver_FilesByPath(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
linkPath string
|
||||
resolveLayer uint
|
||||
resolvePath string
|
||||
name string
|
||||
linkPath string
|
||||
resolveLayer uint
|
||||
resolvePath string
|
||||
forcePositiveHasPath bool
|
||||
}{
|
||||
{
|
||||
name: "link with previous data",
|
||||
@@ -42,11 +43,15 @@ func TestImageSquashResolver_FilesByPath(t *testing.T) {
|
||||
linkPath: "/link-dead",
|
||||
resolveLayer: 8,
|
||||
resolvePath: "",
|
||||
// the path should exist, even if the link is dead
|
||||
forcePositiveHasPath: true,
|
||||
},
|
||||
{
|
||||
name: "ignore directories",
|
||||
linkPath: "/bin",
|
||||
resolvePath: "",
|
||||
// the path should exist, even if we ignore it
|
||||
forcePositiveHasPath: true,
|
||||
},
|
||||
{
|
||||
name: "parent is a link (with overridden data)",
|
||||
@@ -65,6 +70,17 @@ func TestImageSquashResolver_FilesByPath(t *testing.T) {
|
||||
t.Fatalf("could not create resolver: %+v", err)
|
||||
}
|
||||
|
||||
hasPath := resolver.HasPath(c.linkPath)
|
||||
if !c.forcePositiveHasPath {
|
||||
if c.resolvePath != "" && !hasPath {
|
||||
t.Errorf("expected HasPath() to indicate existance, but did not")
|
||||
} else if c.resolvePath == "" && hasPath {
|
||||
t.Errorf("expeced HasPath() to NOT indicate existance, but does")
|
||||
}
|
||||
} else if !hasPath {
|
||||
t.Errorf("expected HasPath() to indicate existance, but did not (force path)")
|
||||
}
|
||||
|
||||
refs, err := resolver.FilesByPath(c.linkPath)
|
||||
if err != nil {
|
||||
t.Fatalf("could not use resolver: %+v", err)
|
||||
@@ -86,8 +102,12 @@ func TestImageSquashResolver_FilesByPath(t *testing.T) {
|
||||
|
||||
actual := refs[0]
|
||||
|
||||
if actual.Path != c.resolvePath {
|
||||
t.Errorf("bad resolve path: '%s'!='%s'", actual.Path, c.resolvePath)
|
||||
if string(actual.ref.RealPath) != c.resolvePath {
|
||||
t.Errorf("bad resolve path: '%s'!='%s'", string(actual.ref.RealPath), c.resolvePath)
|
||||
}
|
||||
|
||||
if c.resolvePath != "" && string(actual.ref.RealPath) != actual.RealPath {
|
||||
t.Errorf("we should always prefer real paths over ones with links")
|
||||
}
|
||||
|
||||
entry, err := img.FileCatalog.Get(actual.ref)
|
||||
@@ -188,8 +208,12 @@ func TestImageSquashResolver_FilesByGlob(t *testing.T) {
|
||||
|
||||
actual := refs[0]
|
||||
|
||||
if actual.Path != c.resolvePath {
|
||||
t.Errorf("bad resolve path: '%s'!='%s'", actual.Path, c.resolvePath)
|
||||
if string(actual.ref.RealPath) != c.resolvePath {
|
||||
t.Errorf("bad resolve path: '%s'!='%s'", string(actual.ref.RealPath), c.resolvePath)
|
||||
}
|
||||
|
||||
if c.resolvePath != "" && string(actual.ref.RealPath) != actual.RealPath {
|
||||
t.Errorf("we should always prefer real paths over ones with links")
|
||||
}
|
||||
|
||||
entry, err := img.FileCatalog.Get(actual.ref)
|
||||
|
||||
+11
-7
@@ -7,9 +7,11 @@ import (
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
)
|
||||
|
||||
// Location represents a path relative to a particular filesystem.
|
||||
// Location represents a path relative to a particular filesystem resolved to a specific file.Reference. This struct is used as a key
|
||||
// in content fetching to uniquely identify a file relative to a request (the VirtualPath).
|
||||
type Location struct {
|
||||
Path string `json:"path"` // The string path of the location (e.g. /etc/hosts)
|
||||
RealPath string `json:"path"` // The path where all path ancestors have no hardlinks / symlinks
|
||||
VirtualPath string `json:"-"` // The path to the file which may or may not have hardlinks / symlinks
|
||||
FileSystemID string `json:"layerID,omitempty"` // An ID representing the filesystem. For container images this is a layer digest, directories or root filesystem this is blank.
|
||||
ref file.Reference // The file reference relative to the stereoscope.FileCatalog that has more information about this location.
|
||||
}
|
||||
@@ -17,23 +19,25 @@ type Location struct {
|
||||
// NewLocation creates a new Location representing a path without denoting a filesystem or FileCatalog reference.
|
||||
func NewLocation(path string) Location {
|
||||
return Location{
|
||||
Path: path,
|
||||
RealPath: path,
|
||||
}
|
||||
}
|
||||
|
||||
// NewLocationFromImage creates a new Location representing the given path (extracted from the ref) relative to the given image.
|
||||
func NewLocationFromImage(ref file.Reference, img *image.Image) Location {
|
||||
func NewLocationFromImage(virtualPath string, ref file.Reference, img *image.Image) Location {
|
||||
entry, err := img.FileCatalog.Get(ref)
|
||||
if err != nil {
|
||||
log.Warnf("unable to find file catalog entry for ref=%+v", ref)
|
||||
return Location{
|
||||
Path: string(ref.RealPath),
|
||||
ref: ref,
|
||||
VirtualPath: virtualPath,
|
||||
RealPath: string(ref.RealPath),
|
||||
ref: ref,
|
||||
}
|
||||
}
|
||||
|
||||
return Location{
|
||||
Path: string(ref.RealPath),
|
||||
VirtualPath: virtualPath,
|
||||
RealPath: string(ref.RealPath),
|
||||
FileSystemID: entry.Layer.Metadata.Digest,
|
||||
ref: ref,
|
||||
}
|
||||
|
||||
@@ -28,9 +28,19 @@ func NewMockResolverForPaths(paths ...string) *MockResolver {
|
||||
return &MockResolver{Locations: locations}
|
||||
}
|
||||
|
||||
// HasPath indicates if the given path exists in the underlying source.
|
||||
func (r MockResolver) HasPath(path string) bool {
|
||||
for _, l := range r.Locations {
|
||||
if l.RealPath == path {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// String returns the string representation of the MockResolver.
|
||||
func (r MockResolver) String() string {
|
||||
return fmt.Sprintf("mock:(%s,...)", r.Locations[0].Path)
|
||||
return fmt.Sprintf("mock:(%s,...)", r.Locations[0].RealPath)
|
||||
}
|
||||
|
||||
// FileContentsByLocation fetches file contents for a single location. If the
|
||||
@@ -38,7 +48,7 @@ func (r MockResolver) String() string {
|
||||
func (r MockResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) {
|
||||
for _, l := range r.Locations {
|
||||
if l == location {
|
||||
return os.Open(location.Path)
|
||||
return os.Open(location.RealPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +74,7 @@ func (r MockResolver) FilesByPath(paths ...string) ([]Location, error) {
|
||||
var results []Location
|
||||
for _, p := range paths {
|
||||
for _, location := range r.Locations {
|
||||
if p == location.Path {
|
||||
if p == location.RealPath {
|
||||
results = append(results, NewLocation(p))
|
||||
}
|
||||
}
|
||||
@@ -78,7 +88,7 @@ func (r MockResolver) FilesByGlob(patterns ...string) ([]Location, error) {
|
||||
var results []Location
|
||||
for _, pattern := range patterns {
|
||||
for _, location := range r.Locations {
|
||||
if file.GlobMatch(pattern, location.Path) {
|
||||
if file.GlobMatch(pattern, location.RealPath) {
|
||||
results = append(results, location)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,14 @@ type Resolver interface {
|
||||
// ContentResolver knows how to get file content for given file.References
|
||||
type ContentResolver interface {
|
||||
FileContentsByLocation(Location) (io.ReadCloser, error)
|
||||
// TODO: it is possible to be given duplicate locations that will be overridden in the map (key), a subtle problem that coule easily be misued.
|
||||
MultipleFileContentsByLocation([]Location) (map[Location]io.ReadCloser, error)
|
||||
// TODO: we should consider refactoring to return a set of io.Readers or file.Openers instead of the full contents themselves (allow for optional buffering).
|
||||
}
|
||||
|
||||
// FileResolver knows how to get file.References for given string paths and globs
|
||||
// FileResolver knows how to get a Location for given string paths and globs
|
||||
type FileResolver interface {
|
||||
// HasPath indicates if the given path exists in the underlying source.
|
||||
HasPath(path string) bool
|
||||
// FilesByPath fetches a set of file references which have the given path (for an image, there may be multiple matches)
|
||||
FilesByPath(paths ...string) ([]Location, error)
|
||||
// FilesByGlob fetches a set of file references which the given glob matches
|
||||
|
||||
@@ -2,6 +2,7 @@ package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
@@ -57,8 +58,6 @@ func TestCatalogFromJSON(t *testing.T) {
|
||||
|
||||
var actualPackages, expectedPackages []*pkg.Package
|
||||
|
||||
// TODO: take out pkg.RpmdbMetadataType filter
|
||||
|
||||
for _, p := range expectedCatalog.Sorted() {
|
||||
expectedPackages = append(expectedPackages, p)
|
||||
}
|
||||
@@ -81,10 +80,15 @@ func TestCatalogFromJSON(t *testing.T) {
|
||||
e.Metadata = metadata
|
||||
}
|
||||
|
||||
// ignore the virtual path on the location for now
|
||||
for l := range a.Locations {
|
||||
a.Locations[l].VirtualPath = ""
|
||||
e.Locations[l].VirtualPath = ""
|
||||
}
|
||||
|
||||
for _, d := range deep.Equal(a, e) {
|
||||
// ignore errors for empty collections vs nil for select fields
|
||||
// TODO: this is brittle, but not dangerously so. We should still find a better way to do this.
|
||||
if d == "Licenses: [] != <nil slice>" {
|
||||
if strings.Contains(d, "[] != <nil slice>") {
|
||||
continue
|
||||
}
|
||||
t.Errorf(" package %d (name=%s) diff: %+v", i, e.Name, d)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJavaNoMainPackage(t *testing.T) { // Regression: https://github.com/anchore/syft/issues/252
|
||||
fixtureImageName := "image-java-no-main-package"
|
||||
_, cleanup := imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName)
|
||||
tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName)
|
||||
defer cleanup()
|
||||
|
||||
_, _, _, err := syft.Catalog("docker-archive:"+tarPath, source.SquashedScope)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to catalog image: %+v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
jsonPresenter "github.com/anchore/syft/syft/presenter/json"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func TestPackageOwnershipRelationships(t *testing.T) {
|
||||
|
||||
// ensure that the json presenter is applying artifact ownership with an image that has expected ownership relationships
|
||||
tests := []struct {
|
||||
fixture string
|
||||
}{
|
||||
{
|
||||
fixture: "image-owning-package",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.fixture, func(t *testing.T) {
|
||||
_, cleanup := imagetest.GetFixtureImage(t, "docker-archive", test.fixture)
|
||||
tarPath := imagetest.GetFixtureImageTarPath(t, test.fixture)
|
||||
defer cleanup()
|
||||
|
||||
src, catalog, d, err := syft.Catalog("docker-archive:"+tarPath, source.SquashedScope)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to catalog image: %+v", err)
|
||||
}
|
||||
|
||||
p := presenter.GetPresenter(presenter.JSONPresenter, src.Metadata, catalog, d)
|
||||
if p == nil {
|
||||
t.Fatal("unable to get presenter")
|
||||
}
|
||||
|
||||
output := bytes.NewBufferString("")
|
||||
err = p.Present(output)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to present: %+v", err)
|
||||
}
|
||||
|
||||
var doc jsonPresenter.Document
|
||||
decoder := json.NewDecoder(output)
|
||||
if err := decoder.Decode(&doc); err != nil {
|
||||
t.Fatalf("unable to decode json doc: %+v", err)
|
||||
}
|
||||
|
||||
if len(doc.ArtifactRelationships) == 0 {
|
||||
t.Errorf("expected to find relationships between packages but found none")
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,10 @@ var imageOnlyTestCases = []testCase{
|
||||
pkgType: pkg.GemPkg,
|
||||
pkgLanguage: pkg.Ruby,
|
||||
pkgInfo: map[string]string{
|
||||
// specifications in the root specification directory
|
||||
"bundler": "2.1.4",
|
||||
// specifications in named directories
|
||||
"unbundler": "3.1.4",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestRegression212ApkBufferSize(t *testing.T) {
|
||||
t.Fatalf("failed to catalog image: %+v", err)
|
||||
}
|
||||
|
||||
expectedPkgs := 57
|
||||
expectedPkgs := 58
|
||||
actualPkgs := 0
|
||||
for range catalog.Enumerate(pkg.ApkPkg) {
|
||||
actualPkgs += 1
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
FROM jenkins:2.60.3
|
||||
|
||||
USER root
|
||||
|
||||
WORKDIR /usr/share/jenkins
|
||||
|
||||
RUN mkdir tmp
|
||||
|
||||
WORKDIR /usr/share/jenkins/tmp
|
||||
|
||||
RUN apt-get update 2>&1 > /dev/null && apt-get install -y less zip 2>&1 > /dev/null
|
||||
|
||||
RUN unzip ../jenkins.war 2>&1 > /dev/null
|
||||
|
||||
RUN rm -f ./META-INF/MANIFEST.MF
|
||||
|
||||
WORKDIR /usr/share/jenkins
|
||||
|
||||
RUN rm -rf jenkins.war
|
||||
|
||||
RUN cd ./tmp && zip -r ../jenkins.war . && cd ..
|
||||
|
||||
RUN rm -rf ./tmp
|
||||
@@ -1,2 +1,5 @@
|
||||
FROM alpine:latest
|
||||
RUN apk add tzdata vim alpine-sdk
|
||||
FROM alpine@sha256:d9a7354e3845ea8466bb00b22224d9116b183e594527fb5b6c3d30bc01a20378
|
||||
RUN apk add --no-cache \
|
||||
tzdata=2021a-r0 \
|
||||
vim=8.2.2320-r0 \
|
||||
alpine-sdk=1.0-r0
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
FROM ubuntu:20.04
|
||||
# this covers rpm-python
|
||||
RUN apt-get update && apt-get install -y python-pil=6.2.1-3
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
# -*- encoding: utf-8 -*-
|
||||
# stub: unbundler 2.1.4 ruby lib
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = "unbundler".freeze
|
||||
s.version = "3.1.4"
|
||||
|
||||
s.required_rubygems_version = Gem::Requirement.new(">= 2.5.2".freeze) if s.respond_to? :required_rubygems_version=
|
||||
s.require_paths = ["lib".freeze]
|
||||
s.authors = ["Andr\u00E9 Arko".freeze, "Samuel Giddins".freeze, "Colby Swandale".freeze, "Hiroshi Shibata".freeze, "David Rodr\u00EDguez".freeze, "Grey Baker".f
|
||||
s.bindir = "exe".freeze
|
||||
s.date = "2020-01-05"
|
||||
s.description = "Bundler manages an application's dependencies through its entire life, across many machines, systematically and repeatably".freeze
|
||||
s.email = ["team@unbundler.io".freeze]
|
||||
s.executables = ["unbundle".freeze, "unbundler".freeze]
|
||||
s.files = ["exe/unbundle".freeze, "exe/unbundler".freeze]
|
||||
s.homepage = "https://unbundler.io".freeze
|
||||
s.licenses = ["MIT".freeze]
|
||||
s.required_ruby_version = Gem::Requirement.new(">= 2.3.0".freeze)
|
||||
s.rubygems_version = "3.1.2".freeze
|
||||
s.summary = "The best way to manage your application's dependencies".freeze
|
||||
|
||||
s.installed_by_version = "3.1.2" if s.respond_to? :installed_by_version
|
||||
end
|
||||
Reference in New Issue
Block a user