package docker import ( "archive/tar" "bytes" "compress/gzip" "errors" "fmt" "io" "strings" "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/remote" "github.com/google/go-containerregistry/pkg/v1/tarball" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" "github.com/trufflesecurity/trufflehog/v3/pkg/context" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb" "github.com/trufflesecurity/trufflehog/v3/pkg/sources" ) type Source struct { name string sourceId int64 jobId int64 verify bool conn sourcespb.Docker sources.Progress sources.CommonSourceUnitUnmarshaller } var FilesizeLimitBytes int64 = 50 * 1024 * 1024 // 50MB // Ensure the Source satisfies the interfaces at compile time. var _ sources.Source = (*Source)(nil) var _ sources.SourceUnitUnmarshaller = (*Source)(nil) // Type returns the type of source. // It is used for matching source types in configuration and job input. func (s *Source) Type() sourcespb.SourceType { return sourcespb.SourceType_SOURCE_TYPE_DOCKER } func (s *Source) SourceID() int64 { return s.sourceId } func (s *Source) JobID() int64 { return s.jobId } // Init initializes the source. func (s *Source) Init(_ context.Context, name string, jobId, sourceId int64, verify bool, connection *anypb.Any, concurrency int) error { s.name = name s.sourceId = sourceId s.jobId = jobId s.verify = verify // Reset metrics for this source at initialization time. dockerImagesScanned.WithLabelValues(s.name).Set(0) dockerLayersScanned.WithLabelValues(s.name).Set(0) if err := anypb.UnmarshalTo(connection, &s.conn, proto.UnmarshalOptions{}); err != nil { return fmt.Errorf("error unmarshalling connection: %w", err) } return nil } // Chunks emits data over a channel that is decoded and scanned for secrets. func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk) error { remoteOpts, err := s.remoteOpts() if err != nil { return err } logger := ctx.Logger().WithValues("source_type", s.Type(), "source_name", s.name) for _, image := range s.conn.GetImages() { var img v1.Image var err error var base, tag string if strings.HasPrefix(image, "file://") { image = strings.TrimPrefix(image, "file://") base = image img, err = tarball.ImageFromPath(image, nil) if err != nil { return err } } else { base, tag = baseAndTagFromImage(image) imageName, err := name.NewTag(image) if err != nil { return err } img, err = remote.Image(imageName, remoteOpts...) if err != nil { return err } } logger = logger.WithValues("image", base, "tag", tag) logger.V(2).Info("scanning image") layers, err := img.Layers() if err != nil { return err } for _, layer := range layers { digest, err := layer.Digest() if err != nil { return err } logger = logger.WithValues("layer", digest.String()) logger.V(2).Info("scanning layer") rc, err := layer.Compressed() if err != nil { return err } defer rc.Close() gzipReader, err := gzip.NewReader(rc) if err != nil { return err } defer gzipReader.Close() tarReader := tar.NewReader(gzipReader) for { header, err := tarReader.Next() if errors.Is(err, io.EOF) { break // End of archive } if err != nil { return err } // Skip files larger than FilesizeLimitBytes if header.Size > FilesizeLimitBytes { continue } file := bytes.NewBuffer(make([]byte, 0, header.Size)) _, err = io.Copy(file, tarReader) if err != nil { return err } chunk := &sources.Chunk{ SourceType: s.Type(), SourceName: s.name, SourceID: s.SourceID(), Data: file.Bytes(), SourceMetadata: &source_metadatapb.MetaData{ Data: &source_metadatapb.MetaData_Docker{ Docker: &source_metadatapb.Docker{ File: "/" + header.Name, // Prepend /, because the file is relative to the root of the image Image: base, Tag: tag, Layer: digest.String(), }, }, }, Verify: s.verify, } select { case chunksChan <- chunk: case <-ctx.Done(): return ctx.Err() } } dockerLayersScanned.WithLabelValues(s.name).Inc() } dockerImagesScanned.WithLabelValues(s.name).Inc() } return nil } func baseAndTagFromImage(image string) (base, tag string) { regRepoDelimiter := "/" tagDelim := ":" parts := strings.Split(image, tagDelim) // Verify that we aren't confusing a tag for a hostname w/ port for the purposes of weak validation. if len(parts) > 1 && !strings.Contains(parts[len(parts)-1], regRepoDelimiter) { base = strings.Join(parts[:len(parts)-1], tagDelim) tag = parts[len(parts)-1] } else { base = image tag = "latest" } return } func (s *Source) remoteOpts() ([]remote.Option, error) { switch s.conn.GetCredential().(type) { case *sourcespb.Docker_Unauthenticated: return nil, nil case *sourcespb.Docker_BasicAuth: return []remote.Option{ remote.WithAuth(&authn.Basic{ Username: s.conn.GetBasicAuth().GetUsername(), Password: s.conn.GetBasicAuth().GetPassword(), }), }, nil case *sourcespb.Docker_BearerToken: return []remote.Option{ remote.WithAuth(&authn.Bearer{ Token: s.conn.GetBearerToken(), }), }, nil case *sourcespb.Docker_DockerKeychain: return []remote.Option{ remote.WithAuthFromKeychain(authn.DefaultKeychain), }, nil default: return nil, fmt.Errorf("unknown credential type: %T", s.conn.Credential) } }