Enable optional symlink following for fs source
This commit is contained in:
+678
-667
File diff suppressed because it is too large
Load Diff
@@ -1703,6 +1703,8 @@ func (m *Filesystem) validate(all bool) error {
|
||||
|
||||
// no validation rules for SkipBinaries
|
||||
|
||||
// no validation rules for FollowSymlinks
|
||||
|
||||
if len(errors) > 0 {
|
||||
return FilesystemMultiError(errors)
|
||||
}
|
||||
|
||||
@@ -26,15 +26,16 @@ import (
|
||||
const SourceType = sourcespb.SourceType_SOURCE_TYPE_FILESYSTEM
|
||||
|
||||
type Source struct {
|
||||
name string
|
||||
sourceId sources.SourceID
|
||||
jobId sources.JobID
|
||||
concurrency int
|
||||
verify bool
|
||||
paths []string
|
||||
log logr.Logger
|
||||
filter *common.Filter
|
||||
skipBinaries bool
|
||||
name string
|
||||
sourceId sources.SourceID
|
||||
jobId sources.JobID
|
||||
concurrency int
|
||||
verify bool
|
||||
paths []string
|
||||
log logr.Logger
|
||||
filter *common.Filter
|
||||
skipBinaries bool
|
||||
followSymlinks bool
|
||||
sources.Progress
|
||||
sources.CommonSourceUnitUnmarshaller
|
||||
}
|
||||
@@ -74,6 +75,7 @@ func (s *Source) Init(aCtx context.Context, name string, jobId sources.JobID, so
|
||||
}
|
||||
s.paths = append(conn.Paths, conn.Directories...)
|
||||
s.skipBinaries = conn.GetSkipBinaries()
|
||||
s.followSymlinks = conn.GetFollowSymlinks()
|
||||
|
||||
filter, err := common.FilterFromFiles(conn.IncludePathsFile, conn.ExcludePathsFile)
|
||||
if err != nil {
|
||||
@@ -94,13 +96,19 @@ func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ .
|
||||
s.SetProgressComplete(i, len(s.paths), fmt.Sprintf("Path: %s", path), "")
|
||||
|
||||
cleanPath := filepath.Clean(path)
|
||||
fileInfo, err := os.Lstat(cleanPath)
|
||||
var fileInfo fs.FileInfo
|
||||
var err error
|
||||
if s.followSymlinks {
|
||||
fileInfo, err = os.Stat(cleanPath)
|
||||
} else {
|
||||
fileInfo, err = os.Lstat(cleanPath)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error(err, "unable to get file info")
|
||||
continue
|
||||
}
|
||||
|
||||
if fileInfo.Mode()&os.ModeSymlink != 0 {
|
||||
if !s.followSymlinks && fileInfo.Mode()&os.ModeSymlink != 0 {
|
||||
logger.Info("skipping, not a regular file", "path", cleanPath)
|
||||
continue
|
||||
}
|
||||
@@ -148,6 +156,28 @@ func (s *Source) scanDir(ctx context.Context, path string, chunksChan chan *sour
|
||||
return nil // skip the file
|
||||
}
|
||||
|
||||
// Handle symlinks when followSymlinks is enabled
|
||||
if s.followSymlinks && d.Type()&fs.ModeSymlink != 0 {
|
||||
// Follow the symlink to see what it points to
|
||||
targetInfo, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
// Broken symlink or permission issue, skip it
|
||||
ctx.Logger().V(5).Info("unable to follow symlink", "path", fullPath, "error", err)
|
||||
return nil
|
||||
}
|
||||
// If the symlink points to a regular file, process it
|
||||
if targetInfo.Mode().IsRegular() {
|
||||
workerPool.Go(func() error {
|
||||
if err := s.scanFile(ctx, fullPath, chunksChan); err != nil {
|
||||
ctx.Logger().Error(err, "error scanning file", "path", fullPath, "error", err)
|
||||
}
|
||||
s.SetEncodedResumeInfoFor(path, fullPath)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip over non-regular files. We do this check here to suppress noisy
|
||||
// logs for trying to scan directories and other non-regular files in
|
||||
// our traversal.
|
||||
@@ -181,11 +211,17 @@ var skipSymlinkErr = errors.New("skipping symlink")
|
||||
|
||||
func (s *Source) scanFile(ctx context.Context, path string, chunksChan chan *sources.Chunk) error {
|
||||
fileCtx := context.WithValues(ctx, "path", path)
|
||||
fileStat, err := os.Lstat(path)
|
||||
var fileStat fs.FileInfo
|
||||
var err error
|
||||
if s.followSymlinks {
|
||||
fileStat, err = os.Stat(path)
|
||||
} else {
|
||||
fileStat, err = os.Lstat(path)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to stat file: %w", err)
|
||||
}
|
||||
if fileStat.Mode()&os.ModeSymlink != 0 {
|
||||
if !s.followSymlinks && fileStat.Mode()&os.ModeSymlink != 0 {
|
||||
return skipSymlinkErr
|
||||
}
|
||||
|
||||
@@ -247,7 +283,13 @@ func (s *Source) ChunkUnit(ctx context.Context, unit sources.SourceUnit, reporte
|
||||
logger := ctx.Logger().WithValues("path", path)
|
||||
|
||||
cleanPath := filepath.Clean(path)
|
||||
fileInfo, err := os.Lstat(cleanPath)
|
||||
var fileInfo fs.FileInfo
|
||||
var err error
|
||||
if s.followSymlinks {
|
||||
fileInfo, err = os.Stat(cleanPath)
|
||||
} else {
|
||||
fileInfo, err = os.Lstat(cleanPath)
|
||||
}
|
||||
if err != nil {
|
||||
return reporter.ChunkErr(ctx, fmt.Errorf("unable to get file info: %w", err))
|
||||
}
|
||||
|
||||
@@ -461,6 +461,136 @@ func TestSkipBinaries(t *testing.T) {
|
||||
require.NotContains(t, processedFiles, binaryFile, "Binary file should be skipped")
|
||||
}
|
||||
|
||||
func TestFollowSymlinks(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a temporary directory with a file and a symlink
|
||||
tempDir, err := os.MkdirTemp("", "trufflehog_symlink_test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create a real file
|
||||
realFile := filepath.Join(tempDir, "real_file.txt")
|
||||
fileContents := "secret data in real file"
|
||||
err = os.WriteFile(realFile, []byte(fileContents), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a symlink pointing to the real file
|
||||
symlinkFile := filepath.Join(tempDir, "symlink_file.txt")
|
||||
err = os.Symlink(realFile, symlinkFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test 1: followSymlinks = false (default) - should skip symlink
|
||||
t.Run("skip symlinks when followSymlinks is false", func(t *testing.T) {
|
||||
conn, err := anypb.New(&sourcespb.Filesystem{
|
||||
Paths: []string{symlinkFile},
|
||||
FollowSymlinks: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
s := Source{}
|
||||
err = s.Init(ctx, "test skip symlinks", 0, 0, true, conn, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
reporter := sourcestest.TestReporter{}
|
||||
err = s.ChunkUnit(ctx, sources.CommonSourceUnit{
|
||||
ID: symlinkFile,
|
||||
}, &reporter)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should not have any chunks because symlink was skipped
|
||||
assert.Equal(t, 0, len(reporter.Chunks), "Expected no chunks when symlinks are skipped")
|
||||
// Should have one error for the skipped symlink
|
||||
assert.Equal(t, 1, len(reporter.ChunkErrs), "Expected one error for skipped symlink")
|
||||
})
|
||||
|
||||
// Test 2: followSymlinks = true - should follow symlink
|
||||
t.Run("follow symlinks when followSymlinks is true", func(t *testing.T) {
|
||||
conn, err := anypb.New(&sourcespb.Filesystem{
|
||||
Paths: []string{symlinkFile},
|
||||
FollowSymlinks: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
s := Source{}
|
||||
err = s.Init(ctx, "test follow symlinks", 0, 0, true, conn, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
reporter := sourcestest.TestReporter{}
|
||||
err = s.ChunkUnit(ctx, sources.CommonSourceUnit{
|
||||
ID: symlinkFile,
|
||||
}, &reporter)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should have chunks because symlink was followed
|
||||
assert.Equal(t, 1, len(reporter.Chunks), "Expected chunks when symlinks are followed")
|
||||
assert.Equal(t, 0, len(reporter.ChunkErrs), "Expected no errors when following symlinks")
|
||||
|
||||
// Verify the content is correct
|
||||
if len(reporter.Chunks) > 0 {
|
||||
assert.Contains(t, string(reporter.Chunks[0].Data), fileContents, "Chunk should contain file contents")
|
||||
}
|
||||
})
|
||||
|
||||
// Test 3: Scanning directory with symlink using followSymlinks = false
|
||||
t.Run("skip symlinks in directory scan when followSymlinks is false", func(t *testing.T) {
|
||||
conn, err := anypb.New(&sourcespb.Filesystem{
|
||||
Paths: []string{tempDir},
|
||||
FollowSymlinks: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
s := Source{}
|
||||
err = s.Init(ctx, "test directory skip symlinks", 0, 0, true, conn, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
reporter := sourcestest.TestReporter{}
|
||||
err = s.ChunkUnit(ctx, sources.CommonSourceUnit{
|
||||
ID: tempDir,
|
||||
}, &reporter)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should have exactly one chunk from the real file only
|
||||
assert.Equal(t, 1, len(reporter.Chunks), "Expected one chunk from real file only")
|
||||
|
||||
// Verify it's the real file, not the symlink
|
||||
if len(reporter.Chunks) > 0 {
|
||||
metadata := reporter.Chunks[0].SourceMetadata.GetFilesystem()
|
||||
assert.NotNil(t, metadata)
|
||||
// The path should be the real file
|
||||
assert.Contains(t, metadata.File, "real_file.txt")
|
||||
}
|
||||
})
|
||||
|
||||
// Test 4: Scanning directory with symlink using followSymlinks = true
|
||||
t.Run("follow symlinks in directory scan when followSymlinks is true", func(t *testing.T) {
|
||||
conn, err := anypb.New(&sourcespb.Filesystem{
|
||||
Paths: []string{tempDir},
|
||||
FollowSymlinks: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
s := Source{}
|
||||
err = s.Init(ctx, "test directory follow symlinks", 0, 0, true, conn, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
reporter := sourcestest.TestReporter{}
|
||||
err = s.ChunkUnit(ctx, sources.CommonSourceUnit{
|
||||
ID: tempDir,
|
||||
}, &reporter)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should have two chunks: one from real file and one from symlink
|
||||
assert.Equal(t, 2, len(reporter.Chunks), "Expected two chunks when following symlinks in directory")
|
||||
|
||||
// Verify both contain the same content
|
||||
for _, chunk := range reporter.Chunks {
|
||||
assert.Contains(t, string(chunk.Data), fileContents, "Both chunks should contain file contents")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// createTempFile is a helper function to create a temporary file in the given
|
||||
// directory with the provided contents. If dir is "", the operating system's
|
||||
// temp directory is used.
|
||||
|
||||
@@ -184,6 +184,7 @@ message Filesystem {
|
||||
string include_paths_file = 3; // path to file containing newline separated list of paths
|
||||
string exclude_paths_file = 4; // path to file containing newline separated list of paths
|
||||
bool skip_binaries = 5; // allows skipping binary files from the scan
|
||||
bool follow_symlinks = 6; // allows following symbolic links during scan
|
||||
}
|
||||
|
||||
message GCS {
|
||||
|
||||
Reference in New Issue
Block a user