mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 14:02:26 +01:00
855 attest registry source only (#856)
Add source.NewFromRegistry function so that the syft attest command can always explicitly ask for an OCIRegistry provider rather than rely on local daemon detection for image sources. Attestation can not be used where local images loaded in a daemon are the source. Digest values for the layer identification step in attestation can sometimes vary across workstations. This fix makes it so that attest is generating an SBOM for, and attesting to, a source that exists in an OCI registry. It should never load a source from a local user docker/podman daemon. Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com> Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
co-authored by
Alex Goodman
parent
edac8c7bf7
commit
afc0c1acd9
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
func TestAttestCmd(t *testing.T) {
|
||||
coverageImage := "docker-archive:" + getFixtureImage(t, "image-pkg-coverage")
|
||||
img := "registry:busybox:latest"
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
@@ -26,19 +26,17 @@ func TestAttestCmd(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "can encode syft.json as the predicate given a password",
|
||||
args: []string{"attest", "-o", "json", coverageImage},
|
||||
args: []string{"attest", "-o", "json", img},
|
||||
assertions: []traitAssertion{
|
||||
assertSuccessfulReturnCode,
|
||||
// assertVerifyAttestation(coverageImage), Follow up on this assertion with verify blog or ephemperal registry
|
||||
},
|
||||
pw: "test",
|
||||
},
|
||||
{
|
||||
name: "can encode syft.json as the predicate given a blank password",
|
||||
args: []string{"attest", "-o", "json", coverageImage},
|
||||
args: []string{"attest", "-o", "json", img},
|
||||
assertions: []traitAssertion{
|
||||
assertSuccessfulReturnCode,
|
||||
// assertVerifyAttestation(coverageImage), Follow up on this assertion with verify blog or ephemperal registry
|
||||
},
|
||||
pw: "",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func runAndShow(t *testing.T, cmd *exec.Cmd) {
|
||||
t.Helper()
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
require.NoErrorf(t, err, "could not get stderr: +v", err)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
require.NoErrorf(t, err, "could not get stdout: +v", err)
|
||||
|
||||
err = cmd.Start()
|
||||
require.NoErrorf(t, err, "failed to start cmd: %+v", err)
|
||||
|
||||
show := func(label string, reader io.ReadCloser) {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
t.Logf("%s: %s", label, scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
show("out", stdout)
|
||||
show("err", stderr)
|
||||
}
|
||||
|
||||
func TestCosignWorkflow(t *testing.T) {
|
||||
// found under test-fixtures/registry/Makefile
|
||||
img := "localhost:5000/attest:latest"
|
||||
attestationFile := "attestation.json"
|
||||
tests := []struct {
|
||||
name string
|
||||
syftArgs []string
|
||||
cosignAttachArgs []string
|
||||
cosignVerifyArgs []string
|
||||
env map[string]string
|
||||
assertions []traitAssertion
|
||||
setup func(*testing.T)
|
||||
cleanup func()
|
||||
}{
|
||||
{
|
||||
name: "cosign verify syft attest",
|
||||
syftArgs: []string{
|
||||
"attest",
|
||||
"-o",
|
||||
"json",
|
||||
img,
|
||||
},
|
||||
// cosign attach attestation --attestation image_latest_sbom_attestation.json caphill4/attest:latest
|
||||
cosignAttachArgs: []string{
|
||||
"attach",
|
||||
"attestation",
|
||||
"--attestation",
|
||||
attestationFile,
|
||||
img,
|
||||
},
|
||||
// cosign verify-attestation -key cosign.pub caphill4/attest:latest
|
||||
cosignVerifyArgs: []string{
|
||||
"verify-attestation",
|
||||
"-key",
|
||||
"cosign.pub",
|
||||
img,
|
||||
},
|
||||
assertions: []traitAssertion{
|
||||
assertSuccessfulReturnCode,
|
||||
},
|
||||
setup: func(t *testing.T) {
|
||||
cwd, err := os.Getwd()
|
||||
require.NoErrorf(t, err, "unable to get cwd: %+v", err)
|
||||
|
||||
// get working directory for local registry
|
||||
fixturesPath := filepath.Join(cwd, "test-fixtures", "registry")
|
||||
makeTask := filepath.Join(fixturesPath, "Makefile")
|
||||
t.Logf("Generating Fixture from 'make %s'", makeTask)
|
||||
|
||||
cmd := exec.Command("make")
|
||||
cmd.Dir = fixturesPath
|
||||
runAndShow(t, cmd)
|
||||
|
||||
var done = make(chan struct{})
|
||||
defer close(done)
|
||||
for interval := range testRetryIntervals(done) {
|
||||
resp, err := http.Get("http://127.0.0.1:5000/v2/")
|
||||
if err != nil {
|
||||
t.Logf("waiting for registry err=%+v", err)
|
||||
} else {
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
break
|
||||
}
|
||||
t.Logf("waiting for registry code=%+v", resp.StatusCode)
|
||||
}
|
||||
|
||||
time.Sleep(interval)
|
||||
}
|
||||
|
||||
cmd = exec.Command("make", "push")
|
||||
cmd.Dir = fixturesPath
|
||||
runAndShow(t, cmd)
|
||||
|
||||
},
|
||||
cleanup: func() {
|
||||
cwd, err := os.Getwd()
|
||||
assert.NoErrorf(t, err, "unable to get cwd: %+v", err)
|
||||
|
||||
fixturesPath := filepath.Join(cwd, "test-fixtures", "registry")
|
||||
makeTask := filepath.Join(fixturesPath, "Makefile")
|
||||
t.Logf("Generating Fixture from 'make %s'", makeTask)
|
||||
|
||||
// delete attestation file
|
||||
os.Remove(attestationFile)
|
||||
|
||||
cmd := exec.Command("make", "stop")
|
||||
cmd.Dir = fixturesPath
|
||||
|
||||
runAndShow(t, cmd)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Cleanup(tt.cleanup)
|
||||
tt.setup(t)
|
||||
pkiCleanup := setupPKI(t, "") // blank password
|
||||
defer pkiCleanup()
|
||||
|
||||
// attest
|
||||
cmd, stdout, stderr := runSyft(t, tt.env, tt.syftArgs...)
|
||||
for _, traitFn := range tt.assertions {
|
||||
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
|
||||
}
|
||||
checkCmdFailure(t, stdout, stderr, cmd)
|
||||
require.NoError(t, os.WriteFile(attestationFile, []byte(stdout), 0666))
|
||||
|
||||
// attach
|
||||
cmd, stdout, stderr = runCosign(t, tt.env, tt.cosignAttachArgs...)
|
||||
for _, traitFn := range tt.assertions {
|
||||
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
|
||||
}
|
||||
checkCmdFailure(t, stdout, stderr, cmd)
|
||||
|
||||
// attest
|
||||
cmd, stdout, stderr = runCosign(t, tt.env, tt.cosignAttachArgs...)
|
||||
for _, traitFn := range tt.assertions {
|
||||
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
|
||||
}
|
||||
checkCmdFailure(t, stdout, stderr, cmd)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkCmdFailure(t testing.TB, stdout, stderr string, cmd *exec.Cmd) {
|
||||
require.Falsef(t, t.Failed(), "%s %s trait assertion failed", cmd.Path, strings.Join(cmd.Args, " "))
|
||||
if t.Failed() {
|
||||
t.Log("STDOUT:\n", stdout)
|
||||
t.Log("STDERR:\n", stderr)
|
||||
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
**/*
|
||||
@@ -0,0 +1 @@
|
||||
FROM alpine:latest
|
||||
@@ -0,0 +1,23 @@
|
||||
all: build start
|
||||
|
||||
.PHONY: stop
|
||||
stop:
|
||||
docker kill registry
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
docker build -t localhost:5000/attest:latest .
|
||||
|
||||
.PHONY: start
|
||||
start:
|
||||
docker run --rm \
|
||||
-d \
|
||||
--name registry \
|
||||
-it \
|
||||
--privileged \
|
||||
-p 5000:5000 \
|
||||
registry:2.8
|
||||
|
||||
.PHONY: push
|
||||
push:
|
||||
docker push localhost:5000/attest:latest
|
||||
@@ -3,6 +3,7 @@ package cli
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
)
|
||||
@@ -95,6 +97,20 @@ func runSyft(t testing.TB, env map[string]string, args ...string) (*exec.Cmd, st
|
||||
return cmd, stdout, stderr
|
||||
}
|
||||
|
||||
func runCosign(t testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) {
|
||||
cmd := getCosignCommand(t, args...)
|
||||
if env == nil {
|
||||
env = make(map[string]string)
|
||||
}
|
||||
|
||||
stdout, stderr := runCommand(cmd, env)
|
||||
return cmd, stdout, stderr
|
||||
}
|
||||
|
||||
func getCosignCommand(t testing.TB, args ...string) *exec.Cmd {
|
||||
return exec.Command(filepath.Join(repoRoot(t), ".tmp/cosign"), args...)
|
||||
}
|
||||
|
||||
func runCommand(cmd *exec.Cmd, env map[string]string) (string, string) {
|
||||
if env != nil {
|
||||
cmd.Env = append(os.Environ(), envMapToSlice(env)...)
|
||||
@@ -154,3 +170,40 @@ func repoRoot(t testing.TB) string {
|
||||
}
|
||||
return absRepoRoot
|
||||
}
|
||||
|
||||
func testRetryIntervals(done <-chan struct{}) <-chan time.Duration {
|
||||
return exponentialBackoffDurations(250*time.Millisecond, 4*time.Second, 2, done)
|
||||
}
|
||||
|
||||
func exponentialBackoffDurations(minDuration, maxDuration time.Duration, step float64, done <-chan struct{}) <-chan time.Duration {
|
||||
sleepDurations := make(chan time.Duration)
|
||||
go func() {
|
||||
defer close(sleepDurations)
|
||||
retryLoop:
|
||||
for attempt := 0; ; attempt++ {
|
||||
duration := exponentialBackoffDuration(minDuration, maxDuration, step, attempt)
|
||||
|
||||
select {
|
||||
case sleepDurations <- duration:
|
||||
break
|
||||
case <-done:
|
||||
break retryLoop
|
||||
}
|
||||
|
||||
if duration == maxDuration {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
return sleepDurations
|
||||
}
|
||||
|
||||
func exponentialBackoffDuration(minDuration, maxDuration time.Duration, step float64, attempt int) time.Duration {
|
||||
duration := time.Duration(float64(minDuration) * math.Pow(step, float64(attempt)))
|
||||
if duration < minDuration {
|
||||
return minDuration
|
||||
} else if duration > maxDuration {
|
||||
return maxDuration
|
||||
}
|
||||
return duration
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user