Add release process (#89)

* add check for app update; fix ETUI error handling

* validate user args

* add goreleaser support

* replace cgo dependencies (go-rpm) with go equivalents

* add acceptance tests against build snapshot

* add brew tap + acceptance test pipeline

* add mac acceptance tests

* fix compare makefile

* fix mac acceptance tests

* add release pipeline with wait checks

* add token to release step

* rm dir presenters int test

* enforce dpkg to be non interactive

Co-authored-by: Alfredo Deza <adeza@anchore.com>

* pin brew formulae

* pin skopeo to formulae url

* only run acceptance tests

Co-authored-by: Alfredo Deza <adeza@anchore.com>
This commit is contained in:
Alex Goodman 2020-07-23 10:52:44 -04:00 committed by GitHub
parent 3cb7c43dbc
commit ba4f63099d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 952 additions and 57 deletions

View file

@ -3,3 +3,4 @@ permit:
- MIT.*
- Apache.*
- MPL.*
- ISC

View file

@ -102,18 +102,19 @@ jobs:
- restore_cache:
keys:
- integration-test-tar-cache-{{ checksum "integration/test-fixtures/tar-cache.fingerprint" }}
- integration-test-tar-cache-{{ checksum "test/integration/test-fixtures/tar-cache.fingerprint" }}
- run:
name: run integration tests
command: make integration
- save_cache:
key: integration-test-tar-cache-{{ checksum "integration/test-fixtures/tar-cache.fingerprint" }}
key: integration-test-tar-cache-{{ checksum "test/integration/test-fixtures/tar-cache.fingerprint" }}
paths:
- "integration/test-fixtures/tar-cache"
- "test/integration/test-fixtures/tar-cache"
workflows:
"Static Analysis & All Tests":
# 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"

83
.github/workflows/acceptance-test.yaml vendored Normal file
View file

@ -0,0 +1,83 @@
name: 'Acceptance'
on:
push:
# ... only act on pushes to master
branches:
- master
# ... do not act on release tags
tags-ignore:
- v*
env:
GO_VERSION: "1.14.x"
jobs:
Build-Snapshot-Artifacts:
runs-on: ubuntu-latest
steps:
# TODO: remove me after release
- name: Configure git for private modules
env:
TOKEN: ${{ secrets.ANCHORE_GIT_READ_TOKEN }}
run: git config --global url."https://anchore:${TOKEN}@github.com".insteadOf "https://github.com"
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v2
- name: Restore bootstrap cache
id: cache
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
${{ github.workspace }}/.tmp
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('Makefile') }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('Makefile') }}-
${{ runner.os }}-go-${{ env.GO_VERSION }}-
- name: Bootstrap dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: make ci-bootstrap
- name: Build snapshot artifacts
run: make snapshot
- uses: actions/upload-artifact@v2
with:
name: artifacts
path: snapshot
# 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
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: artifacts
path: snapshot
- name: Run Acceptance Tests (Linux)
run: make acceptance-linux
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Acceptance-Mac:
needs: [ Build-Snapshot-Artifacts ]
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: artifacts
path: snapshot
- name: Run Acceptance Tests (Mac)
run: make acceptance-mac

101
.github/workflows/release.yaml vendored Normal file
View file

@ -0,0 +1,101 @@
name: 'Release'
on:
push:
# take no actions on push...
branches-ignore:
- '**'
# ... only act on release tags
tags:
- 'v*'
env:
GO_VERSION: "1.14.x"
jobs:
wait-for-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# we don't want to release commits that have been pushed and tagged, but not necessarily merged onto master
- name: Ensure tagged commit is on master
run: |
echo "Tag: ${GITHUB_REF##*/}"
git fetch origin master
git merge-base --is-ancestor ${GITHUB_REF##*/} origin/master && echo "${GITHUB_REF##*/} is a commit on master!"
- name: Check static anaylysis, unit, and integration test results
uses: fountainhead/action-wait-for-check@v1
id: sa-unit-int
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"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (linux)
uses: fountainhead/action-wait-for-check@v1
id: acceptance-linux
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/acceptance-test.yaml)
checkName: "Acceptance-Linux"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (mac)
uses: fountainhead/action-wait-for-check@v1
id: acceptance-mac
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/acceptance-test.yaml)
checkName: "Acceptance-Mac"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Quality gate
if: steps.sa-unit-int.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 "Acceptance Test (Linux) Status: ${{ steps.acceptance-linux.outputs.conclusion }}"
echo "Acceptance Test (Mac) Status: ${{ steps.acceptance-mac.outputs.conclusion }}"
false
release:
runs-on: ubuntu-latest
steps:
# TODO: remove me after release
- name: Configure git for private modules
env:
TOKEN: ${{ secrets.ANCHORE_GIT_READ_TOKEN }}
run: git config --global url."https://anchore:${TOKEN}@github.com".insteadOf "https://github.com"
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v2
- name: Restore bootstrap cache
id: cache
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
${{ github.workspace }}/.tmp
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('Makefile') }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('Makefile') }}-
${{ runner.os }}-go-${{ env.GO_VERSION }}-
- name: Bootstrap dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: make ci-bootstrap
- name: Build snapshot artifacts
run: make release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-artifact@v2
with:
name: artifacts
path: dist

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
/dist
/snapshot
.server/
.vscode/
*.tar

