Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,12 @@ var (
circleCiScan = cli.Command("circleci", "Scan CircleCI")
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, 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()
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, the docker:// prefix to point to the docker daemon, otherwise an image registry is assumed.").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()
dockerScanNamespace = dockerScan.Flag("namespace", "Docker namespace (organization or user). For non-Docker Hub registries, include the registry address as well (e.g., ghcr.io/namespace or quay.io/namespace).").String()
dockerScanRegistryToken = dockerScan.Flag("registry-token", "Optional Docker registry access token. Provide this if you want to include private images within the specified namespace.").String()

travisCiScan = cli.Command("travisci", "Scan TravisCI")
travisCiScanToken = travisCiScan.Flag("token", "TravisCI token. Can also be provided with environment variable").Envar("TRAVISCI_TOKEN").Required().String()
Expand Down Expand Up @@ -931,11 +933,25 @@ func runSingleScan(ctx context.Context, cmd string, cfg engine.Config) (metrics,
refs = []sources.JobProgressRef{ref}
}
case dockerScan.FullCommand():
if *dockerScanImages != nil && *dockerScanNamespace != "" {
return scanMetrics, fmt.Errorf("invalid config: you cannot specify both images and namespace at the same time")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we have both images and namespace at the same time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We’ve been following this convention across other sources as well. The --images and --namespace flags represent two distinct workflows:

  • --images: Used to scan one or more explicitly specified images.
  • --namespace: Used to automatically discover and scan all images under a given namespace (organization or user), similar to how the GitHub source distinguishes between --org and --repo.

Both flags can technically overlap, but they are designed for different use cases. Therefore, in the CLI, we allow the user to choose only one of these options per execution just as we do for GitHub’s organization and repository flags to maintain clarity and prevent conflicting inputs.

}

if *dockerScanImages == nil && *dockerScanNamespace == "" {
return scanMetrics, fmt.Errorf("invalid config: both images and namespace cannot be empty; one is required")
}

if *dockerScanRegistryToken != "" && *dockerScanNamespace == "" {
return scanMetrics, fmt.Errorf("invalid config: registry token can only be used with registry namespace")
}

cfg := sources.DockerConfig{
BearerToken: *dockerScanToken,
Images: *dockerScanImages,
UseDockerKeychain: *dockerScanToken == "",
ExcludePaths: strings.Split(*dockerExcludePaths, ","),
Namespace: *dockerScanNamespace,
RegistryToken: *dockerScanRegistryToken,
}
if ref, err := eng.ScanDocker(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan Docker: %v", err)
Expand Down
6 changes: 4 additions & 2 deletions pkg/engine/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import (
// ScanDocker scans a given docker connection.
func (e *Engine) ScanDocker(ctx context.Context, c sources.DockerConfig) (sources.JobProgressRef, error) {
connection := &sourcespb.Docker{
Images: c.Images,
ExcludePaths: c.ExcludePaths,
Images: c.Images,
ExcludePaths: c.ExcludePaths,
Namespace: c.Namespace,
RegistryToken: c.RegistryToken,
}

switch {
Expand Down
1,337 changes: 679 additions & 658 deletions pkg/pb/sourcespb/sources.pb.go

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions pkg/pb/sourcespb/sources.pb.validate.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 53 additions & 13 deletions pkg/sources/docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@ Docker is a containerization platform that packages applications and their depen
- **Authentication Support**: Multiple authentication methods for private registries
- **File Exclusion**: Configure patterns to skip specific files or directories
- **Size Limits**: Automatically skips files exceeding 50MB to optimize performance
- **Scan All Images Under a Namespace**: Enables automatic discovery and scanning of all container images under a specified namespace (organization or user) in supported registries such as Docker Hub, Quay, and GHCR. Users no longer need to manually list or specify individual image names. The system retrieves all public images within the namespace, and if a valid registry token is provided includes private images as well. This allows for large-scale, automated scanning across all repositories within an organization.

## Configuration

### Connection Types

The Docker source supports several image reference formats:

```go
```text
// Remote registry (default)
"nginx:latest"
"myregistry.com/myapp:v1.0.0"
Expand All @@ -51,6 +52,7 @@ The Docker source supports several image reference formats:
// Tarball file
"file:///path/to/image.tar"
```

### Authentication Methods

#### 1. Unauthenticated (Public Images)
Expand Down Expand Up @@ -159,6 +161,40 @@ docker login quay.io
cat ~/.docker/config.json
```

---

### Namespace Scanning (This feature is currently in beta version and under testing)

To scan **all images** under a namespace (organization or user):

**CLI Usage:**
```bash
trufflehog docker --namespace myorg
```

To include private images within that namespace:
```bash
trufflehog docker --namespace myorg --registry-token <access_token>
```

**YAML Configuration:**
```yaml
sources:
- type: docker
name: org-scan
docker:
namespace: myorg
registry_token: "ghp_xxxxxxxxxxxxxxxxxxxx"
```

Supported registries:
- Docker Hub (`docker.io`)
- Quay (`quay.io`)
- GitHub Container Registry (`ghcr.io`)

This mode automatically enumerates all repositories within the specified namespace before scanning.

---

### File Exclusion

Expand Down Expand Up @@ -200,6 +236,18 @@ trufflehog docker --image myregistry.com/private-image:latest --exclude-paths **
trufflehog docker --image nginx:latest
```

### Scanning All Images Under a Namespace (Beta Version)

```bash
trufflehog docker --namespace trufflesecurity
```

Including private images:

```bash
trufflehog docker --namespace trufflesecurity --registry-token ghp_xxxxxxxxxxxxxxxxxxxx
```

### Scanning Multiple Images

```bash
Expand All @@ -215,10 +263,7 @@ trufflehog docker --image docker://myapp:local
### Scanning a Tarball

```bash
# First, save an image to a tarball
docker save myapp:latest -o myapp.tar

# Then scan it
trufflehog docker --image file:///path/to/myapp.tar
```

Expand All @@ -231,40 +276,35 @@ trufflehog docker --image my-registry.io/private-app:v1.0.0

## Testing Results

### Integration Test Results

| Test Case | Status | Command/Configuration | Registry URL | Notes |
|-----------|--------|----------------------|--------------|-------|
| Scan remote image on DockerHub | ✅ Success | `--image <image_name>` | https://hub.docker.com/ | Public images work without authentication |
| Scan specific tag of image on DockerHub | ✅ Success | `--image <image_name>:<tag_name>` | https://hub.docker.com/ | Tag specification working correctly |
| Scan all images under namespace | In Progress | `--namespace <namespace>` | DockerHub, Quay, GHCR | Automatically discovers all public images |
| Scan remote image on Quay.io | ✅ Success | `--image quay.io/prometheus/prometheus` | https://quay.io/search | Public Quay.io registry supported |
| Scan multiple images | ✅ Success | `--image <image_name> --image <image_name>` | Multiple registries | Sequential scanning of multiple images |
| Scan remote image on DockerHub with token | ✅ Success | Generate token using username and password | https://hub.docker.com/ | Basic auth with PAT working |
| Scan remote image on DockerHub with token | ✅ Success | `--registry-token <token>` | https://hub.docker.com/ | Authenticated scanning for private repos |
| Scan private image on Quay | ⏸️ Halted | N/A | https://quay.io/ | RedHat requires paid account for private repos |
| Scan private image on GHCR | ✅ Success | `--image ghcr.io/<image_name>` | https://github.com/packages | GitHub Container Registry |
| Scan private image on GHCR | ✅ Success | `--image ghcr.io/<image_name>` | https://github.com/packages | GitHub Container Registry supported |

## Troubleshooting

### Common Issues

**Issue**: Authentication failures with private registries

**Solution**: Ensure credentials are correct and have pull permissions. Use `docker login` first when using Docker Keychain method.
**Solution**: Ensure credentials are correct and have pull permissions. Use `docker login` first when using Docker Keychain.

---

**Issue**: Out of memory errors with large images

**Solution**: Reduce concurrency or scan smaller images. Consider increasing available memory.

---

**Issue**: Slow scanning performance

**Solution**: Enable concurrent processing, use local daemon instead of remote registry, or exclude unnecessary directories.

---

**Issue**: Files not being scanned

**Solution**: Check exclude patterns and file size limits. Verify files are under 50MB.
63 changes: 46 additions & 17 deletions pkg/sources/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (s *Source) JobID() sources.JobID {
}

// Init initializes the source.
func (s *Source) Init(_ context.Context, name string, jobId sources.JobID, sourceId sources.SourceID, verify bool, connection *anypb.Any, concurrency int) error {
func (s *Source) Init(ctx context.Context, name string, jobId sources.JobID, sourceId sources.SourceID, verify bool, connection *anypb.Any, concurrency int) error {
s.name = name
s.sourceId = sourceId
s.jobId = jobId
Expand All @@ -77,6 +77,35 @@ func (s *Source) Init(_ context.Context, name string, jobId sources.JobID, sourc
return fmt.Errorf("error unmarshalling connection: %w", err)
}

if namespace := s.conn.GetNamespace(); namespace != "" {
var registry Registry
switch {
case strings.HasPrefix(namespace, "quay.io"):
registry = &Quay{
Token: s.conn.GetRegistryToken(),
}
case strings.HasPrefix(namespace, "ghcr.io"):
registry = &GHCR{
Token: s.conn.GetRegistryToken(),
}
default: // default is dockerhub
registry = &DockerHub{
Token: s.conn.GetRegistryToken(),
}
}

ctx.Logger().Info(fmt.Sprintf("using registry: %s", registry.Name()))

namespaceImages, err := registry.ListImages(ctx, namespace)
if err != nil {
return fmt.Errorf("failed to list namespace images: %w", err)
}

ctx.Logger().Info(fmt.Sprintf("namespace: %s has %d images", namespace, len(namespaceImages)))

s.conn.Images = append(s.conn.Images, namespaceImages...)
}

// Extract exclude paths from connection and compile glob patterns
if paths := s.conn.GetExcludePaths(); len(paths) > 0 {
var err error
Expand Down Expand Up @@ -127,42 +156,42 @@ func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ .
imgInfo, err := s.processImage(ctx, image)
if err != nil {
ctx.Logger().Error(err, "error processing image", "image", image)
return nil
continue
}

ctx = context.WithValues(ctx, "image", imgInfo.base, "tag", imgInfo.tag)
imageCtx := context.WithValues(ctx, "image", imgInfo.base, "tag", imgInfo.tag)

ctx.Logger().V(2).Info("scanning image history")
imageCtx.Logger().V(2).Info("scanning image history")

layers, err := imgInfo.image.Layers()
if err != nil {
ctx.Logger().Error(err, "error getting image layers")
return nil
imageCtx.Logger().Error(err, "error getting image layers")
continue
}

// Get history entries and associate them with layers
historyEntries, err := getHistoryEntries(ctx, imgInfo, layers)
historyEntries, err := getHistoryEntries(imageCtx, imgInfo, layers)
if err != nil {
ctx.Logger().Error(err, "error getting image history entries")
return nil
imageCtx.Logger().Error(err, "error getting image history entries")
continue
}

// Scan each history entry for secrets in build commands
for _, historyEntry := range historyEntries {
if err := s.processHistoryEntry(ctx, historyEntry, chunksChan); err != nil {
ctx.Logger().Error(err, "error processing history entry")
return nil
if err := s.processHistoryEntry(imageCtx, historyEntry, chunksChan); err != nil {
imageCtx.Logger().Error(err, "error processing history entry")
continue
}
dockerHistoryEntriesScanned.WithLabelValues(s.name).Inc()
}

ctx.Logger().V(2).Info("scanning image layers")
imageCtx.Logger().V(2).Info("scanning image layers")

// Process each layer concurrently
for _, layer := range layers {
workers.Go(func() error {
if err := s.processLayer(ctx, layer, imgInfo, chunksChan); err != nil {
ctx.Logger().Error(err, "error processing layer")
if err := s.processLayer(imageCtx, layer, imgInfo, chunksChan); err != nil {
imageCtx.Logger().Error(err, "error processing layer")
return nil
}
dockerLayersScanned.WithLabelValues(s.name).Inc()
Expand All @@ -172,8 +201,8 @@ func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ .
}

if err := workers.Wait(); err != nil {
ctx.Logger().Error(err, "error processing layers")
return nil
imageCtx.Logger().Error(err, "error processing layers")
continue
}

dockerImagesScanned.WithLabelValues(s.name).Inc()
Expand Down
Loading