Skip to content

Commit

Permalink
feat: vex output (#272)
Browse files Browse the repository at this point in the history
  • Loading branch information
sozercan committed Aug 28, 2023
1 parent 67e8932 commit a72c494
Show file tree
Hide file tree
Showing 23 changed files with 835 additions and 194 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,4 @@ jobs:
run: |
set -eu -o pipefail
. .github/workflows/scripts/buildkitenvs/${{ matrix.buildkit_mode}}
go test -v ./integration --addr="${COPA_BUILDKIT_ADDR}" --copa="$(pwd)/copa"
go test -v ./integration --addr="${COPA_BUILDKIT_ADDR}" --copa="$(pwd)/copa" -timeout 0
13 changes: 6 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,14 @@ require (
github.com/moby/buildkit v0.12.1
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0-rc4
github.com/openvex/go-vex v0.2.5
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/sync v0.3.0
)

require (
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect
google.golang.org/grpc v1.57.0
)

require (
Expand Down Expand Up @@ -69,6 +67,7 @@ require (
github.com/moby/patternmatcher v0.5.0 // indirect
github.com/moby/sys/signal v0.7.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/package-url/packageurl-go v0.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand All @@ -92,7 +91,6 @@ require (
go.opentelemetry.io/otel/trace v1.14.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sys v0.10.0 // indirect
Expand All @@ -101,7 +99,8 @@ require (
golang.org/x/tools v0.10.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 // indirect
google.golang.org/grpc v1.57.0
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,10 @@ github.com/opencontainers/runc v1.1.7 h1:y2EZDS8sNng4Ksf0GUYNhKbTShZJPJg1FiXJNH/
github.com/opencontainers/runtime-spec v1.1.0-rc.2 h1:ucBtEms2tamYYW/SvGpvq9yUN0NEVL6oyLEwDcTSrk8=
github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openvex/go-vex v0.2.5 h1:41utdp2rHgAGCsG+UbjmfMG5CWQxs15nGqir1eRgSrQ=
github.com/openvex/go-vex v0.2.5/go.mod h1:j+oadBxSUELkrKh4NfNb+BPo77U3q7gdKME88IO/0Wo=
github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU=
github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
Expand Down
21 changes: 16 additions & 5 deletions integration/patch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,20 @@ func TestPatch(t *testing.T) {
t.Parallel()

dir := t.TempDir()
output := filepath.Join(dir, "output.json")
scanResults := filepath.Join(dir, "scan.json")
ref := fmt.Sprintf("%s:%s@%s", img.Image, img.Tag, img.Digest)
tagPatched := img.Tag + "-patched"
patchedRef := fmt.Sprintf("%s:%s", img.Image, tagPatched)

t.Log("scanning original image")
scanner().
withIgnoreFile(ignoreFile).
withOutput(output).
withOutput(scanResults).
// Do not set a non-zero exit code because we are expecting vulnerabilities.
scan(t, ref, img.IgnoreErrors)

t.Log("patching image")
patch(t, ref, tagPatched, output, img.IgnoreErrors)
patch(t, ref, tagPatched, dir, img.IgnoreErrors)

t.Log("scanning patched image")
scanner().
Expand All @@ -70,11 +70,14 @@ func TestPatch(t *testing.T) {
// here we want a non-zero exit code because we are expecting no vulnerabilities.
withExitCode(1).
scan(t, patchedRef, img.IgnoreErrors)

t.Log("verifying the vex output")
validVEXJSON(t, dir)
})
}
}

func patch(t *testing.T, ref, patchedTag, scan string, ignoreErrors bool) {
func patch(t *testing.T, ref, patchedTag, path string, ignoreErrors bool) {
var addrFl string
if buildkitAddr != "" {
addrFl = "-a=" + buildkitAddr
Expand All @@ -86,10 +89,11 @@ func patch(t *testing.T, ref, patchedTag, scan string, ignoreErrors bool) {
"patch",
"-i="+ref,
"-t="+patchedTag,
"-r="+scan,
"-r="+path+"/scan.json",
"--timeout=20m",
addrFl,
"--ignore-errors="+strconv.FormatBool(ignoreErrors),
"--output="+path+"/vex.json",
)
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
Expand Down Expand Up @@ -153,3 +157,10 @@ func (s *scannerCmd) withExitCode(code int) *scannerCmd {
s.exitCode = code
return s
}

func validVEXJSON(t *testing.T, path string) {
file, err := os.ReadFile(filepath.Join(path, "vex.json"))
require.NoError(t, err)
isValid := json.Valid(file)
assert.True(t, isValid, "vex.json is not valid json")
}
6 changes: 6 additions & 0 deletions pkg/patch/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type patchArgs struct {
buildkitAddr string
timeout time.Duration
ignoreError bool
format string
output string
}

func NewPatchCmd() *cobra.Command {
Expand All @@ -44,6 +46,8 @@ func NewPatchCmd() *cobra.Command {
ua.reportFile,
ua.patchedTag,
ua.workingFolder,
ua.format,
ua.output,
ua.ignoreError)
},
}
Expand All @@ -55,6 +59,8 @@ func NewPatchCmd() *cobra.Command {
flags.StringVarP(&ua.buildkitAddr, "addr", "a", "", "Address of buildkitd service, defaults to local docker daemon with fallback to "+buildkit.DefaultAddr)
flags.DurationVar(&ua.timeout, "timeout", 5*time.Minute, "Timeout for the operation, defaults to '5m'")
flags.BoolVar(&ua.ignoreError, "ignore-errors", false, "Ignore errors and continue patching")
flags.StringVarP(&ua.format, "format", "f", "openvex", "Output format, defaults to 'openvex'")
flags.StringVarP(&ua.output, "output", "o", "", "Output file path")

if err := patchCmd.MarkFlagRequired("image"); err != nil {
panic(err)
Expand Down
34 changes: 29 additions & 5 deletions pkg/patch/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,29 @@ import (
"time"

log "github.com/sirupsen/logrus"
"golang.org/x/exp/slices"

ref "github.com/distribution/distribution/reference"
"github.com/project-copacetic/copacetic/pkg/buildkit"
"github.com/project-copacetic/copacetic/pkg/pkgmgr"
"github.com/project-copacetic/copacetic/pkg/report"
"github.com/project-copacetic/copacetic/pkg/types"
"github.com/project-copacetic/copacetic/pkg/utils"
"github.com/project-copacetic/copacetic/pkg/vex"
)

const (
defaultPatchedTagSuffix = "patched"
)

// Patch command applies package updates to an OCI image given a vulnerability report.
func Patch(ctx context.Context, timeout time.Duration, buildkitAddr, image, reportFile, patchedTag, workingFolder string, ignoreError bool) error {
func Patch(ctx context.Context, timeout time.Duration, buildkitAddr, image, reportFile, patchedTag, workingFolder, format, output string, ignoreError bool) error {
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

ch := make(chan error)
go func() {
ch <- patchWithContext(timeoutCtx, buildkitAddr, image, reportFile, patchedTag, workingFolder, ignoreError)
ch <- patchWithContext(timeoutCtx, buildkitAddr, image, reportFile, patchedTag, workingFolder, format, output, ignoreError)
}()

select {
Expand All @@ -57,7 +60,7 @@ func removeIfNotDebug(workingFolder string) {
}
}

func patchWithContext(ctx context.Context, buildkitAddr, image, reportFile, patchedTag, workingFolder string, ignoreError bool) error {
func patchWithContext(ctx context.Context, buildkitAddr, image, reportFile, patchedTag, workingFolder, format, output string, ignoreError bool) error {
imageName, err := ref.ParseNamed(image)
if err != nil {
return err
Expand Down Expand Up @@ -130,9 +133,30 @@ func patchWithContext(ctx context.Context, buildkitAddr, image, reportFile, patc

// Export the patched image state to Docker
// TODO: Add support for other output modes as buildctl does.
patchedImageState, err := pkgmgr.InstallUpdates(ctx, updates, ignoreError)
patchedImageState, errPkgs, err := pkgmgr.InstallUpdates(ctx, updates, ignoreError)
if err != nil {
return err
}
return buildkit.SolveToDocker(ctx, config.Client, patchedImageState, config.ConfigData, patchedImageName)

if err = buildkit.SolveToDocker(ctx, config.Client, patchedImageState, config.ConfigData, patchedImageName); err != nil {
return err
}

// create a new manifest with the successfully patched packages
validatedManifest := &types.UpdateManifest{
OSType: updates.OSType,
OSVersion: updates.OSVersion,
Arch: updates.Arch,
Updates: []types.UpdatePackage{},
}
for _, update := range updates.Updates {
if !slices.Contains(errPkgs, update.Name) {
validatedManifest.Updates = append(validatedManifest.Updates, update)
}
}
// vex document must contain at least one statement
if output != "" && len(validatedManifest.Updates) > 0 {
return vex.TryOutputVexDocument(validatedManifest, pkgmgr, format, output)
}
return nil
}
39 changes: 23 additions & 16 deletions pkg/pkgmgr/apk.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,17 @@ func apkReadResultsManifest(path string) ([]string, error) {
return lines, nil
}

func validateAPKPackageVersions(updates types.UpdatePackages, cmp VersionComparer, resultsPath string, ignoreErrors bool) error {
func validateAPKPackageVersions(updates types.UpdatePackages, cmp VersionComparer, resultsPath string, ignoreErrors bool) ([]string, error) {
lines, err := apkReadResultsManifest(resultsPath)
if err != nil {
return err
return nil, err
}

// Assert apk info list doesn't contain more entries than expected
if len(lines) > len(updates) {
err = fmt.Errorf("expected %d updates, installed %d", len(updates), len(lines))
log.Error(err)
return err
return nil, err
}

// Not strictly necessary, but sort the two lists to not take a dependency on the
Expand All @@ -88,6 +88,7 @@ func validateAPKPackageVersions(updates types.UpdatePackages, cmp VersionCompare
// <package name>-<version>
// ...
var allErrors *multierror.Error
var errorPkgs []string
lineIndex := 0
for _, update := range updates {
expectedPrefix := update.Name + "-"
Expand All @@ -99,54 +100,56 @@ func validateAPKPackageVersions(updates types.UpdatePackages, cmp VersionCompare
// Found a match, trim prefix- to get version string
version := strings.TrimPrefix(lines[lineIndex], expectedPrefix)
lineIndex++

if !cmp.IsValid(version) {
err := fmt.Errorf("invalid version %s found for package %s", version, update.Name)
log.Error(err)
errorPkgs = append(errorPkgs, update.Name)
allErrors = multierror.Append(allErrors, err)
continue
}
if cmp.LessThan(version, update.Version) {
err = fmt.Errorf("downloaded package %s version %s lower than required %s for update", update.Name, version, update.Version)
if cmp.LessThan(version, update.FixedVersion) {
err = fmt.Errorf("downloaded package %s version %s lower than required %s for update", update.Name, version, update.FixedVersion)
log.Error(err)
errorPkgs = append(errorPkgs, update.Name)
allErrors = multierror.Append(allErrors, err)
continue
}
log.Infof("Validated package %s version %s meets requested version %s", update.Name, version, update.Version)
log.Infof("Validated package %s version %s meets requested version %s", update.Name, version, update.FixedVersion)
}

if ignoreErrors {
return nil
return errorPkgs, nil
}

return allErrors.ErrorOrNil()
return errorPkgs, allErrors.ErrorOrNil()
}

func (am *apkManager) InstallUpdates(ctx context.Context, manifest *types.UpdateManifest, ignoreErrors bool) (*llb.State, error) {
func (am *apkManager) InstallUpdates(ctx context.Context, manifest *types.UpdateManifest, ignoreErrors bool) (*llb.State, []string, error) {
// Resolve set of unique packages to update
apkComparer := VersionComparer{isValidAPKVersion, isLessThanAPKVersion}
updates, err := GetUniqueLatestUpdates(manifest.Updates, apkComparer, ignoreErrors)
if err != nil {
return nil, err
return nil, nil, err
}
if len(updates) == 0 {
log.Warn("No update packages were specified to apply")
return &am.config.ImageState, nil
return &am.config.ImageState, nil, nil
}
log.Debugf("latest unique APKs: %v", updates)

updatedImageState, err := am.upgradePackages(ctx, updates)
if err != nil {
return nil, err
return nil, nil, err
}

// Validate that the deployed packages are of the requested version or better
resultManifestPath := filepath.Join(am.workingFolder, resultsPath, resultManifest)
if err := validateAPKPackageVersions(updates, apkComparer, resultManifestPath, ignoreErrors); err != nil {
return nil, err
errPkgs, err := validateAPKPackageVersions(updates, apkComparer, resultManifestPath, ignoreErrors)
if err != nil {
return nil, nil, err
}

return updatedImageState, nil
return updatedImageState, errPkgs, nil
}

// Patch a regular alpine image with:
Expand Down Expand Up @@ -195,3 +198,7 @@ func (am *apkManager) upgradePackages(ctx context.Context, updates types.UpdateP
patchMerge := llb.Merge([]llb.State{am.config.ImageState, patchDiff})
return &patchMerge, nil
}

func (am *apkManager) GetPackageType() string {
return "apk"
}

0 comments on commit a72c494

Please sign in to comment.