44
.goreleaser.yaml Normal file
View file

@ -0,0 +1,44 @@
builds:
- binary: imgbom
env:
- CGO_ENABLED=0
goos:
# windows not supported yet (due to jotframe)
# - windows
- linux
- darwin
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/imgbom/internal/version.version={{.Version}}
-X github.com/anchore/imgbom/internal/version.gitCommit={{.Commit}}
-X github.com/anchore/imgbom/internal/version.buildDate={{.Date}}
-X github.com/anchore/imgbom/internal/version.gitTreeState={{.Env.BUILD_GIT_TREE_STATE}}
nfpms:
- license: "Apache 2.0"
maintainer: "Anchore, Inc"
homepage: &website "https://github.com/anchore/imgbom"
description: &description "A tool that generates a Software Bill Of Materials (SBOM) from container images and filesystems"
formats:
- rpm
- deb
brews:
- tap:
owner: anchore
name: homebrew-imgbom
homepage: *website
description: *description
archives:
- format: tar.gz
format_overrides:
- goos: windows
format: zip

162
Makefile
View file

@ -1,9 +1,11 @@
BIN = imgbom
TEMPDIR = ./.tmp
RESULTSDIR = $(TEMPDIR)/results
COVER_REPORT = $(RESULTSDIR)/cover.report
COVER_TOTAL = $(RESULTSDIR)/cover.total
LICENSES_REPORT = $(RESULTSDIR)/licenses.json
LINTCMD = $(TEMPDIR)/golangci-lint run --tests=false --config .golangci.yaml
ACC_TEST_IMAGE = centos:8.2.2004
ACC_DIR = ./test/acceptance
BOLD := $(shell tput -T linux bold)
PURPLE := $(shell tput -T linux setaf 5)
GREEN := $(shell tput -T linux setaf 2)
@ -15,57 +17,94 @@ SUCCESS := $(BOLD)$(GREEN)
# the quality gate lower threshold for unit test total % coverage (by function statements)
COVERAGE_THRESHOLD := 72
## Build variables
DISTDIR=./dist
SNAPSHOTDIR=./snapshot
GITTREESTATE=$(if $(shell git status --porcelain),dirty,clean)
ifeq "$(strip $(VERSION))" ""
override VERSION = $(shell git describe --always --tags --dirty)
endif
## Variable assertions
ifndef TEMPDIR
$(error TEMPDIR is not set)
endif
ifndef RESULTSDIR
$(error RESULTSDIR is not set)
endif
ifndef ACC_DIR
$(error ACC_DIR is not set)
endif
ifndef DISTDIR
$(error DISTDIR is not set)
endif
ifndef SNAPSHOTDIR
$(error SNAPSHOTDIR is not set)
endif
define title
@printf '$(TITLE)$(1)$(RESET)\n'
endef
.PHONY: all bootstrap lint lint-fix unit coverage integration check-pipeline clear-cache help test compare
## Tasks
all: lint test ## Run all checks (linting, unit tests, and integration tests)
.PHONY: all
all: clean lint check-licenses test ## Run all linux-based checks (linting, license check, unit, integration, and linux acceptance tests)
@printf '$(SUCCESS)All checks pass!$(RESET)\n'
.PHONY: compare
compare:
@cd comparison && make
@cd test/inline-compare && make
test: unit integration ## Run all tests (currently unit & integration)
.PHONY: test
test: unit integration acceptance-linux ## Run all tests (currently unit, integration, and linux acceptance tests )
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}'
ci-bootstrap: ci-lib-dependencies bootstrap
.PHONY: ci-bootstrap
ci-bootstrap: bootstrap
sudo apt install -y bc
ci-lib-dependencies:
# libdb5.3-dev and libssl-dev are required for Berkeley DB C bindings for RPM DB support
sudo apt install -y libdb5.3-dev libssl-dev
bootstrap: ## Download and install all project dependencies (+ prep tooling in the ./tmp dir)
$(call title,Downloading dependencies)
.PHONY: boostrap
bootstrap: ## Download and install all go dependencies (+ prep tooling in the ./tmp dir)
$(call title,Boostrapping dependencies)
@pwd
# prep temp dirs
mkdir -p $(TEMPDIR)
mkdir -p $(RESULTSDIR)
# install project dependencies
go get ./...
# install golangci-lint
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b .tmp/ v1.26.0
# install bouncer
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b .tmp/ v0.1.0
# install go dependencies
go mod download
# install utilities
[ -f "$(TEMPDIR)/golangci" ] || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.26.0
[ -f "$(TEMPDIR)/bouncer" ] || curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.1.0
[ -f "$(TEMPDIR)/goreleaser" ] || curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- -b $(TEMPDIR)/ v0.140.0
.PHONY: lint
lint: ## Run gofmt + golangci lint checks
$(call title,Running linters)
@printf "files with gofmt issues: [$(shell gofmt -l -s .)]\n"
@test -z "$(shell gofmt -l -s .)"
$(LINTCMD)
.PHONY: lint-fix
lint-fix: ## Auto-format all source code + run golangci lint fixers
$(call title,Running lint fixers)
gofmt -w -s .
$(LINTCMD) --fix
.PHONY: check-licenses
check-licenses:
$(TEMPDIR)/bouncer check
.PHONY: unit
unit: ## Run unit tests (with coverage)
$(call title,Running unit tests)
go test --race -coverprofile $(COVER_REPORT) ./...
@ -73,20 +112,24 @@ unit: ## Run unit tests (with coverage)
@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
.PHONY: integration
integration: ## Run integration tests
$(call title,Running integration tests)
go test -v -tags=integration ./integration
go test -v -tags=integration ./test/integration
integration/test-fixtures/tar-cache.key, integration-fingerprint:
find integration/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee integration/test-fixtures/tar-cache.fingerprint
test/integration/test-fixtures/tar-cache.key, 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
.PHONY: java-packages-fingerprint
java-packages-fingerprint:
@cd imgbom/cataloger/java/test-fixtures/java-builds && \
make packages.fingerprint
.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
@ -96,15 +139,70 @@ check-pipeline: ## Run local CircleCI pipeline locally (sanity check)
circleci local execute -c .tmp/circleci.yml --job "Unit & Integration Tests (go-latest)"
@printf '$(SUCCESS)Pipeline checks pass!$(RESET)\n'
# todo: replace this with goreleaser
build-release: ## Build final release binary
@mkdir -p dist
go build -s -w -X main.version="$(git describe --tags --dirty --always)" \
-X main.commit="$(git describe --dirty --always)" \
-X main.buildTime="$(date --rfc-3339=seconds --utc)"
-o dist/imgbom
.PHONY: build
build: $(SNAPSHOTDIR) ## Build release snapshot binaries and packages
# todo: this should by later used by goreleaser
check-licenses:
$(TEMPDIR)/bouncer list -o json | tee $(LICENSES_REPORT)
$(TEMPDIR)/bouncer check
$(SNAPSHOTDIR): ## Build snapshot release binaries and packages
$(call title,Building snapshot artifacts)
# create a config with the dist dir overridden
echo "dist: $(SNAPSHOTDIR)" > $(TEMPDIR)/goreleaser.yaml
cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml
# build release snapshots
BUILD_GIT_TREE_STATE=$(GITTREESTATE) \
$(TEMPDIR)/goreleaser release --skip-publish --rm-dist --snapshot --config $(TEMPDIR)/goreleaser.yaml
.PHONY: acceptance-mac
acceptance-mac: $(SNAPSHOTDIR) ## Run acceptance tests on build snapshot binaries and packages (Mac)
$(call title,Running acceptance test: Run on Mac)
$(ACC_DIR)/mac.sh \
$(SNAPSHOTDIR) \
$(ACC_DIR)\
$(ACC_TEST_IMAGE)
.PHONY: acceptance-linux
acceptance-linux: acceptance-test-deb-package-install acceptance-test-rpm-package-install ## Run acceptance tests on build snapshot binaries and packages (Linux)
.PHONY: acceptance-test-deb-package-install
acceptance-test-deb-package-install: $(SNAPSHOTDIR)
$(call title,Running acceptance test: DEB install)
$(ACC_DIR)/deb.sh \
$(SNAPSHOTDIR) \
$(ACC_DIR)\
$(ACC_TEST_IMAGE)
.PHONY: acceptance-test-rpm-package-install
acceptance-test-rpm-package-install: $(SNAPSHOTDIR)
$(call title,Running acceptance test: RPM install)
$(ACC_DIR)/rpm.sh \
$(SNAPSHOTDIR) \
$(ACC_DIR)\
$(ACC_TEST_IMAGE)
# TODO: this is not releasing yet
.PHONY: release
release: clean-dist ## Build and publish final binaries and packages
$(call title,Publishing release artifacts)
# 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 --skip-publish --rm-dist --config $(TEMPDIR)/goreleaser.yaml
# create a version file for version-update checks
echo "$(VERSION)" > $(DISTDIR)/VERSION
# TODO: add upload to bucket
.PHONY: clean
clean: clean-dist clean-shapshot ## Remove previous builds and result reports
rm -rf $(RESULTSDIR)/*
.PHONY: clean-shapshot
clean-shapshot:
rm -rf $(SNAPSHOTDIR) $(TEMPDIR)/goreleaser.yaml
.PHONY: clean-dist
clean-dist:
rm -rf $(DISTDIR) $(TEMPDIR)/goreleaser.yaml

View file

@ -18,7 +18,7 @@ import (
var rootCmd = &cobra.Command{
Use: fmt.Sprintf("%s [SOURCE]", internal.ApplicationName),
Short: "A tool that generates a Software Build Of Materials (SBOM)",
Short: "A tool for generating a Software Bill Of Materials (SBOM) 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

View file

@ -1,2 +0,0 @@
*.json
*-reports

2
go.mod
View file

@ -14,7 +14,6 @@ require (
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/knqyf263/go-rpmdb v0.0.0-20190501070121-10a1c42a10dc
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.3.1
github.com/rogpeppe/go-internal v1.5.2
@ -23,6 +22,7 @@ require (
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-20200621153512-2778c704bf22
github.com/wagoodman/go-rpmdb v0.0.0-20200719223757-ce54a4b0607b
github.com/wagoodman/jotframe v0.0.0-20200622123948-2995cbd43525
go.uber.org/zap v1.15.0
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9

9
go.sum
View file

@ -285,6 +285,8 @@ github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-restruct/restruct v0.0.0-20191227155143-5734170a48a1 h1:LoN2wx/aN8JPGebG+2DaUyk4M+xRcqJXfuIbs8AWHdE=
github.com/go-restruct/restruct v0.0.0-20191227155143-5734170a48a1/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
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=
@ -536,10 +538,6 @@ github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/knqyf263/berkeleydb v0.0.0-20190501065933-fafe01fb9662 h1:UGS0RbPHwXJkq8tcba8OD0nvVUWLf2h7uUJznuHPPB0=
github.com/knqyf263/berkeleydb v0.0.0-20190501065933-fafe01fb9662/go.mod h1:bu1CcN4tUtoRcI/B/RFHhxMNKFHVq/c3SV+UTyduoXg=
github.com/knqyf263/go-rpmdb v0.0.0-20190501070121-10a1c42a10dc h1:pumO9pqmRAjvic6oove22RGh9wDZQnj96XQjJSbSEPs=
github.com/knqyf263/go-rpmdb v0.0.0-20190501070121-10a1c42a10dc/go.mod h1:MrSSvdMpTSymaQWk1yFr9sxFSyQmKMj6jkbvGrchBV8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
@ -823,6 +821,8 @@ github.com/wagoodman/go-progress v0.0.0-20200621122631-1a2120f0695a h1:lV3ioFpbq
github.com/wagoodman/go-progress v0.0.0-20200621122631-1a2120f0695a/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA=
github.com/wagoodman/go-progress v0.0.0-20200621153512-2778c704bf22 h1:GYaiTP0ywrCjJ4qMxxCg+YKPSDMeFJg6i1X9X55LJCA=
github.com/wagoodman/go-progress v0.0.0-20200621153512-2778c704bf22/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA=
github.com/wagoodman/go-rpmdb v0.0.0-20200719223757-ce54a4b0607b h1:elYGLFZPymeTWJ6qA3tIzFet3LQ9D/Jl6HLWNyFjdQc=
github.com/wagoodman/go-rpmdb v0.0.0-20200719223757-ce54a4b0607b/go.mod h1:MjoIZzKmbYfcpbC6ARWMcHijAjtLBViDaHcayXKWQWI=
github.com/wagoodman/jotframe v0.0.0-20200622123948-2995cbd43525 h1:fGlwSBQrl9/axciK2+gJ9q86SeQYJpbPx4vOrExvZXY=
github.com/wagoodman/jotframe v0.0.0-20200622123948-2995cbd43525/go.mod h1:DzXZ1wfRedNhC3KQTick8Gf3CEPMFHsP5k4R/ldjKtw=
github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug=
@ -1100,7 +1100,6 @@ golang.org/x/tools v0.0.0-20200502202811-ed308ab3e770/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200527183253-8e7acdbce89d h1:SR+e35rACZFBohNb4Om1ibX6N3iO0FtdbwqGSuD9dBU=
golang.org/x/tools v0.0.0-20200527183253-8e7acdbce89d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=

View file

@ -9,7 +9,7 @@ import (
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/imgbom/internal"
"github.com/anchore/imgbom/internal/log"
rpmdb "github.com/knqyf263/go-rpmdb/pkg"
rpmdb "github.com/wagoodman/go-rpmdb/pkg"
)
func parseRpmDB(_ string, reader io.Reader) ([]pkg.Package, error) {
@ -30,8 +30,7 @@ func parseRpmDB(_ string, reader io.Reader) ([]pkg.Package, error) {
return nil, fmt.Errorf("failed to copy rpmdb contents to temp file: %w", err)
}
db := rpmdb.DB{}
err = db.Open(f.Name())
db, err := rpmdb.Open(f.Name())
if err != nil {
return nil, err
}

View file

@ -1,6 +1,7 @@
package pkg
import (
"sort"
"sync"
"github.com/anchore/imgbom/internal/log"
@ -94,3 +95,19 @@ func (c *Catalog) Enumerate(types ...Type) <-chan *Package {
}()
return channel
}
func (c *Catalog) Sorted(types ...Type) []*Package {
pkgs := make([]*Package, 0)
for p := range c.Enumerate(types...) {
pkgs = append(pkgs, p)
}
sort.SliceStable(pkgs, func(i, j int) bool {
if pkgs[i].Name == pkgs[j].Name {
return pkgs[i].Version < pkgs[j].Version
}
return pkgs[i].Name < pkgs[j].Name
})
return pkgs
}

View file

@ -53,7 +53,7 @@ type artifact struct {
Type string `json:"type"`
Cataloger string `json:"cataloger"`
Sources []source `json:"sources"`
Metadata interface{} `json:"metadata"`
Metadata interface{} `json:"metadata,omitempty"`
}
func (pres *Presenter) Present(output io.Writer) error {
@ -82,7 +82,7 @@ func (pres *Presenter) Present(output io.Writer) error {
return fmt.Errorf("unsupported source: %T", src)
}
for p := range pres.catalog.Enumerate() {
for _, p := range pres.catalog.Sorted() {
art := artifact{
Name: p.Name,
Version: p.Version,

View file

@ -1 +1 @@
{"artifacts":[{"name":"package-1","version":"1.0.1","type":"deb","cataloger":"","sources":[],"metadata":null},{"name":"package-2","version":"2.0.1","type":"deb","cataloger":"","sources":[],"metadata":null}],"image":{"layers":null,"size":0,"digest":"","mediaType":"","tags":null},"Source":"/some/path"}
{"artifacts":[{"name":"package-1","version":"1.0.1","type":"deb","cataloger":"","sources":[]},{"name":"package-2","version":"2.0.1","type":"deb","cataloger":"","sources":[]}],"image":{"layers":null,"size":0,"digest":"","mediaType":"","tags":null},"Source":"/some/path"}

View file

@ -1 +1 @@
{"artifacts":[{"name":"package-1","version":"1.0.1","type":"deb","cataloger":"","sources":[{"foundBy":"","effects":[]}],"metadata":null},{"name":"package-2","version":"2.0.1","type":"deb","cataloger":"","sources":[{"foundBy":"","effects":[]}],"metadata":null}],"image":{"layers":[{"mediaType":"","digest":"","size":0},{"mediaType":"","digest":"","size":0},{"mediaType":"","digest":"","size":0}],"size":65,"digest":"sha256:26e4732b961662cd066976b6cadc25f2cedee52db90be26ee7c120d2ff468ef2","mediaType":"application/vnd.docker.distribution.manifest.v2+json","tags":["anchore-fixture-image-simple:04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7"]},"Source":""}
{"artifacts":[{"name":"package-1","version":"1.0.1","type":"deb","cataloger":"","sources":[{"foundBy":"","effects":[]}]},{"name":"package-2","version":"2.0.1","type":"deb","cataloger":"","sources":[{"foundBy":"","effects":[]}]}],"image":{"layers":[{"mediaType":"","digest":"","size":0},{"mediaType":"","digest":"","size":0},{"mediaType":"","digest":"","size":0}],"size":65,"digest":"sha256:3c53d2d891940f8d8e95acb77b58752f54dc5de9d91d19dd90ced2db76256cea","mediaType":"application/vnd.docker.distribution.manifest.v2+json","tags":["anchore-fixture-image-simple:04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7"]},"Source":""}

View file

@ -48,7 +48,7 @@ func (pres *Presenter) Present(output io.Writer) error {
}
// populate artifacts...
for p := range pres.catalog.Enumerate() {
for _, p := range pres.catalog.Sorted() {
fmt.Fprintln(w, fmt.Sprintf("[%s]", p.Name))
fmt.Fprintln(w, " Version:\t", p.Version)
fmt.Fprintln(w, " Type:\t", p.Type.String())

View file

@ -64,5 +64,9 @@ func fetchLatestApplicationVersion() (*hashiVersion.Version, error) {
}
versionStr := strings.TrimSuffix(string(versionBytes), "\n")
if len(versionStr) > 50 {
return nil, fmt.Errorf("version too long: %q", versionStr[:50])
}
return hashiVersion.NewVersion(versionStr)
}

View file

@ -168,6 +168,13 @@ func TestFetchLatestApplicationVersion(t *testing.T) {
expected: nil,
err: true,
},
{
name: "too long",
response: "this is really long this is really long this is really long this is really long this is really long this is really long this is really long this is really long ",
code: 200,
expected: nil,
err: true,
},
}
for _, test := range tests {

109
test/acceptance/compare.py Executable file
View file

@ -0,0 +1,109 @@
#!/usr/bin/env python3
import sys
import json
import collections
Metadata = collections.namedtuple("Metadata", "metadata sources")
Package = collections.namedtuple("Package", "name type version")
Vulnerability = collections.namedtuple("Vulnerability", "cve package")
class ImgBom:
def __init__(self, report_path):
self.report_path = report_path
def _enumerate_section(self, section):
with open(self.report_path) as json_file:
data = json.load(json_file)
for entry in data[section]:
yield entry
def packages(self):
packages = set()
metadata = collections.defaultdict(dict)
for entry in self._enumerate_section(section="artifacts"):
package = Package(
name=entry["name"], type=entry["type"], version=entry["version"]
)
packages.add(package)
metadata[package.type][package] = Metadata(
metadata=repr(entry["metadata"]), sources=repr(entry["sources"])
)
return packages, metadata
def main(baseline_report, new_report):
report1_obj = ImgBom(report_path=baseline_report)
report1_packages, report1_metadata = report1_obj.packages()
report2_obj = ImgBom(report_path=new_report)
report2_packages, report2_metadata = report2_obj.packages()
if len(report2_packages) == 0 and len(report1_packages) == 0:
print("nobody found any packages")
return 0
same_packages = report2_packages & report1_packages
percent_overlap_packages = (
float(len(same_packages)) / float(len(report1_packages))
) * 100.0
extra_packages = report2_packages - report1_packages
missing_pacakges = report1_packages - report2_packages
report1_metadata_set = set()
for package in report1_packages:
metadata = report1_metadata[package.type][package]
report1_metadata_set.add((package, metadata))
report2_metadata_set = set()
for package in report2_packages:
metadata = report2_metadata[package.type][package]
report2_metadata_set.add((package, metadata))
same_metadata = report2_metadata_set & report1_metadata_set
percent_overlap_metadata = 0
if len(report1_metadata_set) > 0:
percent_overlap_metadata = (
float(len(same_metadata)) / float(len(report1_metadata_set))
) * 100.0
if len(extra_packages) > 0:
print("Extra packages:")
for package in sorted(list(extra_packages)):
print(" " + repr(package))
print()
if len(missing_pacakges) > 0:
print("Missing packages:")
for package in sorted(list(missing_pacakges)):
print(" " + repr(package))
print()
print("Baseline Packages: %d" % len(report1_packages))
print("New Packages: %d" % len(report2_packages))
print()
print(
"Baseline Packages Matched: %.2f %% (%d/%d packages)"
% (percent_overlap_packages, len(same_packages), len(report1_packages))
)
print(
"Baseline Metadata Matched: %.2f %% (%d/%d metadata)"
% (percent_overlap_metadata, len(same_metadata), len(report1_metadata_set))
)
if len(report1_packages) != len(report2_packages):
print("failed quality gate: requires exact name & version match")
return 1
return 0
if __name__ == "__main__":
print("\nComparing two imgbom reports...\n")
if len(sys.argv) != 3:
sys.exit("please provide two imgbom json files")
rc = main(sys.argv[1], sys.argv[2])
sys.exit(rc)

27
test/acceptance/compare.sh Executable file
View file

@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -eu
BOLD="$(tput -T linux bold)"
RED="$(tput -T linux setaf 1)"
RESET="$(tput -T linux sgr0)"
FAIL="${BOLD}${RED}"
SUCCESS="${BOLD}"
JQ_ARGS="-S .artifacts"
if ! command -v jq &> /dev/null ;then
JQ_IMAGE="imega/jq:latest"
JQ_CMD="docker run --rm -i ${JQ_IMAGE} ${JQ_ARGS}"
docker pull "${JQ_IMAGE}"
else
JQ_CMD="jq ${JQ_ARGS}"
fi
if [[ $(cat $1 | ${JQ_CMD}) ]]; then
set -x
# compare the output of both results
diff <(cat $1 | ${JQ_CMD}) <(cat $2 | ${JQ_CMD})
set +x
echo "${SUCCESS}Comparison passed!${RESET}"
else
exit "${FAIL}Failing since one of the test files is empty ($1)${RESET}"
fi

45
test/acceptance/deb.sh Executable file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -eux
DISTDIR=$1
ACC_DIR=$2
TEST_IMAGE=$3
TEST_TYPE=deb
WORK_DIR=`mktemp -d -t "imgbom-acceptance-test-${TEST_TYPE}-XXXXXX"`
REPORT=${WORK_DIR}/acceptance-${TEST_TYPE}-${TEST_IMAGE}.json
GOLDEN_REPORT=${ACC_DIR}/test-fixtures/acceptance-${TEST_IMAGE}.json
# check if tmp dir was created
if [[ ! "${WORK_DIR}" || ! -d "${WORK_DIR}" ]]; then
echo "Could not create temp dir"
exit 1
fi
function cleanup {
rm -rf "${WORK_DIR}"
}
trap cleanup EXIT
# fetch test image
docker pull ${TEST_IMAGE}
# install and run imgbom
docker run --rm \
-v /var/run/docker.sock://var/run/docker.sock \
-v /${PWD}:/src \
-v ${WORK_DIR}:${WORK_DIR} \
-w /src \
ubuntu:latest \
/bin/bash -x -c "\
DEBIAN_FRONTEND=noninteractive apt install ${DISTDIR}/imgbom_*_linux_amd64.deb -y && \
imgbom version -v && \
imgbom ${TEST_IMAGE} -vv -o json > ${REPORT} && \
cat ${REPORT} \
"
# compare the results to a known good output
${ACC_DIR}/compare.py \
${GOLDEN_REPORT} \
${REPORT}

42
test/acceptance/mac.sh Executable file
View file

@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -eux
DISTDIR=$1
ACC_DIR=$2
TEST_IMAGE=$3
TEST_IMAGE_TAR=/tmp/image.tar
TEST_TYPE=mac
WORK_DIR=`mktemp -d -t "imgbom-acceptance-test-${TEST_TYPE}-XXXXXX"`
REPORT=${WORK_DIR}/acceptance-${TEST_TYPE}-${TEST_IMAGE}.json
GOLDEN_REPORT=${ACC_DIR}/test-fixtures/acceptance-${TEST_IMAGE}.json
# check if tmp dir was created
if [[ ! "${WORK_DIR}" || ! -d "${WORK_DIR}" ]]; then
echo "Could not create temp dir"
exit 1
fi
function cleanup {
rm -rf "${WORK_DIR}"
}
trap cleanup EXIT
# install skopeo (pinned to 1.1.0)
skopeo --version || brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/75e8d7a40af77b48cc91f4bdb7d669f891a6de60/Formula/skopeo.rb
# fetch test image
skopeo --override-os linux copy docker://docker.io/${TEST_IMAGE} docker-archive:${TEST_IMAGE_TAR}
ls -alh ${TEST_IMAGE_TAR}
# run imgbom
chmod 755 ${DISTDIR}/imgbom_darwin_amd64/imgbom
${DISTDIR}/imgbom_darwin_amd64/imgbom version -v
${DISTDIR}/imgbom_darwin_amd64/imgbom docker-archive://${TEST_IMAGE_TAR} -vv -o json > ${REPORT}
cat ${REPORT}
# compare the results to a known good output
${ACC_DIR}/compare.py \
${GOLDEN_REPORT} \
${REPORT}

45
test/acceptance/rpm.sh Executable file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -eux
DISTDIR=$1
ACC_DIR=$2
TEST_IMAGE=$3
TEST_TYPE=rpm
WORK_DIR=`mktemp -d -t "imgbom-acceptance-test-${TEST_TYPE}-XXXXXX"`
REPORT=${WORK_DIR}/acceptance-${TEST_TYPE}-${TEST_IMAGE}.json
GOLDEN_REPORT=${ACC_DIR}/test-fixtures/acceptance-${TEST_IMAGE}.json
# check if tmp dir was created
if [[ ! "${WORK_DIR}" || ! -d "${WORK_DIR}" ]]; then
echo "Could not create temp dir"
exit 1
fi
function cleanup {
rm -rf "${WORK_DIR}"
}
trap cleanup EXIT
# fetch test image
docker pull ${TEST_IMAGE}
# install and run imgbom
docker run --rm \
-v /var/run/docker.sock://var/run/docker.sock \
-v /${PWD}:/src \
-v ${WORK_DIR}:${WORK_DIR} \
-w /src \
centos:latest \
/bin/bash -x -c "\
rpm -ivh ${DISTDIR}/imgbom_*_linux_amd64.rpm && \
imgbom version -v && \
imgbom ${TEST_IMAGE} -vv -o json > ${REPORT} && \
cat ${REPORT} \
"
# compare the results to a known good output
${ACC_DIR}/compare.py \
${GOLDEN_REPORT} \
${REPORT}

File diff suppressed because one or more lines are too long

2
test/inline-compare/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.json
inline-reports

View file

@ -5,10 +5,18 @@ IMGBOM_REPORT = $(IMGBOM_DIR)/$(IMAGE_CLEAN).json
INLINE_DIR = inline-reports
INLINE_REPORT = $(INLINE_DIR)/$(IMAGE_CLEAN)-content-os.json
.PHONY: bootstrap
ifndef IMGBOM_DIR
$(error IMGBOM_DIR is not set)
endif
ifndef INLINE_DIR
$(error INLINE_DIR is not set)
endif
.PHONY: all
all: compare
.PHONY: compare
compare: $(INLINE_REPORT) $(IMGBOM_REPORT)
docker build -t compare-imgbom:latest .
docker run compare-imgbom:latest $(IMAGE)
@ -24,4 +32,8 @@ $(IMGBOM_REPORT):
echo "Creating $(IMGBOM_REPORT)..."
mkdir -p $(IMGBOM_DIR)
docker pull $(IMAGE)
go run ../main.go centos:latest -o json > $(IMGBOM_REPORT)
go run ../../main.go $(IMAGE) -o json > $(IMGBOM_REPORT)
.PHONY: clean
clean:
rm -f $(INLINE_DIR)/* $(IMGBOM_DIR)/*

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,256 @@
[Path: test-fixtures]
[actionmailer]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[actionpack]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[actionview]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[activemodel]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[activerecord]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[activesupport]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[arel]
Version: 5.0.1.20140414130214
Type: bundle
Found by: bundler-cataloger
[bootstrap-sass]
Version: 3.1.1.1
Type: bundle
Found by: bundler-cataloger
[builder]
Version: 3.2.2
Type: bundle
Found by: bundler-cataloger
[coffee-rails]
Version: 4.0.1
Type: bundle
Found by: bundler-cataloger
[coffee-script]
Version: 2.2.0
Type: bundle
Found by: bundler-cataloger
[coffee-script-source]
Version: 1.7.0
Type: bundle
Found by: bundler-cataloger
[erubis]
Version: 2.7.0
Type: bundle
Found by: bundler-cataloger
[execjs]
Version: 2.0.2
Type: bundle
Found by: bundler-cataloger
[hike]
Version: 1.2.3
Type: bundle
Found by: bundler-cataloger
[i18n]
Version: 0.6.9
Type: bundle
Found by: bundler-cataloger
[jbuilder]
Version: 2.0.7
Type: bundle
Found by: bundler-cataloger
[jquery-rails]
Version: 3.1.0
Type: bundle
Found by: bundler-cataloger
[json]
Version: 1.8.1
Type: bundle
Found by: bundler-cataloger
[kgio]
Version: 2.9.2
Type: bundle
Found by: bundler-cataloger
[libv8]
Version: 3.16.14.3
Type: bundle
Found by: bundler-cataloger
[mail]
Version: 2.5.4
Type: bundle
Found by: bundler-cataloger
[mime-types]
Version: 1.25.1
Type: bundle
Found by: bundler-cataloger
[minitest]
Version: 5.3.4
Type: bundle
Found by: bundler-cataloger
[multi_json]
Version: 1.10.1
Type: bundle
Found by: bundler-cataloger
[mysql2]
Version: 0.3.16
Type: bundle
Found by: bundler-cataloger
[polyglot]
Version: 0.3.4
Type: bundle
Found by: bundler-cataloger
[rack]
Version: 1.5.2
Type: bundle
Found by: bundler-cataloger
[rack-test]
Version: 0.6.2
Type: bundle
Found by: bundler-cataloger
[rails]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[railties]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[raindrops]
Version: 0.13.0
Type: bundle
Found by: bundler-cataloger
[rake]
Version: 10.3.2
Type: bundle
Found by: bundler-cataloger
[rdoc]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[ref]
Version: 1.0.5
Type: bundle
Found by: bundler-cataloger
[sass]
Version: 3.2.19
Type: bundle
Found by: bundler-cataloger
[sass-rails]
Version: 4.0.3
Type: bundle
Found by: bundler-cataloger
[sdoc]
Version: 0.4.0
Type: bundle
Found by: bundler-cataloger
[spring]
Version: 1.1.3
Type: bundle
Found by: bundler-cataloger
[sprockets]
Version: 2.11.0
Type: bundle
Found by: bundler-cataloger
[sprockets-rails]
Version: 2.1.3
Type: bundle
Found by: bundler-cataloger
[sqlite3]
Version: 1.3.9
Type: bundle
Found by: bundler-cataloger
[therubyracer]
Version: 0.12.1
Type: bundle
Found by: bundler-cataloger
[thor]
Version: 0.19.1
Type: bundle
Found by: bundler-cataloger
[thread_safe]
Version: 0.3.3
Type: bundle
Found by: bundler-cataloger
[tilt]
Version: 1.4.1
Type: bundle
Found by: bundler-cataloger
[treetop]
Version: 1.4.15
Type: bundle
Found by: bundler-cataloger
[turbolinks]
Version: 2.2.2
Type: bundle
Found by: bundler-cataloger
[tzinfo]
Version: 1.2.0
Type: bundle
Found by: bundler-cataloger
[uglifier]
Version: 2.5.0
Type: bundle
Found by: bundler-cataloger
[unicorn]
Version: 4.8.3
Type: bundle
Found by: bundler-cataloger