Lint / golangci-lint (push) Waiting to run
Lint / semgrep (push) Waiting to run
Release / Release (push) Waiting to run
Scan for secrets / test (push) Waiting to run
Snifftest / Run Snifftest (push) Waiting to run
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run
* feat: support docker image history scanning * refactor: collapse error handling into return Style suggestion from review feedback. * fix: associate layers with history entries Where possible, add the associated layer to the history entry record. This may help tracing any issues discovered. This also changes the entry reference format to `image-metadata:history:%d:created-by` which _may_ be more self-explanatory.
169 lines
4.5 KiB
Go
169 lines
4.5 KiB
Go
package docker
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/credentialspb"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
|
|
)
|
|
|
|
func TestDockerImageScan(t *testing.T) {
|
|
dockerConn := &sourcespb.Docker{
|
|
Credential: &sourcespb.Docker_Unauthenticated{
|
|
Unauthenticated: &credentialspb.Unauthenticated{},
|
|
},
|
|
Images: []string{"trufflesecurity/secrets"},
|
|
}
|
|
|
|
conn := &anypb.Any{}
|
|
err := conn.MarshalFrom(dockerConn)
|
|
assert.NoError(t, err)
|
|
|
|
s := &Source{}
|
|
err = s.Init(context.TODO(), "test source", 0, 0, false, conn, 1)
|
|
assert.NoError(t, err)
|
|
|
|
var wg sync.WaitGroup
|
|
chunksChan := make(chan *sources.Chunk, 1)
|
|
chunkCounter := 0
|
|
layerCounter := 0
|
|
historyCounter := 0
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for chunk := range chunksChan {
|
|
assert.NotEmpty(t, chunk)
|
|
chunkCounter++
|
|
|
|
if isHistoryChunk(t, chunk) {
|
|
historyCounter++
|
|
} else {
|
|
layerCounter++
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = s.Chunks(context.TODO(), chunksChan)
|
|
assert.NoError(t, err)
|
|
|
|
close(chunksChan)
|
|
wg.Wait()
|
|
|
|
assert.Equal(t, 2, chunkCounter)
|
|
assert.Equal(t, 1, layerCounter)
|
|
assert.Equal(t, 1, historyCounter)
|
|
}
|
|
|
|
func TestDockerImageScanWithDigest(t *testing.T) {
|
|
dockerConn := &sourcespb.Docker{
|
|
Credential: &sourcespb.Docker_Unauthenticated{
|
|
Unauthenticated: &credentialspb.Unauthenticated{},
|
|
},
|
|
Images: []string{"trufflesecurity/secrets@sha256:864f6d41209462d8e37fc302ba1532656e265f7c361f11e29fed6ca1f4208e11"},
|
|
}
|
|
|
|
conn := &anypb.Any{}
|
|
err := conn.MarshalFrom(dockerConn)
|
|
assert.NoError(t, err)
|
|
|
|
s := &Source{}
|
|
err = s.Init(context.TODO(), "test source", 0, 0, false, conn, 1)
|
|
assert.NoError(t, err)
|
|
|
|
var wg sync.WaitGroup
|
|
chunksChan := make(chan *sources.Chunk, 1)
|
|
chunkCounter := 0
|
|
layerCounter := 0
|
|
historyCounter := 0
|
|
|
|
var historyChunk *source_metadatapb.Docker
|
|
var layerChunk *source_metadatapb.Docker
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for chunk := range chunksChan {
|
|
assert.NotEmpty(t, chunk)
|
|
chunkCounter++
|
|
|
|
if isHistoryChunk(t, chunk) {
|
|
// save last for later comparison
|
|
historyChunk = chunk.SourceMetadata.GetDocker()
|
|
historyCounter++
|
|
} else {
|
|
layerChunk = chunk.SourceMetadata.GetDocker()
|
|
layerCounter++
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = s.Chunks(context.TODO(), chunksChan)
|
|
assert.NoError(t, err)
|
|
|
|
close(chunksChan)
|
|
wg.Wait()
|
|
|
|
// Since this test pins the layer by digest, layers will have consistent
|
|
// hashes. This allows layer digest comparison as they will be stable for
|
|
// given image digest.
|
|
assert.Equal(t, &source_metadatapb.Docker{
|
|
Image: "trufflesecurity/secrets",
|
|
Tag: "sha256:864f6d41209462d8e37fc302ba1532656e265f7c361f11e29fed6ca1f4208e11",
|
|
File: "image-metadata:history:0:created-by",
|
|
Layer: "sha256:a794864de8c4ff087813fd66cff74601b84cbef8fe1a1f17f9923b40cf051b59",
|
|
}, historyChunk)
|
|
|
|
assert.Equal(t, &source_metadatapb.Docker{
|
|
Image: "trufflesecurity/secrets",
|
|
Tag: "sha256:864f6d41209462d8e37fc302ba1532656e265f7c361f11e29fed6ca1f4208e11",
|
|
File: "/aws",
|
|
Layer: "sha256:a794864de8c4ff087813fd66cff74601b84cbef8fe1a1f17f9923b40cf051b59",
|
|
}, layerChunk)
|
|
|
|
assert.Equal(t, 2, chunkCounter)
|
|
assert.Equal(t, 1, layerCounter)
|
|
assert.Equal(t, 1, historyCounter)
|
|
}
|
|
|
|
func TestBaseAndTagFromImage(t *testing.T) {
|
|
tests := []struct {
|
|
image string
|
|
wantBase string
|
|
wantTag string
|
|
wantDigest bool
|
|
}{
|
|
{"golang:1.16", "golang", "1.16", false},
|
|
{"golang@sha256:abcdef", "golang", "sha256:abcdef", true},
|
|
{"ghcr.io/golang:1.16", "ghcr.io/golang", "1.16", false},
|
|
{"ghcr.io/golang:nightly", "ghcr.io/golang", "nightly", false},
|
|
{"ghcr.io/golang", "ghcr.io/golang", "latest", false},
|
|
{"ghcr.io/trufflesecurity/secrets", "ghcr.io/trufflesecurity/secrets", "latest", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
gotBase, gotTag, gotDigest := baseAndTagFromImage(tt.image)
|
|
if gotBase != tt.wantBase || gotTag != tt.wantTag || gotDigest != tt.wantDigest {
|
|
t.Errorf("baseAndTagFromImage(%q) = (%q, %q, %v), want (%q, %q, %v)",
|
|
tt.image, gotBase, gotTag, gotDigest, tt.wantBase, tt.wantTag, tt.wantDigest)
|
|
}
|
|
}
|
|
}
|
|
|
|
func isHistoryChunk(t *testing.T, chunk *sources.Chunk) bool {
|
|
t.Helper()
|
|
|
|
metadata := chunk.SourceMetadata.GetDocker()
|
|
|
|
return metadata != nil &&
|
|
strings.HasPrefix(metadata.File, "image-metadata:history:")
|
|
}
|