Files
trufflehog/pkg/handlers/handlers.go
2024-04-22 17:41:06 -07:00

225 lines
8.5 KiB
Go

package handlers
import (
"fmt"
"io"
"github.com/gabriel-vasile/mimetype"
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/readers"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
// readSeekCloser is an interface that combines the functionality of io.ReadSeekCloser and io.ReaderAt.
// It supports reading data, seeking within an open resource, and closing the resource once operations are complete.
// Additionally, it allows reading from a specific offset within the resource without altering its current position,
// enabling efficient and flexible data access patterns. This interface is particularly useful for handling files
// or other data streams where random access and sequential processing are required.
type readSeekCloser interface {
io.ReadSeekCloser
io.ReaderAt
}
// FileHandler represents a handler for files.
// It has a single method, HandleFile, which takes a context and a readSeekCloser as input,
// and returns a channel of byte slices and an error.
// The readSeekCloser extends io.ReadSeekCloser with io.ReaderAt capabilities,
// allowing handlers to perform random and direct access on the file content efficiently.
type FileHandler interface {
HandleFile(ctx logContext.Context, reader readSeekCloser) (chan []byte, error)
}
// fileHandlingConfig encapsulates configuration settings that control the behavior of file processing.
type fileHandlingConfig struct{ skipArchives bool }
// newFileHandlingConfig creates a default fileHandlingConfig with default settings.
// Optional functional parameters can customize the configuration.
func newFileHandlingConfig(options ...func(*fileHandlingConfig)) fileHandlingConfig {
config := fileHandlingConfig{}
for _, option := range options {
option(&config)
}
return config
}
// WithSkipArchives sets the skipArchives field of the fileHandlingConfig.
// If skip is true, the FileHandler will skip archive files.
func WithSkipArchives(skip bool) func(*fileHandlingConfig) {
return func(c *fileHandlingConfig) { c.skipArchives = skip }
}
type handlerType string
const (
defaultHandlerType handlerType = "default"
arHandlerType handlerType = "ar"
rpmHandlerType handlerType = "rpm"
)
type mimeType string
const (
sevenZMime mimeType = "application/x-7z-compressed"
bzip2Mime mimeType = "application/x-bzip2"
rarCompressedMime mimeType = "application/x-rar-compressed"
rarMime mimeType = "application/x-rar"
tarMime mimeType = "application/x-tar"
zipMime mimeType = "application/zip"
gxzipMime mimeType = "application/x-gzip"
gzipMime mimeType = "application/gzip"
gunzipMime mimeType = "application/x-gunzip"
gzippedMime mimeType = "application/gzipped"
gzipCompressedMime mimeType = "application/x-gzip-compressed"
gzipDocumentMime mimeType = "gzip/document"
xzMime mimeType = "application/x-xz"
msCabCompressedMime mimeType = "application/vnd.ms-cab-compressed"
rpmMime mimeType = "application/x-rpm"
fitsMime mimeType = "application/fits"
xarMime mimeType = "application/x-xar"
warcMime mimeType = "application/warc"
cpioMime mimeType = "application/cpio"
unixArMime mimeType = "application/x-unix-archive"
arMime mimeType = "application/x-archive"
debMime mimeType = "application/vnd.debian.binary-package"
lzipMime mimeType = "application/lzip"
lzipXMime mimeType = "application/x-lzip"
machoMime mimeType = "application/x-mach-binary"
octetStreamMime mimeType = "application/octet-stream"
)
var knownArchiveMimeTypes = map[mimeType]bool{
sevenZMime: true,
bzip2Mime: true,
gzipMime: true,
gxzipMime: true,
rarCompressedMime: true,
rarMime: true,
tarMime: true,
zipMime: true,
gunzipMime: true,
gzippedMime: true,
gzipCompressedMime: true,
gzipDocumentMime: true,
xzMime: true,
msCabCompressedMime: true,
rpmMime: true,
fitsMime: true,
xarMime: true,
warcMime: true,
cpioMime: true,
unixArMime: true,
arMime: true,
debMime: true,
lzipMime: true,
lzipXMime: true,
machoMime: false,
octetStreamMime: false,
}
// getHandlerForType dynamically selects and configures a FileHandler based on the provided MIME type.
// This method uses specialized handlers for specific archive types and RPM packages:
// - arHandler is used for 'arMime', 'unixArMime', and 'debMime' which include Unix archives and Debian packages.
// - rpmHandler is used for 'rpmMime' and 'cpioMime', handling RPM and CPIO archives.
// For all other MIME types, which typically include common archive formats like .zip, .tar, .gz, etc.,
// a defaultHandler is used, leveraging the archiver library to manage these formats.
// The chosen handler is then configured with provided options, adapting it to specific operational needs.
// Returns the configured handler.
func getHandlerForType(mimeT mimeType) FileHandler {
var handler FileHandler
switch mimeT {
case arMime, unixArMime, debMime:
handler = newARHandler()
case rpmMime, cpioMime:
handler = newRPMHandler()
default:
handler = newDefaultHandler(defaultHandlerType)
}
return handler
}
// HandleFile orchestrates the complete file handling process for a given file.
// It determines the MIME type of the file, selects the appropriate handler based on this type, and processes the file.
// This function initializes the handling process and delegates to the specific handler to manage file
// extraction or processing. Errors at any stage (MIME type determination, handler retrieval,
// seeking, or file handling) result in an error return value.
// Successful handling passes the file content through a channel to be chunked and reported.
//
// The function takes an io.Reader as input and creates a readSeekCloser using bufferwriter.NewBufferReadSeekCloser.
// The readSeekCloser supports seeking and provides an io.ReaderAt interface, which is essential for
// file handlers requiring random access to file content.
//
// If the skipArchives option is set to true and the detected MIME type is a known archive type,
// the function will skip processing the file and return nil.
func HandleFile(
ctx logContext.Context,
reader io.Reader,
chunkSkel *sources.Chunk,
reporter sources.ChunkReporter,
options ...func(*fileHandlingConfig),
) error {
rdr, err := readers.NewBufferedFileReader(ctx, reader)
if err != nil {
return fmt.Errorf("error creating random access reader: %w", err)
}
defer rdr.Close()
mimeT, err := mimetype.DetectReader(rdr)
if err != nil {
return fmt.Errorf("error detecting MIME type: %w", err)
}
mime := mimeType(mimeT.String())
if _, err = rdr.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("error seeking to start of file: %w", err)
}
config := newFileHandlingConfig(options...)
if config.skipArchives && knownArchiveMimeTypes[mime] {
ctx.Logger().V(5).Info("skipping archive file", "mime", mimeT.String())
return nil
}
handler := getHandlerForType(mime)
archiveChan, err := handler.HandleFile(ctx, rdr) // Delegate to the specific handler to process the file.
if err != nil {
return fmt.Errorf("error handling file: %w", err)
}
return handleChunks(ctx, archiveChan, chunkSkel, reporter)
}
// handleChunks reads data from the handlerChan and uses it to fill chunks according to a predefined skeleton (chunkSkel).
// Each filled chunk is reported using the provided reporter. This function manages the lifecycle of the channel,
// handling the termination condition when the channel closes and ensuring the cancellation of the operation if the context
// is done. It returns true if all chunks are processed successfully, otherwise returns false on errors or cancellation.
func handleChunks(
ctx logContext.Context,
handlerChan chan []byte,
chunkSkel *sources.Chunk,
reporter sources.ChunkReporter,
) error {
if handlerChan == nil {
return fmt.Errorf("handler channel is nil")
}
for {
select {
case data, open := <-handlerChan:
if !open {
ctx.Logger().V(5).Info("handler channel closed, all chunks processed")
return nil
}
chunk := *chunkSkel
chunk.Data = data
if err := reporter.ChunkOk(ctx, chunk); err != nil {
return fmt.Errorf("error reporting chunk: %w", err)
}
case <-ctx.Done():
return ctx.Err()
}
}
}