mirror of
https://github.com/anchore/syft.git
synced 2026-10-13 14:47:22 +02:00
Compare commits
177
Commits
plugin-system
...
v0.10.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52bac6e2fd | ||
|
|
2d0c127419 | ||
|
|
1a124bd77b | ||
|
|
a5fd83b21d | ||
|
|
f87c59b4eb | ||
|
|
3ce7eabc98 | ||
|
|
6f9ded60ed | ||
|
|
65cbacd135 | ||
|
|
2989d3d975 | ||
|
|
ae71b8832d | ||
|
|
c42b036f46 | ||
|
|
8a17bfb69f | ||
|
|
0ed30138c4 | ||
|
|
3aaa0e5566 | ||
|
|
71939557e6 | ||
|
|
030427bb33 | ||
|
|
569a598df7 | ||
|
|
0ad8c53ec2 | ||
|
|
c892c3609e | ||
|
|
62b03f3a91 | ||
|
|
4b45c42f5a | ||
|
|
f46de19c6b | ||
|
|
91baabe5a1 | ||
|
|
6f7a4fd3e4 | ||
|
|
aa0d444fd4 | ||
|
|
b694dacb21 | ||
|
|
9668341a14 | ||
|
|
495fb0a45f | ||
|
|
a640a2c4cd | ||
|
|
3e8bca6911 | ||
|
|
5afdd574a8 | ||
|
|
0ebe791acd | ||
|
|
a5b72405dd | ||
|
|
4861f69d7c | ||
|
|
6bde075cd3 | ||
|
|
f7be73dbcc | ||
|
|
fc99538d78 | ||
|
|
4d4e242251 | ||
|
|
416afa015e | ||
|
|
bd4c784a64 | ||
|
|
624a090631 | ||
|
|
dad1d9691c | ||
|
|
7964c27bd8 | ||
|
|
9581a0309b | ||
|
|
7e270bf76c | ||
|
|
fde37b4b56 | ||
|
|
8627ea88ce | ||
|
|
2c82c7c0f0 | ||
|
|
4c38ea595d | ||
|
|
6fe5ad0739 | ||
|
|
7bced775c4 | ||
|
|
db98fba3b4 | ||
|
|
29d464c38a | ||
|
|
2a329002b8 | ||
|
|
a6365886ad | ||
|
|
6ae3b47959 | ||
|
|
10b55311df | ||
|
|
64d5554144 | ||
|
|
b3098f3423 | ||
|
|
c2cf4eb7b0 | ||
|
|
1e79986188 | ||
|
|
3699a917fd | ||
|
|
501870f89f | ||
|
|
c878cc361c | ||
|
|
8fb5b17dbd | ||
|
|
b6eb589b78 | ||
|
|
6aba2f48d4 | ||
|
|
0205e72be9 | ||
|
|
8095cd9980 | ||
|
|
ecfc471ce5 | ||
|
|
a52750bdd3 | ||
|
|
773581704c | ||
|
|
6e98752c6b | ||
|
|
e2593cd6b7 | ||
|
|
232cd13035 | ||
|
|
2532928afa | ||
|
|
03dbfb8dfb | ||
|
|
a5cba13ddf | ||
|
|
a4f22e65fc | ||
|
|
ab45be98b8 | ||
|
|
fc991bc62e | ||
|
|
1230650771 | ||
|
|
452426d1d6 | ||
|
|
2675891110 | ||
|
|
bffc4713a7 | ||
|
|
62f6146c37 | ||
|
|
8bcba7d830 | ||
|
|
76c8f24090 | ||
|
|
cc466e47da | ||
|
|
f9407d0ce4 | ||
|
|
f19cb03aa0 | ||
|
|
076454d7a8 | ||
|
|
7c42a7441c | ||
|
|
cbb7e00a52 | ||
|
|
6eb36bc3c8 | ||
|
|
94ffc2caa8 | ||
|
|
15e2e32103 | ||
|
|
1cdf2b9151 | ||
|
|
fd65f363ce | ||
|
|
103f0617f5 | ||
|
|
de2e6a13b8 | ||
|
|
7d55bca0a0 | ||
|
|
48c7dee9da | ||
|
|
94e448a818 | ||
|
|
15379d1075 | ||
|
|
2e5ff4a995 | ||
|
|
1414d1fbc3 | ||
|
|
7fc926d40d | ||
|
|
5b08616e47 | ||
|
|
0ce8701e73 | ||
|
|
beb6afff36 | ||
|
|
d4ca0ab167 | ||
|
|
6cd9c2b771 | ||
|
|
931c796158 | ||
|
|
bb14f3b45b | ||
|
|
c0b9d7854b | ||
|
|
27c62e34f2 | ||
|
|
4c751cb1d4 | ||
|
|
0f92f16eb6 | ||
|
|
1bf1e643eb | ||
|
|
3ba67a6bcc | ||
|
|
4a8ee15e87 | ||
|
|
dcb5d6d08e | ||
|
|
b3387807c1 | ||
|
|
24c4c025e1 | ||
|
|
5d0909f31e | ||
|
|
1f64b3cd49 | ||
|
|
8d25d44ea9 | ||
|
|
1fc46291a6 | ||
|
|
2b932f8d65 | ||
|
|
0d3e153959 | ||
|
|
587589bfe3 | ||
|
|
4ff330e396 | ||
|
|
949bf7bc91 | ||
|
|
956af57d3b | ||
|
|
262a0888a8 | ||
|
|
335a6b022f | ||
|
|
da0eb6f20f | ||
|
|
1be5f5756a | ||
|
|
9edbc65bce | ||
|
|
398d8903e7 | ||
|
|
46c74865e5 | ||
|
|
1f0f6fa3e5 | ||
|
|
abdd00cd24 | ||
|
|
10b44f5311 | ||
|
|
1c320a8382 | ||
|
|
7a8a5419b8 | ||
|
|
16b23e7994 | ||
|
|
e6b71486b0 | ||
|
|
5adfce19e0 | ||
|
|
b6dfdf16b6 | ||
|
|
b5ce187ba7 | ||
|
|
26855a2a9e | ||
|
|
4b78d9a1c0 | ||
|
|
2844b9878f | ||
|
|
8b81c87d18 | ||
|
|
9bd9dad76c | ||
|
|
6d9f9a9b3b | ||
|
|
50cb5612d0 | ||
|
|
080f010746 | ||
|
|
f4502fc824 | ||
|
|
03378e976c | ||
|
|
576fbc898b | ||
|
|
49800b6747 | ||
|
|
c46d004a3b | ||
|
|
3d91a66536 | ||
|
|
45b5cab49a | ||
|
|
1866949eff | ||
|
|
0567206b38 | ||
|
|
f3076c3905 | ||
|
|
b467964044 | ||
|
|
e26627d8b6 | ||
|
|
80ee18d20b | ||
|
|
ef64a6f206 | ||
|
|
22dfcb2d0b | ||
|
|
329b080a37 | ||
|
|
11334b632e |
@@ -1,129 +0,0 @@
|
||||
version: 2.1
|
||||
|
||||
jobs:
|
||||
run-static-analysis:
|
||||
parameters:
|
||||
version:
|
||||
type: string
|
||||
docker:
|
||||
- image: circleci/golang:<< parameters.version >>
|
||||
environment:
|
||||
GO111MODULE: "on"
|
||||
# work around for recent circle CI breaking change
|
||||
# Error: "Error response from daemon: client version 1.39 is too new. Maximum supported API version is 1.38"
|
||||
DOCKER_API_VERSION: "1.38"
|
||||
# 2CPU / 4GB RAM
|
||||
resource_class: medium
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-<< parameters.version >>-{{ checksum "go.sum" }}-{{ checksum "Makefile" }}
|
||||
|
||||
- run: make ci-bootstrap
|
||||
|
||||
- save_cache:
|
||||
key: go-<< parameters.version >>-{{ checksum "go.sum" }}-{{ checksum "Makefile" }}
|
||||
paths:
|
||||
- "/go/pkg/mod"
|
||||
- ".tmp"
|
||||
|
||||
- run:
|
||||
name: run static analysis
|
||||
command: make static-analysis
|
||||
|
||||
run-tests:
|
||||
parameters:
|
||||
version:
|
||||
type: string
|
||||
docker:
|
||||
- image: circleci/golang:<< parameters.version >>
|
||||
environment:
|
||||
GO111MODULE: "on"
|
||||
# work around for recent circle CI breaking change
|
||||
# Error: "Error response from daemon: client version 1.39 is too new. Maximum supported API version is 1.38"
|
||||
DOCKER_API_VERSION: "1.38"
|
||||
# 2CPU / 4GB RAM
|
||||
resource_class: medium
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-<< parameters.version >>-{{ checksum "go.sum" }}-{{ checksum "Makefile" }}
|
||||
|
||||
- run: make ci-bootstrap
|
||||
|
||||
- save_cache:
|
||||
key: go-<< parameters.version >>-{{ checksum "go.sum" }}-{{ checksum "Makefile" }}
|
||||
paths:
|
||||
- "/go/pkg/mod"
|
||||
- ".tmp"
|
||||
|
||||
- setup_remote_docker:
|
||||
version: 18.06.0-ce
|
||||
|
||||
- run:
|
||||
name: enable docker client
|
||||
command: |
|
||||
# all of this to enable "circleci local execute ..." cli commands for /var/run/docker.sock
|
||||
mkdir -p ${HOME}/.local/bin
|
||||
cat \<< EOF > ${HOME}/.local/bin/docker
|
||||
#!/bin/bash
|
||||
set -xue
|
||||
sudo -E ${HOME}/.local/bin/docker.bin \$@
|
||||
EOF
|
||||
sudo mv /usr/bin/docker ${HOME}/.local/bin/docker.bin
|
||||
chmod 755 ${HOME}/.local/bin/docker
|
||||
|
||||
- run:
|
||||
name: build cache key for java test-fixture blobs
|
||||
command: make java-packages-fingerprint
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- syft-unit-test-java-cache-{{ checksum "syft/cataloger/java/test-fixtures/java-builds/packages.fingerprint" }}
|
||||
|
||||
- run:
|
||||
name: run unit tests
|
||||
command: make unit
|
||||
|
||||
- save_cache:
|
||||
key: syft-unit-test-java-cache-{{ checksum "syft/cataloger/java/test-fixtures/java-builds/packages.fingerprint" }}
|
||||
paths:
|
||||
- "syft/cataloger/java/test-fixtures/java-builds/packages"
|
||||
|
||||
- run:
|
||||
name: validate syft output against the CycloneDX schema
|
||||
command: make validate-cyclonedx-schema
|
||||
|
||||
- run:
|
||||
name: build hash key for integration test-fixtures blobs
|
||||
command: make integration-fingerprint
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- syft-integration-test-tar-cache-{{ checksum "test/integration/test-fixtures/tar-cache.fingerprint" }}
|
||||
- run:
|
||||
name: run integration tests
|
||||
command: make integration
|
||||
|
||||
- save_cache:
|
||||
key: syft-integration-test-tar-cache-{{ checksum "test/integration/test-fixtures/tar-cache.fingerprint" }}
|
||||
paths:
|
||||
- "test/integration/test-fixtures/tar-cache"
|
||||
|
||||
workflows:
|
||||
# Note: changing this workflow name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
"Static Analysis + Unit + Integration":
|
||||
jobs:
|
||||
- run-static-analysis:
|
||||
name: "Static Analysis"
|
||||
version: "1.14"
|
||||
- run-tests:
|
||||
name: "Unit & Integration Tests (go-1.13)"
|
||||
version: "1.13"
|
||||
- run-tests:
|
||||
name: "Unit & Integration Tests (go-latest)"
|
||||
version: "latest"
|
||||
@@ -16,5 +16,5 @@ assignees: ''
|
||||
**Anything else we need to know?**:
|
||||
|
||||
**Environment**:
|
||||
- syft version (use `syft version -v`):
|
||||
- Output of `syft version`:
|
||||
- OS (e.g: `cat /etc/os-release` or similar):
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
|
||||
CI_HOME="/Users/runner"
|
||||
if [[ "${HOME}" != "${CI_HOME}" ]]; then
|
||||
printf "WARNING! It looks like this isn't the CI environment. This script modifies the macOS Keychain setup in ways you probably wouldn't want for your own machine. It also requires an Apple Developer ID Certificate that you shouldn't have outside of the CI environment.\n\nExiting early to make sure nothing bad happens.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install gon (see https://github.com/mitchellh/gon for details).
|
||||
brew tap mitchellh/gon
|
||||
brew install mitchellh/gon/gon
|
||||
|
||||
# Write signing certificate to disk from environment variable.
|
||||
CERT_FILE="$HOME/developer_id_certificate.p12"
|
||||
echo -n "$APPLE_DEVELOPER_ID_CERT" | base64 --decode > "$CERT_FILE"
|
||||
|
||||
# In order to have all keychain interactions avoid an interactive user prompt, we need to control the password for the keychain in question, which means we need to create a new keychain into which we'll import the signing certificate and from which we'll later access this certificate during code signing.
|
||||
EPHEMERAL_KEYCHAIN="ci-ephemeral-keychain"
|
||||
EPHEMERAL_KEYCHAIN_PASSWORD="$(openssl rand -base64 100)"
|
||||
security create-keychain -p "${EPHEMERAL_KEYCHAIN_PASSWORD}" "${EPHEMERAL_KEYCHAIN}"
|
||||
|
||||
# Import signing certificate into the keychain. (This is a pre-requisite for gon, which is invoked via goreleaser.)
|
||||
EPHEMERAL_KEYCHAIN_FULL_PATH="$HOME/Library/Keychains/${EPHEMERAL_KEYCHAIN}-db"
|
||||
security import "${CERT_FILE}" -k "${EPHEMERAL_KEYCHAIN_FULL_PATH}" -P "${APPLE_DEVELOPER_ID_CERT_PASS}" -T "$(command -v codesign)"
|
||||
|
||||
# Setting the partition list for this certificate's private key to include "apple-tool:" and "apple:" allows the codesign command to access this keychain item without an interactive user prompt. (codesign is invoked by gon.)
|
||||
security set-key-partition-list -S "apple-tool:,apple:" -s -k "${EPHEMERAL_KEYCHAIN_PASSWORD}" "${EPHEMERAL_KEYCHAIN_FULL_PATH}"
|
||||
|
||||
# Make this new keychain the user's default keychain, so that codesign will be able to find this certificate when we specify it during signing.
|
||||
security default-keychain -d "user" -s "${EPHEMERAL_KEYCHAIN_FULL_PATH}"
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
|
||||
IS_SNAPSHOT="$1" # e.g. "true", "false"
|
||||
|
||||
if [[ "${IS_SNAPSHOT}" == "true" ]]; then
|
||||
# This is a snapshot build —— skipping signing and notarization...
|
||||
exit 0
|
||||
fi
|
||||
|
||||
GON_CONFIG="$2" # e.g. "gon.hcl"
|
||||
NEW_NAME_WITHOUT_EXTENSION="$3" # e.g. "./dist/syft-0.1.0"
|
||||
ORIGINAL_NAME_WITHOUT_EXTENSION="./dist/output" # This should match dmg and zip output_path in the gon config file, without the extension.
|
||||
|
||||
gon "${GON_CONFIG}"
|
||||
|
||||
# Rename outputs with specified desired name
|
||||
mv -v "${ORIGINAL_NAME_WITHOUT_EXTENSION}.dmg" "${NEW_NAME_WITHOUT_EXTENSION}.dmg"
|
||||
mv -v "${ORIGINAL_NAME_WITHOUT_EXTENSION}.zip" "${NEW_NAME_WITHOUT_EXTENSION}.zip"
|
||||
@@ -15,14 +15,9 @@ fi
|
||||
echo "creating and publishing version file"
|
||||
|
||||
# create a version file for version-update checks
|
||||
echo "${VERSION}" | tee ${DISTDIR}/VERSION
|
||||
VERSION_FILE="${DISTDIR}/VERSION"
|
||||
echo "${VERSION}" | tee "${VERSION_FILE}"
|
||||
|
||||
# upload the version file that supports the application version update check
|
||||
docker run --rm \
|
||||
-i \
|
||||
-e AWS_DEFAULT_REGION=us-west-2 \
|
||||
-e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \
|
||||
-e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \
|
||||
-v $(pwd)/${DISTDIR}/:/distmount \
|
||||
amazon/aws-cli \
|
||||
s3 cp /distmount/VERSION s3://toolbox-data.anchore.io/${BIN}/releases/latest/VERSION
|
||||
export AWS_DEFAULT_REGION=us-west-2
|
||||
aws s3 cp "${VERSION_FILE}" s3://toolbox-data.anchore.io/${BIN}/releases/latest/VERSION
|
||||
|
||||
@@ -14,7 +14,7 @@ env:
|
||||
|
||||
jobs:
|
||||
Build-Snapshot-Artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
@@ -35,9 +35,9 @@ jobs:
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: Bootstrap dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: make ci-bootstrap
|
||||
- name: Bootstrap project dependencies
|
||||
if: steps.bootstrap-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Import GPG key
|
||||
id: import_gpg
|
||||
@@ -64,10 +64,20 @@ jobs:
|
||||
name: artifacts
|
||||
path: snapshot/**/*
|
||||
|
||||
- uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
fields: repo,workflow,job,commit,message,author
|
||||
text: The syft acceptance tests have failed tragically!
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
||||
if: ${{ failure() }}
|
||||
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
Acceptance-Linux:
|
||||
needs: [ Build-Snapshot-Artifacts ]
|
||||
runs-on: ubuntu-latest
|
||||
# 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
|
||||
@@ -80,6 +90,15 @@ jobs:
|
||||
- name: Run Acceptance Tests (Linux)
|
||||
run: make acceptance-linux
|
||||
|
||||
- uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
fields: repo,workflow,job,commit,message,author
|
||||
text: The syft acceptance tests have failed tragically!
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
||||
if: ${{ failure() }}
|
||||
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
Acceptance-Mac:
|
||||
needs: [ Build-Snapshot-Artifacts ]
|
||||
@@ -96,10 +115,20 @@ jobs:
|
||||
- name: Run Acceptance Tests (Mac)
|
||||
run: make acceptance-mac
|
||||
|
||||
- uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
fields: repo,workflow,job,commit,message,author
|
||||
text: The syft acceptance tests have failed tragically!
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
||||
if: ${{ failure() }}
|
||||
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
Inline-Compare:
|
||||
needs: [ Build-Snapshot-Artifacts ]
|
||||
runs-on: ubuntu-latest
|
||||
# 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
|
||||
@@ -120,4 +149,13 @@ jobs:
|
||||
path: snapshot
|
||||
|
||||
- name: Compare Anchore inline-scan results against snapshot build output
|
||||
run: make compare-snapshot
|
||||
run: make compare-snapshot
|
||||
|
||||
- uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
fields: repo,workflow,job,commit,message,author
|
||||
text: The syft acceptance tests have failed tragically!
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
||||
if: ${{ failure() }}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
name: "CodeQL Security Scan"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '0 0 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Override automatic language detection by changing the below list
|
||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||
language: ['go', 'python']
|
||||
# Learn more...
|
||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
@@ -13,7 +13,7 @@ env:
|
||||
|
||||
jobs:
|
||||
wait-for-checks:
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
@@ -25,13 +25,22 @@ jobs:
|
||||
git fetch origin main
|
||||
git merge-base --is-ancestor ${GITHUB_REF##*/} origin/main && echo "${GITHUB_REF##*/} is a commit on main!"
|
||||
|
||||
- name: Check static anaylysis, unit, and integration test results
|
||||
- name: Check static analysis results
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
id: sa-unit-int
|
||||
id: static-analysis
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This check name is defined as the circle-ci workflow name (in .circleci/config.yaml)
|
||||
checkName: "Static Analysis + Unit + Integration"
|
||||
checkName: "Static-Analysis (1.x, ubuntu-latest)"
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check unit + integration results (latest go version)
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
id: unit-integration
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This check name is defined as the circle-ci workflow name (in .circleci/config.yaml)
|
||||
checkName: "Tests (1.x, ubuntu-latest)"
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check acceptance test results (linux)
|
||||
@@ -62,9 +71,10 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Quality gate
|
||||
if: steps.sa-unit-int.outputs.conclusion != 'success' || steps.inline-compare.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success'
|
||||
if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit-integration.outputs.conclusion != 'success' || steps.inline-compare.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success'
|
||||
run: |
|
||||
echo "Static/Unit/Integration Status: ${{ steps.sa-unit-int.outputs.conclusion }}"
|
||||
echo "Static Analysis Status: ${{ steps.static-analysis.conclusion }}"
|
||||
echo "Unit & Integration Test Status: ${{ steps.unit-integration.outputs.conclusion }}"
|
||||
echo "Acceptance Test (Linux) Status: ${{ steps.acceptance-linux.outputs.conclusion }}"
|
||||
echo "Acceptance Test (Mac) Status: ${{ steps.acceptance-mac.outputs.conclusion }}"
|
||||
echo "Inline Compare Status: ${{ steps.inline-compare.outputs.conclusion }}"
|
||||
@@ -72,7 +82,7 @@ jobs:
|
||||
|
||||
release:
|
||||
needs: [ wait-for-checks ]
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
@@ -83,6 +93,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# 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
|
||||
@@ -95,10 +106,6 @@ jobs:
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: Bootstrap dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: make ci-bootstrap
|
||||
|
||||
- name: Import GPG key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@v2
|
||||
@@ -122,8 +129,21 @@ jobs:
|
||||
SIGNING_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }}
|
||||
APPLE_DEVELOPER_ID_CERT: ${{ secrets.APPLE_DEVELOPER_ID_CERT }} # Used during macOS code signing.
|
||||
APPLE_DEVELOPER_ID_CERT_PASS: ${{ secrets.APPLE_DEVELOPER_ID_CERT_PASS }} # Used during macOS code signing.
|
||||
AC_USERNAME: ${{ secrets.ENG_CI_APPLE_ID }} # Used during macOS notarization.
|
||||
AC_PASSWORD: ${{ secrets.ENG_CI_APPLE_ID_PASS }} # Used during macOS notarization.
|
||||
|
||||
- uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
fields: repo,workflow,action,eventName
|
||||
text: "A new Syft release is ready to be manually published: https://github.com/anchore/syft/releases"
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
||||
if: ${{ success() }}
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: artifacts
|
||||
path: dist/**/*
|
||||
path: dist/**/*
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
name: 'Static Analysis + Unit + Integration'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
pull_request:
|
||||
jobs:
|
||||
|
||||
Static-Analysis:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.x]
|
||||
platform: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Restore bootstrap cache
|
||||
id: bootstrap-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-
|
||||
${{ runner.os }}-go-${{ matrix.go-version }}-
|
||||
|
||||
- name: Bootstrap project dependencies
|
||||
if: steps.bootstrap-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Bootstrap CI dependencies
|
||||
run: make ci-bootstrap
|
||||
|
||||
- name: Run static analysis
|
||||
run: make static-analysis
|
||||
|
||||
Tests:
|
||||
strategy:
|
||||
matrix:
|
||||
# test the lower bounds of support, and the latest available
|
||||
go-version: [1.13.x, 1.x]
|
||||
platform: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Restore bootstrap cache
|
||||
id: bootstrap-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-
|
||||
${{ runner.os }}-go-${{ matrix.go-version }}-
|
||||
|
||||
- name: Bootstrap project dependencies
|
||||
if: steps.bootstrap-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Bootstrap CI dependencies
|
||||
run: make ci-bootstrap
|
||||
|
||||
- name: Build cache key for java test-fixture blobs (for unit tests)
|
||||
run: make java-packages-fingerprint
|
||||
|
||||
- name: Restore Java test-fixture cache
|
||||
id: unit-java-cache
|
||||
uses: actions/cache@v2
|
||||
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' ) }}
|
||||
|
||||
- name: Run unit tests
|
||||
run: make unit
|
||||
|
||||
- name: Validate syft output against the CycloneDX schema
|
||||
run: make validate-cyclonedx-schema
|
||||
|
||||
- name: Build key for tar cache
|
||||
run: make integration-fingerprint
|
||||
|
||||
- name: Restore integration test cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ github.workspace }}/integration/test-fixtures/cache
|
||||
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('integration/test-fixtures/cache.fingerprint') }}
|
||||
|
||||
- name: Run integration tests
|
||||
run: make integration
|
||||
@@ -3,6 +3,7 @@ CHANGELOG.md
|
||||
/snapshot
|
||||
.server/
|
||||
.vscode/
|
||||
.history/
|
||||
*.fingerprint
|
||||
*.tar
|
||||
*.jar
|
||||
@@ -29,3 +30,6 @@ coverage.txt
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# macOS Finder metadata
|
||||
.DS_STORE
|
||||
|
||||
+48
-12
@@ -1,23 +1,38 @@
|
||||
release:
|
||||
# If set to auto, will mark the release as not ready for production in case there is an indicator for this in the
|
||||
# tag e.g. v1.0.0-rc1 .If set to true, will mark the release as not ready for production.
|
||||
prerelease: false
|
||||
prerelease: auto
|
||||
|
||||
# If set to true, will not auto-publish the release. This is done to allow us to review the changelog before publishing.
|
||||
draft: true
|
||||
|
||||
signs:
|
||||
- artifacts: checksum
|
||||
args: ["--output", "${signature}", "--detach-sign", "${artifact}"]
|
||||
|
||||
builds:
|
||||
- binary: syft
|
||||
id: syft
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
# windows not supported yet (due to jotframe)
|
||||
# - windows
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
# Set the modified timestamp on the output binary to the git timestamp (to ensure a reproducible build)
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
ldflags: |
|
||||
-w
|
||||
-s
|
||||
-extldflags '-static'
|
||||
-X github.com/anchore/syft/internal/version.version={{.Version}}
|
||||
-X github.com/anchore/syft/internal/version.gitCommit={{.Commit}}
|
||||
-X github.com/anchore/syft/internal/version.buildDate={{.Date}}
|
||||
-X github.com/anchore/syft/internal/version.gitTreeState={{.Env.BUILD_GIT_TREE_STATE}}
|
||||
# For more info on this macOS build, see: https://github.com/mitchellh/gon#usage-with-goreleaser
|
||||
- binary: syft
|
||||
id: syft-macos
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
@@ -32,6 +47,34 @@ builds:
|
||||
-X github.com/anchore/syft/internal/version.buildDate={{.Date}}
|
||||
-X github.com/anchore/syft/internal/version.gitTreeState={{.Env.BUILD_GIT_TREE_STATE}}
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
builds:
|
||||
- syft # i.e. Linux only
|
||||
- format: zip # This is a hack! We don't actually intend to use _this_ ZIP file, we just need goreleaser to consider the ZIP file produced by gon (which will have the same file name) to be an artifact so we can use it downstream in publishing (e.g. to a homebrew tap)
|
||||
id: syft-zip
|
||||
builds:
|
||||
- syft-macos
|
||||
|
||||
signs:
|
||||
- artifacts: checksum
|
||||
cmd: sh
|
||||
args:
|
||||
- '-c'
|
||||
# we should not include the zip artifact, as the artifact is mutated throughout the next macOS notarization step
|
||||
# note: sed -i is not portable
|
||||
- 'sed "/.*\.zip/d" ${artifact} > tmpfile && mv tmpfile ${artifact} && gpg --output ${signature} --detach-sign ${artifact}'
|
||||
- id: syft-macos-signing
|
||||
ids:
|
||||
- syft-macos
|
||||
cmd: ./.github/scripts/mac-sign-and-notarize.sh
|
||||
signature: "syft_${VERSION}_darwin_amd64.dmg" # This is somewhat unintuitive. This gets the DMG file recognized as an artifact. In fact, both a DMG and a ZIP file are being produced by this signing step.
|
||||
args:
|
||||
- "{{ .IsSnapshot }}"
|
||||
- "gon.hcl"
|
||||
- "./dist/syft_{{ .Version }}_darwin_amd64"
|
||||
artifacts: all
|
||||
|
||||
nfpms:
|
||||
- license: "Apache 2.0"
|
||||
maintainer: "Anchore, Inc"
|
||||
@@ -47,10 +90,3 @@ brews:
|
||||
name: homebrew-syft
|
||||
homepage: *website
|
||||
description: *description
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ RESET := $(shell tput -T linux sgr0)
|
||||
TITLE := $(BOLD)$(PURPLE)
|
||||
SUCCESS := $(BOLD)$(GREEN)
|
||||
# the quality gate lower threshold for unit test total % coverage (by function statements)
|
||||
COVERAGE_THRESHOLD := 72
|
||||
COVERAGE_THRESHOLD := 68
|
||||
|
||||
## Build variables
|
||||
DISTDIR=./dist
|
||||
@@ -27,6 +27,10 @@ ifeq "$(strip $(VERSION))" ""
|
||||
override VERSION = $(shell git describe --always --tags --dirty)
|
||||
endif
|
||||
|
||||
# used to generate the changelog from the second to last tag to the current tag (used in the release pipeline when the release tag is in place)
|
||||
LAST_TAG := $(shell git describe --abbrev=0 --tags $(shell git rev-list --tags --max-count=1))
|
||||
SECOND_TO_LAST_TAG := $(shell git describe --abbrev=0 --tags $(shell git rev-list --tags --skip=1 --max-count=1))
|
||||
|
||||
## Variable assertions
|
||||
|
||||
ifndef TEMPDIR
|
||||
@@ -67,9 +71,13 @@ help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}'
|
||||
|
||||
.PHONY: ci-bootstrap
|
||||
ci-bootstrap: bootstrap
|
||||
ci-bootstrap:
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y bc jq libxml2-utils
|
||||
|
||||
.PHONY:
|
||||
ci-bootstrap-mac:
|
||||
github_changelog_generator --version || sudo gem install github_changelog_generator
|
||||
|
||||
.PHONY: bootstrap
|
||||
bootstrap: ## Download and install all go dependencies (+ prep tooling in the ./tmp dir)
|
||||
$(call title,Bootstrapping dependencies)
|
||||
@@ -118,7 +126,7 @@ validate-cyclonedx-schema:
|
||||
.PHONY: unit
|
||||
unit: fixtures ## Run unit tests (with coverage)
|
||||
$(call title,Running unit tests)
|
||||
go test -coverprofile $(COVER_REPORT) ./...
|
||||
go test -coverprofile $(COVER_REPORT) $(shell go list ./... | grep -v anchore/syft/test)
|
||||
@go tool cover -func $(COVER_REPORT) | grep total | awk '{print substr($$3, 1, length($$3)-1)}' > $(COVER_TOTAL)
|
||||
@echo "Coverage: $$(cat $(COVER_TOTAL))"
|
||||
@if [ $$(echo "$$(cat $(COVER_TOTAL)) >= $(COVERAGE_THRESHOLD)" | bc -l) -ne 1 ]; then echo "$(RED)$(BOLD)Failed coverage quality gate (> $(COVERAGE_THRESHOLD)%)$(RESET)" && false; fi
|
||||
@@ -126,11 +134,13 @@ unit: fixtures ## Run unit tests (with coverage)
|
||||
.PHONY: integration
|
||||
integration: ## Run integration tests
|
||||
$(call title,Running integration tests)
|
||||
go test -v -tags=integration ./test/integration
|
||||
|
||||
go test -v ./test/integration
|
||||
|
||||
|
||||
# note: this is used by CI to determine if the integration test fixture cache (docker image tars) should be busted
|
||||
integration-fingerprint:
|
||||
find test/integration/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee test/integration/test-fixtures/tar-cache.fingerprint
|
||||
find test/integration/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee test/integration/test-fixtures/cache.fingerprint
|
||||
|
||||
.PHONY: java-packages-fingerprint
|
||||
java-packages-fingerprint:
|
||||
@@ -143,31 +153,13 @@ fixtures:
|
||||
cd syft/cataloger/java/test-fixtures/java-builds && make
|
||||
|
||||
.PHONY: generate-json-schema
|
||||
generate-json-schema: clean-json-schema-examples integration ## Generate a new json schema for the json presenter, derived from integration test cases
|
||||
docker run \
|
||||
-i \
|
||||
--rm \
|
||||
-v $(shell pwd)/schema/json:/work \
|
||||
-w /work \
|
||||
python:3.8 \
|
||||
bash -x -c "\
|
||||
pip install -r requirements.txt && \
|
||||
python generate.py \
|
||||
"
|
||||
generate-json-schema: ## Generate a new json schema
|
||||
cd schema/json
|
||||
go run generate.go
|
||||
|
||||
.PHONY: clear-test-cache
|
||||
clear-test-cache: ## Delete all test cache (built docker image tars)
|
||||
find . -type f -wholename "**/test-fixtures/tar-cache/*.tar" -delete
|
||||
|
||||
.PHONY: check-pipeline
|
||||
check-pipeline: ## Run local CircleCI pipeline locally (sanity check)
|
||||
$(call title,Check pipeline)
|
||||
# note: this is meant for local development & testing of the pipeline, NOT to be run in CI
|
||||
mkdir -p $(TEMPDIR)
|
||||
circleci config process .circleci/config.yml > .tmp/circleci.yml
|
||||
circleci local execute -c .tmp/circleci.yml --job "Static Analysis"
|
||||
circleci local execute -c .tmp/circleci.yml --job "Unit & Integration Tests (go-latest)"
|
||||
@printf '$(SUCCESS)Pipeline checks pass!$(RESET)\n'
|
||||
find . -type f -wholename "**/test-fixtures/cache/*.tar" -delete
|
||||
|
||||
.PHONY: build
|
||||
build: $(SNAPSHOTDIR) ## Build release snapshot binaries and packages
|
||||
@@ -226,32 +218,60 @@ acceptance-test-rpm-package-install: $(SNAPSHOTDIR)
|
||||
$(ACC_TEST_IMAGE) \
|
||||
$(RESULTSDIR)
|
||||
|
||||
.PHONY: changlog
|
||||
changelog:
|
||||
mkdir -p $(DISTDIR)
|
||||
@docker run -it --rm \
|
||||
-v "$(shell pwd)":/usr/local/src/your-app ferrarimarco/github-changelog-generator \
|
||||
.PHONY: changlog-release
|
||||
changelog-release:
|
||||
@echo "Last tag: $(SECOND_TO_LAST_TAG)"
|
||||
@echo "Current tag: $(VERSION)"
|
||||
@github_changelog_generator \
|
||||
--user anchore \
|
||||
--project $(BIN) \
|
||||
-t ${GITHUB_TOKEN} \
|
||||
--exclude-labels 'duplicate,question,invalid,wontfix,size:small,size:medium,size:large,size:x-large' \
|
||||
--no-pr-wo-labels \
|
||||
--no-issues-wo-labels \
|
||||
--unreleased-only \
|
||||
--future-release $(VERSION)
|
||||
--since-tag $(SECOND_TO_LAST_TAG)
|
||||
|
||||
@printf '\n$(BOLD)$(CYAN)Release $(VERSION) Changelog$(RESET)\n\n'
|
||||
@cat CHANGELOG.md
|
||||
|
||||
.PHONY: changelog-unreleased
|
||||
changelog-unreleased: ## show the current changelog that will be produced on the next release (note: requires GITHUB_TOKEN set)
|
||||
@docker run -it --rm \
|
||||
-v "$(shell pwd)":/usr/local/src/your-app \
|
||||
ferrarimarco/github-changelog-generator \
|
||||
--user anchore \
|
||||
--project $(BIN) \
|
||||
-t ${GITHUB_TOKEN} \
|
||||
--exclude-labels 'duplicate,question,invalid,wontfix,size:small,size:medium,size:large,size:x-large' \
|
||||
--since-tag $(LAST_TAG)
|
||||
|
||||
@printf '\n$(BOLD)$(CYAN)Unreleased Changes (closed PRs and issues will not be in the final changelog)$(RESET)\n'
|
||||
|
||||
@docker run -it --rm \
|
||||
-v $(shell pwd)/CHANGELOG.md:/CHANGELOG.md \
|
||||
rawkode/mdv \
|
||||
-t 748.5989 \
|
||||
/CHANGELOG.md
|
||||
|
||||
.PHONY: release
|
||||
release: clean-dist changelog ## Build and publish final binaries and packages
|
||||
release: clean-dist ci-bootstrap-mac changelog-release ## Build and publish final binaries and packages. Intended to be run only on macOS.
|
||||
$(call title,Publishing release artifacts)
|
||||
|
||||
# Prepare for macOS-specific signing process
|
||||
.github/scripts/mac-prepare-for-signing.sh
|
||||
|
||||
# create a config with the dist dir overridden
|
||||
echo "dist: $(DISTDIR)" > $(TEMPDIR)/goreleaser.yaml
|
||||
cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml
|
||||
|
||||
# release
|
||||
BUILD_GIT_TREE_STATE=$(GITTREESTATE) \
|
||||
$(TEMPDIR)/goreleaser \
|
||||
--rm-dist \
|
||||
--config $(TEMPDIR)/goreleaser.yaml \
|
||||
--release-notes <(cat CHANGELOG.md)
|
||||
# release (note the version transformation from v0.7.0 --> 0.7.0)
|
||||
bash -c "\
|
||||
BUILD_GIT_TREE_STATE=$(GITTREESTATE) \
|
||||
VERSION=$(VERSION:v%=%) \
|
||||
$(TEMPDIR)/goreleaser \
|
||||
--rm-dist \
|
||||
--config $(TEMPDIR)/goreleaser.yaml \
|
||||
--release-notes <(cat CHANGELOG.md)"
|
||||
|
||||
# verify checksum signatures
|
||||
.github/scripts/verify-signature.sh "$(DISTDIR)"
|
||||
@@ -260,7 +280,7 @@ release: clean-dist changelog ## Build and publish final binaries and packages
|
||||
.github/scripts/update-version-file.sh "$(DISTDIR)" "$(VERSION)"
|
||||
|
||||
.PHONY: clean
|
||||
clean: clean-dist clean-snapshot clean-json-schema-examples ## Remove previous builds and result reports
|
||||
clean: clean-dist clean-snapshot ## Remove previous builds and result reports
|
||||
rm -rf $(RESULTSDIR)/*
|
||||
|
||||
.PHONY: clean-snapshot
|
||||
@@ -270,7 +290,3 @@ clean-snapshot:
|
||||
.PHONY: clean-dist
|
||||
clean-dist:
|
||||
rm -rf $(DISTDIR) $(TEMPDIR)/goreleaser.yaml
|
||||
|
||||
.PHONY: clean-json-schema-examples
|
||||
clean-json-schema-examples:
|
||||
rm -f schema/json/examples/*
|
||||
@@ -1,6 +1,7 @@
|
||||
# syft
|
||||
|
||||
[](https://circleci.com/gh/anchore/syft)
|
||||
[](https://github.com/anchore/syft/actions?query=workflow%3A%22Static+Analysis+%2B+Unit+%2B+Integration%22)
|
||||
[](https://github.com/anchore/syft/actions?query=workflow%3AAcceptance)
|
||||
[](https://goreportcard.com/report/github.com/anchore/syft)
|
||||
[](https://github.com/anchore/syft/releases/latest)
|
||||
[](https://github.com/anchore/syft/blob/main/LICENSE)
|
||||
@@ -13,12 +14,13 @@ A CLI tool and go library for generating a Software Bill of Materials (SBOM) fro
|
||||
- Catalog container images and filesystems to discover packages and libraries.
|
||||
- Supports packages and libraries from various ecosystems (APK, DEB, RPM, Ruby Bundles, Python Wheel/Egg/requirements.txt, JavaScript NPM/Yarn, Java JAR/EAR/WAR, Jenkins plugins JPI/HPI, Go modules)
|
||||
- Linux distribution identification (supports Alpine, BusyBox, CentOS/RedHat, Debian/Ubuntu flavored distributions)
|
||||
- Supports Docker and OCI image formats
|
||||
|
||||
> :warning: **This is pre-release software** and it may not work as expected. If you encounter an issue, please [let us know using the issue tracker](https://github.com/anchore/syft/issues).
|
||||
If you encounter an issue, please [let us know using the issue tracker](https://github.com/anchore/syft/issues).
|
||||
|
||||
## Getting started
|
||||
|
||||
To generate an SBOM for an image:
|
||||
To generate an SBOM for a Docker or OCI image:
|
||||
```
|
||||
syft <image>
|
||||
```
|
||||
@@ -32,41 +34,41 @@ syft <image> --scope all-layers
|
||||
|
||||
Syft can generate a SBOM from a variety of sources:
|
||||
```
|
||||
# catalog a docker image tar (from the result of "docker image save ... -o image.tar" command)
|
||||
syft docker-archive://path/to/image.tar
|
||||
# catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
|
||||
syft path/to/image.tar
|
||||
|
||||
# catalog a directory
|
||||
syft dir://path/to/dir
|
||||
syft path/to/dir
|
||||
```
|
||||
|
||||
By default Syft shows a summary table, however, more detailed `text` and `json` formats are also available.
|
||||
The output format for Syft is configurable as well:
|
||||
```
|
||||
syft <image> -o json
|
||||
syft <image> -o text
|
||||
syft <image> -o <format>
|
||||
```
|
||||
|
||||
Where the `format`s available are:
|
||||
- `json`: Use this to get as much information out of Syft as possible!
|
||||
- `text`: A row-oriented, human-and-machine-friendly output.
|
||||
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.2](https://cyclonedx.org/) specification.
|
||||
- `table`: A columnar summary (default).
|
||||
|
||||
## Installation
|
||||
|
||||
**Recommended**
|
||||
**Recommended (macOS and Linux)**
|
||||
```bash
|
||||
# install the latest version to /usr/local/bin
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
|
||||
|
||||
# install a specific version into a specific dir
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s <RELEASE_VERSION> -b <SOME_BIN_PATH>
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b <SOME_BIN_PATH> <RELEASE_VERSION>
|
||||
```
|
||||
|
||||
**macOS**
|
||||
**Homebrew (macOS)**
|
||||
```bash
|
||||
brew tap anchore/syft
|
||||
brew install syft
|
||||
```
|
||||
|
||||
You may experience a "macOS cannot verify app is free from malware" error upon running Syft because it is not yet signed and notarized. You can override this using `xattr`.
|
||||
```bash
|
||||
xattr -rd com.apple.quarantine syft
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration search paths:
|
||||
@@ -79,31 +81,58 @@ Configuration search paths:
|
||||
Configuration options (example values are the default):
|
||||
|
||||
```yaml
|
||||
# same as -o ; the output format of the SBOM report (options: table, text, json)
|
||||
# the output format of the SBOM report (options: table, text, json)
|
||||
# same as -o ; SYFT_OUTPUT env var
|
||||
output: "table"
|
||||
|
||||
# same as -s ; the search space to look for packages (options: all-layers, squashed)
|
||||
# the search space to look for packages (options: all-layers, squashed)
|
||||
# same as -s ; SYFT_SCOPE env var
|
||||
scope: "squashed"
|
||||
|
||||
# same as -q ; suppress all output (except for the SBOM report)
|
||||
# suppress all output (except for the SBOM report)
|
||||
# same as -q ; SYFT_QUIET env var
|
||||
quiet: false
|
||||
|
||||
# enable/disable checking for application updates on startup
|
||||
# same as SYFT_CHECK_FOR_APP_UPDATE env var
|
||||
check-for-app-update: true
|
||||
|
||||
log:
|
||||
# use structured logging
|
||||
# same as SYFT_LOG_STRUCTURED env var
|
||||
structured: false
|
||||
|
||||
# the log level; note: detailed logging suppress the ETUI
|
||||
# same as SYFT_LOG_LEVEL env var
|
||||
level: "error"
|
||||
|
||||
# location to write the log file (default is not to have a log file)
|
||||
# same as SYFT_LOG_FILE env var
|
||||
file: ""
|
||||
|
||||
# enable/disable checking for application updates on startup
|
||||
check-for-app-update: true
|
||||
```
|
||||
anchore:
|
||||
# (feature-preview) enable uploading of results to Anchore Enterprise automatically (supported on Enterprise 3.0+)
|
||||
# same as SYFT_ANCHORE_UPLOAD_ENABLED env var
|
||||
upload-enabled: false
|
||||
|
||||
## Future plans
|
||||
# (feature-preview) the Anchore Enterprise Host or URL to upload results to (supported on Enterprise 3.0+)
|
||||
# same as -H ; SYFT_ANCHORE_HOST env var
|
||||
host: ""
|
||||
|
||||
The following areas of potential development are currently being investigated:
|
||||
- Add CycloneDX to list of output formats
|
||||
- Establish a stable interchange format w/Grype
|
||||
# (feature-preview) the path after the host to the Anchore External API (supported on Enterprise 3.0+)
|
||||
# same as SYFT_ANCHORE_PATH env var
|
||||
path: ""
|
||||
|
||||
# (feature-preview) the username to authenticate against Anchore Enterprise (supported on Enterprise 3.0+)
|
||||
# same as -u ; SYFT_ANCHORE_USERNAME env var
|
||||
username: ""
|
||||
|
||||
# (feature-preview) the password to authenticate against Anchore Enterprise (supported on Enterprise 3.0+)
|
||||
# same as -p ; SYFT_ANCHORE_PASSWORD env var
|
||||
password: ""
|
||||
|
||||
# (feature-preview) path to dockerfile to be uploaded with the syft results to Anchore Enterprise (supported on Enterprise 3.0+)
|
||||
# same as -d ; SYFT_ANCHORE_DOCKERFILE env var
|
||||
dockerfile: ""
|
||||
|
||||
```
|
||||
+60
-19
@@ -4,20 +4,17 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/format"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/logger"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/gookit/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var appConfig *config.Application
|
||||
@@ -49,15 +46,20 @@ func setGlobalCliOptions() {
|
||||
// scan options
|
||||
flag := "scope"
|
||||
rootCmd.Flags().StringP(
|
||||
"scope", "s", scope.SquashedScope.String(),
|
||||
fmt.Sprintf("selection of layers to catalog, options=%v", scope.Options))
|
||||
"scope", "s", source.SquashedScope.String(),
|
||||
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
|
||||
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
|
||||
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setGlobalFormatOptions()
|
||||
setGlobalUploadOptions()
|
||||
}
|
||||
|
||||
func setGlobalFormatOptions() {
|
||||
// output & formatting options
|
||||
flag = "output"
|
||||
flag := "output"
|
||||
rootCmd.Flags().StringP(
|
||||
flag, "o", string(presenter.TablePresenter),
|
||||
fmt.Sprintf("report output formatter, options=%v", presenter.Options),
|
||||
@@ -80,12 +82,57 @@ func setGlobalCliOptions() {
|
||||
rootCmd.Flags().CountVarP(&cliOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
|
||||
}
|
||||
|
||||
func setGlobalUploadOptions() {
|
||||
flag := "host"
|
||||
rootCmd.Flags().StringP(
|
||||
flag, "H", "",
|
||||
"the hostname or URL of the Anchore Engine/Enterprise instance to upload to",
|
||||
)
|
||||
if err := viper.BindPFlag("anchore.host", rootCmd.Flags().Lookup(flag)); err != nil {
|
||||
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
flag = "username"
|
||||
rootCmd.Flags().StringP(
|
||||
flag, "u", "",
|
||||
"the username to authenticate against Anchore Engine/Enterprise",
|
||||
)
|
||||
if err := viper.BindPFlag("anchore.username", rootCmd.Flags().Lookup(flag)); err != nil {
|
||||
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
flag = "password"
|
||||
rootCmd.Flags().StringP(
|
||||
flag, "p", "",
|
||||
"the password to authenticate against Anchore Engine/Enterprise",
|
||||
)
|
||||
if err := viper.BindPFlag("anchore.password", rootCmd.Flags().Lookup(flag)); err != nil {
|
||||
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
flag = "dockerfile"
|
||||
rootCmd.Flags().StringP(
|
||||
flag, "d", "",
|
||||
"include dockerfile for upload to Anchore Engine/Enterprise",
|
||||
)
|
||||
if err := viper.BindPFlag("anchore.dockerfile", rootCmd.Flags().Lookup(flag)); err != nil {
|
||||
fmt.Printf("unable to bind flag '#{flag}': #{err}")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func initAppConfig() {
|
||||
cfg, err := config.LoadConfigFromFile(viper.GetViper(), &cliOpts)
|
||||
cfgVehicle := viper.GetViper()
|
||||
wasHostnameSet := rootCmd.Flags().Changed("host")
|
||||
cfg, err := config.LoadApplicationConfig(cfgVehicle, cliOpts, wasHostnameSet)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to load application config: \n\t%+v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
appConfig = cfg
|
||||
}
|
||||
|
||||
@@ -101,18 +148,12 @@ func initLogging() {
|
||||
logWrapper := logger.NewLogrusLogger(cfg)
|
||||
syft.SetLogger(logWrapper)
|
||||
stereoscope.SetLogger(&logger.LogrusNestedLogger{
|
||||
Logger: logWrapper.Logger.WithField("from-lib", "steroscope"),
|
||||
Logger: logWrapper.Logger.WithField("from-lib", "stereoscope"),
|
||||
})
|
||||
}
|
||||
|
||||
func logAppConfig() {
|
||||
appCfgStr, err := yaml.Marshal(&appConfig)
|
||||
|
||||
if err != nil {
|
||||
log.Debugf("Could not display application config: %+v", err)
|
||||
} else {
|
||||
log.Debugf("Application config:\n%+v", format.Magenta.Format(string(appCfgStr)))
|
||||
}
|
||||
log.Debugf("Application config:\n%+v", color.Magenta.Sprint(appConfig.String()))
|
||||
}
|
||||
|
||||
func initEventBus() {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// completionCmd represents the completion command
|
||||
var completionCmd = &cobra.Command{
|
||||
Use: "completion [bash|zsh|fish]",
|
||||
Short: "Generate a shell completion for Syft (listing local docker images)",
|
||||
Long: `To load completions (docker image list):
|
||||
|
||||
Bash:
|
||||
|
||||
$ source <(syft completion bash)
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
Linux:
|
||||
$ syft completion bash > /etc/bash_completion.d/syft
|
||||
MacOS:
|
||||
$ syft completion bash > /usr/local/etc/bash_completion.d/syft
|
||||
|
||||
Zsh:
|
||||
|
||||
# If shell completion is not already enabled in your environment you will need
|
||||
# to enable it. You can execute the following once:
|
||||
|
||||
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
$ syft completion zsh > "${fpath[1]}/_syft"
|
||||
|
||||
# You will need to start a new shell for this setup to take effect.
|
||||
|
||||
Fish:
|
||||
|
||||
$ syft completion fish | source
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
$ syft completion fish > ~/.config/fish/completions/syft.fish
|
||||
`,
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgs: []string{"bash", "zsh", "fish"},
|
||||
Args: cobra.ExactValidArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
switch args[0] {
|
||||
case "bash":
|
||||
err = cmd.Root().GenBashCompletion(os.Stdout)
|
||||
case "zsh":
|
||||
err = cmd.Root().GenZshCompletion(os.Stdout)
|
||||
case "fish":
|
||||
err = cmd.Root().GenFishCompletion(os.Stdout, true)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(completionCmd)
|
||||
}
|
||||
+122
-7
@@ -1,10 +1,20 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/anchore"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
@@ -12,19 +22,27 @@ import (
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: fmt.Sprintf("%s [SOURCE]", internal.ApplicationName),
|
||||
Short: "A tool for generating a Software Bill Of Materials (SBOM) from container images and filesystems",
|
||||
Short: "A tool for generating a Software Bill Of Materials (PackageSBOM) from container images and filesystems",
|
||||
Long: internal.Tprintf(`
|
||||
Supports the following image sources:
|
||||
{{.appName}} yourrepo/yourimage:tag defaults to using images from a docker daemon
|
||||
{{.appName}} docker://yourrepo/yourimage:tag explicitly use the docker daemon
|
||||
{{.appName}} tar://path/to/yourimage.tar use a tarball from disk
|
||||
{{.appName}} dir://path/to/yourproject read directly from a path in disk
|
||||
{{.appName}} yourrepo/yourimage:tag defaults to using images from a Docker daemon
|
||||
{{.appName}} path/to/yourproject a Docker tar, OCI tar, OCI directory, or generic filesystem directory
|
||||
|
||||
You can also explicitly specify the scheme to use:
|
||||
{{.appName}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
||||
{{.appName}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||
{{.appName}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Podman or otherwise)
|
||||
{{.appName}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||
{{.appName}} dir:path/to/yourproject read directly from a path on disk (any directory)
|
||||
`, map[string]interface{}{
|
||||
"appName": internal.ApplicationName,
|
||||
}),
|
||||
@@ -44,6 +62,20 @@ Supports the following image sources:
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
// Since we use ValidArgsFunction, Cobra will call this AFTER having parsed all flags and arguments provided
|
||||
dockerImageRepoTags, err := ListLocalDockerImages(toComplete)
|
||||
if err != nil {
|
||||
// Indicates that an error occurred and completions should be ignored
|
||||
return []string{"completion failed"}, cobra.ShellCompDirectiveError
|
||||
}
|
||||
if len(dockerImageRepoTags) == 0 {
|
||||
return []string{"no docker images found"}, cobra.ShellCompDirectiveError
|
||||
}
|
||||
// ShellCompDirectiveDefault indicates that the shell will perform its default behavior after completions have
|
||||
// been provided (without implying other possible directives)
|
||||
return dockerImageRepoTags, cobra.ShellCompDirectiveDefault
|
||||
},
|
||||
}
|
||||
|
||||
func startWorker(userInput string) <-chan error {
|
||||
@@ -68,23 +100,106 @@ func startWorker(userInput string) <-chan error {
|
||||
}
|
||||
}
|
||||
|
||||
catalog, scope, distro, err := syft.Catalog(userInput, appConfig.ScopeOpt)
|
||||
src, catalog, distro, err := syft.Catalog(userInput, appConfig.ScopeOpt)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to catalog input: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if appConfig.Anchore.UploadEnabled {
|
||||
if err := doImport(src, src.Metadata, catalog, distro); err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.CatalogerFinished,
|
||||
Value: presenter.GetPresenter(appConfig.PresenterOpt, *scope, catalog, distro),
|
||||
Value: presenter.GetPresenter(appConfig.PresenterOpt, src.Metadata, catalog, distro),
|
||||
})
|
||||
}()
|
||||
return errs
|
||||
}
|
||||
|
||||
func doImport(src source.Source, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro) error {
|
||||
// TODO: ETUI element for this
|
||||
log.Infof("uploading results to %s", appConfig.Anchore.Host)
|
||||
|
||||
if src.Metadata.Scheme != source.ImageScheme {
|
||||
return fmt.Errorf("unable to upload results: only images are supported")
|
||||
}
|
||||
|
||||
var dockerfileContents []byte
|
||||
if appConfig.Anchore.Dockerfile != "" {
|
||||
if _, err := os.Stat(appConfig.Anchore.Dockerfile); os.IsNotExist(err) {
|
||||
return fmt.Errorf("unable to read dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
|
||||
}
|
||||
|
||||
fh, err := os.Open(appConfig.Anchore.Dockerfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
|
||||
}
|
||||
|
||||
dockerfileContents, err = ioutil.ReadAll(fh)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
Username: appConfig.Anchore.Username,
|
||||
Password: appConfig.Anchore.Password,
|
||||
Scheme: scheme,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create anchore client: %+v", err)
|
||||
}
|
||||
|
||||
if err := c.Import(context.Background(), src.Image.Metadata, s, catalog, d, dockerfileContents); err != nil {
|
||||
return fmt.Errorf("failed to upload results to host=%s: %+v", appConfig.Anchore.Host, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func doRunCmd(_ *cobra.Command, args []string) error {
|
||||
userInput := args[0]
|
||||
errs := startWorker(userInput)
|
||||
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet)
|
||||
return ux(errs, eventSubscription)
|
||||
}
|
||||
|
||||
func ListLocalDockerImages(prefix string) ([]string, error) {
|
||||
var repoTags = make([]string, 0)
|
||||
ctx := context.Background()
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return repoTags, err
|
||||
}
|
||||
|
||||
// Only want to return tagged images
|
||||
imageListArgs := filters.NewArgs()
|
||||
imageListArgs.Add("dangling", "false")
|
||||
images, err := cli.ImageList(ctx, types.ImageListOptions{All: false, Filters: imageListArgs})
|
||||
if err != nil {
|
||||
return repoTags, err
|
||||
}
|
||||
|
||||
for _, image := range images {
|
||||
// image may have multiple tags
|
||||
for _, tag := range image.RepoTags {
|
||||
if strings.HasPrefix(tag, prefix) {
|
||||
repoTags = append(repoTags, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
return repoTags, nil
|
||||
}
|
||||
|
||||
+28
-6
@@ -1,14 +1,18 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var showVerboseVersionInfo bool
|
||||
var outputFormat string
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
@@ -17,14 +21,15 @@ var versionCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
versionCmd.Flags().BoolVarP(&showVerboseVersionInfo, "verbose", "v", false, "show additional version information")
|
||||
|
||||
versionCmd.Flags().StringVarP(&outputFormat, "output", "o", string(presenter.TextPresenter), "format to show version information (available=[text, json])")
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
func printVersion(_ *cobra.Command, _ []string) {
|
||||
versionInfo := version.FromBuild()
|
||||
if showVerboseVersionInfo {
|
||||
|
||||
switch outputFormat {
|
||||
case "text":
|
||||
fmt.Println("Application: ", internal.ApplicationName)
|
||||
fmt.Println("Version: ", versionInfo.Version)
|
||||
fmt.Println("BuildDate: ", versionInfo.BuildDate)
|
||||
@@ -33,7 +38,24 @@ func printVersion(_ *cobra.Command, _ []string) {
|
||||
fmt.Println("Platform: ", versionInfo.Platform)
|
||||
fmt.Println("GoVersion: ", versionInfo.GoVersion)
|
||||
fmt.Println("Compiler: ", versionInfo.Compiler)
|
||||
} else {
|
||||
fmt.Printf("%s %s\n", internal.ApplicationName, versionInfo.Version)
|
||||
|
||||
case "json":
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
err := enc.Encode(&struct {
|
||||
version.Version
|
||||
Application string `json:"application"`
|
||||
}{
|
||||
Version: versionInfo,
|
||||
Application: internal.ApplicationName,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("failed to show version information: %+v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
fmt.Printf("unsupported output format: %s\n", outputFormat)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,19 @@ go 1.14
|
||||
|
||||
require (
|
||||
github.com/adrg/xdg v0.2.1
|
||||
github.com/anchore/go-rpmdb v0.0.0-20200811175839-cbc751c28e8e
|
||||
github.com/anchore/go-testutils v0.0.0-20200624184116-66aa578126db
|
||||
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921
|
||||
github.com/anchore/client-go v0.0.0-20201210022459-59e7a0749c74
|
||||
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-20200813152757-548b22c8a0b3
|
||||
github.com/bmatcuk/doublestar v1.3.1
|
||||
github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe // indirect
|
||||
github.com/anchore/stereoscope v0.0.0-20201210022249-091f9bddb42e
|
||||
github.com/bmatcuk/doublestar v1.3.3
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/go-test/deep v1.0.6
|
||||
github.com/google/go-containerregistry v0.1.1 // indirect
|
||||
github.com/facebookincubator/nvdtools v0.1.4
|
||||
github.com/go-test/deep v1.0.7
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/gookit/color v1.2.7
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/hashicorp/go-version v1.2.0
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
@@ -23,10 +24,11 @@ require (
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
github.com/package-url/packageurl-go v0.1.0
|
||||
github.com/pelletier/go-toml v1.8.0
|
||||
github.com/rogpeppe/go-internal v1.5.2
|
||||
github.com/scylladb/go-set v1.0.2
|
||||
github.com/sergi/go-diff v1.1.0
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/spf13/cobra v1.0.0
|
||||
github.com/spf13/afero v1.2.2
|
||||
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905
|
||||
github.com/spf13/viper v1.7.0
|
||||
github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d
|
||||
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240
|
||||
@@ -34,10 +36,6 @@ require (
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 // indirect
|
||||
golang.org/x/sys v0.0.0-20200610111108-226ff32320da // indirect
|
||||
google.golang.org/genproto v0.0.0-20200615140333-fd031eab31e7 // indirect
|
||||
gopkg.in/ini.v1 v1.57.0 // indirect
|
||||
golang.org/x/mod v0.3.0
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
)
|
||||
|
||||
@@ -104,10 +104,10 @@ github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvo
|
||||
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
github.com/Microsoft/hcsshim v0.8.9 h1:VrfodqvztU8YSOvygU+DN1BGaSGxmrNfqOv5oOuX2Bk=
|
||||
github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab h1:9pygWVFqbY9lPxM0peffumuVDyMuIMzNLyO9uFjJuQo=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
github.com/Microsoft/hcsshim v0.8.10 h1:k5wTrpnVU2/xv8ZuzGkbXVd3js5zJ8RnumPo5RxiIxU=
|
||||
github.com/Microsoft/hcsshim v0.8.10/go.mod h1:g5uw8EV2mAlzqe94tfNBNdr89fnbD/n3HV0OhsddkmM=
|
||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us=
|
||||
@@ -119,21 +119,35 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko
|
||||
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/adrg/xdg v0.2.1 h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=
|
||||
github.com/adrg/xdg v0.2.1/go.mod h1:ZuOshBmzV4Ta+s23hdfFZnBsdzmoR3US0d7ErpqSbTQ=
|
||||
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921 h1:T3+cD5fYvuH36h7EZq+TDpm+d8a6FSD4pQsbmuGGQ8o=
|
||||
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60=
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
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/go-rpmdb v0.0.0-20200811175839-cbc751c28e8e h1:kty6r0R2JeaNPeWKSYDC+HW3hkqwFh4PP5TQ8pUPYFw=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20200811175839-cbc751c28e8e/go.mod h1:iYuIG0Nai8dR0ri3LhZQKUyO1loxUWAGvoWhXDmjy1A=
|
||||
github.com/anchore/go-testutils v0.0.0-20200624184116-66aa578126db h1:LWKezJnFTFxNkZ4MzajVf+YWvJS0+7hwFr59u6SS7cw=
|
||||
github.com/anchore/go-testutils v0.0.0-20200624184116-66aa578126db/go.mod h1:D3rc2L/q4Hcp9eeX6AIJH4Q+kPjOtJCFhG9za90j+nU=
|
||||
github.com/anchore/client-go v0.0.0-20201120223920-9f812673f4d6/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
|
||||
github.com/anchore/client-go v0.0.0-20201210022459-59e7a0749c74 h1:9kkKTIyXJC+/syUcY6KWxFoJZJ+GWwrIscF+gBY067k=
|
||||
github.com/anchore/client-go v0.0.0-20201210022459-59e7a0749c74/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-20200520221116-025e07f1c93e/go.mod h1:bkyLl5VITnrmgErv4S1vDfVz/TGAZ5il6161IQo7w2g=
|
||||
github.com/anchore/stereoscope v0.0.0-20200813152757-548b22c8a0b3 h1:pl+txuYlhK8Mmio4d+4zQI/1xg8X6BtNErTASrx23Wk=
|
||||
github.com/anchore/stereoscope v0.0.0-20200813152757-548b22c8a0b3/go.mod h1:WntReQTI/I27FOQ87UgLVVzWgku6+ZsqfOTLxpIZFCs=
|
||||
github.com/anchore/stereoscope v0.0.0-20201106140100-12e75c48f409 h1:xKSpDRjmYrEFrdMeDh4AuSUAFc99pdro6YFBKxy2um0=
|
||||
github.com/anchore/stereoscope v0.0.0-20201106140100-12e75c48f409/go.mod h1:2Jja/4l0zYggW52og+nn0rut4i+OYjCf9vTyrM8RT4E=
|
||||
github.com/anchore/stereoscope v0.0.0-20201130153727-b3f1fad856b0 h1:wa0hdnvBeCpI+rmzDbPG7k5SKlsGkot7aZ8Az1i/vws=
|
||||
github.com/anchore/stereoscope v0.0.0-20201130153727-b3f1fad856b0/go.mod h1:2Jja/4l0zYggW52og+nn0rut4i+OYjCf9vTyrM8RT4E=
|
||||
github.com/anchore/stereoscope v0.0.0-20201203153145-3f9a05a624d7 h1:G3LnRqHL/IIeQZTAMtDOJNYfSYsXLNCZX4DCiS0R0FY=
|
||||
github.com/anchore/stereoscope v0.0.0-20201203153145-3f9a05a624d7/go.mod h1:2Jja/4l0zYggW52og+nn0rut4i+OYjCf9vTyrM8RT4E=
|
||||
github.com/anchore/stereoscope v0.0.0-20201203222654-09e79bf5fef4 h1:XDuCqOWKyQQlKhd9kEDnyKbvSCwShKBDCsyBmD/ALYs=
|
||||
github.com/anchore/stereoscope v0.0.0-20201203222654-09e79bf5fef4/go.mod h1:/dHAFjYflH/1tzhdHAcnMCjprMch+YzHJKi59m/1KCM=
|
||||
github.com/anchore/stereoscope v0.0.0-20201210022249-091f9bddb42e h1:vHUqHTvH9/oxdDDh1fxS9Ls9gWGytKO7XbbzcQ9MBwI=
|
||||
github.com/anchore/stereoscope v0.0.0-20201210022249-091f9bddb42e/go.mod h1:/dHAFjYflH/1tzhdHAcnMCjprMch+YzHJKi59m/1KCM=
|
||||
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=
|
||||
github.com/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ=
|
||||
github.com/apex/log v1.3.0 h1:1fyfbPvUwD10nMoh3hY6MXzvZShJQn9/ck7ATgAt5pA=
|
||||
github.com/apex/log v1.3.0/go.mod h1:jd8Vpsr46WAe3EZSQ/IUMs2qQD/GOycT5rPWCO1yGcs=
|
||||
@@ -161,8 +175,8 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
|
||||
github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/bmatcuk/doublestar v1.3.1 h1:rT8rxDPsavp9G+4ZULzqhhUSaI/OPsTZNG88Z3i0xvY=
|
||||
github.com/bmatcuk/doublestar v1.3.1/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/bmatcuk/doublestar v1.3.3 h1:pVP1d49CcQQaNOl+PI6sPybIrIOD/6sux31PFdmhTH0=
|
||||
github.com/bmatcuk/doublestar v1.3.3/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTKY95VwV8U=
|
||||
github.com/bombsimon/wsl/v2 v2.2.0/go.mod h1:Azh8c3XGEJl9LyX0/sFC+CKMc7Ssgua0g+6abzXN4Pg=
|
||||
github.com/bombsimon/wsl/v3 v3.0.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
|
||||
@@ -181,10 +195,11 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
|
||||
github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=
|
||||
github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
|
||||
github.com/containerd/containerd v1.3.0 h1:xjvXQWABwS2uiv3TWgQt5Uth60Gu86LTGZXMJkjc7rY=
|
||||
github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
@@ -193,7 +208,6 @@ github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMX
|
||||
github.com/containerd/containerd v1.3.4 h1:3o0smo5SKY7H6AJCmJhsnCjR2/V2T8VmiHt7seN2/kI=
|
||||
github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||
github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY=
|
||||
github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe h1:PEmIrUvwG9Yyv+0WKZqjXfSFDeZjs/q15g0m08BYS9k=
|
||||
github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=
|
||||
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
|
||||
@@ -209,10 +223,12 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
@@ -253,9 +269,12 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/facebookincubator/nvdtools v0.1.4 h1:x1Ucw9+bSkMd8DJJN4jNQ1Lk4PSFlJarGOxp9D6WUMo=
|
||||
github.com/facebookincubator/nvdtools v0.1.4/go.mod h1:0/FIVnSEl9YHXLq3tKBPpKaI0iUceDhdSHPlIwIX44Y=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
|
||||
github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
@@ -294,8 +313,8 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
|
||||
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
|
||||
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
|
||||
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
|
||||
github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8=
|
||||
@@ -321,7 +340,7 @@ github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4
|
||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b h1:ekuhfTjngPhisSjOJ0QWKpPQE8/rbknHaes6WVJj5Hw=
|
||||
github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
@@ -399,11 +418,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-containerregistry v0.0.0-20200430153450-5cbd060f5c92/go.mod h1:pD1UFYs7MCAx+ZLShBdttcaOSbyc8F9Na/9IZLNwJeA=
|
||||
github.com/google/go-containerregistry v0.1.0 h1:hL5mVw7cTX3SBr64Arpv+cJH93L+Z9Q6WjckImYLB3g=
|
||||
github.com/google/go-containerregistry v0.1.0/go.mod h1:npTSyywOeILcgWqd+rvtzGWflIPPcBQhYoOONaY4ltM=
|
||||
github.com/google/go-containerregistry v0.1.1 h1:AG8FSAfXglim2l5qSrqp5VK2Xl03PiBf25NiTGGamws=
|
||||
github.com/google/go-containerregistry v0.1.1/go.mod h1:npTSyywOeILcgWqd+rvtzGWflIPPcBQhYoOONaY4ltM=
|
||||
github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo=
|
||||
github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
@@ -444,9 +460,8 @@ github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1K
|
||||
github.com/gookit/color v1.2.7 h1:4qePMNWZhrmbfYJDix+J4V2l0iVW+6jQGjicELlN14E=
|
||||
github.com/gookit/color v1.2.7/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
|
||||
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/goreleaser/goreleaser v0.136.0 h1:Z+7XPrfGK11s/Sp+a06sx2FzGuCjTBdxN2ubpGvQbjY=
|
||||
github.com/goreleaser/goreleaser v0.136.0/go.mod h1:wiKrPUeSNh6Wu8nUHxZydSOVQ/OZvOaO7DTtFqie904=
|
||||
github.com/goreleaser/nfpm v1.2.1/go.mod h1:TtWrABZozuLOttX2uDlYyECfQX7x5XYkVxhjYcR6G9w=
|
||||
@@ -502,6 +517,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk=
|
||||
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
@@ -667,7 +684,7 @@ github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM
|
||||
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
|
||||
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/package-url/packageurl-go v0.1.0 h1:efWBc98O/dBZRg1pw2xiDzovnlMjCa9NPnfaiBduh8I=
|
||||
github.com/package-url/packageurl-go v0.1.0/go.mod h1:C/ApiuWpmbpni4DIOECf6WCjFUZV7O1Fx7VAzrZHgBw=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
@@ -721,6 +738,8 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
|
||||
github.com/scylladb/go-set v1.0.2 h1:SkvlMCKhP0wyyct6j+0IHJkBkSZL+TDzZ4E7f7BCcRE=
|
||||
github.com/scylladb/go-set v1.0.2/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/securego/gosec v0.0.0-20200103095621-79fbf3af8d83/go.mod h1:vvbZ2Ae7AzSq3/kywjUDxSNq2SJ27RxCz2un0H3ePqE=
|
||||
github.com/securego/gosec v0.0.0-20200401082031-e946c8c39989 h1:rq2/kILQnPtq5oL4+IAjgVOjh5e2yj2aaCYi7squEvI=
|
||||
@@ -740,7 +759,6 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
|
||||
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
|
||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
@@ -766,6 +784,8 @@ github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905 h1:Xn3y6St5vlEgvxekK6JuArarc79GKAj/jbcsMRCaRH4=
|
||||
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905/go.mod h1:jCpPr/pv3OLY1D1nS9lN5HIdIpmOO/WTYJcAmnPq1nU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
@@ -787,7 +807,9 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
|
||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
|
||||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -820,8 +842,8 @@ github.com/ultraware/funlen v0.0.2 h1:Av96YVBwwNSe4MLR7iI/BIa3VyI7/djnto/pK3Uxbd
|
||||
github.com/ultraware/funlen v0.0.2/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
|
||||
github.com/ultraware/whitespace v0.0.4 h1:If7Va4cM03mpgrNH9k49/VOicWpGoG70XPBFFODYDsg=
|
||||
github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA=
|
||||
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/uudashr/gocognit v1.0.1 h1:MoG2fZ0b/Eo7NXoIwCVFLG5JED3qgQz5/NEE+rOsjPs=
|
||||
github.com/uudashr/gocognit v1.0.1/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
@@ -959,11 +981,10 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 h1:eDrdRpKgkcCqKZQwyZRyeFZgfqt37SL7Kv3tok06cKE=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -980,8 +1001,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1000,7 +1019,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190620070143-6f217b454f45/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -1012,7 +1030,7 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -1020,6 +1038,7 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -1028,14 +1047,11 @@ golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200610111108-226ff32320da h1:bGb80FudwxpeucJUjPYJXuJ8Hk91vNtfvrymzwiei38=
|
||||
golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1098,7 +1114,6 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204192400-7124308813f3/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200210192313-1ace956b0e17/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
@@ -1174,20 +1189,17 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200519141106-08726f379972/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece h1:1YM0uhfumvoDu9sx8+RyWwTI63zoCQvI23IYFRlvte0=
|
||||
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200604104852-0b0486081ffb h1:ek2py5bOqzR7MR/6obzk0rXUgYCLmjyLnaO9ssT+l6w=
|
||||
google.golang.org/genproto v0.0.0-20200604104852-0b0486081ffb/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200615140333-fd031eab31e7 h1:1N7l1PuXZwEK7OhHdmKQROOM75PnUjABGwvVRbLBgFk=
|
||||
google.golang.org/genproto v0.0.0-20200615140333-fd031eab31e7/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
@@ -1223,8 +1235,6 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.56.0 h1:DPMeDvGTM54DXbPkVIZsp19fp/I2K7zwA/itHYHKo8Y=
|
||||
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
source = ["./dist/syft-macos_darwin_amd64/syft"] # The 'dist' directory path should ideally reference an env var, where the source of truth is the Makefile. I wasn't able to figure out how to solve this.
|
||||
bundle_id = "com.anchore.toolbox.syft"
|
||||
|
||||
sign {
|
||||
application_identity = "Developer ID Application: ANCHORE, INC. (9MJHKYX5AT)"
|
||||
}
|
||||
|
||||
dmg {
|
||||
output_path = "./dist/output.dmg"
|
||||
volume_name = "Syft"
|
||||
}
|
||||
|
||||
zip {
|
||||
output_path = "./dist/output.zip"
|
||||
}
|
||||
+27
-13
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
# Code generated by godownloader on 2020-08-10T20:55:46Z. DO NOT EDIT.
|
||||
# Code generated by godownloader on 2020-08-10T20:55:46Z.
|
||||
#
|
||||
|
||||
usage() {
|
||||
@@ -45,11 +45,16 @@ parse_args() {
|
||||
execute() {
|
||||
tmpdir=$(mktemp -d)
|
||||
log_debug "downloading files into ${tmpdir}"
|
||||
http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}"
|
||||
http_download "${tmpdir}/${ARCHIVE}" "${ARCHIVE_URL}"
|
||||
http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}"
|
||||
hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}"
|
||||
|
||||
# macOS has its own secure verification mechanism, and checksums.txt is not used.
|
||||
if [ "$OS" != "darwin" ]; then
|
||||
hash_sha256_verify "${tmpdir}/${ARCHIVE}" "${tmpdir}/${CHECKSUM}"
|
||||
fi
|
||||
|
||||
srcdir="${tmpdir}"
|
||||
(cd "${tmpdir}" && untar "${TARBALL}")
|
||||
(cd "${tmpdir}" && unpack "${ARCHIVE}")
|
||||
test ! -d "${BINDIR}" && install -d "${BINDIR}"
|
||||
for binexe in $BINARIES; do
|
||||
if [ "$OS" = "windows" ]; then
|
||||
@@ -89,6 +94,7 @@ tag_to_version() {
|
||||
adjust_format() {
|
||||
# change format (tar.gz or zip) based on OS
|
||||
case ${OS} in
|
||||
darwin) FORMAT=dmg ;;
|
||||
windows) FORMAT=zip ;;
|
||||
esac
|
||||
true
|
||||
@@ -221,18 +227,26 @@ uname_arch_check() {
|
||||
log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib"
|
||||
return 1
|
||||
}
|
||||
untar() {
|
||||
tarball=$1
|
||||
case "${tarball}" in
|
||||
*.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;;
|
||||
*.tar) tar --no-same-owner -xf "${tarball}" ;;
|
||||
*.zip) unzip "${tarball}" ;;
|
||||
unpack() {
|
||||
archive=$1
|
||||
case "${archive}" in
|
||||
*.tar.gz | *.tgz) tar --no-same-owner -xzf "${archive}" ;;
|
||||
*.tar) tar --no-same-owner -xf "${archive}" ;;
|
||||
*.zip) unzip "${archive}" ;;
|
||||
*.dmg) extract_from_dmg "${archive}" ;;
|
||||
*)
|
||||
log_err "untar unknown archive format for ${tarball}"
|
||||
log_err "unpack unknown archive format for ${archive}"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
extract_from_dmg() {
|
||||
dmg_file=$1
|
||||
mount_point="/Volumes/tmp-dmg"
|
||||
hdiutil attach -quiet -nobrowse -mountpoint "${mount_point}" "${dmg_file}"
|
||||
cp -fR "${mount_point}/" ./
|
||||
hdiutil detach -quiet -force "${mount_point}"
|
||||
}
|
||||
http_download_curl() {
|
||||
local_file=$1
|
||||
source_url=$2
|
||||
@@ -366,8 +380,8 @@ adjust_arch
|
||||
log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}"
|
||||
|
||||
NAME=${PROJECT_NAME}_${VERSION}_${OS}_${ARCH}
|
||||
TARBALL=${NAME}.${FORMAT}
|
||||
TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
|
||||
ARCHIVE=${NAME}.${FORMAT}
|
||||
ARCHIVE_URL=${GITHUB_DOWNLOAD}/${TAG}/${ARCHIVE}
|
||||
CHECKSUM=${PROJECT_NAME}_${VERSION}_checksums.txt
|
||||
CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
Hostname string
|
||||
Username string
|
||||
Password string
|
||||
UserAgent string
|
||||
Scheme string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
config Configuration
|
||||
client *external.APIClient
|
||||
}
|
||||
|
||||
func NewClient(cfg Configuration) (*Client, error) {
|
||||
if cfg.UserAgent == "" {
|
||||
versionInfo := version.FromBuild()
|
||||
// format: product / product-version comment
|
||||
cfg.UserAgent = fmt.Sprintf("%s / %s %s", internal.ApplicationName, versionInfo.Version, versionInfo.Platform)
|
||||
}
|
||||
|
||||
if cfg.Scheme == "" {
|
||||
cfg.Scheme = "https"
|
||||
}
|
||||
|
||||
return &Client{
|
||||
config: cfg,
|
||||
client: external.NewAPIClient(&external.Configuration{
|
||||
Host: cfg.Hostname,
|
||||
UserAgent: cfg.UserAgent,
|
||||
Scheme: cfg.Scheme,
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) newRequestContext(parentContext context.Context) context.Context {
|
||||
if parentContext == nil {
|
||||
parentContext = context.Background()
|
||||
}
|
||||
return context.WithValue(
|
||||
parentContext,
|
||||
external.ContextBasicAuth,
|
||||
external.BasicAuth{
|
||||
UserName: c.config.Username,
|
||||
Password: c.config.Password,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
|
||||
func importProgress(source string) (*progress.Stage, *progress.Manual) {
|
||||
stage := &progress.Stage{}
|
||||
prog := &progress.Manual{
|
||||
// this is the number of stages to expect; start + individual endpoints + stop
|
||||
Total: 6,
|
||||
}
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.ImportStarted,
|
||||
Source: source,
|
||||
Value: progress.StagedProgressable(&struct {
|
||||
progress.Stager
|
||||
progress.Progressable
|
||||
}{
|
||||
Stager: progress.Stager(stage),
|
||||
Progressable: prog,
|
||||
}),
|
||||
})
|
||||
|
||||
return stage, prog
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func (c *Client) Import(ctx context.Context, imageMetadata image.Metadata, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, dockerfile []byte) error {
|
||||
stage, prog := importProgress(imageMetadata.ID)
|
||||
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
authedCtx := c.newRequestContext(ctxWithTimeout)
|
||||
|
||||
stage.Current = "starting session"
|
||||
startOperation, _, err := c.client.ImportsApi.CreateOperation(authedCtx)
|
||||
if err != nil {
|
||||
var detail = "no details given"
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
detail = string(openAPIErr.Body())
|
||||
}
|
||||
return fmt.Errorf("unable to start import session: %w: %s", err, detail)
|
||||
}
|
||||
prog.N++
|
||||
sessionID := startOperation.Uuid
|
||||
|
||||
packageDigest, err := importPackageSBOM(authedCtx, c.client.ImportsApi, sessionID, s, catalog, d, stage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to import Package SBOM: %w", err)
|
||||
}
|
||||
prog.N++
|
||||
|
||||
manifestDigest, err := importManifest(authedCtx, c.client.ImportsApi, sessionID, imageMetadata.RawManifest, stage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to import Manifest: %w", err)
|
||||
}
|
||||
prog.N++
|
||||
|
||||
configDigest, err := importConfig(authedCtx, c.client.ImportsApi, sessionID, imageMetadata.RawConfig, stage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to import Config: %w", err)
|
||||
}
|
||||
prog.N++
|
||||
|
||||
dockerfileDigest, err := importDockerfile(authedCtx, c.client.ImportsApi, sessionID, dockerfile, stage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to import Dockerfile: %w", err)
|
||||
}
|
||||
prog.N++
|
||||
|
||||
stage.Current = "finalizing"
|
||||
imageModel := addImageModel(imageMetadata, packageDigest, manifestDigest, dockerfileDigest, configDigest, sessionID)
|
||||
_, _, err = c.client.ImagesApi.AddImage(authedCtx, imageModel, nil)
|
||||
if err != nil {
|
||||
var detail = "no details given"
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
detail = string(openAPIErr.Body())
|
||||
}
|
||||
return fmt.Errorf("unable to complete import session=%q: %w: %s", sessionID, err, detail)
|
||||
}
|
||||
prog.N++
|
||||
|
||||
stage.Current = ""
|
||||
prog.SetCompleted()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addImageModel(imageMetadata image.Metadata, packageDigest, manifestDigest, dockerfileDigest, configDigest, sessionID string) external.ImageAnalysisRequest {
|
||||
var tags = make([]string, len(imageMetadata.Tags))
|
||||
for i, t := range imageMetadata.Tags {
|
||||
tags[i] = t.String()
|
||||
}
|
||||
|
||||
return external.ImageAnalysisRequest{
|
||||
Source: external.ImageSource{
|
||||
Import: &external.ImageImportManifest{
|
||||
Contents: external.ImportContentDigests{
|
||||
Packages: packageDigest,
|
||||
Manifest: manifestDigest,
|
||||
Dockerfile: dockerfileDigest,
|
||||
ImageConfig: configDigest,
|
||||
},
|
||||
Tags: tags,
|
||||
Digest: imageMetadata.ManifestDigest,
|
||||
LocalImageId: imageMetadata.ID,
|
||||
OperationUuid: sessionID,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// nolint:dupl
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
type configImportAPI interface {
|
||||
ImportImageConfig(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error)
|
||||
}
|
||||
|
||||
func importConfig(ctx context.Context, api configImportAPI, sessionID string, manifest []byte, stage *progress.Stage) (string, error) {
|
||||
if len(manifest) > 0 {
|
||||
log.Debug("importing image config")
|
||||
stage.Current = "image config"
|
||||
|
||||
response, httpResponse, err := api.ImportImageConfig(ctx, sessionID, string(manifest))
|
||||
if err != nil {
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
log.Errorf("api response: %+v", string(openAPIErr.Body()))
|
||||
}
|
||||
return "", fmt.Errorf("unable to import Config: %w", err)
|
||||
}
|
||||
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
if httpResponse.StatusCode != 200 {
|
||||
return "", fmt.Errorf("unable to import Config: %s", httpResponse.Status)
|
||||
}
|
||||
|
||||
return response.Digest, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
type mockConfigImportAPI struct {
|
||||
sessionID string
|
||||
model string
|
||||
httpResponse *http.Response
|
||||
err error
|
||||
ctx context.Context
|
||||
responseDigest string
|
||||
wasCalled bool
|
||||
}
|
||||
|
||||
func (m *mockConfigImportAPI) ImportImageConfig(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error) {
|
||||
m.wasCalled = true
|
||||
m.model = contents
|
||||
m.sessionID = sessionID
|
||||
m.ctx = ctx
|
||||
if m.httpResponse == nil {
|
||||
m.httpResponse = &http.Response{}
|
||||
}
|
||||
m.httpResponse.Body = ioutils.NewReadCloserWrapper(strings.NewReader(""), func() error { return nil })
|
||||
return external.ImageImportContentResponse{Digest: m.responseDigest}, m.httpResponse, m.err
|
||||
}
|
||||
|
||||
func TestConfigImport(t *testing.T) {
|
||||
|
||||
sessionID := "my-session"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
manifest string
|
||||
api *mockConfigImportAPI
|
||||
expectsError bool
|
||||
expectsCall bool
|
||||
}{
|
||||
|
||||
{
|
||||
name: "Go case: import works",
|
||||
manifest: "the-manifest-contents!",
|
||||
api: &mockConfigImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 200},
|
||||
responseDigest: "digest!",
|
||||
},
|
||||
expectsCall: true,
|
||||
},
|
||||
{
|
||||
name: "No manifest provided",
|
||||
manifest: "",
|
||||
api: &mockConfigImportAPI{},
|
||||
expectsCall: false,
|
||||
},
|
||||
{
|
||||
name: "API returns an error",
|
||||
manifest: "the-manifest-contents!",
|
||||
api: &mockConfigImportAPI{
|
||||
err: fmt.Errorf("api error, something went wrong"),
|
||||
},
|
||||
expectsError: true,
|
||||
expectsCall: true,
|
||||
},
|
||||
{
|
||||
name: "API HTTP-level error",
|
||||
manifest: "the-manifest-contents!",
|
||||
api: &mockConfigImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 404},
|
||||
},
|
||||
expectsError: true,
|
||||
expectsCall: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
digest, err := importConfig(context.TODO(), test.api, sessionID, []byte(test.manifest), &progress.Stage{})
|
||||
|
||||
// validate error handling
|
||||
if err != nil && !test.expectsError {
|
||||
t.Fatalf("did not expect an error, but got: %+v", err)
|
||||
} else if err == nil && test.expectsError {
|
||||
t.Fatalf("did expect an error, but got none")
|
||||
}
|
||||
|
||||
if !test.api.wasCalled && test.expectsCall {
|
||||
t.Fatalf("was not called!")
|
||||
} else if test.api.wasCalled && !test.expectsCall {
|
||||
t.Fatalf("should not have been called")
|
||||
}
|
||||
|
||||
if !test.expectsCall {
|
||||
return
|
||||
}
|
||||
|
||||
if digest != test.api.responseDigest {
|
||||
t.Errorf("unexpected content digest: %q != %q", digest, test.api.responseDigest)
|
||||
}
|
||||
|
||||
// validating that the mock got the right parameters
|
||||
if test.api.sessionID != sessionID {
|
||||
t.Errorf("different session ID: %s != %s", test.api.sessionID, sessionID)
|
||||
}
|
||||
|
||||
for _, d := range deep.Equal(test.api.model, test.manifest) {
|
||||
t.Errorf("model difference: %s", d)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// nolint:dupl
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
)
|
||||
|
||||
type dockerfileImportAPI interface {
|
||||
ImportImageDockerfile(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error)
|
||||
}
|
||||
|
||||
func importDockerfile(ctx context.Context, api dockerfileImportAPI, sessionID string, dockerfile []byte, stage *progress.Stage) (string, error) {
|
||||
if len(dockerfile) > 0 {
|
||||
log.Debug("importing dockerfile")
|
||||
stage.Current = "dockerfile"
|
||||
|
||||
response, httpResponse, err := api.ImportImageDockerfile(ctx, sessionID, string(dockerfile))
|
||||
if err != nil {
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
log.Errorf("api response: %+v", string(openAPIErr.Body()))
|
||||
}
|
||||
return "", fmt.Errorf("unable to import Dockerfile: %w", err)
|
||||
}
|
||||
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
if httpResponse.StatusCode != 200 {
|
||||
return "", fmt.Errorf("unable to import Dockerfile: %s", httpResponse.Status)
|
||||
}
|
||||
|
||||
return response.Digest, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
type mockDockerfileImportAPI struct {
|
||||
sessionID string
|
||||
model string
|
||||
httpResponse *http.Response
|
||||
err error
|
||||
ctx context.Context
|
||||
responseDigest string
|
||||
wasCalled bool
|
||||
}
|
||||
|
||||
func (m *mockDockerfileImportAPI) ImportImageDockerfile(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error) {
|
||||
m.wasCalled = true
|
||||
m.model = contents
|
||||
m.sessionID = sessionID
|
||||
m.ctx = ctx
|
||||
if m.httpResponse == nil {
|
||||
m.httpResponse = &http.Response{}
|
||||
}
|
||||
m.httpResponse.Body = ioutils.NewReadCloserWrapper(strings.NewReader(""), func() error { return nil })
|
||||
return external.ImageImportContentResponse{Digest: m.responseDigest}, m.httpResponse, m.err
|
||||
}
|
||||
|
||||
func TestDockerfileImport(t *testing.T) {
|
||||
|
||||
sessionID := "my-session"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dockerfile string
|
||||
api *mockDockerfileImportAPI
|
||||
expectsError bool
|
||||
expectsCall bool
|
||||
}{
|
||||
|
||||
{
|
||||
name: "Go case: import works",
|
||||
dockerfile: "the-manifest-contents!",
|
||||
api: &mockDockerfileImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 200},
|
||||
responseDigest: "digest!",
|
||||
},
|
||||
expectsCall: true,
|
||||
},
|
||||
{
|
||||
name: "No manifest provided",
|
||||
dockerfile: "",
|
||||
api: &mockDockerfileImportAPI{},
|
||||
expectsCall: false,
|
||||
},
|
||||
{
|
||||
name: "API returns an error",
|
||||
dockerfile: "the-manifest-contents!",
|
||||
api: &mockDockerfileImportAPI{
|
||||
err: fmt.Errorf("api error, something went wrong"),
|
||||
},
|
||||
expectsError: true,
|
||||
expectsCall: true,
|
||||
},
|
||||
{
|
||||
name: "API HTTP-level error",
|
||||
dockerfile: "the-manifest-contents!",
|
||||
api: &mockDockerfileImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 404},
|
||||
},
|
||||
expectsError: true,
|
||||
expectsCall: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
digest, err := importDockerfile(context.TODO(), test.api, sessionID, []byte(test.dockerfile), &progress.Stage{})
|
||||
|
||||
// validate error handling
|
||||
if err != nil && !test.expectsError {
|
||||
t.Fatalf("did not expect an error, but got: %+v", err)
|
||||
} else if err == nil && test.expectsError {
|
||||
t.Fatalf("did expect an error, but got none")
|
||||
}
|
||||
|
||||
if !test.api.wasCalled && test.expectsCall {
|
||||
t.Fatalf("was not called!")
|
||||
} else if test.api.wasCalled && !test.expectsCall {
|
||||
t.Fatalf("should not have been called")
|
||||
}
|
||||
|
||||
if !test.expectsCall {
|
||||
return
|
||||
}
|
||||
|
||||
if digest != test.api.responseDigest {
|
||||
t.Errorf("unexpected content digest: %q != %q", digest, test.api.responseDigest)
|
||||
}
|
||||
|
||||
// validating that the mock got the right parameters
|
||||
if test.api.sessionID != sessionID {
|
||||
t.Errorf("different session ID: %s != %s", test.api.sessionID, sessionID)
|
||||
}
|
||||
|
||||
for _, d := range deep.Equal(test.api.model, test.dockerfile) {
|
||||
t.Errorf("model difference: %s", d)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// nolint: dupl
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
type manifestImportAPI interface {
|
||||
ImportImageManifest(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error)
|
||||
}
|
||||
|
||||
func importManifest(ctx context.Context, api manifestImportAPI, sessionID string, manifest []byte, stage *progress.Stage) (string, error) {
|
||||
if len(manifest) > 0 {
|
||||
log.Debug("importing image manifest")
|
||||
stage.Current = "image manifest"
|
||||
|
||||
response, httpResponse, err := api.ImportImageManifest(ctx, sessionID, string(manifest))
|
||||
if err != nil {
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
log.Errorf("api response: %+v", string(openAPIErr.Body()))
|
||||
}
|
||||
return "", fmt.Errorf("unable to import Manifest: %w", err)
|
||||
}
|
||||
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
if httpResponse.StatusCode != 200 {
|
||||
return "", fmt.Errorf("unable to import Manifest: %s", httpResponse.Status)
|
||||
}
|
||||
|
||||
return response.Digest, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
type mockManifestImportAPI struct {
|
||||
sessionID string
|
||||
model string
|
||||
httpResponse *http.Response
|
||||
err error
|
||||
ctx context.Context
|
||||
responseDigest string
|
||||
wasCalled bool
|
||||
}
|
||||
|
||||
func (m *mockManifestImportAPI) ImportImageManifest(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error) {
|
||||
m.wasCalled = true
|
||||
m.model = contents
|
||||
m.sessionID = sessionID
|
||||
m.ctx = ctx
|
||||
if m.httpResponse == nil {
|
||||
m.httpResponse = &http.Response{}
|
||||
}
|
||||
m.httpResponse.Body = ioutils.NewReadCloserWrapper(strings.NewReader(""), func() error { return nil })
|
||||
return external.ImageImportContentResponse{Digest: m.responseDigest}, m.httpResponse, m.err
|
||||
}
|
||||
|
||||
func TestManifestImport(t *testing.T) {
|
||||
|
||||
sessionID := "my-session"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
manifest string
|
||||
api *mockManifestImportAPI
|
||||
expectsError bool
|
||||
expectsCall bool
|
||||
}{
|
||||
|
||||
{
|
||||
name: "Go case: import works",
|
||||
manifest: "the-manifest-contents!",
|
||||
api: &mockManifestImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 200},
|
||||
responseDigest: "digest!",
|
||||
},
|
||||
expectsCall: true,
|
||||
},
|
||||
{
|
||||
name: "No manifest provided",
|
||||
manifest: "",
|
||||
api: &mockManifestImportAPI{},
|
||||
expectsCall: false,
|
||||
},
|
||||
{
|
||||
name: "API returns an error",
|
||||
manifest: "the-manifest-contents!",
|
||||
api: &mockManifestImportAPI{
|
||||
err: fmt.Errorf("api error, something went wrong"),
|
||||
},
|
||||
expectsError: true,
|
||||
expectsCall: true,
|
||||
},
|
||||
{
|
||||
name: "API HTTP-level error",
|
||||
manifest: "the-manifest-contents!",
|
||||
api: &mockManifestImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 404},
|
||||
},
|
||||
expectsError: true,
|
||||
expectsCall: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
digest, err := importManifest(context.TODO(), test.api, sessionID, []byte(test.manifest), &progress.Stage{})
|
||||
|
||||
// validate error handling
|
||||
if err != nil && !test.expectsError {
|
||||
t.Fatalf("did not expect an error, but got: %+v", err)
|
||||
} else if err == nil && test.expectsError {
|
||||
t.Fatalf("did expect an error, but got none")
|
||||
}
|
||||
|
||||
if !test.api.wasCalled && test.expectsCall {
|
||||
t.Fatalf("was not called!")
|
||||
} else if test.api.wasCalled && !test.expectsCall {
|
||||
t.Fatalf("should not have been called")
|
||||
}
|
||||
|
||||
if !test.expectsCall {
|
||||
return
|
||||
}
|
||||
|
||||
if digest != test.api.responseDigest {
|
||||
t.Errorf("unexpected content digest: %q != %q", digest, test.api.responseDigest)
|
||||
}
|
||||
|
||||
// validating that the mock got the right parameters
|
||||
if test.api.sessionID != sessionID {
|
||||
t.Errorf("different session ID: %s != %s", test.api.sessionID, sessionID)
|
||||
}
|
||||
|
||||
for _, d := range deep.Equal(test.api.model, test.manifest) {
|
||||
t.Errorf("model difference: %s", d)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
jsonPresenter "github.com/anchore/syft/syft/presenter/json"
|
||||
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
type packageSBOMImportAPI interface {
|
||||
ImportImagePackages(context.Context, string, external.ImagePackageManifest) (external.ImageImportContentResponse, *http.Response, error)
|
||||
}
|
||||
|
||||
func packageSbomModel(s source.Metadata, catalog *pkg.Catalog, d *distro.Distro) (*external.ImagePackageManifest, error) {
|
||||
var buf bytes.Buffer
|
||||
pres := jsonPresenter.NewPresenter(catalog, s, d)
|
||||
err := pres.Present(&buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to serialize results: %w", err)
|
||||
}
|
||||
|
||||
// the model is 1:1 the JSON output of today. As the schema changes, this will need to be converted into individual mappings.
|
||||
var model external.ImagePackageManifest
|
||||
if err = json.Unmarshal(buf.Bytes(), &model); err != nil {
|
||||
return nil, fmt.Errorf("unable to convert JSON presenter output to import model: %w", err)
|
||||
}
|
||||
|
||||
return &model, nil
|
||||
}
|
||||
|
||||
func importPackageSBOM(ctx context.Context, api packageSBOMImportAPI, sessionID string, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, stage *progress.Stage) (string, error) {
|
||||
log.Debug("importing package SBOM")
|
||||
stage.Current = "package SBOM"
|
||||
|
||||
model, err := packageSbomModel(s, catalog, d)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to create PackageSBOM model: %w", err)
|
||||
}
|
||||
|
||||
response, httpResponse, err := api.ImportImagePackages(ctx, sessionID, *model)
|
||||
if err != nil {
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
log.Errorf("api response: %+v", string(openAPIErr.Body()))
|
||||
}
|
||||
return "", fmt.Errorf("unable to import PackageSBOM: %w", err)
|
||||
}
|
||||
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
if httpResponse.StatusCode != 200 {
|
||||
return "", fmt.Errorf("unable to import PackageSBOM: %s", httpResponse.Status)
|
||||
}
|
||||
|
||||
return response.Digest, nil
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
jsonPresenter "github.com/anchore/syft/syft/presenter/json"
|
||||
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
func must(c pkg.CPE, e error) pkg.CPE {
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// this test is tailored towards the assumption that the import doc shape and the syft json shape are the same.
|
||||
// TODO: replace this as the document shapes diverge.
|
||||
func TestPackageSbomToModel(t *testing.T) {
|
||||
|
||||
m := source.Metadata{
|
||||
Scheme: source.ImageScheme,
|
||||
ImageMetadata: source.ImageMetadata{
|
||||
UserInput: "user-in",
|
||||
Scope: "scope!",
|
||||
Layers: []source.LayerMetadata{
|
||||
{
|
||||
MediaType: "layer-metadata-type!",
|
||||
Digest: "layer-digest",
|
||||
Size: 20,
|
||||
},
|
||||
},
|
||||
Size: 10,
|
||||
ManifestDigest: "sha256:digest!",
|
||||
MediaType: "mediatype!",
|
||||
Tags: nil,
|
||||
},
|
||||
}
|
||||
|
||||
d, _ := distro.NewDistro(distro.CentOS, "8.0", "")
|
||||
|
||||
p := pkg.Package{
|
||||
Name: "name",
|
||||
Version: "version",
|
||||
FoundBy: "foundBy",
|
||||
Locations: []source.Location{
|
||||
{
|
||||
Path: "path",
|
||||
FileSystemID: "layerID",
|
||||
},
|
||||
},
|
||||
Licenses: []string{"license"},
|
||||
Language: pkg.Python,
|
||||
Type: pkg.PythonPkg,
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
|
||||
},
|
||||
PURL: "purl",
|
||||
}
|
||||
|
||||
c := pkg.NewCatalog(p)
|
||||
|
||||
model, err := packageSbomModel(m, c, &d)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate model from source material: %+v", err)
|
||||
}
|
||||
|
||||
var modelJSON []byte
|
||||
|
||||
modelJSON, err = json.Marshal(&model)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to marshal model: %+v", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
pres := jsonPresenter.NewPresenter(c, m, &d)
|
||||
if err := pres.Present(&buf); err != nil {
|
||||
t.Fatalf("unable to get expected json: %+v", err)
|
||||
}
|
||||
|
||||
// unmarshal expected result
|
||||
var expectedDoc jsonPresenter.Document
|
||||
if err := json.Unmarshal(buf.Bytes(), &expectedDoc); err != nil {
|
||||
t.Fatalf("unable to parse json doc: %+v", err)
|
||||
}
|
||||
|
||||
// unmarshal actual result
|
||||
var actualDoc jsonPresenter.Document
|
||||
if err := json.Unmarshal(modelJSON, &actualDoc); err != nil {
|
||||
t.Fatalf("unable to parse json doc: %+v", err)
|
||||
}
|
||||
|
||||
for _, d := range deep.Equal(actualDoc, expectedDoc) {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
}
|
||||
|
||||
type mockPackageSBOMImportAPI struct {
|
||||
sessionID string
|
||||
model external.ImagePackageManifest
|
||||
httpResponse *http.Response
|
||||
err error
|
||||
ctx context.Context
|
||||
responseDigest string
|
||||
}
|
||||
|
||||
func (m *mockPackageSBOMImportAPI) ImportImagePackages(ctx context.Context, sessionID string, model external.ImagePackageManifest) (external.ImageImportContentResponse, *http.Response, error) {
|
||||
m.model = model
|
||||
m.sessionID = sessionID
|
||||
m.ctx = ctx
|
||||
if m.httpResponse == nil {
|
||||
m.httpResponse = &http.Response{}
|
||||
}
|
||||
m.httpResponse.Body = ioutils.NewReadCloserWrapper(strings.NewReader(""), func() error { return nil })
|
||||
return external.ImageImportContentResponse{Digest: m.responseDigest}, m.httpResponse, m.err
|
||||
}
|
||||
|
||||
func TestPackageSbomImport(t *testing.T) {
|
||||
|
||||
catalog := pkg.NewCatalog(pkg.Package{
|
||||
Name: "name",
|
||||
Version: "version",
|
||||
FoundBy: "foundBy",
|
||||
Locations: []source.Location{
|
||||
{
|
||||
Path: "path",
|
||||
FileSystemID: "layerID",
|
||||
},
|
||||
},
|
||||
Licenses: []string{"license"},
|
||||
Language: pkg.Python,
|
||||
Type: pkg.PythonPkg,
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
|
||||
},
|
||||
PURL: "purl",
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "p-name",
|
||||
Version: "p-version",
|
||||
License: "p-license",
|
||||
Author: "p-author",
|
||||
AuthorEmail: "p-email",
|
||||
Platform: "p-platform",
|
||||
Files: []pkg.PythonFileRecord{
|
||||
{
|
||||
Path: "p-path",
|
||||
Digest: &pkg.PythonFileDigest{
|
||||
Algorithm: "p-alg",
|
||||
Value: "p-digest",
|
||||
},
|
||||
Size: "p-size",
|
||||
},
|
||||
},
|
||||
SitePackagesRootPath: "p-site-packages-root",
|
||||
TopLevelPackages: []string{"top-level"},
|
||||
},
|
||||
})
|
||||
|
||||
m := source.Metadata{
|
||||
Scheme: "a-schema",
|
||||
ImageMetadata: source.ImageMetadata{
|
||||
UserInput: "user-in",
|
||||
Scope: "scope!",
|
||||
Layers: nil,
|
||||
Size: 10,
|
||||
ManifestDigest: "sha256:digest!",
|
||||
MediaType: "mediatype!",
|
||||
Tags: nil,
|
||||
},
|
||||
}
|
||||
|
||||
d, _ := distro.NewDistro(distro.CentOS, "8.0", "")
|
||||
|
||||
theModel, err := packageSbomModel(m, catalog, &d)
|
||||
if err != nil {
|
||||
t.Fatalf("could not get sbom model: %+v", err)
|
||||
}
|
||||
|
||||
sessionID := "my-session"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
api *mockPackageSBOMImportAPI
|
||||
expectsError bool
|
||||
}{
|
||||
|
||||
{
|
||||
name: "Go case: import works",
|
||||
api: &mockPackageSBOMImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 200},
|
||||
responseDigest: "digest!",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "API returns an error",
|
||||
api: &mockPackageSBOMImportAPI{
|
||||
err: fmt.Errorf("API error, something went wrong."),
|
||||
},
|
||||
expectsError: true,
|
||||
},
|
||||
{
|
||||
name: "API HTTP-level error",
|
||||
api: &mockPackageSBOMImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 404},
|
||||
},
|
||||
expectsError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
digest, err := importPackageSBOM(context.TODO(), test.api, sessionID, m, catalog, &d, &progress.Stage{})
|
||||
|
||||
// validate error handling
|
||||
if err != nil && !test.expectsError {
|
||||
t.Fatalf("did not expect an error, but got: %+v", err)
|
||||
} else if err == nil && test.expectsError {
|
||||
t.Fatalf("did expect an error, but got none")
|
||||
}
|
||||
|
||||
if digest != test.api.responseDigest {
|
||||
t.Errorf("unexpected content digest: %q != %q", digest, test.api.responseDigest)
|
||||
}
|
||||
|
||||
// validating that the mock got the right parameters (api.ImportImagePackages)
|
||||
if test.api.sessionID != sessionID {
|
||||
t.Errorf("different session ID: %s != %s", test.api.sessionID, sessionID)
|
||||
}
|
||||
|
||||
for _, d := range deep.Equal(&test.api.model, theModel) {
|
||||
t.Errorf("model difference: %s", d)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
+84
-41
@@ -8,70 +8,75 @@ import (
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type CliOnlyOptions struct {
|
||||
ConfigPath string
|
||||
Verbosity int
|
||||
}
|
||||
|
||||
// Application is the main syft application configuration.
|
||||
type Application struct {
|
||||
ConfigPath string
|
||||
PresenterOpt presenter.Option
|
||||
Output string `mapstructure:"output"`
|
||||
ScopeOpt scope.Option
|
||||
Scope string `mapstructure:"scope"`
|
||||
Quiet bool `mapstructure:"quiet"`
|
||||
Log Logging `mapstructure:"log"`
|
||||
CliOptions CliOnlyOptions
|
||||
CheckForAppUpdate bool `mapstructure:"check-for-app-update"`
|
||||
ConfigPath string `yaml:",omitempty"` // the location where the application config was read from (either from -c or discovered while loading)
|
||||
PresenterOpt presenter.Option `yaml:"-"` // -o, the native Presenter.Option to use for report formatting
|
||||
Output string `yaml:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting
|
||||
ScopeOpt source.Scope `yaml:"-"` // -s, the native source.Scope option to use for how to catalog the container image
|
||||
Scope string `yaml:"scope" mapstructure:"scope"` // -s, the source.Scope string hint for how to catalog the container image
|
||||
Quiet bool `yaml:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
|
||||
Log logging `yaml:"log" mapstructure:"log"` // all logging-related options
|
||||
CliOptions CliOnlyOptions `yaml:"-"` // all options only available through the CLI (not via env vars or config)
|
||||
CheckForAppUpdate bool `yaml:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
|
||||
Anchore anchore `yaml:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise
|
||||
}
|
||||
|
||||
type Logging struct {
|
||||
Structured bool `mapstructure:"structured"`
|
||||
LevelOpt logrus.Level
|
||||
Level string `mapstructure:"level"`
|
||||
FileLocation string `mapstructure:"file"`
|
||||
// CliOnlyOptions are options that are in the application config in memory, but are only exposed via CLI switches (not from unmarshaling a config file)
|
||||
type CliOnlyOptions struct {
|
||||
ConfigPath string // -c. where the read config is on disk
|
||||
Verbosity int // -v or -vv , controlling which UI (ETUI vs logging) and what the log level should be
|
||||
}
|
||||
|
||||
func setNonCliDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("log.level", "")
|
||||
v.SetDefault("log.file", "")
|
||||
v.SetDefault("log.structured", false)
|
||||
v.SetDefault("check-for-app-update", true)
|
||||
// logging contains all logging-related configuration options available to the user via the application config.
|
||||
type logging struct {
|
||||
Structured bool `yaml:"structured" mapstructure:"structured"` // show all log entries as JSON formatted strings
|
||||
LevelOpt logrus.Level `yaml:"level"` // the native log level object used by the logger
|
||||
Level string `yaml:"-" mapstructure:"level"` // the log level string hint
|
||||
FileLocation string `yaml:"file" mapstructure:"file"` // the file path to write logs to
|
||||
}
|
||||
|
||||
func LoadConfigFromFile(v *viper.Viper, cliOpts *CliOnlyOptions) (*Application, error) {
|
||||
type anchore struct {
|
||||
// upload options
|
||||
UploadEnabled bool `yaml:"upload-enabled" mapstructure:"upload-enabled"` // whether to upload results to Anchore Engine/Enterprise (defaults to "false" unless there is the presence of -h CLI option)
|
||||
Host string `yaml:"host" mapstructure:"host"` // -H , hostname of the engine/enterprise instance to upload to
|
||||
Path string `yaml:"path" mapstructure:"path"` // override the engine/enterprise API upload path
|
||||
Username string `yaml:"username" mapstructure:"username"` // -u , username to authenticate upload
|
||||
Password string `yaml:"password" mapstructure:"password"` // -p , password to authenticate upload
|
||||
Dockerfile string `yaml:"dockerfile" mapstructure:"dockerfile"` // -d , dockerfile to attach for upload
|
||||
}
|
||||
|
||||
// LoadApplicationConfig populates the given viper object with application configuration discovered on disk
|
||||
func LoadApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions, wasHostnameSet bool) (*Application, error) {
|
||||
// the user may not have a config, and this is OK, we can use the default config + default cobra cli values instead
|
||||
setNonCliDefaultValues(v)
|
||||
if cliOpts != nil {
|
||||
_ = readConfig(v, cliOpts.ConfigPath)
|
||||
} else {
|
||||
_ = readConfig(v, "")
|
||||
}
|
||||
_ = readConfig(v, cliOpts.ConfigPath)
|
||||
|
||||
config := &Application{
|
||||
CliOptions: *cliOpts,
|
||||
CliOptions: cliOpts,
|
||||
}
|
||||
err := v.Unmarshal(config)
|
||||
if err != nil {
|
||||
|
||||
if err := v.Unmarshal(config); err != nil {
|
||||
return nil, fmt.Errorf("unable to parse config: %w", err)
|
||||
}
|
||||
config.ConfigPath = v.ConfigFileUsed()
|
||||
|
||||
err = config.Build()
|
||||
if err != nil {
|
||||
if err := config.build(v, wasHostnameSet); err != nil {
|
||||
return nil, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (cfg *Application) Build() error {
|
||||
// build inflates simple config values into syft native objects (or other complex objects) after the config is fully read in.
|
||||
func (cfg *Application) build(v *viper.Viper, wasHostnameSet bool) error {
|
||||
// set the presenter
|
||||
presenterOption := presenter.ParseOption(cfg.Output)
|
||||
if presenterOption == presenter.UnknownPresenter {
|
||||
@@ -79,9 +84,9 @@ func (cfg *Application) Build() error {
|
||||
}
|
||||
cfg.PresenterOpt = presenterOption
|
||||
|
||||
// set the scope
|
||||
scopeOption := scope.ParseOption(cfg.Scope)
|
||||
if scopeOption == scope.UnknownScope {
|
||||
// set the source
|
||||
scopeOption := source.ParseScope(cfg.Scope)
|
||||
if scopeOption == source.UnknownScope {
|
||||
return fmt.Errorf("bad --scope value '%s'", cfg.Scope)
|
||||
}
|
||||
cfg.ScopeOpt = scopeOption
|
||||
@@ -111,14 +116,44 @@ func (cfg *Application) Build() error {
|
||||
case v >= 2:
|
||||
cfg.Log.LevelOpt = logrus.DebugLevel
|
||||
default:
|
||||
cfg.Log.LevelOpt = logrus.ErrorLevel
|
||||
cfg.Log.LevelOpt = logrus.WarnLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
// check if upload should be done relative to the CLI and config behavior
|
||||
if !v.IsSet("anchore.upload-enabled") && wasHostnameSet {
|
||||
// we know the user didn't specify to upload in the config file and a --hostname option was provided (so set upload)
|
||||
cfg.Anchore.UploadEnabled = true
|
||||
}
|
||||
|
||||
if !cfg.Anchore.UploadEnabled && cfg.Anchore.Dockerfile != "" {
|
||||
return fmt.Errorf("cannot provide dockerfile option without enabling upload")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg Application) String() string {
|
||||
// redact sensitive information
|
||||
if cfg.Anchore.Username != "" {
|
||||
cfg.Anchore.Username = "********"
|
||||
}
|
||||
|
||||
if cfg.Anchore.Password != "" {
|
||||
cfg.Anchore.Password = "********"
|
||||
}
|
||||
|
||||
// yaml is pretty human friendly (at least when compared to json)
|
||||
appCfgStr, err := yaml.Marshal(&cfg)
|
||||
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return string(appCfgStr)
|
||||
}
|
||||
|
||||
// readConfig attempts to read the given config path from disk or discover an alternate store location
|
||||
func readConfig(v *viper.Viper, configPath string) error {
|
||||
v.AutomaticEnv()
|
||||
v.SetEnvPrefix(internal.ApplicationName)
|
||||
@@ -174,3 +209,11 @@ func readConfig(v *viper.Viper, configPath string) error {
|
||||
|
||||
return fmt.Errorf("application config not found")
|
||||
}
|
||||
|
||||
// setNonCliDefaultValues ensures that there are sane defaults for values that do not have CLI equivalent options (where there would already be a default value)
|
||||
func setNonCliDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("log.level", "")
|
||||
v.SetDefault("log.file", "")
|
||||
v.SetDefault("log.structured", false)
|
||||
v.SetDefault("check-for-app-update", true)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
package internal
|
||||
|
||||
// note: do not change this
|
||||
const ApplicationName = "syft"
|
||||
const (
|
||||
// ApplicationName is the non-capitalized name of the application (do not change this)
|
||||
ApplicationName = "syft"
|
||||
|
||||
// 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.0"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package internal contains miscellaneous functions and objects useful within syft but should not be used externally.
|
||||
*/
|
||||
package internal
|
||||
@@ -1,5 +1,6 @@
|
||||
package file
|
||||
|
||||
// GlobMatch evaluates the given glob pattern against the given "name" string, indicating if there is a match or not.
|
||||
// Source: https://research.swtch.com/glob.go
|
||||
func GlobMatch(pattern, name string) bool {
|
||||
px := 0
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Opener is an object that stores a path to later be opened as a file.
|
||||
type Opener struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// Open the stored path as a io.ReadCloser.
|
||||
func (o Opener) Open() (io.ReadCloser, error) {
|
||||
return os.Open(o.path)
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eux
|
||||
|
||||
zip -r "$1" zip-source
|
||||
# $1 —— absolute path to destination file, should end with .zip, ideally
|
||||
# $2 —— absolute path to directory from which to add entries to the archive
|
||||
|
||||
pushd "$2" && find . -print | zip "$1" -@ && popd
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var expectedZipArchiveEntries = []string{
|
||||
"some-dir" + string(os.PathSeparator),
|
||||
filepath.Join("some-dir", "a-file.txt"),
|
||||
"b-file.txt",
|
||||
"nested.zip",
|
||||
}
|
||||
|
||||
// fatalIfError calls the supplied function. If the function returns a non-nil error, t.Fatal(err) is called.
|
||||
func fatalIfError(t *testing.T, fn func() error) {
|
||||
t.Helper()
|
||||
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := fn()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// createZipArchive creates a new ZIP archive file at destinationArchivePath based on the directory found at
|
||||
// sourceDirPath.
|
||||
func createZipArchive(t *testing.T, sourceDirPath, destinationArchivePath string) error {
|
||||
t.Helper()
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get cwd: %+v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("./generate-zip-fixture.sh", destinationArchivePath, path.Base(sourceDirPath))
|
||||
cmd.Dir = filepath.Join(cwd, "test-fixtures")
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("unable to start generate zip fixture script: %+v", err)
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||
// The program has exited with an exit code != 0
|
||||
|
||||
// This works on both Unix and Windows. Although package
|
||||
// syscall is generally platform dependent, WaitStatus is
|
||||
// defined for both Unix and Windows and in both cases has
|
||||
// an ExitStatus() method with the same signature.
|
||||
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
||||
if status.ExitStatus() != 0 {
|
||||
return fmt.Errorf("failed to generate fixture: rc=%d", status.ExitStatus())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("unable to get generate fixture script result: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupZipFileTest encapsulates common test setup work for zip file tests. It returns a cleanup function,
|
||||
// which should be called (typically deferred) by the caller, the path of the created zip archive, and an error,
|
||||
// which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil
|
||||
// (even if there's an error), and it should always be called.
|
||||
func setupZipFileTest(t *testing.T, sourceDirPath string) (func() error, string, error) {
|
||||
t.Helper()
|
||||
|
||||
// Keep track of any needed cleanup work as we go
|
||||
var cleanupFns []func() error
|
||||
cleanup := func(fns []func() error) func() error {
|
||||
return func() error {
|
||||
for _, fn := range fns {
|
||||
err := fn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
|
||||
if err != nil {
|
||||
return cleanup(cleanupFns), "", fmt.Errorf("unable to create tempfile: %+v", err)
|
||||
}
|
||||
cleanupFns = append(cleanupFns, func() error { return os.Remove(archivePrefix.Name()) })
|
||||
|
||||
destinationArchiveFilePath := archivePrefix.Name() + ".zip"
|
||||
t.Logf("archive path: %s", destinationArchiveFilePath)
|
||||
err = createZipArchive(t, sourceDirPath, destinationArchiveFilePath)
|
||||
cleanupFns = append(cleanupFns, func() error { return os.Remove(destinationArchiveFilePath) })
|
||||
if err != nil {
|
||||
return cleanup(cleanupFns), "", err
|
||||
}
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return cleanup(cleanupFns), "", fmt.Errorf("unable to get cwd: %+v", err)
|
||||
}
|
||||
|
||||
t.Logf("running from: %s", cwd)
|
||||
|
||||
return cleanup(cleanupFns), destinationArchiveFilePath, nil
|
||||
}
|
||||
|
||||
// TODO: Consider moving any non-git asset generation to a task (e.g. make) that's run ahead of running go tests.
|
||||
func ensureNestedZipExists(t *testing.T, sourceDirPath string) error {
|
||||
t.Helper()
|
||||
|
||||
nestedArchiveFilePath := path.Join(sourceDirPath, "nested.zip")
|
||||
err := createZipArchive(t, sourceDirPath, nestedArchiveFilePath)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create nested archive for test fixture: %+v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -5,28 +5,37 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// ZipFileManifest is a collection of paths and their file metadata.
|
||||
type ZipFileManifest map[string]os.FileInfo
|
||||
|
||||
// newZipManifest creates an empty ZipFileManifest.
|
||||
func newZipManifest() ZipFileManifest {
|
||||
return make(ZipFileManifest)
|
||||
}
|
||||
|
||||
// Add a new path and it's file metadata to the collection.
|
||||
func (z ZipFileManifest) Add(entry string, info os.FileInfo) {
|
||||
z[entry] = info
|
||||
}
|
||||
|
||||
// GlobMatch returns the path keys that match the given value(s).
|
||||
func (z ZipFileManifest) GlobMatch(patterns ...string) []string {
|
||||
uniqueMatches := internal.NewStringSet()
|
||||
|
||||
for _, pattern := range patterns {
|
||||
for entry := range z {
|
||||
if GlobMatch(pattern, entry) {
|
||||
// We want to match globs as if entries begin with a leading slash (akin to an absolute path)
|
||||
// so that glob logic is consistent inside and outside of ZIP archives
|
||||
normalizedEntry := normalizeZipEntryName(entry)
|
||||
|
||||
if GlobMatch(pattern, normalizedEntry) {
|
||||
uniqueMatches.Add(entry)
|
||||
}
|
||||
}
|
||||
@@ -38,6 +47,7 @@ func (z ZipFileManifest) GlobMatch(patterns ...string) []string {
|
||||
return results
|
||||
}
|
||||
|
||||
// NewZipFileManifest creates and returns a new ZipFileManifest populated with path and metadata from the given zip archive path.
|
||||
func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
|
||||
zipReader, err := zip.OpenReader(archivePath)
|
||||
manifest := newZipManifest()
|
||||
@@ -47,7 +57,7 @@ func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
|
||||
defer func() {
|
||||
err = zipReader.Close()
|
||||
if err != nil {
|
||||
log.Errorf("unable to close zip archive (%s): %w", archivePath, err)
|
||||
log.Errorf("unable to close zip archive (%s): %+v", archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -56,3 +66,12 @@ func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// normalizeZipEntryName takes the given path entry and ensures it is prefixed with "/".
|
||||
func normalizeZipEntryName(entry string) string {
|
||||
if !strings.HasPrefix(entry, "/") {
|
||||
return "/" + entry
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewZipFileManifest(t *testing.T) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sourceDirPath := path.Join(cwd, "test-fixtures", "zip-source")
|
||||
err = ensureNestedZipExists(t, sourceDirPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cleanup, archiveFilePath, err := setupZipFileTest(t, sourceDirPath)
|
||||
defer fatalIfError(t, cleanup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actual, err := NewZipFileManifest(archiveFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to extract from unzip archive: %+v", err)
|
||||
}
|
||||
|
||||
if len(expectedZipArchiveEntries) != len(actual) {
|
||||
t.Fatalf("mismatched manifest: %d != %d", len(actual), len(expectedZipArchiveEntries))
|
||||
}
|
||||
|
||||
for _, e := range expectedZipArchiveEntries {
|
||||
_, ok := actual[e]
|
||||
if !ok {
|
||||
t.Errorf("missing path: %s", e)
|
||||
}
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
b, err := json.MarshalIndent(actual, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("can't show results: %+v", err)
|
||||
}
|
||||
|
||||
t.Errorf("full result: %s", string(b))
|
||||
}
|
||||
}
|
||||
|
||||
func TestZipFileManifest_GlobMatch(t *testing.T) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sourceDirPath := path.Join(cwd, "test-fixtures", "zip-source")
|
||||
err = ensureNestedZipExists(t, sourceDirPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cleanup, archiveFilePath, err := setupZipFileTest(t, sourceDirPath)
|
||||
//goland:noinspection GoNilness
|
||||
defer fatalIfError(t, cleanup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
z, err := NewZipFileManifest(archiveFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to extract from unzip archive: %+v", err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
glob string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"/b*",
|
||||
"b-file.txt",
|
||||
},
|
||||
{
|
||||
"*/a-file.txt",
|
||||
"some-dir/a-file.txt",
|
||||
},
|
||||
{
|
||||
"**/*.zip",
|
||||
"nested.zip",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.glob, func(t *testing.T) {
|
||||
glob := tc.glob
|
||||
|
||||
results := z.GlobMatch(glob)
|
||||
|
||||
if len(results) == 1 && results[0] == tc.expected {
|
||||
return
|
||||
}
|
||||
|
||||
t.Errorf("unexpected results for glob '%s': %+v", glob, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// represents the order of bytes
|
||||
_ = iota
|
||||
KB = 1 << (10 * iota)
|
||||
MB
|
||||
@@ -33,6 +34,7 @@ func newZipTraverseRequest(paths ...string) zipTraversalRequest {
|
||||
return results
|
||||
}
|
||||
|
||||
// TraverseFilesInZip enumerates all paths stored within a zip archive using the visitor pattern.
|
||||
func TraverseFilesInZip(archivePath string, visitor func(*zip.File) error, paths ...string) error {
|
||||
request := newZipTraverseRequest(paths...)
|
||||
|
||||
@@ -43,7 +45,7 @@ func TraverseFilesInZip(archivePath string, visitor func(*zip.File) error, paths
|
||||
defer func() {
|
||||
err = zipReader.Close()
|
||||
if err != nil {
|
||||
log.Errorf("unable to close zip archive (%s): %w", archivePath, err)
|
||||
log.Errorf("unable to close zip archive (%s): %+v", archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -63,8 +65,9 @@ func TraverseFilesInZip(archivePath string, visitor func(*zip.File) error, paths
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (map[string]io.Reader, error) {
|
||||
results := make(map[string]io.Reader)
|
||||
// ExtractFromZipToUniqueTempFile extracts select paths for the given archive to a temporary directory, returning file openers for each file extracted.
|
||||
func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (map[string]Opener, error) {
|
||||
results := make(map[string]Opener)
|
||||
|
||||
// don't allow for full traversal, only select traversal from given paths
|
||||
if len(paths) == 0 {
|
||||
@@ -78,11 +81,21 @@ func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (m
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create temp file: %w", err)
|
||||
}
|
||||
// we shouldn't try and keep the tempfile open as the returned result may have several files, which takes up
|
||||
// resources (leading to "too many open files"). Instead we'll return a file opener to the caller which
|
||||
// provides a ReadCloser. It is up to the caller to handle closing the file explicitly.
|
||||
defer tempFile.Close()
|
||||
|
||||
zippedFile, err := file.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read file=%q from zip=%q: %w", file.Name, archivePath, err)
|
||||
}
|
||||
defer func() {
|
||||
err := zippedFile.Close()
|
||||
if err != nil {
|
||||
log.Errorf("unable to close source file=%q from zip=%q: %+v", file.Name, archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if file.FileInfo().IsDir() {
|
||||
return fmt.Errorf("unable to extract directories, only files: %s", file.Name)
|
||||
@@ -103,18 +116,15 @@ func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (m
|
||||
return fmt.Errorf("unable to reset file pointer (%s): %w", tempFile.Name(), err)
|
||||
}
|
||||
|
||||
results[file.Name] = tempFile
|
||||
results[file.Name] = Opener{path: tempFile.Name()}
|
||||
|
||||
err = zippedFile.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to close source file=%q from zip=%q: %w", file.Name, archivePath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return results, TraverseFilesInZip(archivePath, visitor, paths...)
|
||||
}
|
||||
|
||||
// ContentsFromZip extracts select paths for the given archive and returns a set of string contents for each path.
|
||||
func ContentsFromZip(archivePath string, paths ...string) (map[string]string, error) {
|
||||
results := make(map[string]string)
|
||||
|
||||
@@ -156,6 +166,7 @@ func ContentsFromZip(archivePath string, paths ...string) (map[string]string, er
|
||||
return results, TraverseFilesInZip(archivePath, visitor, paths...)
|
||||
}
|
||||
|
||||
// UnzipToDir extracts a zip archive to a target directory.
|
||||
func UnzipToDir(archivePath, targetDir string) error {
|
||||
visitor := func(file *zip.File) error {
|
||||
// the zip-slip attack protection is still being erroneously detected
|
||||
|
||||
@@ -6,47 +6,14 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
func generateFixture(t *testing.T, archivePath string) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Errorf("unable to get cwd: %+v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("./generate-zip-fixture.sh", archivePath)
|
||||
cmd.Dir = filepath.Join(cwd, "test-fixtures")
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("unable to start generate zip fixture script: %+v", err)
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||
// The program has exited with an exit code != 0
|
||||
|
||||
// This works on both Unix and Windows. Although package
|
||||
// syscall is generally platform dependent, WaitStatus is
|
||||
// defined for both Unix and Windows and in both cases has
|
||||
// an ExitStatus() method with the same signature.
|
||||
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
||||
if status.ExitStatus() != 0 {
|
||||
t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("unable to get generate fixture script result: %+v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func equal(r1, r2 io.Reader) (bool, error) {
|
||||
w1 := sha256.New()
|
||||
w2 := sha256.New()
|
||||
@@ -67,54 +34,50 @@ func equal(r1, r2 io.Reader) (bool, error) {
|
||||
}
|
||||
|
||||
func TestUnzipToDir(t *testing.T) {
|
||||
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempfile: %+v", err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(archivePrefix.Name())
|
||||
// the zip utility will add ".zip" to the end of the given name
|
||||
archivePath := archivePrefix.Name() + ".zip"
|
||||
defer os.Remove(archivePath)
|
||||
t.Logf("archive path: %s", archivePath)
|
||||
|
||||
generateFixture(t, archivePrefix.Name())
|
||||
goldenRootDir := filepath.Join(cwd, "test-fixtures")
|
||||
sourceDirPath := path.Join(goldenRootDir, "zip-source")
|
||||
cleanup, archiveFilePath, err := setupZipFileTest(t, sourceDirPath)
|
||||
defer fatalIfError(t, cleanup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
contentsDir, err := ioutil.TempDir("", "syft-ziputil-contents-TEST-")
|
||||
unzipDestinationDir, err := ioutil.TempDir("", "syft-ziputil-contents-TEST-")
|
||||
defer os.RemoveAll(unzipDestinationDir)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempdir: %+v", err)
|
||||
}
|
||||
defer os.RemoveAll(contentsDir)
|
||||
|
||||
t.Logf("content path: %s", contentsDir)
|
||||
t.Logf("content path: %s", unzipDestinationDir)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Errorf("unable to get cwd: %+v", err)
|
||||
}
|
||||
|
||||
t.Logf("running from: %s", cwd)
|
||||
|
||||
// note: zip utility already includes "zip-source" as a parent dir for all contained files
|
||||
goldenRootDir := filepath.Join(cwd, "test-fixtures")
|
||||
expectedPaths := 4
|
||||
expectedPaths := len(expectedZipArchiveEntries)
|
||||
observedPaths := 0
|
||||
|
||||
err = UnzipToDir(archivePath, contentsDir)
|
||||
err = UnzipToDir(archiveFilePath, unzipDestinationDir)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to unzip archive: %+v", err)
|
||||
}
|
||||
|
||||
// compare the source dir tree and the unzipped tree
|
||||
err = filepath.Walk(filepath.Join(contentsDir, "zip-source"),
|
||||
err = filepath.Walk(unzipDestinationDir,
|
||||
func(path string, info os.FileInfo, err error) error {
|
||||
t.Logf("unzipped path: %s", path)
|
||||
observedPaths++
|
||||
// We don't unzip the root archive dir, since there's no archive entry for it
|
||||
if path != unzipDestinationDir {
|
||||
t.Logf("unzipped path: %s", path)
|
||||
observedPaths++
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("this should not happen")
|
||||
return err
|
||||
}
|
||||
|
||||
goldenPath := filepath.Join(goldenRootDir, strings.TrimPrefix(path, contentsDir))
|
||||
goldenPath := filepath.Join(sourceDirPath, strings.TrimPrefix(path, unzipDestinationDir))
|
||||
|
||||
if info.IsDir() {
|
||||
i, err := os.Stat(goldenPath)
|
||||
@@ -156,12 +119,11 @@ func TestUnzipToDir(t *testing.T) {
|
||||
}
|
||||
|
||||
if observedPaths != expectedPaths {
|
||||
t.Errorf("missed test paths: %d!=%d", observedPaths, expectedPaths)
|
||||
t.Errorf("missed test paths: %d != %d", observedPaths, expectedPaths)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestExtractFilesFromZipFile(t *testing.T) {
|
||||
func TestContentsFromZip(t *testing.T) {
|
||||
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempfile: %+v", err)
|
||||
@@ -172,7 +134,10 @@ func TestExtractFilesFromZipFile(t *testing.T) {
|
||||
defer os.Remove(archivePath)
|
||||
t.Logf("archive path: %s", archivePath)
|
||||
|
||||
generateFixture(t, archivePrefix.Name())
|
||||
err = createZipArchive(t, "zip-source", archivePrefix.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
@@ -181,8 +146,8 @@ func TestExtractFilesFromZipFile(t *testing.T) {
|
||||
|
||||
t.Logf("running from: %s", cwd)
|
||||
|
||||
aFilePath := filepath.Join("zip-source", "some-dir", "a-file.txt")
|
||||
bFilePath := filepath.Join("zip-source", "b-file.txt")
|
||||
aFilePath := filepath.Join("some-dir", "a-file.txt")
|
||||
bFilePath := filepath.Join("b-file.txt")
|
||||
|
||||
expected := map[string]string{
|
||||
aFilePath: "A file! nice!",
|
||||
@@ -207,60 +172,4 @@ func TestExtractFilesFromZipFile(t *testing.T) {
|
||||
|
||||
t.Errorf("full result: %s", string(b))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestZipFileManifest(t *testing.T) {
|
||||
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempfile: %+v", err)
|
||||
}
|
||||
defer os.Remove(archivePrefix.Name())
|
||||
// the zip utility will add ".zip" to the end of the given name
|
||||
archivePath := archivePrefix.Name() + ".zip"
|
||||
defer os.Remove(archivePath)
|
||||
t.Logf("archive path: %s", archivePath)
|
||||
|
||||
generateFixture(t, archivePrefix.Name())
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Errorf("unable to get cwd: %+v", err)
|
||||
}
|
||||
|
||||
t.Logf("running from: %s", cwd)
|
||||
|
||||
expected := []string{
|
||||
filepath.Join("zip-source") + string(os.PathSeparator),
|
||||
filepath.Join("zip-source", "some-dir") + string(os.PathSeparator),
|
||||
filepath.Join("zip-source", "some-dir", "a-file.txt"),
|
||||
filepath.Join("zip-source", "b-file.txt"),
|
||||
}
|
||||
|
||||
actual, err := NewZipFileManifest(archivePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to extract from unzip archive: %+v", err)
|
||||
}
|
||||
|
||||
if len(expected) != len(actual) {
|
||||
t.Fatalf("mismatched manifest: %d != %d", len(actual), len(expected))
|
||||
}
|
||||
|
||||
for _, e := range expected {
|
||||
_, ok := actual[e]
|
||||
if !ok {
|
||||
t.Errorf("missing path: %s", e)
|
||||
}
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
|
||||
b, err := json.MarshalIndent(actual, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("can't show results: %+v", err)
|
||||
}
|
||||
|
||||
t.Errorf("full result: %s", string(b))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package format
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
DefaultColor Color = iota + 30
|
||||
Red
|
||||
Green
|
||||
Yellow
|
||||
Blue
|
||||
Magenta
|
||||
Cyan
|
||||
White
|
||||
)
|
||||
|
||||
type Color uint8
|
||||
|
||||
// TODO: not cross platform (windows...)
|
||||
func (c Color) Format(s string) string {
|
||||
return fmt.Sprintf("\x1b[%dm%s\x1b[0m", c, s)
|
||||
}
|
||||
@@ -1,25 +1,49 @@
|
||||
/*
|
||||
Package log contains the singleton object and helper functions for facilitating logging within the syft library.
|
||||
*/
|
||||
package log
|
||||
|
||||
import "github.com/anchore/syft/syft/logger"
|
||||
|
||||
// Log is the singleton used to facilitate logging internally within syft
|
||||
var Log logger.Logger = &nopLogger{}
|
||||
|
||||
// Errorf takes a formatted template string and template arguments for the error logging level.
|
||||
func Errorf(format string, args ...interface{}) {
|
||||
Log.Errorf(format, args...)
|
||||
}
|
||||
|
||||
// Error logs the given arguments at the error logging level.
|
||||
func Error(args ...interface{}) {
|
||||
Log.Error(args...)
|
||||
}
|
||||
|
||||
// Warnf takes a formatted template string and template arguments for the warning logging level.
|
||||
func Warnf(format string, args ...interface{}) {
|
||||
Log.Warnf(format, args...)
|
||||
}
|
||||
|
||||
// Warn logs the given arguments at the warning logging level.
|
||||
func Warn(args ...interface{}) {
|
||||
Log.Warn(args...)
|
||||
}
|
||||
|
||||
// Infof takes a formatted template string and template arguments for the info logging level.
|
||||
func Infof(format string, args ...interface{}) {
|
||||
Log.Infof(format, args...)
|
||||
}
|
||||
|
||||
// Info logs the given arguments at the info logging level.
|
||||
func Info(args ...interface{}) {
|
||||
Log.Info(args...)
|
||||
}
|
||||
|
||||
// Debugf takes a formatted template string and template arguments for the debug logging level.
|
||||
func Debugf(format string, args ...interface{}) {
|
||||
Log.Debugf(format, args...)
|
||||
}
|
||||
|
||||
// Debug logs the given arguments at the debug logging level.
|
||||
func Debug(args ...interface{}) {
|
||||
Log.Debug(args...)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package log
|
||||
type nopLogger struct{}
|
||||
|
||||
func (l *nopLogger) Errorf(format string, args ...interface{}) {}
|
||||
func (l *nopLogger) Error(args ...interface{}) {}
|
||||
func (l *nopLogger) Warnf(format string, args ...interface{}) {}
|
||||
func (l *nopLogger) Warn(args ...interface{}) {}
|
||||
func (l *nopLogger) Infof(format string, args ...interface{}) {}
|
||||
func (l *nopLogger) Info(args ...interface{}) {}
|
||||
func (l *nopLogger) Debugf(format string, args ...interface{}) {}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package logger contains implementations for the syft.logger.Logger interface.
|
||||
*/
|
||||
package logger
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
prefixed "github.com/x-cray/logrus-prefixed-formatter"
|
||||
)
|
||||
|
||||
// LogrusConfig contains all configurable values for the Logrus logger
|
||||
type LogrusConfig struct {
|
||||
EnableConsole bool
|
||||
EnableFile bool
|
||||
@@ -18,16 +19,19 @@ type LogrusConfig struct {
|
||||
FileLocation string
|
||||
}
|
||||
|
||||
// LogrusLogger contains all runtime values for using Logrus with the configured output target and input configuration values.
|
||||
type LogrusLogger struct {
|
||||
Config LogrusConfig
|
||||
Logger *logrus.Logger
|
||||
Output io.Writer
|
||||
}
|
||||
|
||||
// LogrusNestedLogger is a wrapper for Logrus to enable nested logging configuration (loggers that always attach key-value pairs to all log entries)
|
||||
type LogrusNestedLogger struct {
|
||||
Logger *logrus.Entry
|
||||
}
|
||||
|
||||
// NewLogrusLogger creates a new LogrusLogger with the given configuration
|
||||
func NewLogrusLogger(cfg LogrusConfig) *LogrusLogger {
|
||||
appLogger := logrus.New()
|
||||
|
||||
@@ -76,42 +80,82 @@ func NewLogrusLogger(cfg LogrusConfig) *LogrusLogger {
|
||||
}
|
||||
}
|
||||
|
||||
// Debugf takes a formatted template string and template arguments for the debug logging level.
|
||||
func (l *LogrusLogger) Debugf(format string, args ...interface{}) {
|
||||
l.Logger.Debugf(format, args...)
|
||||
}
|
||||
|
||||
// Infof takes a formatted template string and template arguments for the info logging level.
|
||||
func (l *LogrusLogger) Infof(format string, args ...interface{}) {
|
||||
l.Logger.Infof(format, args...)
|
||||
}
|
||||
|
||||
func (l *LogrusLogger) Debug(args ...interface{}) {
|
||||
l.Logger.Debug(args...)
|
||||
}
|
||||
|
||||
func (l *LogrusLogger) Info(args ...interface{}) {
|
||||
l.Logger.Info(args...)
|
||||
// Warnf takes a formatted template string and template arguments for the warning logging level.
|
||||
func (l *LogrusLogger) Warnf(format string, args ...interface{}) {
|
||||
l.Logger.Warnf(format, args...)
|
||||
}
|
||||
|
||||
// Errorf takes a formatted template string and template arguments for the error logging level.
|
||||
func (l *LogrusLogger) Errorf(format string, args ...interface{}) {
|
||||
l.Logger.Errorf(format, args...)
|
||||
}
|
||||
|
||||
// Debug logs the given arguments at the debug logging level.
|
||||
func (l *LogrusLogger) Debug(args ...interface{}) {
|
||||
l.Logger.Debug(args...)
|
||||
}
|
||||
|
||||
// Info logs the given arguments at the info logging level.
|
||||
func (l *LogrusLogger) Info(args ...interface{}) {
|
||||
l.Logger.Info(args...)
|
||||
}
|
||||
|
||||
// Warn logs the given arguments at the warning logging level.
|
||||
func (l *LogrusLogger) Warn(args ...interface{}) {
|
||||
l.Logger.Warn(args...)
|
||||
}
|
||||
|
||||
// Error logs the given arguments at the error logging level.
|
||||
func (l *LogrusLogger) Error(args ...interface{}) {
|
||||
l.Logger.Error(args...)
|
||||
}
|
||||
|
||||
// Debugf takes a formatted template string and template arguments for the debug logging level.
|
||||
func (l *LogrusNestedLogger) Debugf(format string, args ...interface{}) {
|
||||
l.Logger.Debugf(format, args...)
|
||||
}
|
||||
|
||||
// Infof takes a formatted template string and template arguments for the info logging level.
|
||||
func (l *LogrusNestedLogger) Infof(format string, args ...interface{}) {
|
||||
l.Logger.Infof(format, args...)
|
||||
}
|
||||
|
||||
// Warnf takes a formatted template string and template arguments for the warning logging level.
|
||||
func (l *LogrusNestedLogger) Warnf(format string, args ...interface{}) {
|
||||
l.Logger.Warnf(format, args...)
|
||||
}
|
||||
|
||||
// Errorf takes a formatted template string and template arguments for the error logging level.
|
||||
func (l *LogrusNestedLogger) Errorf(format string, args ...interface{}) {
|
||||
l.Logger.Errorf(format, args...)
|
||||
}
|
||||
|
||||
// Debug logs the given arguments at the debug logging level.
|
||||
func (l *LogrusNestedLogger) Debug(args ...interface{}) {
|
||||
l.Logger.Debug(args...)
|
||||
}
|
||||
|
||||
// Info logs the given arguments at the info logging level.
|
||||
func (l *LogrusNestedLogger) Info(args ...interface{}) {
|
||||
l.Logger.Info(args...)
|
||||
}
|
||||
|
||||
func (l *LogrusNestedLogger) Errorf(format string, args ...interface{}) {
|
||||
l.Logger.Errorf(format, args...)
|
||||
// Warn logs the given arguments at the warning logging level.
|
||||
func (l *LogrusNestedLogger) Warn(args ...interface{}) {
|
||||
l.Logger.Warn(args...)
|
||||
}
|
||||
|
||||
// Error logs the given arguments at the error logging level.
|
||||
func (l *LogrusNestedLogger) Error(args ...interface{}) {
|
||||
l.Logger.Error(args...)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package internal
|
||||
|
||||
import "regexp"
|
||||
|
||||
// MatchCaptureGroups takes a regular expression and string and returns all of the named capture group results in a map.
|
||||
func MatchCaptureGroups(regEx *regexp.Regexp, str string) map[string]string {
|
||||
match := regEx.FindStringSubmatch(str)
|
||||
results := make(map[string]string)
|
||||
for i, name := range regEx.SubexpNames() {
|
||||
if i > 0 && i <= len(match) {
|
||||
results[name] = match[i]
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
package internal
|
||||
|
||||
import "sort"
|
||||
|
||||
// StringSet represents a set of string types.
|
||||
type StringSet map[string]struct{}
|
||||
|
||||
// NewStringSet creates a new empty StringSet.
|
||||
func NewStringSet() StringSet {
|
||||
return make(StringSet)
|
||||
}
|
||||
|
||||
// NewStringSetFromSlice creates a StringSet populated with values from the given slice.
|
||||
func NewStringSetFromSlice(start []string) StringSet {
|
||||
ret := make(StringSet)
|
||||
for _, s := range start {
|
||||
@@ -14,19 +19,23 @@ func NewStringSetFromSlice(start []string) StringSet {
|
||||
return ret
|
||||
}
|
||||
|
||||
// Add a string to the set.
|
||||
func (s StringSet) Add(i string) {
|
||||
s[i] = struct{}{}
|
||||
}
|
||||
|
||||
// Remove a string from the set.
|
||||
func (s StringSet) Remove(i string) {
|
||||
delete(s, i)
|
||||
}
|
||||
|
||||
// Contains indicates if the given string is contained within the set.
|
||||
func (s StringSet) Contains(i string) bool {
|
||||
_, ok := s[i]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ToSlice returns a sorted slice of strings that are contained within the set.
|
||||
func (s StringSet) ToSlice() []string {
|
||||
ret := make([]string, len(s))
|
||||
idx := 0
|
||||
@@ -34,5 +43,6 @@ func (s StringSet) ToSlice() []string {
|
||||
ret[idx] = v
|
||||
idx++
|
||||
}
|
||||
sort.Strings(ret)
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/*
|
||||
Package version contains all build time metadata (version, build time, git commit, etc).
|
||||
*/
|
||||
package version
|
||||
|
||||
import (
|
||||
@@ -16,13 +19,13 @@ var platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
// Version defines the application version details (generally from build information)
|
||||
type Version struct {
|
||||
Version string // application semantic version
|
||||
GitCommit string // git SHA at build-time
|
||||
GitTreeState string // indication of git tree (either "clean" or "dirty") at build-time
|
||||
BuildDate string // date of the build
|
||||
GoVersion string // go runtime version at build-time
|
||||
Compiler string // compiler used at build-time
|
||||
Platform string // GOOS and GOARCH at build-time
|
||||
Version string `json:"version"` // application semantic version
|
||||
GitCommit string `json:"gitCommit"` // git SHA at build-time
|
||||
GitTreeState string `json:"gitTreeState"` // indication of git tree (either "clean" or "dirty") at build-time
|
||||
BuildDate string `json:"buildDate"` // date of the build
|
||||
GoVersion string `json:"goVersion"` // go runtime version at build-time
|
||||
Compiler string `json:"compiler"` // compiler used at build-time
|
||||
Platform string `json:"platform"` // GOOS and GOARCH at build-time
|
||||
}
|
||||
|
||||
// FromBuild provides all version details
|
||||
|
||||
@@ -18,6 +18,7 @@ var latestAppVersionURL = struct {
|
||||
path: fmt.Sprintf("/%s/releases/latest/VERSION", internal.ApplicationName),
|
||||
}
|
||||
|
||||
// IsUpdateAvailable indicates if there is a newer application version available, and if so, what the new version is.
|
||||
func IsUpdateAvailable() (bool, string, error) {
|
||||
currentVersionStr := FromBuild().Version
|
||||
currentVersion, err := hashiVersion.NewVersion(currentVersionStr)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Source this file in your shell sessions in order to use this shell completion.
|
||||
|
||||
complete -W "$(docker images --format '{{.Repository}}:{{.Tag}}')" syft
|
||||
@@ -1,183 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
CycloneDX BOM Descriptor Extension
|
||||
|
||||
Licensed 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.
|
||||
-->
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning"
|
||||
xmlns:bom="http://cyclonedx.org/schema/bom/1.1"
|
||||
xmlns:bd="http://cyclonedx.org/schema/ext/bom-descriptor/1.0"
|
||||
elementFormDefault="qualified"
|
||||
targetNamespace="http://cyclonedx.org/schema/ext/bom-descriptor/1.0"
|
||||
vc:minVersion="1.0"
|
||||
vc:maxVersion="1.1"
|
||||
version="1.0">
|
||||
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
<name>CycloneDX BOM Descriptor Extension</name>
|
||||
<url>https://cyclonedx.org/ext/bom-descriptor</url>
|
||||
<license uri="http://www.apache.org/licenses/LICENSE-2.0"
|
||||
version="2.0">Apache License, Version 2.0</license>
|
||||
<authors>
|
||||
<author>Steve Springett</author>
|
||||
</authors>
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
|
||||
<xs:import namespace="http://cyclonedx.org/schema/bom/1.1" schemaLocation="http://cyclonedx.org/schema/bom/1.1"/>
|
||||
|
||||
<xs:complexType name="metadata">
|
||||
<xs:sequence minOccurs="0" maxOccurs="1">
|
||||
<xs:element name="timestamp" type="xs:dateTime" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The date and time (timestamp) when the document was created.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="tool" minOccurs="0" type="bd:toolType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The tool used to create the BOM.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="authors" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The person(s) who created the BOM. Authors are common in BOMs created through
|
||||
manual processes. BOMs created through automated means may not have authors.</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:complexType>
|
||||
<xs:sequence minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="author" type="bd:organizationalPerson"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="component" type="bom:component" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The component that the BOM describes.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="manufacture" type="bd:organizationalEntity" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The organization that manufactured the component that the BOM describes.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="supplier" type="bd:organizationalEntity" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The organization that supplied the component that the BOM describes. The
|
||||
supplier may often be the manufacture, but may also be a distributor or repackager.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
<xs:anyAttribute namespace="##other" processContents="lax">
|
||||
<xs:annotation>
|
||||
<xs:documentation>User-defined attributes may be used on this element as long as they
|
||||
do not have the same name as an existing attribute used by the schema.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:anyAttribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="organizationalEntity">
|
||||
<xs:sequence minOccurs="0" maxOccurs="1">
|
||||
<xs:element name="name" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The name of the organization</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="url" type="xs:anyURI" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The URL of the organization. Multiple URLs are allowed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="contact" type="bd:organizationalPerson" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A contact person at the organization. Multiple contacts are allowed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
<xs:anyAttribute namespace="##other" processContents="lax">
|
||||
<xs:annotation>
|
||||
<xs:documentation>User-defined attributes may be used on this element as long as they
|
||||
do not have the same name as an existing attribute used by the schema.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:anyAttribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="toolType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Specifies a tool (manual or automated).</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:sequence minOccurs="0" maxOccurs="1">
|
||||
<xs:element name="vendor" minOccurs="0" maxOccurs="1" type="xs:normalizedString">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The vendor of the tool used to create the BOM.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="name" minOccurs="0" maxOccurs="1" type="xs:normalizedString">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The name of the tool used to create the BOM.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="version" minOccurs="0" maxOccurs="1" type="xs:normalizedString">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The version of the tool used to create the BOM.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="hashes" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:sequence minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="hash" type="bom:hashType"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
<xs:anyAttribute namespace="##other" processContents="lax">
|
||||
<xs:annotation>
|
||||
<xs:documentation>User-defined attributes may be used on this element as long as they
|
||||
do not have the same name as an existing attribute used by the schema.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:anyAttribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="organizationalPerson">
|
||||
<xs:sequence minOccurs="0" maxOccurs="1">
|
||||
<xs:element name="name" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The name of the person</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="email" type="xs:normalizedString" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The email address of the person. Multiple email addresses are allowed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="phone" type="xs:normalizedString" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The phone number of the person. Multiple phone numbers are allowed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
<xs:anyAttribute namespace="##other" processContents="lax">
|
||||
<xs:annotation>
|
||||
<xs:documentation>User-defined attributes may be used on this element as long as they
|
||||
do not have the same name as an existing attribute used by the schema.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:anyAttribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:element name="metadata" type="bd:metadata">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Provides additional information about a BOM.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
|
||||
</xs:schema>
|
||||
@@ -1 +0,0 @@
|
||||
examples/
|
||||
@@ -0,0 +1,39 @@
|
||||
# JSON Schema
|
||||
|
||||
This is the JSON schema for output from the JSON presenter (`syft <img> -o json`). The required inputs for defining the JSON schema are as follows:
|
||||
|
||||
- the value of `internal.JSONSchemaVersion` that governs the schema filename
|
||||
- the `Document` struct definition within `syft/presenters/json/document.go` that governs the overall document shape
|
||||
- the `metadataContainer` struct definition within `schema/json/generate.go` that governs the allowable shapes of `pkg.Package.Metadata`
|
||||
|
||||
With regard to testing the JSON schema, integration test cases provided by the developer are used as examples to validate that JSON output from Syft is always valid relative to the `schema/json/schema-$VERSION.json` file.
|
||||
|
||||
## Versioning
|
||||
|
||||
Versioning the JSON schema must be done manually by changing the `JSONSchemaVersion` constant within `internal/constants.go`.
|
||||
|
||||
This schema is being versioned based off of the "SchemaVer" guidelines, which slightly diverges from Semantic Versioning to tailor for the purposes of data models.
|
||||
|
||||
Given a version number format `MODEL.REVISION.ADDITION`:
|
||||
|
||||
- `MODEL`: increment when you make a breaking schema change which will prevent interaction with any historical data
|
||||
- `REVISION`: increment when you make a schema change which may prevent interaction with some historical data
|
||||
- `ADDITION`: increment when you make a schema change that is compatible with all historical data
|
||||
|
||||
## Adding a New `pkg.*Metadata` Type
|
||||
|
||||
When adding a new `pkg.*Metadata` that is assigned to the `pkg.Package.Metadata` struct field it is important that a few things
|
||||
are done:
|
||||
|
||||
- a new integration test case is added to `test/integration/pkg_cases_test.go` that exercises the new package type with the new metadata
|
||||
- the new metadata struct is added to the `metadataContainer` struct within `schema/json/generate.go`
|
||||
|
||||
## Generating a New Schema
|
||||
|
||||
Create the new schema by running `cd schema/json && go run generate.go` (note you must be in the `schema/json` dir while running this):
|
||||
|
||||
- If there is **not** an existing schema for the given version, then the new schema file will be written to `schema/json/schema-$VERSION.json`
|
||||
- If there is an existing schema for the given version and the new schema matches the existing schema, no action is taken
|
||||
- If there is an existing schema for the given version and the new schema **does not** match the existing schema, an error is shown indicating to increment the version appropriately (see the "Versioning" section)
|
||||
|
||||
***Note: never delete a JSON schema and never change an existing JSON schema once it has been published in a release!*** Only add new schemas with a newly incremented version. All previous schema files must be stored in the `schema/json/` directory.
|
||||
@@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/jsonschema"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
jsonPresenter "github.com/anchore/syft/syft/presenter/json"
|
||||
)
|
||||
|
||||
/*
|
||||
This method of creating the JSON schema only captures strongly typed fields for the purpose of integrations between syft
|
||||
JSON output and integrations. The downside to this approach is that any values and types used on weakly typed fields
|
||||
are not captured (empty interfaces). This means that pkg.Package.Metadata is not validated at this time. This approach
|
||||
can be extended to include specific package metadata struct shapes in the future.
|
||||
*/
|
||||
|
||||
// This should represent all possible metadatas represented in the pkg.Package.Metadata field (an interface{}).
|
||||
// When a new package metadata definition is created it will need to be manually added here. The variable name does
|
||||
// not matter as long as it is exported.
|
||||
type metadataContainer struct {
|
||||
Apk pkg.ApkMetadata
|
||||
Dpkg pkg.DpkgMetadata
|
||||
Gem pkg.GemMetadata
|
||||
Java pkg.JavaMetadata
|
||||
Npm pkg.NpmPackageJSONMetadata
|
||||
Python pkg.PythonPackageMetadata
|
||||
Rpm pkg.RpmdbMetadata
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func main() {
|
||||
metadataSchema := jsonschema.Reflect(&metadataContainer{})
|
||||
documentSchema := jsonschema.Reflect(&jsonPresenter.Document{})
|
||||
|
||||
// TODO: inject source definitions
|
||||
|
||||
// inject the definitions of all metadatas into the schema definitions
|
||||
|
||||
var metadataNames []string
|
||||
for name, definition := range metadataSchema.Definitions {
|
||||
if name == "metadataContainer" {
|
||||
// ignore the definition for the fake container
|
||||
continue
|
||||
}
|
||||
documentSchema.Definitions[name] = definition
|
||||
if strings.HasSuffix(name, "Metadata") {
|
||||
metadataNames = append(metadataNames, name)
|
||||
}
|
||||
}
|
||||
|
||||
// ensure the generated list of names is stable between runs
|
||||
sort.Strings(metadataNames)
|
||||
|
||||
var metadataTypes = []map[string]string{
|
||||
// allow for no metadata to be provided
|
||||
{"type": "null"},
|
||||
}
|
||||
for _, name := range metadataNames {
|
||||
metadataTypes = append(metadataTypes, map[string]string{
|
||||
"$ref": fmt.Sprintf("#/definitions/%s", name),
|
||||
})
|
||||
}
|
||||
|
||||
// set the "anyOf" field for Package.Metadata to be a conjunction of several types
|
||||
documentSchema.Definitions["Package"].Properties.Set("metadata", map[string][]map[string]string{
|
||||
"anyOf": metadataTypes,
|
||||
})
|
||||
|
||||
filename := fmt.Sprintf("schema-%s.json", internal.JSONSchemaVersion)
|
||||
|
||||
var newSchemaBuffer = new(bytes.Buffer)
|
||||
enc := json.NewEncoder(newSchemaBuffer)
|
||||
// prevent > and < from being escaped in the payload
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
err := enc.Encode(&documentSchema)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filename); !os.IsNotExist(err) {
|
||||
// check if the schema is the same...
|
||||
existingFh, err := os.Open(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
existingSchemaBytes, err := ioutil.ReadAll(existingFh)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if bytes.Equal(existingSchemaBytes, newSchemaBuffer.Bytes()) {
|
||||
// the generated schema is the same, bail with no error :)
|
||||
fmt.Println("No change to the existing schema!")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// the generated schema is different, bail with error :(
|
||||
fmt.Printf("Cowardly refusing to overwrite existing schema (%s)!\nSee the scheam/json/README.md for how to increment\n", filename)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fh, err := os.Create(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = fh.Write(newSchemaBuffer.Bytes())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer fh.Close()
|
||||
|
||||
fmt.Printf("wrote new schema to %q\n", filename)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/env/bin python3
|
||||
import os
|
||||
import glob
|
||||
import json
|
||||
|
||||
from genson import SchemaBuilder
|
||||
|
||||
EXAMPLES_DIR = "examples/"
|
||||
OUTPUT = "schema.json"
|
||||
|
||||
|
||||
def main():
|
||||
builder = SchemaBuilder()
|
||||
|
||||
print("Generating new Syft json schema...")
|
||||
for filepath in glob.glob(os.path.join(EXAMPLES_DIR, '*.json')):
|
||||
with open(filepath, 'r') as f:
|
||||
print(f" adding {filepath}")
|
||||
builder.add_object(json.loads(f.read()))
|
||||
|
||||
print("Building schema...")
|
||||
new_schema = builder.to_schema()
|
||||
with open(OUTPUT, 'w') as f:
|
||||
f.write(json.dumps(new_schema, sort_keys=True, indent=4))
|
||||
|
||||
print(f"New schema written to '{OUTPUT}'")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1 +0,0 @@
|
||||
genson
|
||||
@@ -0,0 +1,678 @@
|
||||
{
|
||||
"$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"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"DpkgFileRecord": {
|
||||
"required": [
|
||||
"path",
|
||||
"md5"
|
||||
],
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"md5": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"DpkgMetadata": {
|
||||
"required": [
|
||||
"package",
|
||||
"source",
|
||||
"version",
|
||||
"architecture",
|
||||
"maintainer",
|
||||
"installedSize",
|
||||
"files"
|
||||
],
|
||||
"properties": {
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"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": [
|
||||
"name",
|
||||
"version",
|
||||
"type",
|
||||
"foundBy",
|
||||
"locations",
|
||||
"licenses",
|
||||
"language",
|
||||
"cpes",
|
||||
"purl",
|
||||
"metadataType",
|
||||
"metadata"
|
||||
],
|
||||
"properties": {
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,534 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/schema#",
|
||||
"properties": {
|
||||
"artifacts": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"found-by": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"locations": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"layer-index": {
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"layer-index",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"metadata": {
|
||||
"properties": {
|
||||
"architecture": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"epoch": {
|
||||
"type": "integer"
|
||||
},
|
||||
"files": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"checksum": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner-gid": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner-uid": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"checksum",
|
||||
"owner-gid",
|
||||
"owner-uid",
|
||||
"path",
|
||||
"permissions"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"git-commit-of-apk-port": {
|
||||
"type": "string"
|
||||
},
|
||||
"installed-size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"license": {
|
||||
"type": "string"
|
||||
},
|
||||
"maintainer": {
|
||||
"type": "string"
|
||||
},
|
||||
"manifest": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"extra-fields": {
|
||||
"properties": {
|
||||
"Archiver-Version": {
|
||||
"type": "string"
|
||||
},
|
||||
"Build-Jdk": {
|
||||
"type": "string"
|
||||
},
|
||||
"Built-By": {
|
||||
"type": "string"
|
||||
},
|
||||
"Created-By": {
|
||||
"type": "string"
|
||||
},
|
||||
"Extension-Name": {
|
||||
"type": "string"
|
||||
},
|
||||
"Group-Id": {
|
||||
"type": "string"
|
||||
},
|
||||
"Hudson-Version": {
|
||||
"type": "string"
|
||||
},
|
||||
"Jenkins-Version": {
|
||||
"type": "string"
|
||||
},
|
||||
"Long-Name": {
|
||||
"type": "string"
|
||||
},
|
||||
"Main-Class": {
|
||||
"type": "string"
|
||||
},
|
||||
"Minimum-Java-Version": {
|
||||
"type": "string"
|
||||
},
|
||||
"Plugin-Dependencies": {
|
||||
"type": "string"
|
||||
},
|
||||
"Plugin-Developers": {
|
||||
"type": "string"
|
||||
},
|
||||
"Plugin-License-Name": {
|
||||
"type": "string"
|
||||
},
|
||||
"Plugin-License-Url": {
|
||||
"type": "string"
|
||||
},
|
||||
"Plugin-ScmUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"Plugin-Version": {
|
||||
"type": "string"
|
||||
},
|
||||
"Short-Name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Archiver-Version",
|
||||
"Build-Jdk",
|
||||
"Built-By",
|
||||
"Created-By"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"implementation-title": {
|
||||
"type": "string"
|
||||
},
|
||||
"implementation-vendor": {
|
||||
"type": "string"
|
||||
},
|
||||
"implementation-version": {
|
||||
"type": "string"
|
||||
},
|
||||
"manifest-version": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"specification-title": {
|
||||
"type": "string"
|
||||
},
|
||||
"specification-vendor": {
|
||||
"type": "string"
|
||||
},
|
||||
"specification-version": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"extra-fields",
|
||||
"implementation-title",
|
||||
"implementation-vendor",
|
||||
"implementation-version",
|
||||
"manifest-version",
|
||||
"name",
|
||||
"specification-title",
|
||||
"specification-vendor",
|
||||
"specification-version"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"origin-package": {
|
||||
"type": "string"
|
||||
},
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent-package": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"found-by": {
|
||||
"type": "string"
|
||||
},
|
||||
"language": {
|
||||
"type": "integer"
|
||||
},
|
||||
"licenses": {
|
||||
"type": "null"
|
||||
},
|
||||
"manifest": {
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"properties": {
|
||||
"manifest": {
|
||||
"properties": {
|
||||
"extra-fields": {
|
||||
"properties": {
|
||||
"Archiver-Version": {
|
||||
"type": "string"
|
||||
},
|
||||
"Build-Jdk": {
|
||||
"type": "string"
|
||||
},
|
||||
"Built-By": {
|
||||
"type": "string"
|
||||
},
|
||||
"Created-By": {
|
||||
"type": "string"
|
||||
},
|
||||
"Extension-Name": {
|
||||
"type": "string"
|
||||
},
|
||||
"Group-Id": {
|
||||
"type": "string"
|
||||
},
|
||||
"Hudson-Version": {
|
||||
"type": "string"
|
||||
},
|
||||
"Jenkins-Version": {
|
||||
"type": "string"
|
||||
},
|
||||
"Long-Name": {
|
||||
"type": "string"
|
||||
},
|
||||
"Main-Class": {
|
||||
"type": "string"
|
||||
},
|
||||
"Minimum-Java-Version": {
|
||||
"type": "string"
|
||||
},
|
||||
"Plugin-Dependencies": {
|
||||
"type": "string"
|
||||
},
|
||||
"Plugin-Developers": {
|
||||
"type": "string"
|
||||
},
|
||||
"Plugin-License-Name": {
|
||||
"type": "string"
|
||||
},
|
||||
"Plugin-License-Url": {
|
||||
"type": "string"
|
||||
},
|
||||
"Plugin-ScmUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"Plugin-Version": {
|
||||
"type": "string"
|
||||
},
|
||||
"Short-Name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Archiver-Version",
|
||||
"Build-Jdk",
|
||||
"Built-By",
|
||||
"Created-By"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"implementation-title": {
|
||||
"type": "string"
|
||||
},
|
||||
"implementation-vendor": {
|
||||
"type": "string"
|
||||
},
|
||||
"implementation-version": {
|
||||
"type": "string"
|
||||
},
|
||||
"manifest-version": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"specification-title": {
|
||||
"type": "string"
|
||||
},
|
||||
"specification-vendor": {
|
||||
"type": "string"
|
||||
},
|
||||
"specification-version": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"extra-fields",
|
||||
"implementation-title",
|
||||
"implementation-vendor",
|
||||
"implementation-version",
|
||||
"manifest-version",
|
||||
"name",
|
||||
"specification-title",
|
||||
"specification-vendor",
|
||||
"specification-version"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"parent-package": {
|
||||
"type": "null"
|
||||
},
|
||||
"pom-properties": {
|
||||
"properties": {
|
||||
"Path": {
|
||||
"type": "string"
|
||||
},
|
||||
"artifact-id": {
|
||||
"type": "string"
|
||||
},
|
||||
"extra-fields": {
|
||||
"type": "null"
|
||||
},
|
||||
"group-id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Path",
|
||||
"artifact-id",
|
||||
"extra-fields",
|
||||
"group-id",
|
||||
"name",
|
||||
"version"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"manifest",
|
||||
"parent-package",
|
||||
"pom-properties"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"sources": {
|
||||
"type": "null"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"found-by",
|
||||
"language",
|
||||
"licenses",
|
||||
"manifest",
|
||||
"metadata",
|
||||
"sources",
|
||||
"type",
|
||||
"version"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pom-properties": {
|
||||
"properties": {
|
||||
"Path": {
|
||||
"type": "string"
|
||||
},
|
||||
"artifact-id": {
|
||||
"type": "string"
|
||||
},
|
||||
"extra-fields": {
|
||||
"type": "null"
|
||||
},
|
||||
"group-id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Path",
|
||||
"artifact-id",
|
||||
"extra-fields",
|
||||
"group-id",
|
||||
"name",
|
||||
"version"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"pull-checksum": {
|
||||
"type": "string"
|
||||
},
|
||||
"pull-dependencies": {
|
||||
"type": "string"
|
||||
},
|
||||
"release": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"found-by",
|
||||
"locations",
|
||||
"name",
|
||||
"type",
|
||||
"version"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string"
|
||||
},
|
||||
"image": {
|
||||
"properties": {
|
||||
"digest": {
|
||||
"type": "string"
|
||||
},
|
||||
"layers": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"digest": {
|
||||
"type": "string"
|
||||
},
|
||||
"media-type": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"digest",
|
||||
"media-type",
|
||||
"size"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"media-type": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"digest",
|
||||
"layers",
|
||||
"media-type",
|
||||
"size",
|
||||
"tags"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"artifacts"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
@@ -4,39 +4,14 @@ Package apkdb provides a concrete Cataloger implementation for Alpine DB files.
|
||||
package apkdb
|
||||
|
||||
import (
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/syft/cataloger/common"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
)
|
||||
|
||||
// Cataloger catalogs pkg.ApkPkg Package Types defined in Alpine DB files.
|
||||
type Cataloger struct {
|
||||
cataloger common.GenericCataloger
|
||||
}
|
||||
|
||||
// New returns a new Alpine DB cataloger object.
|
||||
func New() *Cataloger {
|
||||
// NewApkdbCataloger returns a new Alpine DB cataloger object.
|
||||
func NewApkdbCataloger() *common.GenericCataloger {
|
||||
globParsers := map[string]common.ParserFn{
|
||||
"**/lib/apk/db/installed": parseApkDB,
|
||||
}
|
||||
|
||||
return &Cataloger{
|
||||
cataloger: common.NewGenericCataloger(nil, globParsers),
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns a string that uniquely describes this cataloger.
|
||||
func (a *Cataloger) Name() string {
|
||||
return "apkdb-cataloger"
|
||||
}
|
||||
|
||||
// SelectFiles returns a set of discovered Alpine DB files from the user content source.
|
||||
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
||||
return a.cataloger.SelectFiles(resolver)
|
||||
}
|
||||
|
||||
// Catalog returns the Packages indexed from all Alpine DB files discovered.
|
||||
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
||||
return a.cataloger.Catalog(contents, a.Name())
|
||||
return common.NewGenericCataloger(nil, globParsers, "apkdb-cataloger")
|
||||
}
|
||||
|
||||
@@ -20,9 +20,14 @@ var _ common.ParserFn = parseApkDB
|
||||
// parseApkDb parses individual packages from a given Alpine DB file. For more information on specific fields
|
||||
// see https://wiki.alpinelinux.org/wiki/Apk_spec .
|
||||
func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||
// larger capacity for the scanner.
|
||||
const maxScannerCapacity = 1024 * 1024
|
||||
// a new larger buffer for the scanner
|
||||
bufScan := make([]byte, maxScannerCapacity)
|
||||
packages := make([]pkg.Package, 0)
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Buffer(bufScan, maxScannerCapacity)
|
||||
onDoubleLF := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
for i := 0; i < len(data); i++ {
|
||||
if i > 0 && data[i-1] == '\n' && data[i] == '\n' {
|
||||
@@ -44,11 +49,12 @@ func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||
}
|
||||
if metadata != nil {
|
||||
packages = append(packages, pkg.Package{
|
||||
Name: metadata.Package,
|
||||
Version: metadata.Version,
|
||||
Licenses: strings.Split(metadata.License, " "),
|
||||
Type: pkg.ApkPkg,
|
||||
Metadata: *metadata,
|
||||
Name: metadata.Package,
|
||||
Version: metadata.Version,
|
||||
Licenses: strings.Split(metadata.License, " "),
|
||||
Type: pkg.ApkPkg,
|
||||
MetadataType: pkg.ApkMetadataType,
|
||||
Metadata: *metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -83,7 +89,16 @@ func parseApkDBEntry(reader io.Reader) (*pkg.ApkMetadata, error) {
|
||||
|
||||
switch key {
|
||||
case "F":
|
||||
lastFile = "/" + value
|
||||
currentFile := "/" + value
|
||||
|
||||
newFileRecord := pkg.ApkFileRecord{
|
||||
Path: currentFile,
|
||||
}
|
||||
files = append(files, newFileRecord)
|
||||
fileRecord = &files[len(files)-1]
|
||||
|
||||
// future aux references are relative to previous "F" records
|
||||
lastFile = currentFile
|
||||
continue
|
||||
case "R":
|
||||
newFileRecord := pkg.ApkFileRecord{
|
||||
@@ -91,22 +106,24 @@ func parseApkDBEntry(reader io.Reader) (*pkg.ApkMetadata, error) {
|
||||
}
|
||||
files = append(files, newFileRecord)
|
||||
fileRecord = &files[len(files)-1]
|
||||
case "a":
|
||||
case "a", "M":
|
||||
ownershipFields := strings.Split(value, ":")
|
||||
if len(ownershipFields) != 3 {
|
||||
log.Errorf("unexpected APK ownership field: %q", value)
|
||||
if len(ownershipFields) < 3 {
|
||||
log.Warnf("unexpected APK ownership field: %q", value)
|
||||
continue
|
||||
}
|
||||
if fileRecord == nil {
|
||||
log.Errorf("ownership field with no parent record: %q", value)
|
||||
log.Warnf("ownership field with no parent record: %q", value)
|
||||
continue
|
||||
}
|
||||
fileRecord.OwnerUID = ownershipFields[0]
|
||||
fileRecord.OwnerGUI = ownershipFields[1]
|
||||
fileRecord.OwnerGID = ownershipFields[1]
|
||||
fileRecord.Permissions = ownershipFields[2]
|
||||
// note: there are more optional fields available that we are not capturing, e.g.:
|
||||
// "0:0:755:Q1JaDEHQHBbizhEzoWK1YxuraNU/4="
|
||||
case "Z":
|
||||
if fileRecord == nil {
|
||||
log.Errorf("checksum field with no parent record: %q", value)
|
||||
log.Warnf("checksum field with no parent record: %q", value)
|
||||
continue
|
||||
}
|
||||
fileRecord.Checksum = value
|
||||
|
||||
@@ -10,13 +10,78 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func TestSinglePackage(t *testing.T) {
|
||||
func TestExtraFileAttributes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expected pkg.ApkMetadata
|
||||
}{
|
||||
{
|
||||
name: "Test Single Package",
|
||||
name: "test extra file attributes (checksum) are ignored",
|
||||
expected: pkg.ApkMetadata{
|
||||
Files: []pkg.ApkFileRecord{
|
||||
{
|
||||
Path: "/usr",
|
||||
},
|
||||
{
|
||||
Path: "/usr/lib",
|
||||
},
|
||||
{
|
||||
Path: "/usr/lib/jvm",
|
||||
},
|
||||
{
|
||||
Path: "/usr/lib/jvm/java-1.8-openjdk",
|
||||
},
|
||||
{
|
||||
Path: "/usr/lib/jvm/java-1.8-openjdk/bin",
|
||||
},
|
||||
{
|
||||
Path: "/usr/lib/jvm/java-1.8-openjdk/bin/policytool",
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Checksum: "Q1M0C9qfC/+kdRiOodeihG2GMRtkE=",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
file, err := os.Open("test-fixtures/extra-file-attributes")
|
||||
if err != nil {
|
||||
t.Fatal("Unable to read test-fixtures/extra-file-attributes: ", err)
|
||||
}
|
||||
defer func() {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
t.Fatal("closing file failed:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
|
||||
entry, err := parseApkDBEntry(reader)
|
||||
if err != nil {
|
||||
t.Fatal("Unable to read file contents: ", err)
|
||||
}
|
||||
|
||||
if diff := deep.Equal(entry.Files, test.expected.Files); diff != nil {
|
||||
for _, d := range diff {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSinglePackageDetails(t *testing.T) {
|
||||
tests := []struct {
|
||||
fixture string
|
||||
expected pkg.ApkMetadata
|
||||
}{
|
||||
{
|
||||
fixture: "test-fixtures/single",
|
||||
expected: pkg.ApkMetadata{
|
||||
Package: "musl-utils",
|
||||
OriginPackage: "musl",
|
||||
@@ -32,51 +97,428 @@ func TestSinglePackage(t *testing.T) {
|
||||
PullChecksum: "Q1bTtF5526tETKfL+lnigzIDvm+2o=",
|
||||
GitCommitOfAport: "4024cc3b29ad4c65544ad068b8f59172b5494306",
|
||||
Files: []pkg.ApkFileRecord{
|
||||
{
|
||||
Path: "/sbin",
|
||||
},
|
||||
{
|
||||
Path: "/sbin/ldconfig",
|
||||
OwnerUID: "0",
|
||||
OwnerGUI: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Checksum: "Q1Kja2+POZKxEkUOZqwSjC6kmaED4=",
|
||||
},
|
||||
{
|
||||
Path: "/usr",
|
||||
},
|
||||
{
|
||||
Path: "/usr/bin",
|
||||
},
|
||||
{
|
||||
Path: "/usr/bin/iconv",
|
||||
OwnerUID: "0",
|
||||
OwnerGUI: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Checksum: "Q1CVmFbdY+Hv6/jAHl1gec2Kbx1EY=",
|
||||
},
|
||||
{
|
||||
Path: "/usr/bin/ldd",
|
||||
OwnerUID: "0",
|
||||
OwnerGUI: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Checksum: "Q1yFAhGggmL7ERgbIA7KQxyTzf3ks=",
|
||||
},
|
||||
{
|
||||
Path: "/usr/bin/getconf",
|
||||
OwnerUID: "0",
|
||||
OwnerGUI: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Checksum: "Q1dAdYK8M/INibRQF5B3Rw7cmNDDA=",
|
||||
},
|
||||
{
|
||||
Path: "/usr/bin/getent",
|
||||
OwnerUID: "0",
|
||||
OwnerGUI: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Checksum: "Q1eR2Dz/WylabgbWMTkd2+hGmEya4=",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/base",
|
||||
expected: pkg.ApkMetadata{
|
||||
Package: "alpine-baselayout",
|
||||
OriginPackage: "alpine-baselayout",
|
||||
Version: "3.2.0-r6",
|
||||
Description: "Alpine base dir structure and init scripts",
|
||||
Maintainer: "Natanael Copa <ncopa@alpinelinux.org>",
|
||||
License: "GPL-2.0-only",
|
||||
Architecture: "x86_64",
|
||||
URL: "https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout",
|
||||
Size: 19917,
|
||||
InstalledSize: 409600,
|
||||
PullDependencies: "/bin/sh so:libc.musl-x86_64.so.1",
|
||||
PullChecksum: "Q1myMNfd7u5v5UTgNHeq1e31qTjZU=",
|
||||
GitCommitOfAport: "e1c51734fa96fa4bac92e9f14a474324c67916fc",
|
||||
Files: []pkg.ApkFileRecord{
|
||||
{
|
||||
Path: "/dev",
|
||||
},
|
||||
{
|
||||
Path: "/dev/pts",
|
||||
},
|
||||
{
|
||||
Path: "/dev/shm",
|
||||
},
|
||||
{
|
||||
Path: "/etc",
|
||||
},
|
||||
{
|
||||
Path: "/etc/fstab",
|
||||
Checksum: "Q11Q7hNe8QpDS531guqCdrXBzoA/o=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/group",
|
||||
Checksum: "Q1oJ16xWudgKOrXIEquEDzlF2Lsm4=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/hostname",
|
||||
Checksum: "Q16nVwYVXP/tChvUPdukVD2ifXOmc=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/hosts",
|
||||
Checksum: "Q1BD6zJKZTRWyqGnPi4tSfd3krsMU=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/inittab",
|
||||
Checksum: "Q1TsthbhW7QzWRe1E/NKwTOuD4pHc=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/modules",
|
||||
Checksum: "Q1toogjUipHGcMgECgPJX64SwUT1M=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/motd",
|
||||
Checksum: "Q1XmduVVNURHQ27TvYp1Lr5TMtFcA=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/mtab",
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "777",
|
||||
Checksum: "Q1kiljhXXH1LlQroHsEJIkPZg2eiw=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/passwd",
|
||||
Checksum: "Q1TchuuLUfur0izvfZQZxgN/LJhB8=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/profile",
|
||||
Checksum: "Q1KpFb8kl5LvwXWlY3e58FNsjrI34=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/protocols",
|
||||
Checksum: "Q13FqXUnvuOpMDrH/6rehxuYAEE34=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/services",
|
||||
Checksum: "Q1C6HJNgQvLWqt5VY+n7MZJ1rsDuY=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/shadow",
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "42",
|
||||
Permissions: "640",
|
||||
Checksum: "Q1ltrPIAW2zHeDiajsex2Bdmq3uqA=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/shells",
|
||||
Checksum: "Q1ojm2YdpCJ6B/apGDaZ/Sdb2xJkA=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/sysctl.conf",
|
||||
Checksum: "Q14upz3tfnNxZkIEsUhWn7Xoiw96g=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/apk",
|
||||
},
|
||||
{
|
||||
Path: "/etc/conf.d",
|
||||
},
|
||||
{
|
||||
Path: "/etc/crontabs",
|
||||
},
|
||||
{
|
||||
Path: "/etc/crontabs/root",
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "600",
|
||||
Checksum: "Q1vfk1apUWI4yLJGhhNRd0kJixfvY=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/init.d",
|
||||
},
|
||||
{
|
||||
Path: "/etc/modprobe.d",
|
||||
},
|
||||
{
|
||||
Path: "/etc/modprobe.d/aliases.conf",
|
||||
Checksum: "Q1WUbh6TBYNVK7e4Y+uUvLs/7viqk=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/modprobe.d/blacklist.conf",
|
||||
Checksum: "Q1xxYGU6S6TLQvb7ervPrWWwAWqMg=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/modprobe.d/i386.conf",
|
||||
Checksum: "Q1pnay/njn6ol9cCssL7KiZZ8etlc=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/modprobe.d/kms.conf",
|
||||
Checksum: "Q1ynbLn3GYDpvajba/ldp1niayeog=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/modules-load.d",
|
||||
},
|
||||
{
|
||||
Path: "/etc/network",
|
||||
},
|
||||
{
|
||||
Path: "/etc/network/if-down.d",
|
||||
},
|
||||
{
|
||||
Path: "/etc/network/if-post-down.d",
|
||||
},
|
||||
{
|
||||
Path: "/etc/network/if-pre-up.d",
|
||||
},
|
||||
{
|
||||
Path: "/etc/network/if-up.d",
|
||||
},
|
||||
{
|
||||
Path: "/etc/opt",
|
||||
},
|
||||
{
|
||||
Path: "/etc/periodic",
|
||||
},
|
||||
{
|
||||
Path: "/etc/periodic/15min",
|
||||
},
|
||||
{
|
||||
Path: "/etc/periodic/daily",
|
||||
},
|
||||
{
|
||||
Path: "/etc/periodic/hourly",
|
||||
},
|
||||
{
|
||||
Path: "/etc/periodic/monthly",
|
||||
},
|
||||
{
|
||||
Path: "/etc/periodic/weekly",
|
||||
},
|
||||
{
|
||||
Path: "/etc/profile.d",
|
||||
},
|
||||
{
|
||||
Path: "/etc/profile.d/color_prompt",
|
||||
Checksum: "Q10wL23GuSCVfumMRgakabUI6EsSk=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/profile.d/locale",
|
||||
Checksum: "Q1R4bIEpnKxxOSrlnZy9AoawqZ5DU=",
|
||||
},
|
||||
{
|
||||
Path: "/etc/sysctl.d",
|
||||
},
|
||||
{
|
||||
Path: "/home",
|
||||
},
|
||||
{
|
||||
Path: "/lib",
|
||||
},
|
||||
{
|
||||
Path: "/lib/firmware",
|
||||
},
|
||||
{
|
||||
Path: "/lib/mdev",
|
||||
},
|
||||
{
|
||||
Path: "/lib/modules-load.d",
|
||||
},
|
||||
{
|
||||
Path: "/lib/sysctl.d",
|
||||
},
|
||||
{
|
||||
Path: "/lib/sysctl.d/00-alpine.conf",
|
||||
Checksum: "Q1HpElzW1xEgmKfERtTy7oommnq6c=",
|
||||
},
|
||||
{
|
||||
Path: "/media",
|
||||
},
|
||||
{
|
||||
Path: "/media/cdrom",
|
||||
},
|
||||
{
|
||||
Path: "/media/floppy",
|
||||
},
|
||||
{
|
||||
Path: "/media/usb",
|
||||
},
|
||||
{
|
||||
Path: "/mnt",
|
||||
},
|
||||
{
|
||||
Path: "/opt",
|
||||
},
|
||||
{
|
||||
Path: "/proc",
|
||||
},
|
||||
{
|
||||
Path: "/root",
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "700",
|
||||
},
|
||||
{
|
||||
Path: "/run",
|
||||
},
|
||||
{
|
||||
Path: "/sbin",
|
||||
},
|
||||
{
|
||||
Path: "/sbin/mkmntdirs",
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Checksum: "Q1YeuSmC7iDbEWrusPzA/zUQF6YSg=",
|
||||
},
|
||||
{
|
||||
Path: "/srv",
|
||||
},
|
||||
{
|
||||
Path: "/sys",
|
||||
},
|
||||
{
|
||||
Path: "/tmp",
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "1777",
|
||||
},
|
||||
{
|
||||
Path: "/usr",
|
||||
},
|
||||
{
|
||||
Path: "/usr/lib",
|
||||
},
|
||||
{
|
||||
Path: "/usr/lib/modules-load.d",
|
||||
},
|
||||
{
|
||||
Path: "/usr/local",
|
||||
},
|
||||
{
|
||||
Path: "/usr/local/bin",
|
||||
},
|
||||
{
|
||||
Path: "/usr/local/lib",
|
||||
},
|
||||
{
|
||||
Path: "/usr/local/share",
|
||||
},
|
||||
{
|
||||
Path: "/usr/sbin",
|
||||
},
|
||||
{
|
||||
Path: "/usr/share",
|
||||
},
|
||||
{
|
||||
Path: "/usr/share/man",
|
||||
},
|
||||
{
|
||||
Path: "/usr/share/misc",
|
||||
},
|
||||
{
|
||||
Path: "/var",
|
||||
},
|
||||
{
|
||||
Path: "/var/run",
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "777",
|
||||
Checksum: "Q11/SNZz/8cK2dSKK+cJpVrZIuF4Q=",
|
||||
},
|
||||
{
|
||||
Path: "/var/cache",
|
||||
},
|
||||
{
|
||||
Path: "/var/cache/misc",
|
||||
},
|
||||
{
|
||||
Path: "/var/empty",
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "555",
|
||||
},
|
||||
{
|
||||
Path: "/var/lib",
|
||||
},
|
||||
{
|
||||
Path: "/var/lib/misc",
|
||||
},
|
||||
{
|
||||
Path: "/var/local",
|
||||
},
|
||||
{
|
||||
Path: "/var/lock",
|
||||
},
|
||||
{
|
||||
Path: "/var/lock/subsys",
|
||||
},
|
||||
{
|
||||
Path: "/var/log",
|
||||
},
|
||||
{
|
||||
Path: "/var/mail",
|
||||
},
|
||||
{
|
||||
Path: "/var/opt",
|
||||
},
|
||||
{
|
||||
Path: "/var/spool",
|
||||
},
|
||||
{
|
||||
Path: "/var/spool/mail",
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "777",
|
||||
Checksum: "Q1dzbdazYZA2nTzSIG3YyNw7d4Juc=",
|
||||
},
|
||||
{
|
||||
Path: "/var/spool/cron",
|
||||
},
|
||||
{
|
||||
Path: "/var/spool/cron/crontabs",
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "777",
|
||||
Checksum: "Q1OFZt+ZMp7j0Gny0rqSKuWJyqYmA=",
|
||||
},
|
||||
{
|
||||
Path: "/var/tmp",
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "1777",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
file, err := os.Open("test-fixtures/single")
|
||||
t.Run(test.fixture, func(t *testing.T) {
|
||||
file, err := os.Open(test.fixture)
|
||||
if err != nil {
|
||||
t.Fatal("Unable to read test_fixtures/single: ", err)
|
||||
t.Fatal("Unable to read fixture: ", err)
|
||||
}
|
||||
defer func() {
|
||||
err := file.Close()
|
||||
@@ -110,10 +552,11 @@ func TestMultiplePackages(t *testing.T) {
|
||||
fixture: "test-fixtures/multiple",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "libc-utils",
|
||||
Version: "0.7.2-r0",
|
||||
Licenses: []string{"BSD"},
|
||||
Type: pkg.ApkPkg,
|
||||
Name: "libc-utils",
|
||||
Version: "0.7.2-r0",
|
||||
Licenses: []string{"BSD"},
|
||||
Type: pkg.ApkPkg,
|
||||
MetadataType: pkg.ApkMetadataType,
|
||||
Metadata: pkg.ApkMetadata{
|
||||
Package: "libc-utils",
|
||||
OriginPackage: "libc-dev",
|
||||
@@ -132,10 +575,11 @@ func TestMultiplePackages(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "musl-utils",
|
||||
Version: "1.1.24-r2",
|
||||
Licenses: []string{"MIT", "BSD", "GPL2+"},
|
||||
Type: pkg.ApkPkg,
|
||||
Name: "musl-utils",
|
||||
Version: "1.1.24-r2",
|
||||
Licenses: []string{"MIT", "BSD", "GPL2+"},
|
||||
Type: pkg.ApkPkg,
|
||||
MetadataType: pkg.ApkMetadataType,
|
||||
Metadata: pkg.ApkMetadata{
|
||||
Package: "musl-utils",
|
||||
OriginPackage: "musl",
|
||||
@@ -151,38 +595,47 @@ func TestMultiplePackages(t *testing.T) {
|
||||
PullChecksum: "Q1bTtF5526tETKfL+lnigzIDvm+2o=",
|
||||
GitCommitOfAport: "4024cc3b29ad4c65544ad068b8f59172b5494306",
|
||||
Files: []pkg.ApkFileRecord{
|
||||
{
|
||||
Path: "/sbin",
|
||||
},
|
||||
{
|
||||
Path: "/sbin/ldconfig",
|
||||
OwnerUID: "0",
|
||||
OwnerGUI: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Checksum: "Q1Kja2+POZKxEkUOZqwSjC6kmaED4=",
|
||||
},
|
||||
{
|
||||
Path: "/usr",
|
||||
},
|
||||
{
|
||||
Path: "/usr/bin",
|
||||
},
|
||||
{
|
||||
Path: "/usr/bin/iconv",
|
||||
OwnerUID: "0",
|
||||
OwnerGUI: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Checksum: "Q1CVmFbdY+Hv6/jAHl1gec2Kbx1EY=",
|
||||
},
|
||||
{
|
||||
Path: "/usr/bin/ldd",
|
||||
OwnerUID: "0",
|
||||
OwnerGUI: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Checksum: "Q1yFAhGggmL7ERgbIA7KQxyTzf3ks=",
|
||||
},
|
||||
{
|
||||
Path: "/usr/bin/getconf",
|
||||
OwnerUID: "0",
|
||||
OwnerGUI: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Checksum: "Q1dAdYK8M/INibRQF5B3Rw7cmNDDA=",
|
||||
},
|
||||
{
|
||||
Path: "/usr/bin/getent",
|
||||
OwnerUID: "0",
|
||||
OwnerGUI: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Checksum: "Q1eR2Dz/WylabgbWMTkd2+hGmEya4=",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
C:Q1myMNfd7u5v5UTgNHeq1e31qTjZU=
|
||||
P:alpine-baselayout
|
||||
V:3.2.0-r6
|
||||
A:x86_64
|
||||
S:19917
|
||||
I:409600
|
||||
T:Alpine base dir structure and init scripts
|
||||
U:https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout
|
||||
L:GPL-2.0-only
|
||||
o:alpine-baselayout
|
||||
m:Natanael Copa <ncopa@alpinelinux.org>
|
||||
t:1590679557
|
||||
c:e1c51734fa96fa4bac92e9f14a474324c67916fc
|
||||
D:/bin/sh so:libc.musl-x86_64.so.1
|
||||
p:cmd:mkmntdirs
|
||||
F:dev
|
||||
F:dev/pts
|
||||
F:dev/shm
|
||||
F:etc
|
||||
R:fstab
|
||||
Z:Q11Q7hNe8QpDS531guqCdrXBzoA/o=
|
||||
R:group
|
||||
Z:Q1oJ16xWudgKOrXIEquEDzlF2Lsm4=
|
||||
R:hostname
|
||||
Z:Q16nVwYVXP/tChvUPdukVD2ifXOmc=
|
||||
R:hosts
|
||||
Z:Q1BD6zJKZTRWyqGnPi4tSfd3krsMU=
|
||||
R:inittab
|
||||
Z:Q1TsthbhW7QzWRe1E/NKwTOuD4pHc=
|
||||
R:modules
|
||||
Z:Q1toogjUipHGcMgECgPJX64SwUT1M=
|
||||
R:motd
|
||||
Z:Q1XmduVVNURHQ27TvYp1Lr5TMtFcA=
|
||||
R:mtab
|
||||
a:0:0:777
|
||||
Z:Q1kiljhXXH1LlQroHsEJIkPZg2eiw=
|
||||
R:passwd
|
||||
Z:Q1TchuuLUfur0izvfZQZxgN/LJhB8=
|
||||
R:profile
|
||||
Z:Q1KpFb8kl5LvwXWlY3e58FNsjrI34=
|
||||
R:protocols
|
||||
Z:Q13FqXUnvuOpMDrH/6rehxuYAEE34=
|
||||
R:services
|
||||
Z:Q1C6HJNgQvLWqt5VY+n7MZJ1rsDuY=
|
||||
R:shadow
|
||||
a:0:42:640
|
||||
Z:Q1ltrPIAW2zHeDiajsex2Bdmq3uqA=
|
||||
R:shells
|
||||
Z:Q1ojm2YdpCJ6B/apGDaZ/Sdb2xJkA=
|
||||
R:sysctl.conf
|
||||
Z:Q14upz3tfnNxZkIEsUhWn7Xoiw96g=
|
||||
F:etc/apk
|
||||
F:etc/conf.d
|
||||
F:etc/crontabs
|
||||
R:root
|
||||
a:0:0:600
|
||||
Z:Q1vfk1apUWI4yLJGhhNRd0kJixfvY=
|
||||
F:etc/init.d
|
||||
F:etc/modprobe.d
|
||||
R:aliases.conf
|
||||
Z:Q1WUbh6TBYNVK7e4Y+uUvLs/7viqk=
|
||||
R:blacklist.conf
|
||||
Z:Q1xxYGU6S6TLQvb7ervPrWWwAWqMg=
|
||||
R:i386.conf
|
||||
Z:Q1pnay/njn6ol9cCssL7KiZZ8etlc=
|
||||
R:kms.conf
|
||||
Z:Q1ynbLn3GYDpvajba/ldp1niayeog=
|
||||
F:etc/modules-load.d
|
||||
F:etc/network
|
||||
F:etc/network/if-down.d
|
||||
F:etc/network/if-post-down.d
|
||||
F:etc/network/if-pre-up.d
|
||||
F:etc/network/if-up.d
|
||||
F:etc/opt
|
||||
F:etc/periodic
|
||||
F:etc/periodic/15min
|
||||
F:etc/periodic/daily
|
||||
F:etc/periodic/hourly
|
||||
F:etc/periodic/monthly
|
||||
F:etc/periodic/weekly
|
||||
F:etc/profile.d
|
||||
R:color_prompt
|
||||
Z:Q10wL23GuSCVfumMRgakabUI6EsSk=
|
||||
R:locale
|
||||
Z:Q1R4bIEpnKxxOSrlnZy9AoawqZ5DU=
|
||||
F:etc/sysctl.d
|
||||
F:home
|
||||
F:lib
|
||||
F:lib/firmware
|
||||
F:lib/mdev
|
||||
F:lib/modules-load.d
|
||||
F:lib/sysctl.d
|
||||
R:00-alpine.conf
|
||||
Z:Q1HpElzW1xEgmKfERtTy7oommnq6c=
|
||||
F:media
|
||||
F:media/cdrom
|
||||
F:media/floppy
|
||||
F:media/usb
|
||||
F:mnt
|
||||
F:opt
|
||||
F:proc
|
||||
F:root
|
||||
M:0:0:700
|
||||
F:run
|
||||
F:sbin
|
||||
R:mkmntdirs
|
||||
a:0:0:755
|
||||
Z:Q1YeuSmC7iDbEWrusPzA/zUQF6YSg=
|
||||
F:srv
|
||||
F:sys
|
||||
F:tmp
|
||||
M:0:0:1777
|
||||
F:usr
|
||||
F:usr/lib
|
||||
F:usr/lib/modules-load.d
|
||||
F:usr/local
|
||||
F:usr/local/bin
|
||||
F:usr/local/lib
|
||||
F:usr/local/share
|
||||
F:usr/sbin
|
||||
F:usr/share
|
||||
F:usr/share/man
|
||||
F:usr/share/misc
|
||||
F:var
|
||||
R:run
|
||||
a:0:0:777
|
||||
Z:Q11/SNZz/8cK2dSKK+cJpVrZIuF4Q=
|
||||
F:var/cache
|
||||
F:var/cache/misc
|
||||
F:var/empty
|
||||
M:0:0:555
|
||||
F:var/lib
|
||||
F:var/lib/misc
|
||||
F:var/local
|
||||
F:var/lock
|
||||
F:var/lock/subsys
|
||||
F:var/log
|
||||
F:var/mail
|
||||
F:var/opt
|
||||
F:var/spool
|
||||
R:mail
|
||||
a:0:0:777
|
||||
Z:Q1dzbdazYZA2nTzSIG3YyNw7d4Juc=
|
||||
F:var/spool/cron
|
||||
R:crontabs
|
||||
a:0:0:777
|
||||
Z:Q1OFZt+ZMp7j0Gny0rqSKuWJyqYmA=
|
||||
F:var/tmp
|
||||
M:0:0:1777
|
||||
@@ -0,0 +1,22 @@
|
||||
P:openjdk8-jre
|
||||
V:8.212.04-r0
|
||||
A:x86_64
|
||||
S:359029
|
||||
I:970752
|
||||
T:OpenJDK 8 Java Runtime
|
||||
U:https://icedtea.classpath.org/
|
||||
L:custom
|
||||
o:openjdk8
|
||||
m:Timo Teras <timo.teras@iki.fi>
|
||||
t:1556993127
|
||||
c:46badd1aabd3375ae4b41c7194bc448f74b2ea4a
|
||||
D:java-cacerts nss so:libX11.so.6 so:libXcomposite.so.1 so:libXext.so.6 so:libXi.so.6 so:libXrender.so.1 so:libXtst.so.6 so:libasound.so.2 so:libc.musl-x86_64.so.1 so:libfreetype.so.6 so:libgcc_s.so.1 so:libgif.so.7 so:libjpeg.so.8 so:libpng16.so.16 so:libstdc++.so.6 so:openjdk8:libawt.so so:openjdk8:libjava.so so:openjdk8:libjli.so so:openjdk8:libjvm.so
|
||||
p:so:openjdk8:libawt_xawt.so=0 so:openjdk8:libfontmanager.so=0 so:openjdk8:libjawt.so=0 so:openjdk8:libjsoundalsa.so=0 so:openjdk8:libsplashscreen.so=0
|
||||
F:usr
|
||||
F:usr/lib
|
||||
F:usr/lib/jvm
|
||||
F:usr/lib/jvm/java-1.8-openjdk
|
||||
F:usr/lib/jvm/java-1.8-openjdk/bin
|
||||
R:policytool
|
||||
a:0:0:755:Q1JaDEHQHBbizhEzoWK1YxuraNU/4=
|
||||
Z:Q1M0C9qfC/+kdRiOodeihG2GMRtkE=
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
Package bundler provides a concrete Cataloger implementation for Ruby Gemfile.lock bundler files.
|
||||
*/
|
||||
package bundler
|
||||
|
||||
import (
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/syft/cataloger/common"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
)
|
||||
|
||||
// Cataloger catalogs pkg.GemPkg Package Types defined in Bundler Gemfile.lock files.
|
||||
type Cataloger struct {
|
||||
cataloger common.GenericCataloger
|
||||
}
|
||||
|
||||
// New returns a new Bundler cataloger object.
|
||||
func New() *Cataloger {
|
||||
globParsers := map[string]common.ParserFn{
|
||||
"**/Gemfile.lock": parseGemfileLockEntries,
|
||||
}
|
||||
|
||||
return &Cataloger{
|
||||
cataloger: common.NewGenericCataloger(nil, globParsers),
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns a string that uniquely describes this cataloger.
|
||||
func (a *Cataloger) Name() string {
|
||||
return "bundler-cataloger"
|
||||
}
|
||||
|
||||
// SelectFiles returns a set of discovered Gemfile.lock files from the user content source.
|
||||
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
||||
return a.cataloger.SelectFiles(resolver)
|
||||
}
|
||||
|
||||
// Catalog returns the Packages indexed from all Gemfile.lock files discovered.
|
||||
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
||||
return a.cataloger.Catalog(contents, a.Name())
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package bundler
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
var expected = map[string]string{
|
||||
"actionmailer": "4.1.1",
|
||||
"actionpack": "4.1.1",
|
||||
"actionview": "4.1.1",
|
||||
"activemodel": "4.1.1",
|
||||
"activerecord": "4.1.1",
|
||||
"activesupport": "4.1.1",
|
||||
"arel": "5.0.1.20140414130214",
|
||||
"bootstrap-sass": "3.1.1.1",
|
||||
"builder": "3.2.2",
|
||||
"coffee-rails": "4.0.1",
|
||||
"coffee-script": "2.2.0",
|
||||
"coffee-script-source": "1.7.0",
|
||||
"erubis": "2.7.0",
|
||||
"execjs": "2.0.2",
|
||||
"hike": "1.2.3",
|
||||
"i18n": "0.6.9",
|
||||
"jbuilder": "2.0.7",
|
||||
"jquery-rails": "3.1.0",
|
||||
"json": "1.8.1",
|
||||
"kgio": "2.9.2",
|
||||
"libv8": "3.16.14.3",
|
||||
"mail": "2.5.4",
|
||||
"mime-types": "1.25.1",
|
||||
"minitest": "5.3.4",
|
||||
"multi_json": "1.10.1",
|
||||
"mysql2": "0.3.16",
|
||||
"polyglot": "0.3.4",
|
||||
"rack": "1.5.2",
|
||||
"rack-test": "0.6.2",
|
||||
"rails": "4.1.1",
|
||||
"railties": "4.1.1",
|
||||
"raindrops": "0.13.0",
|
||||
"rake": "10.3.2",
|
||||
"rdoc": "4.1.1",
|
||||
"ref": "1.0.5",
|
||||
"sass": "3.2.19",
|
||||
"sass-rails": "4.0.3",
|
||||
"sdoc": "0.4.0",
|
||||
"spring": "1.1.3",
|
||||
"sprockets": "2.11.0",
|
||||
"sprockets-rails": "2.1.3",
|
||||
"sqlite3": "1.3.9",
|
||||
"therubyracer": "0.12.1",
|
||||
"thor": "0.19.1",
|
||||
"thread_safe": "0.3.3",
|
||||
"tilt": "1.4.1",
|
||||
"treetop": "1.4.15",
|
||||
"turbolinks": "2.2.2",
|
||||
"tzinfo": "1.2.0",
|
||||
"uglifier": "2.5.0",
|
||||
"unicorn": "4.8.3",
|
||||
}
|
||||
|
||||
func TestParseGemfileLockEntries(t *testing.T) {
|
||||
fixture, err := os.Open("test-fixtures/Gemfile.lock")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open fixture: %+v", err)
|
||||
}
|
||||
|
||||
actual, err := parseGemfileLockEntries(fixture.Name(), fixture)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse gemfile lock: %+v", err)
|
||||
}
|
||||
|
||||
if len(actual) != len(expected) {
|
||||
for _, a := range actual {
|
||||
t.Log(" ", a)
|
||||
}
|
||||
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(expected))
|
||||
}
|
||||
|
||||
for _, a := range actual {
|
||||
expectedVersion, ok := expected[a.Name]
|
||||
if !ok {
|
||||
t.Errorf("unexpected package found: %s", a.Name)
|
||||
}
|
||||
|
||||
if expectedVersion != a.Version {
|
||||
t.Errorf("unexpected package version (pkg=%s): %s", a.Name, a.Version)
|
||||
}
|
||||
|
||||
if a.Language != pkg.Ruby {
|
||||
t.Errorf("bad language (pkg=%+v): %+v", a.Name, a.Language)
|
||||
}
|
||||
|
||||
if a.Type != pkg.BundlerPkg {
|
||||
t.Errorf("bad package type (pkg=%+v): %+v", a.Name, a.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-25
@@ -1,12 +1,12 @@
|
||||
package cataloger
|
||||
|
||||
import (
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
@@ -33,44 +33,37 @@ func newMonitor() (*progress.Manual, *progress.Manual) {
|
||||
return &filesProcessed, &packagesDiscovered
|
||||
}
|
||||
|
||||
// Catalog a given scope (container image or filesystem) with the given catalogers, returning all discovered packages.
|
||||
// Catalog a given source (container image or filesystem) with the given catalogers, returning all discovered packages.
|
||||
// In order to efficiently retrieve contents from a underlying container image the content fetch requests are
|
||||
// done in bulk. Specifically, all files of interest are collected from each catalogers and accumulated into a single
|
||||
// request.
|
||||
func Catalog(resolver scope.Resolver, catalogers ...Cataloger) (*pkg.Catalog, error) {
|
||||
func Catalog(resolver source.Resolver, theDistro *distro.Distro, catalogers ...Cataloger) (*pkg.Catalog, error) {
|
||||
catalog := pkg.NewCatalog()
|
||||
fileSelection := make([]file.Reference, 0)
|
||||
|
||||
filesProcessed, packagesDiscovered := newMonitor()
|
||||
|
||||
// ask catalogers for files to extract from the image tar
|
||||
for _, a := range catalogers {
|
||||
fileSelection = append(fileSelection, a.SelectFiles(resolver)...)
|
||||
log.Debugf("cataloger '%s' selected '%d' files", a.Name(), len(fileSelection))
|
||||
filesProcessed.N += int64(len(fileSelection))
|
||||
}
|
||||
|
||||
// fetch contents for requested selection by catalogers
|
||||
// TODO: we should consider refactoring to return a set of io.Readers instead of the full contents themselves (allow for optional buffering).
|
||||
contents, err := resolver.MultipleFileContentsByRef(fileSelection...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// perform analysis, accumulating errors for each failed analysis
|
||||
var errs error
|
||||
for _, a := range catalogers {
|
||||
// TODO: check for multiple rounds of analyses by Iterate error
|
||||
packages, err := a.Catalog(contents)
|
||||
for _, theCataloger := range catalogers {
|
||||
// find packages from the underlying raw data
|
||||
packages, err := theCataloger.Catalog(resolver)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("cataloger '%s' discovered '%d' packages", a.Name(), len(packages))
|
||||
packagesDiscovered.N += int64(len(packages))
|
||||
catalogedPackages := len(packages)
|
||||
|
||||
log.Debugf("cataloger '%s' discovered '%d' packages", theCataloger.Name(), catalogedPackages)
|
||||
packagesDiscovered.N += int64(catalogedPackages)
|
||||
|
||||
for _, p := range packages {
|
||||
// generate CPEs
|
||||
p.CPEs = generatePackageCPEs(p)
|
||||
|
||||
// generate PURL
|
||||
p.PURL = generatePackageURL(p, theDistro)
|
||||
|
||||
// add to catalog
|
||||
catalog.Add(p)
|
||||
}
|
||||
}
|
||||
|
||||
+30
-20
@@ -6,17 +6,16 @@ catalogers defined in child packages as well as the interface definition to impl
|
||||
package cataloger
|
||||
|
||||
import (
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/syft/cataloger/apkdb"
|
||||
"github.com/anchore/syft/syft/cataloger/bundler"
|
||||
"github.com/anchore/syft/syft/cataloger/dpkg"
|
||||
"github.com/anchore/syft/syft/cataloger/deb"
|
||||
"github.com/anchore/syft/syft/cataloger/golang"
|
||||
"github.com/anchore/syft/syft/cataloger/java"
|
||||
"github.com/anchore/syft/syft/cataloger/javascript"
|
||||
"github.com/anchore/syft/syft/cataloger/python"
|
||||
"github.com/anchore/syft/syft/cataloger/rpmdb"
|
||||
"github.com/anchore/syft/syft/cataloger/ruby"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// Cataloger describes behavior for an object to participate in parsing container image or file system
|
||||
@@ -25,24 +24,35 @@ import (
|
||||
type Cataloger interface {
|
||||
// Name returns a string that uniquely describes a cataloger
|
||||
Name() string
|
||||
// SelectFiles discovers and returns specific files that the cataloger would like to inspect the contents of.
|
||||
SelectFiles(scope.FileResolver) []file.Reference
|
||||
// Catalog is given the file contents and should return any discovered Packages after analyzing the contents.
|
||||
Catalog(map[file.Reference]string) ([]pkg.Package, error)
|
||||
// TODO: add "IterationNeeded" error to indicate to the driver to continue with another Select/Catalog pass
|
||||
// TODO: we should consider refactoring to return a set of io.Readers instead of the full contents themselves (allow for optional buffering).
|
||||
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing the catalog source.
|
||||
Catalog(resolver source.Resolver) ([]pkg.Package, error)
|
||||
}
|
||||
|
||||
// All returns a slice of all locally defined catalogers (defined in child packages).
|
||||
func All() []Cataloger {
|
||||
// ImageCatalogers returns a slice of locally implemented catalogers that are fit for detecting installations of packages.
|
||||
func ImageCatalogers() []Cataloger {
|
||||
return []Cataloger{
|
||||
dpkg.New(),
|
||||
bundler.New(),
|
||||
python.New(),
|
||||
rpmdb.New(),
|
||||
java.New(),
|
||||
apkdb.New(),
|
||||
golang.New(),
|
||||
javascript.New(),
|
||||
ruby.NewGemSpecCataloger(),
|
||||
python.NewPythonPackageCataloger(),
|
||||
javascript.NewJavascriptPackageCataloger(),
|
||||
deb.NewDpkgdbCataloger(),
|
||||
rpmdb.NewRpmdbCataloger(),
|
||||
java.NewJavaCataloger(),
|
||||
apkdb.NewApkdbCataloger(),
|
||||
golang.NewGoModCataloger(),
|
||||
}
|
||||
}
|
||||
|
||||
// DirectoryCatalogers returns a slice of locally implemented catalogers that are fit for detecting packages from index files (and select installations)
|
||||
func DirectoryCatalogers() []Cataloger {
|
||||
return []Cataloger{
|
||||
ruby.NewGemFileLockCataloger(),
|
||||
python.NewPythonIndexCataloger(),
|
||||
python.NewPythonPackageCataloger(),
|
||||
javascript.NewJavascriptLockCataloger(),
|
||||
deb.NewDpkgdbCataloger(),
|
||||
rpmdb.NewRpmdbCataloger(),
|
||||
java.NewJavaCataloger(),
|
||||
apkdb.NewApkdbCataloger(),
|
||||
golang.NewGoModCataloger(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,95 +6,111 @@ package common
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// GenericCataloger implements the Catalog interface and is responsible for dispatching the proper parser function for
|
||||
// a given path or glob pattern. This is intended to be reusable across many package cataloger types.
|
||||
type GenericCataloger struct {
|
||||
globParsers map[string]ParserFn
|
||||
pathParsers map[string]ParserFn
|
||||
selectedFiles []file.Reference
|
||||
parsers map[file.Reference]ParserFn
|
||||
globParsers map[string]ParserFn
|
||||
pathParsers map[string]ParserFn
|
||||
selectedFiles []source.Location
|
||||
parsers map[source.Location]ParserFn
|
||||
upstreamCataloger string
|
||||
}
|
||||
|
||||
// NewGenericCataloger if provided path-to-parser-function and glob-to-parser-function lookups creates a GenericCataloger
|
||||
func NewGenericCataloger(pathParsers map[string]ParserFn, globParsers map[string]ParserFn) GenericCataloger {
|
||||
return GenericCataloger{
|
||||
globParsers: globParsers,
|
||||
pathParsers: pathParsers,
|
||||
selectedFiles: make([]file.Reference, 0),
|
||||
parsers: make(map[file.Reference]ParserFn),
|
||||
func NewGenericCataloger(pathParsers map[string]ParserFn, globParsers map[string]ParserFn, upstreamCataloger string) *GenericCataloger {
|
||||
return &GenericCataloger{
|
||||
globParsers: globParsers,
|
||||
pathParsers: pathParsers,
|
||||
selectedFiles: make([]source.Location, 0),
|
||||
parsers: make(map[source.Location]ParserFn),
|
||||
upstreamCataloger: upstreamCataloger,
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns a string that uniquely describes the upstream cataloger that this Generic Cataloger represents.
|
||||
func (c *GenericCataloger) Name() string {
|
||||
return c.upstreamCataloger
|
||||
}
|
||||
|
||||
// register pairs a set of file references with a parser function for future cataloging (when the file contents are resolved)
|
||||
func (a *GenericCataloger) register(files []file.Reference, parser ParserFn) {
|
||||
a.selectedFiles = append(a.selectedFiles, files...)
|
||||
func (c *GenericCataloger) register(files []source.Location, parser ParserFn) {
|
||||
c.selectedFiles = append(c.selectedFiles, files...)
|
||||
for _, f := range files {
|
||||
a.parsers[f] = parser
|
||||
c.parsers[f] = parser
|
||||
}
|
||||
}
|
||||
|
||||
// clear deletes all registered file-reference-to-parser-function pairings from former SelectFiles() and register() calls
|
||||
func (a *GenericCataloger) clear() {
|
||||
a.selectedFiles = make([]file.Reference, 0)
|
||||
a.parsers = make(map[file.Reference]ParserFn)
|
||||
func (c *GenericCataloger) clear() {
|
||||
c.selectedFiles = make([]source.Location, 0)
|
||||
c.parsers = make(map[source.Location]ParserFn)
|
||||
}
|
||||
|
||||
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing the catalog source.
|
||||
func (c *GenericCataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) {
|
||||
fileSelection := c.selectFiles(resolver)
|
||||
contents, err := resolver.MultipleFileContentsByLocation(fileSelection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.catalog(contents)
|
||||
}
|
||||
|
||||
// SelectFiles takes a set of file trees and resolves and file references of interest for future cataloging
|
||||
func (a *GenericCataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
||||
func (c *GenericCataloger) selectFiles(resolver source.FileResolver) []source.Location {
|
||||
// select by exact path
|
||||
for path, parser := range a.pathParsers {
|
||||
files, err := resolver.FilesByPath(file.Path(path))
|
||||
for path, parser := range c.pathParsers {
|
||||
files, err := resolver.FilesByPath(path)
|
||||
if err != nil {
|
||||
log.Errorf("cataloger failed to select files by path: %w", err)
|
||||
log.Warnf("cataloger failed to select files by path: %+v", err)
|
||||
}
|
||||
if files != nil {
|
||||
a.register(files, parser)
|
||||
c.register(files, parser)
|
||||
}
|
||||
}
|
||||
|
||||
// select by glob pattern
|
||||
for globPattern, parser := range a.globParsers {
|
||||
for globPattern, parser := range c.globParsers {
|
||||
fileMatches, err := resolver.FilesByGlob(globPattern)
|
||||
if err != nil {
|
||||
log.Errorf("failed to find files by glob: %s", globPattern)
|
||||
log.Warnf("failed to find files by glob: %s", globPattern)
|
||||
}
|
||||
if fileMatches != nil {
|
||||
a.register(fileMatches, parser)
|
||||
c.register(fileMatches, parser)
|
||||
}
|
||||
}
|
||||
|
||||
return a.selectedFiles
|
||||
return c.selectedFiles
|
||||
}
|
||||
|
||||
// Catalog takes a set of file contents and uses any configured parser functions to resolve and return discovered packages
|
||||
func (a *GenericCataloger) Catalog(contents map[file.Reference]string, upstreamMatcher string) ([]pkg.Package, error) {
|
||||
defer a.clear()
|
||||
// catalog takes a set of file contents and uses any configured parser functions to resolve and return discovered packages
|
||||
func (c *GenericCataloger) catalog(contents map[source.Location]string) ([]pkg.Package, error) {
|
||||
defer c.clear()
|
||||
|
||||
packages := make([]pkg.Package, 0)
|
||||
|
||||
for reference, parser := range a.parsers {
|
||||
content, ok := contents[reference]
|
||||
for location, parser := range c.parsers {
|
||||
content, ok := contents[location]
|
||||
if !ok {
|
||||
log.Errorf("cataloger '%s' missing file content: %+v", upstreamMatcher, reference)
|
||||
log.Warnf("cataloger '%s' missing file content: %+v", c.upstreamCataloger, location)
|
||||
continue
|
||||
}
|
||||
|
||||
entries, err := parser(string(reference.Path), strings.NewReader(content))
|
||||
entries, err := parser(location.Path, strings.NewReader(content))
|
||||
if err != nil {
|
||||
// TODO: should we fail? or only log?
|
||||
log.Errorf("cataloger '%s' failed to parse entries (reference=%+v): %w", upstreamMatcher, reference, err)
|
||||
log.Warnf("cataloger '%s' failed to parse entries (location=%+v): %+v", c.upstreamCataloger, location, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
entry.FoundBy = upstreamMatcher
|
||||
entry.Source = []file.Reference{reference}
|
||||
entry.FoundBy = c.upstreamCataloger
|
||||
entry.Locations = []source.Location{location}
|
||||
|
||||
packages = append(packages, entry)
|
||||
}
|
||||
|
||||
@@ -6,37 +6,50 @@ import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
type testResolver struct {
|
||||
contents map[file.Reference]string
|
||||
type testResolverMock struct {
|
||||
contents map[source.Location]string
|
||||
}
|
||||
|
||||
func newTestResolver() *testResolver {
|
||||
return &testResolver{
|
||||
contents: make(map[file.Reference]string),
|
||||
func newTestResolver() *testResolverMock {
|
||||
return &testResolverMock{
|
||||
contents: make(map[source.Location]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *testResolver) FilesByPath(paths ...file.Path) ([]file.Reference, error) {
|
||||
results := make([]file.Reference, len(paths))
|
||||
func (r *testResolverMock) FileContentsByLocation(_ source.Location) (string, error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (r *testResolverMock) MultipleFileContentsByLocation([]source.Location) (map[source.Location]string, error) {
|
||||
return r.contents, nil
|
||||
}
|
||||
|
||||
func (r *testResolverMock) FilesByPath(paths ...string) ([]source.Location, error) {
|
||||
results := make([]source.Location, len(paths))
|
||||
|
||||
for idx, p := range paths {
|
||||
results[idx] = file.NewFileReference(p)
|
||||
results[idx] = source.NewLocation(p)
|
||||
r.contents[results[idx]] = fmt.Sprintf("%s file contents!", p)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *testResolver) FilesByGlob(patterns ...string) ([]file.Reference, error) {
|
||||
func (r *testResolverMock) FilesByGlob(_ ...string) ([]source.Location, error) {
|
||||
path := "/a-path.txt"
|
||||
ref := file.NewFileReference(file.Path(path))
|
||||
r.contents[ref] = fmt.Sprintf("%s file contents!", path)
|
||||
return []file.Reference{ref}, nil
|
||||
location := source.NewLocation(path)
|
||||
r.contents[location] = fmt.Sprintf("%s file contents!", path)
|
||||
return []source.Location{location}, nil
|
||||
}
|
||||
|
||||
func (r *testResolverMock) RelativeFileByPath(_ source.Location, _ string) *source.Location {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func parser(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||
@@ -60,36 +73,20 @@ func TestGenericCataloger(t *testing.T) {
|
||||
"/another-path.txt": parser,
|
||||
"/last/path.txt": parser,
|
||||
}
|
||||
|
||||
resolver := newTestResolver()
|
||||
cataloger := NewGenericCataloger(pathParsers, globParsers)
|
||||
|
||||
selected := cataloger.SelectFiles(resolver)
|
||||
|
||||
if len(selected) != 3 {
|
||||
t.Fatalf("unexpected selection length: %d", len(selected))
|
||||
}
|
||||
|
||||
expectedSelection := internal.NewStringSetFromSlice([]string{"/last/path.txt", "/another-path.txt", "/a-path.txt"})
|
||||
selectionByPath := make(map[string]file.Reference)
|
||||
for _, s := range selected {
|
||||
if !expectedSelection.Contains(string(s.Path)) {
|
||||
t.Errorf("unexpected selection path: %+v", s.Path)
|
||||
}
|
||||
selectionByPath[string(s.Path)] = s
|
||||
}
|
||||
|
||||
upstream := "some-other-cataloger"
|
||||
expectedPkgs := make(map[file.Reference]pkg.Package)
|
||||
for path, ref := range selectionByPath {
|
||||
expectedPkgs[ref] = pkg.Package{
|
||||
resolver := newTestResolver()
|
||||
cataloger := NewGenericCataloger(pathParsers, globParsers, upstream)
|
||||
|
||||
expectedSelection := []string{"/last/path.txt", "/another-path.txt", "/a-path.txt"}
|
||||
expectedPkgs := make(map[string]pkg.Package)
|
||||
for _, path := range expectedSelection {
|
||||
expectedPkgs[path] = pkg.Package{
|
||||
FoundBy: upstream,
|
||||
Source: []file.Reference{ref},
|
||||
Name: fmt.Sprintf("%s file contents!", path),
|
||||
}
|
||||
}
|
||||
|
||||
actualPkgs, err := cataloger.Catalog(resolver.contents, upstream)
|
||||
actualPkgs, err := cataloger.Catalog(resolver)
|
||||
if err != nil {
|
||||
t.Fatalf("cataloger catalog action failed: %+v", err)
|
||||
}
|
||||
@@ -99,8 +96,8 @@ func TestGenericCataloger(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, p := range actualPkgs {
|
||||
ref := p.Source[0]
|
||||
exP, ok := expectedPkgs[ref]
|
||||
ref := p.Locations[0]
|
||||
exP, ok := expectedPkgs[ref.Path]
|
||||
if !ok {
|
||||
t.Errorf("missing expected pkg: ref=%+v", ref)
|
||||
continue
|
||||
@@ -111,7 +108,7 @@ func TestGenericCataloger(t *testing.T) {
|
||||
}
|
||||
|
||||
if exP.Name != p.Name {
|
||||
t.Errorf("bad contents mapping: %+v", p.Source)
|
||||
t.Errorf("bad contents mapping: %+v", p.Locations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package cataloger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/facebookincubator/nvdtools/wfn"
|
||||
)
|
||||
|
||||
// this is functionally equivalent to "*" and consistent with no input given (thus easier to test)
|
||||
const any = ""
|
||||
|
||||
// generatePackageCPEs Create a list of CPEs, trying to guess the vendor, product tuple and setting TargetSoftware if possible
|
||||
func generatePackageCPEs(p pkg.Package) []pkg.CPE {
|
||||
targetSws := candidateTargetSoftwareAttrs(p)
|
||||
vendors := candidateVendors(p)
|
||||
products := candidateProducts(p)
|
||||
|
||||
keys := internal.NewStringSet()
|
||||
cpes := make([]pkg.CPE, 0)
|
||||
for _, product := range products {
|
||||
for _, vendor := range append([]string{any}, vendors...) {
|
||||
for _, targetSw := range append([]string{any}, targetSws...) {
|
||||
// prevent duplicate entries...
|
||||
key := fmt.Sprintf("%s|%s|%s|%s", product, vendor, p.Version, targetSw)
|
||||
if keys.Contains(key) {
|
||||
continue
|
||||
}
|
||||
keys.Add(key)
|
||||
|
||||
// add a new entry...
|
||||
candidateCpe := wfn.NewAttributesWithAny()
|
||||
candidateCpe.Part = "a"
|
||||
candidateCpe.Product = product
|
||||
candidateCpe.Vendor = vendor
|
||||
candidateCpe.Version = p.Version
|
||||
candidateCpe.TargetSW = targetSw
|
||||
|
||||
cpes = append(cpes, *candidateCpe)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cpes
|
||||
}
|
||||
|
||||
func candidateTargetSoftwareAttrs(p pkg.Package) []string {
|
||||
// TODO: would be great to allow these to be overridden by user data/config
|
||||
var targetSw []string
|
||||
switch p.Language {
|
||||
case pkg.Java:
|
||||
targetSw = append(targetSw, "java", "maven")
|
||||
case pkg.JavaScript:
|
||||
targetSw = append(targetSw, "node.js", "nodejs")
|
||||
case pkg.Ruby:
|
||||
targetSw = append(targetSw, "ruby", "rails")
|
||||
case pkg.Python:
|
||||
targetSw = append(targetSw, "python")
|
||||
}
|
||||
|
||||
if p.Type == pkg.JenkinsPluginPkg {
|
||||
targetSw = append(targetSw, "jenkins", "cloudbees_jenkins")
|
||||
}
|
||||
|
||||
return targetSw
|
||||
}
|
||||
|
||||
func candidateVendors(p pkg.Package) []string {
|
||||
vendors := candidateProducts(p)
|
||||
switch p.Language {
|
||||
case pkg.Python:
|
||||
vendors = append(vendors, fmt.Sprintf("python-%s", p.Name))
|
||||
case pkg.Java:
|
||||
if p.MetadataType == pkg.JavaMetadataType {
|
||||
if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok && metadata.PomProperties != nil {
|
||||
// derive the vendor from the groupID (e.g. org.sonatype.nexus --> sonatype)
|
||||
if strings.HasPrefix(metadata.PomProperties.GroupID, "org.") || strings.HasPrefix(metadata.PomProperties.GroupID, "com.") {
|
||||
fields := strings.Split(metadata.PomProperties.GroupID, ".")
|
||||
if len(fields) >= 3 {
|
||||
vendors = append(vendors, fields[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return vendors
|
||||
}
|
||||
|
||||
func candidateProducts(p pkg.Package) []string {
|
||||
var products = []string{p.Name}
|
||||
switch p.Language {
|
||||
case pkg.Java:
|
||||
if p.MetadataType == pkg.JavaMetadataType {
|
||||
if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok && metadata.PomProperties != nil {
|
||||
// derive the product from the groupID (e.g. org.sonatype.nexus --> nexus)
|
||||
if strings.HasPrefix(metadata.PomProperties.GroupID, "org.") || strings.HasPrefix(metadata.PomProperties.GroupID, "com.") {
|
||||
fields := strings.Split(metadata.PomProperties.GroupID, ".")
|
||||
if len(fields) >= 3 {
|
||||
products = append(products, fields[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
return products
|
||||
}
|
||||
return products
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package cataloger
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/scylladb/go-set"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
p pkg.Package
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "python language",
|
||||
p: pkg.Package{
|
||||
Name: "name",
|
||||
Version: "3.2",
|
||||
FoundBy: "some-analyzer",
|
||||
Language: pkg.Python,
|
||||
Type: pkg.DebPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:python:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:python:*:*",
|
||||
"cpe:2.3:a:python-name:name:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:python-name:name:3.2:*:*:*:*:python:*:*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "javascript language",
|
||||
p: pkg.Package{
|
||||
Name: "name",
|
||||
Version: "3.2",
|
||||
FoundBy: "some-analyzer",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.DebPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:node.js:*:*",
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:nodejs:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:node.js:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:nodejs:*:*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ruby language",
|
||||
p: pkg.Package{
|
||||
Name: "name",
|
||||
Version: "3.2",
|
||||
FoundBy: "some-analyzer",
|
||||
Language: pkg.Ruby,
|
||||
Type: pkg.DebPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:ruby:*:*",
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:rails:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:ruby:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:rails:*:*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "java language",
|
||||
p: pkg.Package{
|
||||
Name: "name",
|
||||
Version: "3.2",
|
||||
FoundBy: "some-analyzer",
|
||||
Language: pkg.Java,
|
||||
Type: pkg.DebPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:java:*:*",
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:maven:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:java:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:maven:*:*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "java language with groupID",
|
||||
p: pkg.Package{
|
||||
Name: "name",
|
||||
Version: "3.2",
|
||||
FoundBy: "some-analyzer",
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JavaPkg,
|
||||
MetadataType: pkg.JavaMetadataType,
|
||||
Metadata: pkg.JavaMetadata{
|
||||
PomProperties: &pkg.PomProperties{
|
||||
GroupID: "org.sonatype.nexus",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:java:*:*",
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:maven:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:java:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:maven:*:*",
|
||||
"cpe:2.3:a:sonatype:name:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:sonatype:name:3.2:*:*:*:*:java:*:*",
|
||||
"cpe:2.3:a:sonatype:name:3.2:*:*:*:*:maven:*:*",
|
||||
"cpe:2.3:a:*:nexus:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:*:nexus:3.2:*:*:*:*:java:*:*",
|
||||
"cpe:2.3:a:*:nexus:3.2:*:*:*:*:maven:*:*",
|
||||
"cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:java:*:*",
|
||||
"cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:maven:*:*",
|
||||
"cpe:2.3:a:nexus:name:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:nexus:name:3.2:*:*:*:*:java:*:*",
|
||||
"cpe:2.3:a:nexus:name:3.2:*:*:*:*:maven:*:*",
|
||||
"cpe:2.3:a:name:nexus:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:name:nexus:3.2:*:*:*:*:java:*:*",
|
||||
"cpe:2.3:a:name:nexus:3.2:*:*:*:*:maven:*:*",
|
||||
"cpe:2.3:a:nexus:nexus:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:nexus:nexus:3.2:*:*:*:*:java:*:*",
|
||||
"cpe:2.3:a:nexus:nexus:3.2:*:*:*:*:maven:*:*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "jenkins package",
|
||||
p: pkg.Package{
|
||||
Name: "name",
|
||||
Version: "3.2",
|
||||
FoundBy: "some-analyzer",
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JenkinsPluginPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:java:*:*",
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:maven:*:*",
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:jenkins:*:*",
|
||||
"cpe:2.3:a:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:java:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:maven:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*",
|
||||
"cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := generatePackageCPEs(test.p)
|
||||
|
||||
expectedCpeSet := set.NewStringSet(test.expected...)
|
||||
actualCpeSet := set.NewStringSet()
|
||||
for _, a := range actual {
|
||||
actualCpeSet.Add(a.BindToFmtString())
|
||||
}
|
||||
|
||||
extra := strset.Difference(expectedCpeSet, actualCpeSet).List()
|
||||
sort.Strings(extra)
|
||||
for _, d := range extra {
|
||||
t.Errorf("extra CPE: %+v", d)
|
||||
}
|
||||
|
||||
missing := strset.Difference(actualCpeSet, expectedCpeSet).List()
|
||||
sort.Strings(missing)
|
||||
for _, d := range missing {
|
||||
t.Errorf("missing CPE: %+v", d)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
Package dpkg provides a concrete Cataloger implementation for Debian package DB status files.
|
||||
*/
|
||||
package deb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
const (
|
||||
dpkgStatusGlob = "**/var/lib/dpkg/status"
|
||||
md5sumsExt = ".md5sums"
|
||||
docsPath = "/usr/share/doc"
|
||||
)
|
||||
|
||||
type Cataloger struct{}
|
||||
|
||||
// NewDpkgdbCataloger returns a new Deb package cataloger object.
|
||||
func NewDpkgdbCataloger() *Cataloger {
|
||||
return &Cataloger{}
|
||||
}
|
||||
|
||||
// Name returns a string that uniquely describes a cataloger
|
||||
func (c *Cataloger) Name() string {
|
||||
return "dpkgdb-cataloger"
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find dpkg status files's by glob: %w", err)
|
||||
}
|
||||
|
||||
var pkgs []pkg.Package
|
||||
for _, dbLocation := range dbFileMatches {
|
||||
dbContents, err := resolver.FileContentsByLocation(dbLocation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pkgs, err = parseDpkgStatus(strings.NewReader(dbContents))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to catalog dpkg package=%+v: %w", dbLocation.Path, err)
|
||||
}
|
||||
|
||||
md5ContentsByName, md5RefsByName, err := fetchMd5Contents(resolver, dbLocation, pkgs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to find dpkg md5 contents: %w", err)
|
||||
}
|
||||
|
||||
copyrightContentsByName, copyrightRefsByName, err := fetchCopyrightContents(resolver, dbLocation, pkgs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to find dpkg copyright contents: %w", err)
|
||||
}
|
||||
|
||||
for i := range pkgs {
|
||||
p := &pkgs[i]
|
||||
p.FoundBy = c.Name()
|
||||
p.Locations = []source.Location{dbLocation}
|
||||
|
||||
metadata := p.Metadata.(pkg.DpkgMetadata)
|
||||
|
||||
if md5Reader, ok := md5ContentsByName[md5Key(*p)]; ok {
|
||||
// attach the file list
|
||||
metadata.Files = parseDpkgMD5Info(md5Reader)
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
if ref, ok := md5RefsByName[md5Key(*p)]; ok {
|
||||
p.Locations = append(p.Locations, ref)
|
||||
}
|
||||
} else {
|
||||
// ensure the file list is an empty collection (not nil)
|
||||
metadata.Files = make([]pkg.DpkgFileRecord, 0)
|
||||
}
|
||||
|
||||
// persist alterations
|
||||
p.Metadata = metadata
|
||||
|
||||
copyrightReader, ok := copyrightContentsByName[p.Name]
|
||||
if ok {
|
||||
// attach the licenses
|
||||
p.Licenses = parseLicensesFromCopyright(copyrightReader)
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
if ref, ok := copyrightRefsByName[p.Name]; ok {
|
||||
p.Locations = append(p.Locations, ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return pkgs, nil
|
||||
}
|
||||
|
||||
func fetchMd5Contents(resolver source.Resolver, dbLocation source.Location, pkgs []pkg.Package) (map[string]io.Reader, map[string]source.Location, error) {
|
||||
// fetch all MD5 file contents. This approach is more efficient than fetching each MD5 file one at a time
|
||||
|
||||
var md5FileMatches []source.Location
|
||||
var nameByRef = make(map[source.Location]string)
|
||||
parentPath := filepath.Dir(dbLocation.Path)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
// we should have at least one reference
|
||||
if md5SumLocation != nil {
|
||||
md5FileMatches = append(md5FileMatches, *md5SumLocation)
|
||||
nameByRef[*md5SumLocation] = name
|
||||
}
|
||||
}
|
||||
|
||||
// fetch the md5 contents
|
||||
md5ContentsByLocation, err := resolver.MultipleFileContentsByLocation(md5FileMatches)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 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)
|
||||
for location, contents := range md5ContentsByLocation {
|
||||
name := nameByRef[location]
|
||||
contentsByName[name] = strings.NewReader(contents)
|
||||
refsByName[name] = location
|
||||
}
|
||||
|
||||
return contentsByName, refsByName, nil
|
||||
}
|
||||
|
||||
func fetchCopyrightContents(resolver source.Resolver, dbLocation source.Location, pkgs []pkg.Package) (map[string]io.Reader, map[string]source.Location, error) {
|
||||
// fetch all copyright file contents. This approach is more efficient than fetching each copyright file one at a time
|
||||
|
||||
var copyrightFileMatches []source.Location
|
||||
var nameByLocation = make(map[source.Location]string)
|
||||
for _, p := range pkgs {
|
||||
// look for /usr/share/docs/NAME/copyright files
|
||||
name := p.Name
|
||||
copyrightPath := path.Join(docsPath, name, "copyright")
|
||||
copyrightLocation := resolver.RelativeFileByPath(dbLocation, copyrightPath)
|
||||
|
||||
// we may not have a copyright file for each package, ignore missing files
|
||||
if copyrightLocation != nil {
|
||||
copyrightFileMatches = append(copyrightFileMatches, *copyrightLocation)
|
||||
nameByLocation[*copyrightLocation] = name
|
||||
}
|
||||
}
|
||||
|
||||
// fetch the copyright contents
|
||||
copyrightContentsByLocation, err := resolver.MultipleFileContentsByLocation(copyrightFileMatches)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// organize content results and refs by package name
|
||||
var contentsByName = make(map[string]io.Reader)
|
||||
var refsByName = make(map[string]source.Location)
|
||||
for location, contents := range copyrightContentsByLocation {
|
||||
name := nameByLocation[location]
|
||||
contentsByName[name] = strings.NewReader(contents)
|
||||
refsByName[name] = location
|
||||
}
|
||||
|
||||
return contentsByName, refsByName, nil
|
||||
}
|
||||
|
||||
func md5Key(p pkg.Package) string {
|
||||
metadata := p.Metadata.(pkg.DpkgMetadata)
|
||||
|
||||
contentKey := p.Name
|
||||
if metadata.Architecture != "" && metadata.Architecture != "all" {
|
||||
contentKey = contentKey + ":" + metadata.Architecture
|
||||
}
|
||||
return contentKey
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package deb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
func TestDpkgCataloger(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sources map[string][]string
|
||||
expected []pkg.Package
|
||||
}{
|
||||
{
|
||||
name: "go-case",
|
||||
sources: map[string][]string{
|
||||
"libpam-runtime": {"/var/lib/dpkg/status", "/var/lib/dpkg/info/libpam-runtime.md5sums", "/usr/share/doc/libpam-runtime/copyright"},
|
||||
},
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "libpam-runtime",
|
||||
Version: "1.1.8-3.6",
|
||||
FoundBy: "dpkgdb-cataloger",
|
||||
Licenses: []string{"GPL-2", "LGPL-2.1"},
|
||||
Type: pkg.DebPkg,
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "libpam-runtime",
|
||||
Source: "pam",
|
||||
Version: "1.1.8-3.6",
|
||||
Architecture: "all",
|
||||
Maintainer: "Steve Langasek <vorlon@debian.org>",
|
||||
InstalledSize: 1016,
|
||||
Files: []pkg.DpkgFileRecord{
|
||||
{Path: "/lib/x86_64-linux-gnu/libz.so.1.2.11", MD5: "55f905631797551d4d936a34c7e73474"},
|
||||
{Path: "/usr/share/doc/zlib1g/changelog.Debian.gz", MD5: "cede84bda30d2380217f97753c8ccf3a"},
|
||||
{Path: "/usr/share/doc/zlib1g/changelog.gz", MD5: "f3c9dafa6da7992c47328b4464f6d122"},
|
||||
{Path: "/usr/share/doc/zlib1g/copyright", MD5: "a4fae96070439a5209a62ae5b8017ab2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
img, cleanup := imagetest.GetFixtureImage(t, "docker-archive", "image-dpkg")
|
||||
defer cleanup()
|
||||
|
||||
s, err := source.NewFromImage(img, source.AllLayersScope, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c := NewDpkgdbCataloger()
|
||||
|
||||
actual, err := c.Catalog(s.Resolver)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to catalog: %+v", err)
|
||||
}
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
for _, a := range actual {
|
||||
t.Logf(" %+v", a)
|
||||
}
|
||||
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(test.expected))
|
||||
}
|
||||
|
||||
// test sources...
|
||||
for idx := range actual {
|
||||
a := &actual[idx]
|
||||
// we will test the sources separately
|
||||
var sourcesList = make([]string, len(a.Locations))
|
||||
for i, s := range a.Locations {
|
||||
sourcesList[i] = s.Path
|
||||
}
|
||||
a.Locations = nil
|
||||
|
||||
for _, d := range deep.Equal(sourcesList, test.sources[a.Name]) {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
}
|
||||
|
||||
// test remaining fields...
|
||||
for _, d := range deep.Equal(actual, test.expected) {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package deb
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
)
|
||||
|
||||
// For more information see: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-syntax
|
||||
|
||||
func parseLicensesFromCopyright(reader io.Reader) []string {
|
||||
findings := internal.NewStringSet()
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "License:") {
|
||||
candidate := strings.Replace(line, "License:", "", 1)
|
||||
candidate = strings.TrimSpace(candidate)
|
||||
if strings.Contains(candidate, " or ") || strings.Contains(candidate, " and ") {
|
||||
// this is a multi-license summary, ignore this as other recurrent license lines should cover this
|
||||
continue
|
||||
}
|
||||
if candidate != "" && strings.ToLower(candidate) != "none" {
|
||||
findings.Add(candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results := findings.ToSlice()
|
||||
|
||||
sort.Strings(results)
|
||||
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package deb
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
func TestParseLicensesFromCopyright(t *testing.T) {
|
||||
tests := []struct {
|
||||
fixture string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
fixture: "test-fixtures/copyright/trilicense",
|
||||
expected: []string{"GPL-2", "LGPL-2.1", "MPL-1.1"},
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/copyright/liblzma5",
|
||||
expected: []string{"Autoconf", "GPL-2", "GPL-2+", "LGPL-2.1+", "PD", "PD-debian", "config-h", "noderivs", "permissive-fsf", "permissive-nowarranty", "probably-PD"},
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/copyright/libaudit-common",
|
||||
expected: []string{"GPL-2", "LGPL-2.1"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.fixture, func(t *testing.T) {
|
||||
file, err := os.Open(test.fixture)
|
||||
if err != nil {
|
||||
t.Fatal("Unable to read: ", err)
|
||||
}
|
||||
defer func() {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
t.Fatal("closing file failed:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
actual := parseLicensesFromCopyright(file)
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
for _, a := range actual {
|
||||
t.Logf(" %+v", a)
|
||||
}
|
||||
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(test.expected))
|
||||
}
|
||||
|
||||
diffs := deep.Equal(actual, test.expected)
|
||||
for _, d := range diffs {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package deb
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func parseDpkgMD5Info(reader io.Reader) []pkg.DpkgFileRecord {
|
||||
// we must preallocate to ensure the resulting struct does not have null
|
||||
var findings = make([]pkg.DpkgFileRecord, 0)
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
fields := strings.SplitN(line, " ", 2)
|
||||
if len(fields) == 2 {
|
||||
path := strings.TrimSpace(fields[1])
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
findings = append(findings, pkg.DpkgFileRecord{
|
||||
Path: path,
|
||||
MD5: strings.TrimSpace(fields[0]),
|
||||
})
|
||||
}
|
||||
}
|
||||
return findings
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package deb
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func TestMD5SumInfoParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
fixture string
|
||||
expected []pkg.DpkgFileRecord
|
||||
}{
|
||||
{
|
||||
fixture: "test-fixtures/info/zlib1g.md5sums",
|
||||
expected: []pkg.DpkgFileRecord{
|
||||
{Path: "/lib/x86_64-linux-gnu/libz.so.1.2.11", MD5: "55f905631797551d4d936a34c7e73474"},
|
||||
{Path: "/usr/share/doc/zlib1g/changelog.Debian.gz", MD5: "cede84bda30d2380217f97753c8ccf3a"},
|
||||
{Path: "/usr/share/doc/zlib1g/changelog.gz", MD5: "f3c9dafa6da7992c47328b4464f6d122"},
|
||||
{Path: "/usr/share/doc/zlib1g/copyright", MD5: "a4fae96070439a5209a62ae5b8017ab2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.fixture, func(t *testing.T) {
|
||||
file, err := os.Open(test.fixture)
|
||||
if err != nil {
|
||||
t.Fatal("Unable to read: ", err)
|
||||
}
|
||||
defer func() {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
t.Fatal("closing file failed:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
actual := parseDpkgMD5Info(file)
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
for _, a := range actual {
|
||||
t.Logf(" %+v", a)
|
||||
}
|
||||
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(test.expected))
|
||||
}
|
||||
|
||||
diffs := deep.Equal(actual, test.expected)
|
||||
for _, d := range diffs {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
+58
-30
@@ -1,40 +1,44 @@
|
||||
package dpkg
|
||||
package deb
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/cataloger/common"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
// integrity check
|
||||
var _ common.ParserFn = parseDpkgStatus
|
||||
|
||||
var errEndOfPackages = fmt.Errorf("no more packages to read")
|
||||
|
||||
// parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed.
|
||||
func parseDpkgStatus(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||
func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) {
|
||||
buffedReader := bufio.NewReader(reader)
|
||||
var packages = make([]pkg.Package, 0)
|
||||
|
||||
for {
|
||||
continueProcessing := true
|
||||
for continueProcessing {
|
||||
entry, err := parseDpkgStatusEntry(buffedReader)
|
||||
if err != nil {
|
||||
if err == errEndOfPackages {
|
||||
break
|
||||
if errors.Is(err, errEndOfPackages) {
|
||||
continueProcessing = false
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
packages = append(packages, pkg.Package{
|
||||
Name: entry.Package,
|
||||
Version: entry.Version,
|
||||
Type: pkg.DebPkg,
|
||||
Metadata: entry,
|
||||
})
|
||||
|
||||
if entry.Package != "" {
|
||||
packages = append(packages, pkg.Package{
|
||||
Name: entry.Package,
|
||||
Version: entry.Version,
|
||||
Type: pkg.DebPkg,
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: entry,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return packages, nil
|
||||
@@ -42,14 +46,16 @@ func parseDpkgStatus(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||
|
||||
// parseDpkgStatusEntry returns an individual Dpkg entry, or returns errEndOfPackages if there are no more packages to parse from the reader.
|
||||
func parseDpkgStatusEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err error) {
|
||||
dpkgFields := make(map[string]string)
|
||||
dpkgFields := make(map[string]interface{})
|
||||
var retErr error
|
||||
var key string
|
||||
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return pkg.DpkgMetadata{}, errEndOfPackages
|
||||
retErr = errEndOfPackages
|
||||
break
|
||||
}
|
||||
return pkg.DpkgMetadata{}, err
|
||||
}
|
||||
@@ -81,18 +87,16 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err err
|
||||
dpkgFields[key] = val
|
||||
default:
|
||||
// parse a new key
|
||||
if i := strings.Index(line, ":"); i > 0 {
|
||||
key = strings.TrimSpace(line[0:i])
|
||||
val := strings.TrimSpace(line[i+1:])
|
||||
|
||||
if _, ok := dpkgFields[key]; ok {
|
||||
return pkg.DpkgMetadata{}, fmt.Errorf("duplicate key discovered: %s", key)
|
||||
}
|
||||
|
||||
dpkgFields[key] = val
|
||||
} else {
|
||||
return pkg.DpkgMetadata{}, fmt.Errorf("cannot parse field from line: '%s'", line)
|
||||
var val interface{}
|
||||
key, val, err = handleNewKeyValue(line)
|
||||
if err != nil {
|
||||
return pkg.DpkgMetadata{}, err
|
||||
}
|
||||
|
||||
if _, ok := dpkgFields[key]; ok {
|
||||
return pkg.DpkgMetadata{}, fmt.Errorf("duplicate key discovered: %s", key)
|
||||
}
|
||||
dpkgFields[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,5 +105,29 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err err
|
||||
return pkg.DpkgMetadata{}, err
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
return entry, retErr
|
||||
}
|
||||
|
||||
// handleNewKeyValue parse a new key-value pair from the given unprocessed line
|
||||
func handleNewKeyValue(line string) (string, interface{}, error) {
|
||||
if i := strings.Index(line, ":"); i > 0 {
|
||||
var key = strings.TrimSpace(line[0:i])
|
||||
// mapstruct cant handle "-"
|
||||
key = strings.ReplaceAll(key, "-", "")
|
||||
val := strings.TrimSpace(line[i+1:])
|
||||
|
||||
// further processing of values based on the key that was discovered
|
||||
switch key {
|
||||
case "InstalledSize":
|
||||
numVal, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("bad installed-size value=%q: %w", val, err)
|
||||
}
|
||||
return key, numVal, nil
|
||||
default:
|
||||
return key, val, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil, fmt.Errorf("cannot parse field from line: '%s'", line)
|
||||
}
|
||||
+21
-15
@@ -1,4 +1,4 @@
|
||||
package dpkg
|
||||
package deb
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -24,17 +24,19 @@ func TestSinglePackage(t *testing.T) {
|
||||
{
|
||||
name: "Test Single Package",
|
||||
expected: pkg.DpkgMetadata{
|
||||
Package: "apt",
|
||||
Source: "apt-dev",
|
||||
Version: "1.8.2",
|
||||
Architecture: "amd64",
|
||||
Package: "apt",
|
||||
Source: "apt-dev",
|
||||
Version: "1.8.2",
|
||||
Architecture: "amd64",
|
||||
InstalledSize: 4064,
|
||||
Maintainer: "APT Development Team <deity@lists.debian.org>",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
file, err := os.Open("test-fixtures/single")
|
||||
file, err := os.Open("test-fixtures/status/single")
|
||||
if err != nil {
|
||||
t.Fatal("Unable to read test_fixtures/single: ", err)
|
||||
}
|
||||
@@ -66,15 +68,19 @@ func TestMultiplePackages(t *testing.T) {
|
||||
name: "Test Multiple Package",
|
||||
expected: []pkg.DpkgMetadata{
|
||||
{
|
||||
Package: "tzdata",
|
||||
Version: "2020a-0+deb10u1",
|
||||
Source: "tzdata-dev",
|
||||
Architecture: "all",
|
||||
Package: "tzdata",
|
||||
Version: "2020a-0+deb10u1",
|
||||
Source: "tzdata-dev",
|
||||
Architecture: "all",
|
||||
InstalledSize: 3036,
|
||||
Maintainer: "GNU Libc Maintainers <debian-glibc@lists.debian.org>",
|
||||
},
|
||||
{
|
||||
Package: "util-linux",
|
||||
Version: "2.33.1-0.1",
|
||||
Architecture: "amd64",
|
||||
Package: "util-linux",
|
||||
Version: "2.33.1-0.1",
|
||||
Architecture: "amd64",
|
||||
InstalledSize: 4327,
|
||||
Maintainer: "LaMont Jones <lamont@debian.org>",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -82,7 +88,7 @@ func TestMultiplePackages(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
file, err := os.Open("test-fixtures/multiple")
|
||||
file, err := os.Open("test-fixtures/status/multiple")
|
||||
if err != nil {
|
||||
t.Fatal("Unable to read: ", err)
|
||||
}
|
||||
@@ -93,7 +99,7 @@ func TestMultiplePackages(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
pkgs, err := parseDpkgStatus(file.Name(), file)
|
||||
pkgs, err := parseDpkgStatus(file)
|
||||
if err != nil {
|
||||
t.Fatal("Unable to read file contents: ", err)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: auditd
|
||||
Source: https://people.redhat.com/sgrubb/audit/
|
||||
|
||||
Files: *
|
||||
Copyright: 2012-2016 Steve Grubb <sgrubb@redhat.com>
|
||||
2006-2012 Rik Faith
|
||||
License: GPL-2
|
||||
|
||||
Files: src/libev/*
|
||||
Copyright: 2007-2009 Marc Alexamder Lehmann
|
||||
License: GPL-2
|
||||
|
||||
Files: lib/*
|
||||
Copyright: 2005-2008 Steve Grubb <sgrubb@redhat.com>
|
||||
License: LGPL-2.1
|
||||
The audit daemon's library libaudit.* is released under LGPL
|
||||
so that it may be linked with 3rd party software.
|
||||
.
|
||||
On Debian systems, refer to /usr/share/common-licenses/LGPL-2.1
|
||||
for the complete text of the GNU Lesser General Public License.
|
||||
|
||||
Files: debian/*
|
||||
Copyright: 2007-2011 Philipp Matthias Hahn <pmhahn@debian.org>
|
||||
2012-2016 Laurent Bigonville <bigon@debian.org>
|
||||
License: GPL-2
|
||||
|
||||
License: GPL-2
|
||||
This package is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License version 2,
|
||||
as published by the Free Software Foundation.
|
||||
.
|
||||
This package is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this package; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License can be found in `/usr/share/common-licenses/GPL-1'.
|
||||
@@ -0,0 +1,383 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: XZ Utils
|
||||
Upstream-Contact:
|
||||
Lasse Collin <lasse.collin@tukaani.org>
|
||||
http://tukaani.org/xz/lists.html
|
||||
Source:
|
||||
http://tukaani.org/xz
|
||||
http://git.tukaani.org/xz.git
|
||||
Comment:
|
||||
XZ Utils is developed and maintained upstream by Lasse Collin. Major
|
||||
portions are based on code by other authors; see AUTHORS for details.
|
||||
Most of the source has been put into the public domain, but some files
|
||||
have not (details below).
|
||||
.
|
||||
This file describes the source package. The binary packages contain
|
||||
some files derived from other works: for example, images in the API
|
||||
documentation come from Doxygen.
|
||||
License:
|
||||
Different licenses apply to different files in this package. Here
|
||||
is a rough summary of which licenses apply to which parts of this
|
||||
package (but check the individual files to be sure!):
|
||||
.
|
||||
- liblzma is in the public domain.
|
||||
.
|
||||
- xz, xzdec, and lzmadec command line tools are in the public
|
||||
domain unless GNU getopt_long had to be compiled and linked
|
||||
in from the lib directory. The getopt_long code is under
|
||||
GNU LGPLv2.1+.
|
||||
.
|
||||
- The scripts to grep, diff, and view compressed files have been
|
||||
adapted from gzip. These scripts and their documentation are
|
||||
under GNU GPLv2+.
|
||||
.
|
||||
- All the documentation in the doc directory and most of the
|
||||
XZ Utils specific documentation files in other directories
|
||||
are in the public domain.
|
||||
.
|
||||
- Translated messages are in the public domain.
|
||||
.
|
||||
- The build system contains public domain files, and files that
|
||||
are under GNU GPLv2+ or GNU GPLv3+. None of these files end up
|
||||
in the binaries being built.
|
||||
.
|
||||
- Test files and test code in the tests directory, and debugging
|
||||
utilities in the debug directory are in the public domain.
|
||||
.
|
||||
- The extra directory may contain public domain files, and files
|
||||
that are under various free software licenses.
|
||||
.
|
||||
You can do whatever you want with the files that have been put into
|
||||
the public domain. If you find public domain legally problematic,
|
||||
take the previous sentence as a license grant. If you still find
|
||||
the lack of copyright legally problematic, you have too many
|
||||
lawyers.
|
||||
.
|
||||
As usual, this software is provided "as is", without any warranty.
|
||||
.
|
||||
If you copy significant amounts of public domain code from XZ Utils
|
||||
into your project, acknowledging this somewhere in your software is
|
||||
polite (especially if it is proprietary, non-free software), but
|
||||
naturally it is not legally required. Here is an example of a good
|
||||
notice to put into "about box" or into documentation:
|
||||
.
|
||||
This software includes code from XZ Utils <http://tukaani.org/xz/>.
|
||||
.
|
||||
The following license texts are included in the following files:
|
||||
- COPYING.LGPLv2.1: GNU Lesser General Public License version 2.1
|
||||
- COPYING.GPLv2: GNU General Public License version 2
|
||||
- COPYING.GPLv3: GNU General Public License version 3
|
||||
.
|
||||
Note that the toolchain (compiler, linker etc.) may add some code
|
||||
pieces that are copyrighted. Thus, it is possible that e.g. liblzma
|
||||
binary wouldn't actually be in the public domain in its entirety
|
||||
even though it contains no copyrighted code from the XZ Utils source
|
||||
package.
|
||||
.
|
||||
If you have questions, don't hesitate to ask the author(s) for more
|
||||
information.
|
||||
|
||||
Files: *
|
||||
Copyright: 2006-2012, Lasse Collin
|
||||
1999-2008, Igor Pavlov
|
||||
2006, Ville Koskinen
|
||||
1998, Steve Reid
|
||||
2000, Wei Dai
|
||||
2003, Kevin Springle
|
||||
2009, Jonathan Nieder
|
||||
2010, Anders F Björklund
|
||||
License: PD
|
||||
This file has been put in the public domain.
|
||||
You can do whatever you want with this file.
|
||||
Comment:
|
||||
From: Lasse Collin <lasse.collin@tukaani.org>
|
||||
To: Jonathan Nieder <jrnieder@gmail.com>
|
||||
Subject: Re: XZ utils for Debian
|
||||
Date: Sun, 19 Jul 2009 13:28:23 +0300
|
||||
Message-Id: <200907191328.23816.lasse.collin@tukaani.org>
|
||||
.
|
||||
[...]
|
||||
.
|
||||
> AUTHORS, ChangeLog, COPYING, README, THANKS, TODO,
|
||||
> dos/README, windows/README
|
||||
.
|
||||
COPYING says that most docs are in the public domain. Maybe that's not
|
||||
clear enough, but on the other hand it looks a bit stupid to put
|
||||
copyright information in tiny and relatively small docs like README.
|
||||
.
|
||||
I don't dare to say that _all_ XZ Utils specific docs are in the public
|
||||
domain unless otherwise mentioned in the file. I'm including PDF files
|
||||
generated by groff + ps2pdf, and some day I might include Doxygen-
|
||||
generated HTML docs too. Those don't include any copyright notices, but
|
||||
it seems likely that groff + ps2pdf or at least Doxygen put some
|
||||
copyrighted content into the generated files.
|
||||
|
||||
Files: INSTALL NEWS PACKAGERS
|
||||
windows/README-Windows.txt
|
||||
windows/INSTALL-Windows.txt
|
||||
Copyright: 2009-2010, Lasse Collin
|
||||
License: probably-PD
|
||||
See the note on AUTHORS, README, and so on above.
|
||||
|
||||
Files: src/scripts/* lib/* extra/scanlzma/scanlzma.c
|
||||
Copyright: © 1993, Jean-loup Gailly
|
||||
© 1989-1994, 1996-1999, 2001-2007, Free Software Foundation, Inc.
|
||||
© 2006 Timo Lindfors
|
||||
2005, Charles Levert
|
||||
2005, 2009, Lasse Collin
|
||||
2009, Andrew Dudman
|
||||
Other-Authors: Paul Eggert, Ulrich Drepper
|
||||
License: GPL-2+
|
||||
|
||||
Files: src/scripts/Makefile.am src/scripts/xzless.1
|
||||
Copyright: 2009, Andrew Dudman
|
||||
2009, Lasse Collin
|
||||
License: PD
|
||||
This file has been put in the public domain.
|
||||
You can do whatever you want with this file.
|
||||
|
||||
Files: doc/examples/xz_pipe_comp.c doc/examples/xz_pipe_decomp.c
|
||||
Copyright: 2010, Daniel Mealha Cabrita
|
||||
License: PD
|
||||
Not copyrighted -- provided to the public domain.
|
||||
|
||||
Files: lib/getopt.c lib/getopt1.c lib/getopt.in.h
|
||||
Copyright: © 1987-2007 Free Software Foundation, Inc.
|
||||
Other-Authors: Ulrich Drepper
|
||||
License: LGPL-2.1+
|
||||
|
||||
Files: m4/getopt.m4 m4/posix-shell.m4
|
||||
Copyright: © 2002-2006, 2008 Free Software Foundation, Inc.
|
||||
© 2007-2008 Free Software Foundation, Inc.
|
||||
Other-Authors: Bruno Haible, Paul Eggert
|
||||
License: permissive-fsf
|
||||
|
||||
Files: m4/acx_pthread.m4
|
||||
Copyright: © 2008, Steven G. Johnson <stevenj@alum.mit.edu>
|
||||
License: Autoconf
|
||||
|
||||
Files: Doxyfile.in
|
||||
Copyright: © 1997-2007 by Dimitri van Heesch
|
||||
Origin: Doxygen 1.4.7
|
||||
License: GPL-2
|
||||
|
||||
Files: src/liblzma/check/crc32_table_?e.h
|
||||
src/liblzma/check/crc64_table_?e.h
|
||||
src/liblzma/lzma/fastpos_table.c
|
||||
src/liblzma/rangecoder/price_table.c
|
||||
Copyright: none, automatically generated data
|
||||
Generated-With:
|
||||
src/liblzma/check/crc32_tablegen.c
|
||||
src/liblzma/check/crc64_tablegen.c
|
||||
src/liblzma/lzma/fastpos_tablegen.c
|
||||
src/liblzma/rangecoder/price_tablegen.c
|
||||
License: none
|
||||
No copyright to license.
|
||||
|
||||
Files: .gitignore m4/.gitignore po/.gitignore po/LINGUAS po/POTFILES.in
|
||||
Copyright: none; these are just short lists.
|
||||
License: none
|
||||
No copyright to license.
|
||||
|
||||
Files: tests/compress_prepared_bcj_*
|
||||
Copyright: 2008-2009, Lasse Collin
|
||||
Source-Code: tests/bcj_test.c
|
||||
License: PD
|
||||
This file has been put into the public domain.
|
||||
You can do whatever you want with this file.
|
||||
Comment:
|
||||
changelog.gz (commit 975d8fd) explains:
|
||||
.
|
||||
Recreated the BCJ test files for x86 and SPARC. The old files
|
||||
were linked with crt*.o, which are copyrighted, and thus the
|
||||
old test files were not in the public domain as a whole. They
|
||||
are freely distributable though, but it is better to be careful
|
||||
and avoid including any copyrighted pieces in the test files.
|
||||
The new files are just compiled and assembled object files,
|
||||
and thus don't contain any copyrighted code.
|
||||
|
||||
Files: po/cs.po po/de.po po/fr.po
|
||||
Copyright: 2010, Marek Černocký
|
||||
2010, Andre Noll
|
||||
2011, Adrien Nader
|
||||
License: PD
|
||||
This file is put in the public domain.
|
||||
|
||||
Files: po/it.po po/pl.po
|
||||
Copyright: 2009, 2010, Gruppo traduzione italiano di Ubuntu-it
|
||||
2010, Lorenzo De Liso
|
||||
2009, 2010, 2011, Milo Casagrande
|
||||
2011, Jakub Bogusz
|
||||
License: PD
|
||||
This file is in the public domain
|
||||
|
||||
Files: INSTALL.generic
|
||||
Copyright: © 1994, 1995, 1996, 1999, 2000, 2001, 2002, 2004, 2005,
|
||||
2006, 2007, 2008, 2009, 2010 Free Software Foundation, Inc.
|
||||
License: permissive-nowarranty
|
||||
|
||||
Files: dos/config.h
|
||||
Copyright: © 1992, 1993, 1994, 1999, 2000, 2001, 2002, 2005
|
||||
Free Software Foundation, Inc.
|
||||
2007-2010, Lasse Collin
|
||||
Other-Authors: Roland McGrath, Akim Demaille, Paul Eggert,
|
||||
David Mackenzie, Bruno Haible, and many others.
|
||||
Origin: configure.ac from XZ Utils,
|
||||
visibility.m4 serial 1 (gettext-0.15),
|
||||
Autoconf 2.52g
|
||||
License: config-h
|
||||
configure.ac:
|
||||
.
|
||||
# Author: Lasse Collin
|
||||
#
|
||||
# This file has been put into the public domain.
|
||||
# You can do whatever you want with this file.
|
||||
.
|
||||
visibility.m4:
|
||||
.
|
||||
dnl Copyright (C) 2005 Free Software Foundation, Inc.
|
||||
dnl This file is free software; the Free Software Foundation
|
||||
dnl gives unlimited permission to copy and/or distribute it,
|
||||
dnl with or without modifications, as long as this notice is preserved.
|
||||
.
|
||||
dnl From Bruno Haible.
|
||||
.
|
||||
comments from Autoconf 2.52g:
|
||||
.
|
||||
# Copyright 1992, 1993, 1994, 1999, 2000, 2001, 2002
|
||||
# Free Software Foundation, Inc.
|
||||
.
|
||||
[...]
|
||||
.
|
||||
# As a special exception, the Free Software Foundation gives unlimited
|
||||
# permission to copy, distribute and modify the configure scripts that
|
||||
# are the output of Autoconf. You need not follow the terms of the GNU
|
||||
# General Public License when using or distributing such scripts, even
|
||||
# though portions of the text of Autoconf appear in them. The GNU
|
||||
# General Public License (GPL) does govern all other use of the material
|
||||
# that constitutes the Autoconf program.
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General Public
|
||||
License version 2 can be found in ‘/usr/share/common-licenses/GPL-2’.
|
||||
dos/config.h was generated with autoheader, which tells Autoconf to
|
||||
output a script to generate a config.h file and then runs it.
|
||||
|
||||
Files: po/Makevars
|
||||
Origin: gettext-runtime/po/Makevars (gettext-0.12)
|
||||
Copyright: © 2003 Free Software Foundation, Inc.
|
||||
Authors: Bruno Haible
|
||||
License: LGPL-2.1+
|
||||
The gettext-runtime package is under the LGPL, see files intl/COPYING.LIB-2.0
|
||||
and intl/COPYING.LIB-2.1.
|
||||
.
|
||||
On Debian systems, the complete text of intl/COPYING.LIB-2.0 from
|
||||
gettext-runtime 0.12 can be found in ‘/usr/share/common-licenses/LGPL-2’
|
||||
and the text of intl/COPYING.LIB-2.1 can be found in
|
||||
‘/usr/share/common-licenses/LGPL-2.1’.
|
||||
.
|
||||
po/Makevars consists mostly of helpful comments and does not contain a
|
||||
copyright and license notice.
|
||||
|
||||
Files: COPYING.GPLv2 COPYING.GPLv3 COPYING.LGPLv2.1
|
||||
Copyright: © 1989, 1991, 1999, 2007 Free Software Foundation, Inc.
|
||||
License: noderivs
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Files: debian/*
|
||||
Copyright: 2009-2012, Jonathan Nieder
|
||||
License: PD-debian
|
||||
The Debian packaging files are in the public domain.
|
||||
You may freely use, modify, distribute, and relicense them.
|
||||
|
||||
License: LGPL-2.1+
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1, or (at your option)
|
||||
any later version.
|
||||
.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
.
|
||||
On Debian systems, the complete text of the GNU Lesser General Public
|
||||
License version 2.1 can be found in ‘/usr/share/common-licenses/LGPL-2.1’.
|
||||
|
||||
License: GPL-2
|
||||
Permission to use, copy, modify, and distribute this software and its
|
||||
documentation under the terms of the GNU General Public License is
|
||||
hereby granted. No representations are made about the suitability of
|
||||
this software for any purpose. It is provided "as is" without express
|
||||
or implied warranty. See the GNU General Public License for more
|
||||
details.
|
||||
.
|
||||
Documents produced by doxygen are derivative works derived from the
|
||||
input used in their production; they are not affected by this license.
|
||||
.
|
||||
On Debian systems, the complete text of the version of the GNU General
|
||||
Public License distributed with Doxygen can be found in
|
||||
‘/usr/share/common-licenses/GPL-2’.
|
||||
|
||||
License: GPL-2+
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General Public License
|
||||
version 2 can be found in ‘/usr/share/common-licenses/GPL-2’.
|
||||
|
||||
License: Autoconf
|
||||
This program is free software: you can redistribute it and/or modify it
|
||||
under the terms of the GNU General Public License as published by the
|
||||
Free Software Foundation, either version 3 of the License, or (at your
|
||||
option) any later version.
|
||||
.
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
.
|
||||
As a special exception, the respective Autoconf Macro's copyright owner
|
||||
gives unlimited permission to copy, distribute and modify the configure
|
||||
scripts that are the output of Autoconf when processing the Macro. You
|
||||
need not follow the terms of the GNU General Public License when using
|
||||
or distributing such scripts, even though portions of the text of the
|
||||
Macro appear in them. The GNU General Public License (GPL) does govern
|
||||
all other use of the material that constitutes the Autoconf Macro.
|
||||
.
|
||||
This special exception to the GPL applies to versions of the Autoconf
|
||||
Macro released by the Autoconf Archive. When you make and distribute a
|
||||
modified version of the Autoconf Macro, you may extend this special
|
||||
exception to the GPL to apply to your modified version as well.
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General Public
|
||||
License version 3 can be found in ‘/usr/share/common-licenses/GPL-3’.
|
||||
|
||||
License: permissive-fsf
|
||||
This file is free software; the Free Software Foundation
|
||||
gives unlimited permission to copy and/or distribute it,
|
||||
with or without modifications, as long as this notice is preserved.
|
||||
|
||||
License: permissive-nowarranty
|
||||
Copying and distribution of this file, with or without modification,
|
||||
are permitted in any medium without royalty provided the copyright
|
||||
notice and this notice are preserved. This file is offered as-is,
|
||||
without warranty of any kind.
|
||||
@@ -0,0 +1,13 @@
|
||||
Files: src/js/editline/*
|
||||
Copyright: 1993, John Doe
|
||||
1993, Joe Average
|
||||
License: MPL-1.1 or GPL-2 or LGPL-2.1
|
||||
|
||||
License: MPL-1.1
|
||||
[LICENSE TEXT]
|
||||
|
||||
License: GPL-2
|
||||
[LICENSE TEXT]
|
||||
|
||||
License: LGPL-2.1
|
||||
[LICENSE TEXT]
|
||||
@@ -0,0 +1,2 @@
|
||||
FROM scratch
|
||||
COPY . .
|
||||
@@ -0,0 +1,43 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: auditd
|
||||
Source: https://people.redhat.com/sgrubb/audit/
|
||||
|
||||
Files: *
|
||||
Copyright: 2012-2016 Steve Grubb <sgrubb@redhat.com>
|
||||
2006-2012 Rik Faith
|
||||
License: GPL-2
|
||||
|
||||
Files: src/libev/*
|
||||
Copyright: 2007-2009 Marc Alexamder Lehmann
|
||||
License: GPL-2
|
||||
|
||||
Files: lib/*
|
||||
Copyright: 2005-2008 Steve Grubb <sgrubb@redhat.com>
|
||||
License: LGPL-2.1
|
||||
The audit daemon's library libaudit.* is released under LGPL
|
||||
so that it may be linked with 3rd party software.
|
||||
.
|
||||
On Debian systems, refer to /usr/share/common-licenses/LGPL-2.1
|
||||
for the complete text of the GNU Lesser General Public License.
|
||||
|
||||
Files: debian/*
|
||||
Copyright: 2007-2011 Philipp Matthias Hahn <pmhahn@debian.org>
|
||||
2012-2016 Laurent Bigonville <bigon@debian.org>
|
||||
License: GPL-2
|
||||
|
||||
License: GPL-2
|
||||
This package is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License version 2,
|
||||
as published by the Free Software Foundation.
|
||||
.
|
||||
This package is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this package; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License can be found in `/usr/share/common-licenses/GPL-1'.
|
||||
@@ -0,0 +1,4 @@
|
||||
55f905631797551d4d936a34c7e73474 lib/x86_64-linux-gnu/libz.so.1.2.11
|
||||
cede84bda30d2380217f97753c8ccf3a usr/share/doc/zlib1g/changelog.Debian.gz
|
||||
f3c9dafa6da7992c47328b4464f6d122 usr/share/doc/zlib1g/changelog.gz
|
||||
a4fae96070439a5209a62ae5b8017ab2 usr/share/doc/zlib1g/copyright
|
||||
@@ -0,0 +1,21 @@
|
||||
Package: libpam-runtime
|
||||
Status: install ok installed
|
||||
Priority: required
|
||||
Section: admin
|
||||
Installed-Size: 1016
|
||||
Maintainer: Steve Langasek <vorlon@debian.org>
|
||||
Architecture: all
|
||||
Multi-Arch: foreign
|
||||
Source: pam
|
||||
Version: 1.1.8-3.6
|
||||
Replaces: libpam0g-dev, libpam0g-util
|
||||
Depends: debconf (>= 0.5) | debconf-2.0, debconf (>= 1.5.19) | cdebconf, libpam-modules (>= 1.0.1-6)
|
||||
Conflicts: libpam0g-util
|
||||
Conffiles:
|
||||
/etc/pam.conf 87fc76f18e98ee7d3848f6b81b3391e5
|
||||
/etc/pam.d/other 31aa7f2181889ffb00b87df4126d1701
|
||||
Description: Runtime support for the PAM library
|
||||
Contains configuration files and directories required for
|
||||
authentication to work on Debian systems. This package is required
|
||||
on almost all installations.
|
||||
Homepage: http://www.linux-pam.org/
|
||||
@@ -0,0 +1,4 @@
|
||||
55f905631797551d4d936a34c7e73474 lib/x86_64-linux-gnu/libz.so.1.2.11
|
||||
cede84bda30d2380217f97753c8ccf3a usr/share/doc/zlib1g/changelog.Debian.gz
|
||||
f3c9dafa6da7992c47328b4464f6d122 usr/share/doc/zlib1g/changelog.gz
|
||||
a4fae96070439a5209a62ae5b8017ab2 usr/share/doc/zlib1g/copyright
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
Package dpkg provides a concrete Cataloger implementation for Debian package DB status files.
|
||||
*/
|
||||
package dpkg
|
||||
|
||||
import (
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/syft/cataloger/common"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
)
|
||||
|
||||
// Cataloger catalogs pkg.DebPkg Package Types defined in DPKG status files.
|
||||
type Cataloger struct {
|
||||
cataloger common.GenericCataloger
|
||||
}
|
||||
|
||||
// New returns a new Deb package cataloger object.
|
||||
func New() *Cataloger {
|
||||
globParsers := map[string]common.ParserFn{
|
||||
"**/var/lib/dpkg/status": parseDpkgStatus,
|
||||
}
|
||||
|
||||
return &Cataloger{
|
||||
cataloger: common.NewGenericCataloger(nil, globParsers),
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns a string that uniquely describes this cataloger.
|
||||
func (a *Cataloger) Name() string {
|
||||
return "dpkg-cataloger"
|
||||
}
|
||||
|
||||
// SelectFiles returns a set of discovered DPKG status files from the user content source.
|
||||
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
||||
return a.cataloger.SelectFiles(resolver)
|
||||
}
|
||||
|
||||
// Catalog returns the Packages indexed from all DPKG status files discovered.
|
||||
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
||||
return a.cataloger.Catalog(contents, a.Name())
|
||||
}
|
||||
@@ -4,39 +4,14 @@ Package golang provides a concrete Cataloger implementation for go.mod files.
|
||||
package golang
|
||||
|
||||
import (
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/syft/cataloger/common"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
)
|
||||
|
||||
// Cataloger catalogs pkg.GoModulePkg Package Types defined in go.mod files.
|
||||
type Cataloger struct {
|
||||
cataloger common.GenericCataloger
|
||||
}
|
||||
|
||||
// New returns a new Go module cataloger object.
|
||||
func New() *Cataloger {
|
||||
// NewGoModCataloger returns a new Go module cataloger object.
|
||||
func NewGoModCataloger() *common.GenericCataloger {
|
||||
globParsers := map[string]common.ParserFn{
|
||||
"**/go.mod": parseGoMod,
|
||||
}
|
||||
|
||||
return &Cataloger{
|
||||
cataloger: common.NewGenericCataloger(nil, globParsers),
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns a string that uniquely describes this cataloger.
|
||||
func (a *Cataloger) Name() string {
|
||||
return "go-cataloger"
|
||||
}
|
||||
|
||||
// SelectFiles returns a set of discovered go.mod files from the user content source.
|
||||
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
||||
return a.cataloger.SelectFiles(resolver)
|
||||
}
|
||||
|
||||
// Catalog returns the Packages indexed from all go.mod files discovered.
|
||||
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
||||
return a.cataloger.Catalog(contents, a.Name())
|
||||
return common.NewGenericCataloger(nil, globParsers, "go-cataloger")
|
||||
}
|
||||
|
||||
@@ -6,9 +6,8 @@ import (
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
|
||||
"github.com/rogpeppe/go-internal/modfile"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"golang.org/x/mod/modfile"
|
||||
)
|
||||
|
||||
// parseGoMod takes a go.mod and lists all packages discovered.
|
||||
|
||||
@@ -10,36 +10,100 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
// match examples:
|
||||
// pkg-extra-field-4.3.2-rc1 --> match(name=pkg-extra-field version=4.3.2-rc1)
|
||||
// pkg-extra-field-4.3-rc1 --> match(name=pkg-extra-field version=4.3-rc1)
|
||||
// pkg-extra-field-4.3 --> match(name=pkg-extra-field version=4.3)
|
||||
var versionPattern = regexp.MustCompile(`(?P<name>.+)-(?P<version>(\d+\.)?(\d+\.)?(\*|\d+)(-[a-zA-Z0-9\-\.]+)*)`)
|
||||
// nameAndVersionPattern finds the package name and version (as named capture
|
||||
// groups) in a string. The pattern's strategy is to start at the beginning of
|
||||
// the string, and for every next dash-delimited group, consider the group to be
|
||||
// a continuation of the package name, unless the group begins with a number or
|
||||
// matches any of a specified set of "version-indicating" patterns. When a given
|
||||
// group meets this criterion, consider the group and the remainder of the
|
||||
// string to be the package version.
|
||||
//
|
||||
// Regex components of note:
|
||||
//
|
||||
// (?Ui) ... Sets the "U" and the "i" options for this Regex —— (ungreedy,
|
||||
// and case-insensitive, respectively). "Ungreedy" is important so that the '*' that trails the package name
|
||||
// component doesn't consume the rest of the string.
|
||||
//
|
||||
// [[:alpha:]][[:word:]]* ... Matches any word, and the word can include "word" characters (
|
||||
// which includes numbers and underscores), but the first character of the word MUST be a letter.
|
||||
//
|
||||
// (?:\.[[:alpha:]][[:word:]]*)* ... This looks redundant, but it's not. It
|
||||
// extends the previous pattern such that the net effect of both components is
|
||||
// that words can also include a period and more words (thus, when combined, not
|
||||
// only is "something" matched, but so is "com.prefix.thing"
|
||||
//
|
||||
// (?:\d.*|(?:build\d*.*)|(?:rc?\d+(?:^[[:alpha:]].*)?)) ...
|
||||
// This match group covers the "version-indicating" patterns mentioned in the above description. Given the pipes (
|
||||
// '|'), this functions as a series of 'OR'-joined conditions:
|
||||
//
|
||||
// \d.* ... "If it starts with a numeric digit, this is a version, no matter what follows."
|
||||
// build\d*.* ... "If it starts with "build" and then a numeric digit immediately after, this is a version."
|
||||
// rc?\d+(?:^[[:alpha:]].*)? ... "If it starts with "r" or "rc" and then one or more numeric digits immediately
|
||||
// after, but no alpha characters right after that (in the same word), this is a version."
|
||||
//
|
||||
// Match examples:
|
||||
// some-package-4.0.1 --> name="some-package", version="4.0.1"
|
||||
// prefix.thing-4 --> name="prefix.thing", version="4"
|
||||
// my-http2-server-5 --> name="my-http2-server", version="5"
|
||||
// jetpack-build235-rc5 --> name="jetpack", version="build2.0-rc5"
|
||||
// ironman-r4-2009 --> name="ironman", version="r4-2009"
|
||||
var nameAndVersionPattern = regexp.MustCompile(`(?Ui)^(?P<name>(?:[[:alpha:]][[:word:]]*(?:\.[[:alpha:]][[:word:]]*)*-?)+)(?:-(?P<version>(?:\d.*|(?:build\d*.*)|(?:rc?\d+(?:^[[:alpha:]].*)?))))?$`)
|
||||
|
||||
type archiveFilename struct {
|
||||
raw string
|
||||
fields []map[string]string
|
||||
raw string
|
||||
name string
|
||||
version string
|
||||
}
|
||||
|
||||
// TODO: Remove this method once we're using Go 1.15+.
|
||||
//
|
||||
// Go 1.15 introduces a `SubexpIndex` method for the Regexp type that would let
|
||||
// this code be made more elegant. Once we've reached 1.15, we should eliminate
|
||||
// this function in favor of that method.
|
||||
func subexpIndex(re *regexp.Regexp, name string) int {
|
||||
for i, subexpName := range re.SubexpNames() {
|
||||
if subexpName == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func getSubexp(matches []string, subexpName string, re *regexp.Regexp, raw string) string {
|
||||
if len(matches) < 1 {
|
||||
log.Warnf("unexpectedly empty matches for archive '%s'", raw)
|
||||
return ""
|
||||
}
|
||||
|
||||
index := subexpIndex(re, subexpName)
|
||||
if index < 1 {
|
||||
log.Warnf("unexpected index of '%s' capture group for Java archive '%s'", subexpName, raw)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Prevent out-of-range panic
|
||||
if len(matches) < index+1 {
|
||||
log.Warnf("no match found for '%s' in '%s'", subexpName, matches[0])
|
||||
return ""
|
||||
}
|
||||
|
||||
return matches[index]
|
||||
}
|
||||
|
||||
func newJavaArchiveFilename(raw string) archiveFilename {
|
||||
// trim the file extension and remove any path prefixes
|
||||
name := strings.TrimSuffix(filepath.Base(raw), filepath.Ext(raw))
|
||||
cleanedFileName := strings.TrimSuffix(filepath.Base(raw), filepath.Ext(raw))
|
||||
|
||||
matches := versionPattern.FindAllStringSubmatch(name, -1)
|
||||
fields := make([]map[string]string, 0)
|
||||
for _, match := range matches {
|
||||
item := make(map[string]string)
|
||||
for i, name := range versionPattern.SubexpNames() {
|
||||
if i != 0 && name != "" {
|
||||
item[name] = match[i]
|
||||
}
|
||||
}
|
||||
fields = append(fields, item)
|
||||
}
|
||||
matches := nameAndVersionPattern.FindStringSubmatch(cleanedFileName)
|
||||
|
||||
name := getSubexp(matches, "name", nameAndVersionPattern, raw)
|
||||
version := getSubexp(matches, "version", nameAndVersionPattern, raw)
|
||||
|
||||
return archiveFilename{
|
||||
raw: raw,
|
||||
fields: fields,
|
||||
raw: raw,
|
||||
name: name,
|
||||
version: version,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,23 +121,3 @@ func (a archiveFilename) pkgType() pkg.Type {
|
||||
return pkg.UnknownPkg
|
||||
}
|
||||
}
|
||||
|
||||
func (a archiveFilename) version() string {
|
||||
if len(a.fields) > 1 {
|
||||
log.Errorf("discovered multiple name-version pairings from %q: %+v", a.raw, a.fields)
|
||||
return ""
|
||||
} else if len(a.fields) < 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return a.fields[0]["version"]
|
||||
}
|
||||
|
||||
func (a archiveFilename) name() string {
|
||||
// there should be only one name, if there is more or less then something is wrong
|
||||
if len(a.fields) != 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return a.fields[0]["name"]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package java
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
|
||||
@@ -56,30 +57,98 @@ func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
|
||||
name: "pkg-extra-field-maven",
|
||||
ty: pkg.JenkinsPluginPkg,
|
||||
},
|
||||
{
|
||||
filename: "/some/path-with-version-5.4.3/wagon-webdav-1.0.2-beta-2.2.3a-hudson.jar",
|
||||
version: "1.0.2-beta-2.2.3a-hudson",
|
||||
extension: "jar",
|
||||
name: "wagon-webdav",
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
{
|
||||
filename: "/some/path-with-version-5.4.3/wagon-webdav-1.0.2-beta-2.2.3-hudson.jar",
|
||||
version: "1.0.2-beta-2.2.3-hudson",
|
||||
extension: "jar",
|
||||
name: "wagon-webdav",
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
{
|
||||
filename: "/some/path-with-version-5.4.3/windows-remote-command-1.0.jar",
|
||||
version: "1.0",
|
||||
extension: "jar",
|
||||
name: "windows-remote-command",
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
{
|
||||
filename: "/some/path-with-version-5.4.3/wagon-http-lightweight-1.0.5-beta-2.jar",
|
||||
version: "1.0.5-beta-2",
|
||||
extension: "jar",
|
||||
name: "wagon-http-lightweight",
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
{
|
||||
filename: "/hudson.war:WEB-INF/lib/commons-jelly-1.1-hudson-20100305.jar",
|
||||
version: "1.1-hudson-20100305",
|
||||
extension: "jar",
|
||||
name: "commons-jelly",
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
{
|
||||
filename: "/hudson.war:WEB-INF/lib/jtidy-4aug2000r7-dev-hudson-1.jar",
|
||||
version: "4aug2000r7-dev-hudson-1",
|
||||
extension: "jar",
|
||||
name: "jtidy",
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
{
|
||||
filename: "/hudson.war:WEB-INF/lib/trilead-ssh2-build212-hudson-5.jar",
|
||||
version: "build212-hudson-5",
|
||||
extension: "jar",
|
||||
name: "trilead-ssh2",
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
{
|
||||
filename: "/hudson.war:WEB-INF/lib/guava-r06.jar",
|
||||
version: "r06",
|
||||
extension: "jar",
|
||||
name: "guava",
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
{
|
||||
filename: "BOOT-INF/lib/spring-data-r2dbc-1.1.0.RELEASE.jar", // Regression: https://github.com/anchore/syft/issues/255
|
||||
version: "1.1.0.RELEASE",
|
||||
extension: "jar",
|
||||
name: "spring-data-r2dbc",
|
||||
ty: pkg.JavaPkg,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.filename, func(t *testing.T) {
|
||||
obj := newJavaArchiveFilename(test.filename)
|
||||
|
||||
version := obj.version()
|
||||
ty := obj.pkgType()
|
||||
if ty != test.ty {
|
||||
t.Errorf("mismatched type: %+v != %v", ty, test.ty)
|
||||
}
|
||||
|
||||
version := obj.version
|
||||
if version != test.version {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(version, test.version, true)
|
||||
diffs := dmp.DiffMain(test.version, version, true)
|
||||
t.Errorf("mismatched version:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
|
||||
extension := obj.extension()
|
||||
if extension != test.extension {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(extension, test.extension, true)
|
||||
diffs := dmp.DiffMain(test.extension, extension, true)
|
||||
t.Errorf("mismatched extension:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
|
||||
name := obj.name()
|
||||
name := obj.name
|
||||
if name != test.name {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(name, test.name, true)
|
||||
diffs := dmp.DiffMain(test.name, name, true)
|
||||
t.Errorf("mismatched name:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/syft/cataloger/common"
|
||||
@@ -64,13 +66,17 @@ func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested boo
|
||||
return nil, cleanupFn, fmt.Errorf("unable to read files from java archive: %w", err)
|
||||
}
|
||||
|
||||
// fetch the last element of the virtual path
|
||||
virtualElements := strings.Split(virtualPath, ":")
|
||||
currentFilepath := virtualElements[len(virtualElements)-1]
|
||||
|
||||
return &archiveParser{
|
||||
discoveredPkgs: internal.NewStringSet(),
|
||||
fileManifest: fileManifest,
|
||||
virtualPath: virtualPath,
|
||||
archivePath: archivePath,
|
||||
contentPath: contentPath,
|
||||
fileInfo: newJavaArchiveFilename(virtualPath),
|
||||
fileInfo: newJavaArchiveFilename(currentFilepath),
|
||||
detectNested: detectNested,
|
||||
}, cleanupFn, nil
|
||||
}
|
||||
@@ -118,7 +124,7 @@ func (j *archiveParser) parse() ([]pkg.Package, error) {
|
||||
// discoverMainPackage parses the root Java manifest used as the parent package to all discovered nested packages.
|
||||
func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
|
||||
// search and parse java manifest files
|
||||
manifestMatches := j.fileManifest.GlobMatch(manifestPath)
|
||||
manifestMatches := j.fileManifest.GlobMatch(manifestGlob)
|
||||
if len(manifestMatches) > 1 {
|
||||
return nil, fmt.Errorf("found multiple manifests in the jar: %+v", manifestMatches)
|
||||
} else if len(manifestMatches) == 0 {
|
||||
@@ -134,24 +140,27 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
|
||||
|
||||
// parse the manifest file into a rich object
|
||||
manifestContents := contents[manifestMatches[0]]
|
||||
manifest, err := parseJavaManifest(strings.NewReader(manifestContents))
|
||||
manifest, err := parseJavaManifest(j.archivePath, strings.NewReader(manifestContents))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse java manifest (%s): %w", j.virtualPath, err)
|
||||
}
|
||||
|
||||
return &pkg.Package{
|
||||
Name: selectName(manifest, j.fileInfo),
|
||||
Version: selectVersion(manifest, j.fileInfo),
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JavaPkg,
|
||||
Name: selectName(manifest, j.fileInfo),
|
||||
Version: selectVersion(manifest, j.fileInfo),
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JavaPkg,
|
||||
MetadataType: pkg.JavaMetadataType,
|
||||
Metadata: pkg.JavaMetadata{
|
||||
Manifest: manifest,
|
||||
VirtualPath: j.virtualPath,
|
||||
Manifest: manifest,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// discoverPkgsFromPomProperties parses Maven POM properties for a given parent package, returning all listed Java packages found and
|
||||
// associating each discovered package to the given parent package.
|
||||
// nolint:funlen,gocognit
|
||||
func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([]pkg.Package, error) {
|
||||
var pkgs = make([]pkg.Package, 0)
|
||||
parentKey := uniquePkgKey(parentPkg)
|
||||
@@ -173,13 +182,22 @@ func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([
|
||||
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
|
||||
|
||||
// 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
|
||||
|
||||
// discovered props = new package
|
||||
p := pkg.Package{
|
||||
Name: propsObj.ArtifactID,
|
||||
Version: propsObj.Version,
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JavaPkg,
|
||||
Name: propsObj.ArtifactID,
|
||||
Version: propsObj.Version,
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JavaPkg,
|
||||
MetadataType: pkg.JavaMetadataType,
|
||||
Metadata: pkg.JavaMetadata{
|
||||
VirtualPath: virtualPath,
|
||||
PomProperties: propsObj,
|
||||
Parent: parentPkg,
|
||||
},
|
||||
@@ -187,16 +205,35 @@ func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([
|
||||
|
||||
pkgKey := uniquePkgKey(&p)
|
||||
|
||||
if !j.discoveredPkgs.Contains(pkgKey) {
|
||||
// only keep packages we haven't seen yet
|
||||
pkgs = append(pkgs, p)
|
||||
} else if pkgKey == parentKey {
|
||||
// the name/version pair matches...
|
||||
matchesParentPkg := pkgKey == parentKey
|
||||
|
||||
// the virtual path matches...
|
||||
matchesParentPkg = matchesParentPkg || parentPkg.Metadata.(pkg.JavaMetadata).VirtualPath == virtualPath
|
||||
|
||||
// the pom artifactId has the parent name or vice versa
|
||||
if propsObj.ArtifactID != "" {
|
||||
matchesParentPkg = matchesParentPkg || strings.Contains(parentPkg.Name, propsObj.ArtifactID) || strings.Contains(propsObj.ArtifactID, parentPkg.Name)
|
||||
}
|
||||
|
||||
if matchesParentPkg {
|
||||
// we've run across more information about our parent package, add this info to the parent package metadata
|
||||
// the pom properties is typically a better source of information for name and version than the manifest
|
||||
if p.Name != parentPkg.Name {
|
||||
parentPkg.Name = p.Name
|
||||
}
|
||||
if p.Version != parentPkg.Version {
|
||||
parentPkg.Version = p.Version
|
||||
}
|
||||
|
||||
parentMetadata, ok := parentPkg.Metadata.(pkg.JavaMetadata)
|
||||
if ok {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,18 +251,28 @@ func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) (
|
||||
}
|
||||
|
||||
// search and parse pom.properties files & fetch the contents
|
||||
readers, err := file.ExtractFromZipToUniqueTempFile(j.archivePath, j.contentPath, j.fileManifest.GlobMatch(archiveFormatGlobs...)...)
|
||||
openers, err := file.ExtractFromZipToUniqueTempFile(j.archivePath, j.contentPath, j.fileManifest.GlobMatch(archiveFormatGlobs...)...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to extract files from zip: %w", err)
|
||||
}
|
||||
|
||||
// discover nested artifacts
|
||||
for archivePath, archiveReader := range readers {
|
||||
nestedPath := fmt.Sprintf("%s:%s", j.virtualPath, archivePath)
|
||||
nestedPkgs, err := parseJavaArchive(nestedPath, archiveReader)
|
||||
for archivePath, archiveOpener := range openers {
|
||||
archiveReadCloser, err := archiveOpener.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open archived file from tempdir: %w", err)
|
||||
}
|
||||
nestedPath := fmt.Sprintf("%s:%s", j.virtualPath, archivePath)
|
||||
nestedPkgs, err := parseJavaArchive(nestedPath, archiveReadCloser)
|
||||
if err != nil {
|
||||
if closeErr := archiveReadCloser.Close(); closeErr != nil {
|
||||
log.Warnf("unable to close archived file from tempdir: %+v", closeErr)
|
||||
}
|
||||
return nil, fmt.Errorf("unable to process nested java archive (%s): %w", archivePath, err)
|
||||
}
|
||||
if err = archiveReadCloser.Close(); err != nil {
|
||||
return nil, fmt.Errorf("unable to close archived file from tempdir: %w", err)
|
||||
}
|
||||
|
||||
// attach the parent package to all discovered packages that are not already associated with a java archive
|
||||
for _, p := range nestedPkgs {
|
||||
|
||||
@@ -78,51 +78,6 @@ func generateJavaBuildFixture(t *testing.T, fixturePath string) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectName(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
manifest pkg.JavaManifest
|
||||
archive archiveFilename
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "name from Implementation-Title",
|
||||
archive: archiveFilename{},
|
||||
manifest: pkg.JavaManifest{
|
||||
Name: "",
|
||||
SpecTitle: "",
|
||||
ImplTitle: "maven-wrapper",
|
||||
},
|
||||
expected: "maven-wrapper",
|
||||
},
|
||||
{
|
||||
desc: "Implementation-Title does not override",
|
||||
manifest: pkg.JavaManifest{
|
||||
Name: "Foo",
|
||||
SpecTitle: "",
|
||||
ImplTitle: "maven-wrapper",
|
||||
},
|
||||
archive: archiveFilename{
|
||||
fields: []map[string]string{
|
||||
{"name": "omg"},
|
||||
},
|
||||
},
|
||||
expected: "omg",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
result := selectName(&test.manifest, test.archive)
|
||||
|
||||
if result != test.expected {
|
||||
t.Errorf("mismatch in names: '%s' != '%s'", result, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestParseJar(t *testing.T) {
|
||||
tests := []struct {
|
||||
fixture string
|
||||
@@ -137,17 +92,20 @@ func TestParseJar(t *testing.T) {
|
||||
},
|
||||
expected: map[string]pkg.Package{
|
||||
"example-jenkins-plugin": {
|
||||
Name: "example-jenkins-plugin",
|
||||
Version: "1.0-SNAPSHOT",
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JenkinsPluginPkg,
|
||||
Name: "example-jenkins-plugin",
|
||||
Version: "1.0-SNAPSHOT",
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JenkinsPluginPkg,
|
||||
MetadataType: pkg.JavaMetadataType,
|
||||
Metadata: pkg.JavaMetadata{
|
||||
VirtualPath: "test-fixtures/java-builds/packages/example-jenkins-plugin.hpi",
|
||||
Manifest: &pkg.JavaManifest{
|
||||
ManifestVersion: "1.0",
|
||||
SpecTitle: "The Jenkins Plugins Parent POM Project",
|
||||
ImplTitle: "example-jenkins-plugin",
|
||||
ImplVersion: "1.0-SNAPSHOT",
|
||||
Extra: map[string]string{
|
||||
Main: map[string]string{
|
||||
"Manifest-Version": "1.0",
|
||||
"Specification-Title": "The Jenkins Plugins Parent POM Project",
|
||||
"Implementation-Title": "example-jenkins-plugin",
|
||||
"Implementation-Version": "1.0-SNAPSHOT",
|
||||
// extra fields...
|
||||
"Archiver-Version": "Plexus Archiver",
|
||||
"Plugin-License-Url": "https://opensource.org/licenses/MIT",
|
||||
"Plugin-License-Name": "MIT License",
|
||||
@@ -172,6 +130,7 @@ func TestParseJar(t *testing.T) {
|
||||
GroupID: "io.jenkins.plugins",
|
||||
ArtifactID: "example-jenkins-plugin",
|
||||
Version: "1.0-SNAPSHOT",
|
||||
Extra: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -181,13 +140,17 @@ func TestParseJar(t *testing.T) {
|
||||
fixture: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar",
|
||||
expected: map[string]pkg.Package{
|
||||
"example-java-app-gradle": {
|
||||
Name: "example-java-app-gradle",
|
||||
Version: "0.1.0",
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JavaPkg,
|
||||
Name: "example-java-app-gradle",
|
||||
Version: "0.1.0",
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JavaPkg,
|
||||
MetadataType: pkg.JavaMetadataType,
|
||||
Metadata: pkg.JavaMetadata{
|
||||
VirtualPath: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar",
|
||||
Manifest: &pkg.JavaManifest{
|
||||
ManifestVersion: "1.0",
|
||||
Main: map[string]string{
|
||||
"Manifest-Version": "1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -200,14 +163,17 @@ func TestParseJar(t *testing.T) {
|
||||
},
|
||||
expected: map[string]pkg.Package{
|
||||
"example-java-app-maven": {
|
||||
Name: "example-java-app-maven",
|
||||
Version: "0.1.0",
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JavaPkg,
|
||||
Name: "example-java-app-maven",
|
||||
Version: "0.1.0",
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JavaPkg,
|
||||
MetadataType: pkg.JavaMetadataType,
|
||||
Metadata: pkg.JavaMetadata{
|
||||
VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar",
|
||||
Manifest: &pkg.JavaManifest{
|
||||
ManifestVersion: "1.0",
|
||||
Extra: map[string]string{
|
||||
Main: map[string]string{
|
||||
"Manifest-Version": "1.0",
|
||||
// extra fields...
|
||||
"Archiver-Version": "Plexus Archiver",
|
||||
"Created-By": "Apache Maven 3.6.3",
|
||||
"Built-By": "?",
|
||||
@@ -220,20 +186,26 @@ func TestParseJar(t *testing.T) {
|
||||
GroupID: "org.anchore",
|
||||
ArtifactID: "example-java-app-maven",
|
||||
Version: "0.1.0",
|
||||
Extra: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
"joda-time": {
|
||||
Name: "joda-time",
|
||||
Version: "2.9.2",
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JavaPkg,
|
||||
Name: "joda-time",
|
||||
Version: "2.9.2",
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JavaPkg,
|
||||
MetadataType: pkg.JavaMetadataType,
|
||||
Metadata: pkg.JavaMetadata{
|
||||
// ensure that nested packages with different names than that of the parent are appended as
|
||||
// a suffix on the virtual path
|
||||
VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar:joda-time",
|
||||
PomProperties: &pkg.PomProperties{
|
||||
Path: "META-INF/maven/joda-time/joda-time/pom.properties",
|
||||
GroupID: "joda-time",
|
||||
ArtifactID: "joda-time",
|
||||
Version: "2.9.2",
|
||||
Extra: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -295,11 +267,11 @@ func TestParseJar(t *testing.T) {
|
||||
metadata := a.Metadata.(pkg.JavaMetadata)
|
||||
metadata.Parent = nil
|
||||
|
||||
// ignore select fields
|
||||
// ignore select fields (only works for the main section)
|
||||
for _, field := range test.ignoreExtras {
|
||||
if metadata.Manifest != nil && metadata.Manifest.Extra != nil {
|
||||
if _, ok := metadata.Manifest.Extra[field]; ok {
|
||||
delete(metadata.Manifest.Extra, field)
|
||||
if metadata.Manifest != nil && metadata.Manifest.Main != nil {
|
||||
if _, ok := metadata.Manifest.Main[field]; ok {
|
||||
delete(metadata.Manifest.Main, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -527,7 +499,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
t.Fatalf("failed to parse java archive: %+v", err)
|
||||
}
|
||||
|
||||
nameVersionPairSet := internal.NewStringSet()
|
||||
expectedNameVersionPairSet := internal.NewStringSet()
|
||||
|
||||
makeKey := func(p *pkg.Package) string {
|
||||
if p == nil {
|
||||
@@ -537,23 +509,31 @@ func TestParseNestedJar(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, e := range test.expected {
|
||||
nameVersionPairSet.Add(makeKey(&e))
|
||||
expectedNameVersionPairSet.Add(makeKey(&e))
|
||||
}
|
||||
|
||||
if len(actual) != len(nameVersionPairSet) {
|
||||
for _, a := range actual {
|
||||
t.Log(" ", a)
|
||||
actualNameVersionPairSet := internal.NewStringSet()
|
||||
for _, a := range actual {
|
||||
key := makeKey(&a)
|
||||
actualNameVersionPairSet.Add(key)
|
||||
if !expectedNameVersionPairSet.Contains(key) {
|
||||
t.Errorf("extra package: %s", a)
|
||||
}
|
||||
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(nameVersionPairSet))
|
||||
}
|
||||
|
||||
for _, key := range expectedNameVersionPairSet.ToSlice() {
|
||||
if !actualNameVersionPairSet.Contains(key) {
|
||||
t.Errorf("missing package: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
if len(actual) != len(expectedNameVersionPairSet) {
|
||||
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(expectedNameVersionPairSet))
|
||||
}
|
||||
|
||||
for _, a := range actual {
|
||||
actualKey := makeKey(&a)
|
||||
|
||||
if !nameVersionPairSet.Contains(actualKey) {
|
||||
t.Errorf("unexpected pkg: %q", actualKey)
|
||||
}
|
||||
|
||||
metadata := a.Metadata.(pkg.JavaMetadata)
|
||||
if actualKey == "spring-boot|0.0.1-SNAPSHOT" {
|
||||
if metadata.Parent != nil {
|
||||
@@ -563,7 +543,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
if metadata.Parent == nil {
|
||||
t.Errorf("unassigned error for pkg=%q", actualKey)
|
||||
} else if makeKey(metadata.Parent) != "spring-boot|0.0.1-SNAPSHOT" {
|
||||
// NB: this is a hard-coded condition to simplify the test harness
|
||||
// NB: this is a hard-coded condition to simplify the test harness to account for https://github.com/micrometer-metrics/micrometer/issues/1785
|
||||
if a.Name == "pcollections" {
|
||||
if metadata.Parent.Name != "micrometer-core" {
|
||||
t.Errorf("nested 'pcollections' pkg has wrong parent: %q", metadata.Parent.Name)
|
||||
|
||||
@@ -4,40 +4,15 @@ Package java provides a concrete Cataloger implementation for Java archives (jar
|
||||
package java
|
||||
|
||||
import (
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/syft/cataloger/common"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
)
|
||||
|
||||
// Cataloger catalogs pkg.JavaPkg and pkg.JenkinsPluginPkg Package Types defined in java archive files.
|
||||
type Cataloger struct {
|
||||
cataloger common.GenericCataloger
|
||||
}
|
||||
|
||||
// New returns a new Java archive cataloger object.
|
||||
func New() *Cataloger {
|
||||
// NewJavaCataloger returns a new Java archive cataloger object.
|
||||
func NewJavaCataloger() *common.GenericCataloger {
|
||||
globParsers := make(map[string]common.ParserFn)
|
||||
for _, pattern := range archiveFormatGlobs {
|
||||
globParsers[pattern] = parseJavaArchive
|
||||
}
|
||||
|
||||
return &Cataloger{
|
||||
cataloger: common.NewGenericCataloger(nil, globParsers),
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns a string that uniquely describes this cataloger.
|
||||
func (a *Cataloger) Name() string {
|
||||
return "java-cataloger"
|
||||
}
|
||||
|
||||
// SelectFiles returns a set of discovered Java archive files from the user content source.
|
||||
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
||||
return a.cataloger.SelectFiles(resolver)
|
||||
}
|
||||
|
||||
// Catalog returns the Packages indexed from all Java archive files discovered.
|
||||
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
||||
return a.cataloger.Catalog(contents, a.Name())
|
||||
return common.NewGenericCataloger(nil, globParsers, "java-cataloger")
|
||||
}
|
||||
|
||||
@@ -4,25 +4,41 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
const manifestPath = "META-INF/MANIFEST.MF"
|
||||
const manifestGlob = "/META-INF/MANIFEST.MF"
|
||||
|
||||
func parseJavaManifest(reader io.Reader) (*pkg.JavaManifest, error) {
|
||||
// nolint:funlen
|
||||
// parseJavaManifest takes MANIFEST.MF file content and returns sections of parsed key/value pairs.
|
||||
// For more information: https://docs.oracle.com/en/java/javase/11/docs/specs/jar/jar.html#jar-manifest
|
||||
func parseJavaManifest(path string, reader io.Reader) (*pkg.JavaManifest, error) {
|
||||
var manifest pkg.JavaManifest
|
||||
manifestMap := make(map[string]string)
|
||||
sections := []map[string]string{
|
||||
make(map[string]string),
|
||||
}
|
||||
currentSection := 0
|
||||
scanner := bufio.NewScanner(reader)
|
||||
var lastKey string
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// ignore empty lines
|
||||
// empty lines denote section separators
|
||||
if strings.TrimSpace(line) == "" {
|
||||
currentSection++
|
||||
// we don't want to allocate a new section map that wont necessarily be used, do that once there is
|
||||
// a non-empty line to process
|
||||
|
||||
// do not process line continuations after this
|
||||
lastKey = ""
|
||||
continue
|
||||
} else if currentSection >= len(sections) {
|
||||
sections = append(sections, make(map[string]string))
|
||||
}
|
||||
|
||||
if line[0] == ' ' {
|
||||
@@ -30,7 +46,7 @@ func parseJavaManifest(reader io.Reader) (*pkg.JavaManifest, error) {
|
||||
if lastKey == "" {
|
||||
return nil, fmt.Errorf("found continuation with no previous key (%s)", line)
|
||||
}
|
||||
manifestMap[lastKey] += strings.TrimSpace(line)
|
||||
sections[currentSection][lastKey] += strings.TrimSpace(line)
|
||||
} else {
|
||||
// this is a new key-value pair
|
||||
idx := strings.Index(line, ":")
|
||||
@@ -40,9 +56,9 @@ func parseJavaManifest(reader io.Reader) (*pkg.JavaManifest, error) {
|
||||
|
||||
key := strings.TrimSpace(line[0:idx])
|
||||
value := strings.TrimSpace(line[idx+1:])
|
||||
manifestMap[key] = value
|
||||
sections[currentSection][key] = value
|
||||
|
||||
// keep track of key for future continuations
|
||||
// keep track of key for potential future continuations
|
||||
lastKey = key
|
||||
}
|
||||
}
|
||||
@@ -51,14 +67,24 @@ func parseJavaManifest(reader io.Reader) (*pkg.JavaManifest, error) {
|
||||
return nil, fmt.Errorf("unable to read java manifest: %w", err)
|
||||
}
|
||||
|
||||
if err := mapstructure.Decode(manifestMap, &manifest); err != nil {
|
||||
return nil, fmt.Errorf("unable to parse java manifest: %w", err)
|
||||
}
|
||||
|
||||
// clean select fields
|
||||
if strings.Trim(manifest.ImplVersion, " ") != "" {
|
||||
// transform versions with dates attached to just versions (e.g. "1.3 2244 October 5 2008" --> "1.3")
|
||||
manifest.ImplVersion = strings.Split(manifest.ImplVersion, " ")[0]
|
||||
if len(sections) > 0 {
|
||||
manifest.Main = sections[0]
|
||||
if len(sections) > 1 {
|
||||
manifest.NamedSections = make(map[string]map[string]string)
|
||||
for i, s := range sections[1:] {
|
||||
name, ok := s["Name"]
|
||||
if !ok {
|
||||
// per the manifest spec (https://docs.oracle.com/en/java/javase/11/docs/specs/jar/jar.html#jar-manifest)
|
||||
// this should never happen. If it does we want to know about it, but not necessarily stop
|
||||
// cataloging entirely... for this reason we only log.
|
||||
log.Warnf("java manifest section found without a name: %s", path)
|
||||
name = strconv.Itoa(i)
|
||||
} else {
|
||||
delete(s, "Name")
|
||||
}
|
||||
manifest.NamedSections[name] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &manifest, nil
|
||||
@@ -67,26 +93,23 @@ func parseJavaManifest(reader io.Reader) (*pkg.JavaManifest, error) {
|
||||
func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string {
|
||||
var name string
|
||||
switch {
|
||||
case filenameObj.name() != "":
|
||||
name = filenameObj.name()
|
||||
case manifest.Name != "":
|
||||
case filenameObj.name != "":
|
||||
name = filenameObj.name
|
||||
case manifest.Main["Name"] != "":
|
||||
// Manifest original spec...
|
||||
name = manifest.Name
|
||||
case manifest.Extra["Bundle-Name"] != "":
|
||||
name = manifest.Main["Name"]
|
||||
case manifest.Main["Bundle-Name"] != "":
|
||||
// BND tooling...
|
||||
name = manifest.Extra["Bundle-Name"]
|
||||
case manifest.Extra["Short-Name"] != "":
|
||||
name = manifest.Main["Bundle-Name"]
|
||||
case manifest.Main["Short-Name"] != "":
|
||||
// Jenkins...
|
||||
name = manifest.Extra["Short-Name"]
|
||||
case manifest.Extra["Extension-Name"] != "":
|
||||
name = manifest.Main["Short-Name"]
|
||||
case manifest.Main["Extension-Name"] != "":
|
||||
// Jenkins...
|
||||
name = manifest.Extra["Extension-Name"]
|
||||
}
|
||||
|
||||
// in situations where we hit this point and no name was
|
||||
// determined, look at the Implementation-Title
|
||||
if name == "" && manifest.ImplTitle != "" {
|
||||
name = manifest.ImplTitle
|
||||
name = manifest.Main["Extension-Name"]
|
||||
case manifest.Main["Implementation-Title"] != "":
|
||||
// last ditch effort...
|
||||
name = manifest.Main["Implementation-Title"]
|
||||
}
|
||||
return name
|
||||
}
|
||||
@@ -94,14 +117,14 @@ func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string
|
||||
func selectVersion(manifest *pkg.JavaManifest, filenameObj archiveFilename) string {
|
||||
var version string
|
||||
switch {
|
||||
case manifest.ImplVersion != "":
|
||||
version = manifest.ImplVersion
|
||||
case filenameObj.version() != "":
|
||||
version = filenameObj.version()
|
||||
case manifest.SpecVersion != "":
|
||||
version = manifest.SpecVersion
|
||||
case manifest.Extra["Plugin-Version"] != "":
|
||||
version = manifest.Extra["Plugin-Version"]
|
||||
case filenameObj.version != "":
|
||||
version = filenameObj.version
|
||||
case manifest.Main["Implementation-Version"] != "":
|
||||
version = manifest.Main["Implementation-Version"]
|
||||
case manifest.Main["Specification-Version"] != "":
|
||||
version = manifest.Main["Specification-Version"]
|
||||
case manifest.Main["Plugin-Version"] != "":
|
||||
version = manifest.Main["Plugin-Version"]
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user