Add support for docker daemon as a source (#4306)
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
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run
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
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run
Now you can scan an image directly after building it with docker build by using the docker:// prefix. This is ideal for local development and CI/CD pipelines that want to ensure images do not contain leaked secrets before pushing to an image registry.
This resolves "Add support for scanning images from the Docker daemon" #4275.
This reverts commit 562dd7242b, which reverted the original version of this change that had some issues with its tests that we did not notice until after we merged it.
This commit is contained in:
@@ -280,7 +280,14 @@ trufflehog gcs --project-id=<project-ID> --cloud-environment --results=verified,
|
||||
Use the `--image` flag multiple times to scan multiple images.
|
||||
|
||||
```bash
|
||||
# to scan from a remote registry
|
||||
trufflehog docker --image trufflesecurity/secrets --results=verified,unknown
|
||||
|
||||
# to scan from the local docker daemon
|
||||
trufflehog docker --image docker://new_image:tag --results=verified,unknown
|
||||
|
||||
# to scan from an image saved as a tarball
|
||||
trufflehog docker --image file://path_to_image.tar --results=verified,unknown
|
||||
```
|
||||
|
||||
## 12: Scan in CI
|
||||
@@ -672,7 +679,7 @@ TruffleHog will send a JSON POST request containing the regex matches to a
|
||||
configured webhook endpoint. If the endpoint responds with a `200 OK` response
|
||||
status code, the secret is considered verified.
|
||||
|
||||
Custom Detectors support a few different filtering mechanisms: entropy, regex targeting the entire match, regex targeting the captured secret,
|
||||
Custom Detectors support a few different filtering mechanisms: entropy, regex targeting the entire match, regex targeting the captured secret,
|
||||
and excluded word lists checked against the secret (captured group if present, entire match if capture group is not present). Note that if
|
||||
your custom detector has multiple `regex` set (in this example `hogID`, and `hogToken`), then the filters get applied to each regex. [Here](examples/generic_with_filters.yml) is an example of a custom detector using these filters.
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ var (
|
||||
circleCiScanToken = circleCiScan.Flag("token", "CircleCI token. Can also be provided with environment variable").Envar("CIRCLECI_TOKEN").Required().String()
|
||||
|
||||
dockerScan = cli.Command("docker", "Scan Docker Image")
|
||||
dockerScanImages = dockerScan.Flag("image", "Docker image to scan. Use the file:// prefix to point to a local tarball, otherwise a image registry is assumed.").Required().Strings()
|
||||
dockerScanImages = dockerScan.Flag("image", "Docker image to scan. Use the file:// prefix to point to a local tarball, the docker:// prefix to point to the docker daemon, otherwise an image registry is assumed.").Required().Strings()
|
||||
dockerScanToken = dockerScan.Flag("token", "Docker bearer token. Can also be provided with environment variable").Envar("DOCKER_TOKEN").String()
|
||||
dockerExcludePaths = dockerScan.Flag("exclude-paths", "Comma separated list of paths to exclude from scan").String()
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/daemon"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
||||
gzip "github.com/klauspost/pgzip"
|
||||
@@ -179,8 +180,8 @@ func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ .
|
||||
func (s *Source) processImage(ctx context.Context, image string) (imageInfo, error) {
|
||||
var (
|
||||
imgInfo imageInfo
|
||||
hasDigest bool
|
||||
imageName name.Reference
|
||||
err error
|
||||
)
|
||||
|
||||
remoteOpts, err := s.remoteOpts()
|
||||
@@ -189,6 +190,7 @@ func (s *Source) processImage(ctx context.Context, image string) (imageInfo, err
|
||||
}
|
||||
|
||||
const filePrefix = "file://"
|
||||
const dockerPrefix = "docker://"
|
||||
if strings.HasPrefix(image, filePrefix) {
|
||||
image = strings.TrimPrefix(image, filePrefix)
|
||||
imgInfo.base = image
|
||||
@@ -196,18 +198,21 @@ func (s *Source) processImage(ctx context.Context, image string) (imageInfo, err
|
||||
if err != nil {
|
||||
return imgInfo, err
|
||||
}
|
||||
} else if strings.HasPrefix(image, dockerPrefix) {
|
||||
image = strings.TrimPrefix(image, dockerPrefix)
|
||||
imgInfo, imageName, err = s.extractImageNameTagDigest(image)
|
||||
if err != nil {
|
||||
return imgInfo, err
|
||||
}
|
||||
imgInfo.image, err = daemon.Image(imageName)
|
||||
if err != nil {
|
||||
return imgInfo, err
|
||||
}
|
||||
} else {
|
||||
imgInfo.base, imgInfo.tag, hasDigest = baseAndTagFromImage(image)
|
||||
|
||||
if hasDigest {
|
||||
imageName, err = name.NewDigest(image)
|
||||
} else {
|
||||
imageName, err = name.NewTag(image)
|
||||
}
|
||||
imgInfo, imageName, err = s.extractImageNameTagDigest(image)
|
||||
if err != nil {
|
||||
return imgInfo, err
|
||||
}
|
||||
|
||||
imgInfo.image, err = remote.Image(imageName, remoteOpts...)
|
||||
if err != nil {
|
||||
return imgInfo, err
|
||||
@@ -219,6 +224,29 @@ func (s *Source) processImage(ctx context.Context, image string) (imageInfo, err
|
||||
return imgInfo, nil
|
||||
}
|
||||
|
||||
// extractImageNameTagDigest parses the provided Docker image string and returns a name.Reference
|
||||
// representing either the image's tag or digest, and any error encountered during parsing.
|
||||
func (*Source) extractImageNameTagDigest(image string) (imageInfo, name.Reference, error) {
|
||||
var (
|
||||
hasDigest bool
|
||||
imgInfo imageInfo
|
||||
imgName name.Reference
|
||||
err error
|
||||
)
|
||||
imgInfo.base, imgInfo.tag, hasDigest = baseAndTagFromImage(image)
|
||||
|
||||
if hasDigest {
|
||||
imgName, err = name.NewDigest(image)
|
||||
} else {
|
||||
imgName, err = name.NewTag(image)
|
||||
}
|
||||
if err != nil {
|
||||
return imgInfo, imgName, err
|
||||
}
|
||||
|
||||
return imgInfo, imgName, nil
|
||||
}
|
||||
|
||||
// getHistoryEntries collates an image's configuration history together with the
|
||||
// corresponding layer digests for any non-empty layers.
|
||||
func getHistoryEntries(ctx context.Context, imgInfo imageInfo, layers []v1.Layer) ([]historyEntryInfo, error) {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
image "github.com/docker/docker/api/types/image"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
@@ -134,6 +137,100 @@ func TestDockerImageScanWithDigest(t *testing.T) {
|
||||
assert.Equal(t, 1, historyCounter)
|
||||
}
|
||||
|
||||
func TestDockerImageScanFromLocalDaemon(t *testing.T) {
|
||||
dockerDaemonTestCases := []struct {
|
||||
name string
|
||||
image string
|
||||
}{
|
||||
{
|
||||
name: "TestDockerImageScanFromLocalDaemon",
|
||||
image: "docker://trufflesecurity/secrets",
|
||||
},
|
||||
{
|
||||
name: "TestDockerImageScanFromLocalDaemonWithDigest",
|
||||
image: "docker://trufflesecurity/secrets@sha256:864f6d41209462d8e37fc302ba1532656e265f7c361f11e29fed6ca1f4208e11",
|
||||
},
|
||||
{
|
||||
name: "TestDockerImageScanFromLocalDaemonWithTag",
|
||||
image: "docker://trufflesecurity/secrets:latest",
|
||||
},
|
||||
}
|
||||
|
||||
// pull the image here to ensure it exists locally
|
||||
img := "docker.io/trufflesecurity/secrets:latest"
|
||||
|
||||
client, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv, dockerClient.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
t.Errorf("Failed to create Docker client: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.ImagePull(context.TODO(), img, image.PullOptions{})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load image %s: %v", img, err)
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Close()
|
||||
|
||||
// if we don't read the response, the image will not be available in the local Docker daemon
|
||||
_, err = io.ReadAll(resp)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
for _, tt := range dockerDaemonTestCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// This test assumes the local Docker daemon is running
|
||||
dockerConn := &sourcespb.Docker{
|
||||
Credential: &sourcespb.Docker_Unauthenticated{
|
||||
Unauthenticated: &credentialspb.Unauthenticated{},
|
||||
},
|
||||
Images: []string{tt.image},
|
||||
}
|
||||
|
||||
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 TestBaseAndTagFromImage(t *testing.T) {
|
||||
tests := []struct {
|
||||
image string
|
||||
|
||||
Reference in New Issue
Block a user