Files
trufflehog/pkg/handlers/default_test.go
Gleb Haranin f3eff52825 fix: report accurate line numbers for chunked file scanning (#1876) (#4615)
Fixes #1876

Problem
Files are processed in ~13KB chunks by default (10KB of actual data plus a 3KB peek into the next chunk). To infer the line the engine would take the line from chunk's metadata SourceMetadata.Line and would consider it absolute, that is it's relative to the entire file. The problem is it was never set anywhere. Since every chunk had line equals to 1 in metadata, all secret scan results appeared at line 1 + FragmentLineOffset() which resulted in line being relative to the chunk not the file itself.

Solution
Track cumulative line numbers in handleNonArchiveContent() by counting newlines as chunks are processed. Each DataOrErr now carries the correct starting line, which flows through to SourceMetadata.Filesystem.Line, giving FragmentFirstLineAndLink() the correct fragStart for the final calculation.

Affected Sources
Directly fixed:

Filesystem - now reports accurate line numbers. I have tested it using the test file provided in the issue wget https://gist.githubusercontent.com/det/1526b4c16d0e07ac023d75c912a68658/raw/c3061c14a811205a65cbdcf0065bd3c11d88bfcb/test.txt

Not affected (no Line field in proto):

S3, GCS, Jenkins, stdin - use handlers but their metadata protos lack a Line field. I think it makes sense for S3 and GCS to report line numbers so it could be a good future change.

Partially affected (own line tracking):

Git, GitHub, GitLab - regular text diffs use git-based scanning with built-in line tracking (unaffected). However, binary archives (tar.gz, zip) go through HandleBinary() → handlers.HandleFile() and benefit from this fix.
others like BitBucket use their own implementations so not affected
2026-01-15 13:21:29 -05:00

120 lines
4.1 KiB
Go

package handlers
import (
"os"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
func TestHandleNonArchiveFile(t *testing.T) {
file, err := os.Open("testdata/nonarchive.txt")
assert.Nil(t, err)
defer file.Close()
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
rdr, err := newFileReader(ctx, file)
assert.NoError(t, err)
defer rdr.Close()
handler := newDefaultHandler(defaultHandlerType)
dataOrErrChan := handler.HandleFile(context.AddLogger(ctx), rdr)
assert.NoError(t, err)
wantChunkCount := 6
count := 0
for range dataOrErrChan {
count++
}
assert.Equal(t, wantChunkCount, count)
}
// TestHandleFileLineNumbers verifies that line numbers are correctly tracked
// across multiple chunks when processing filesystem files.
// This is a regression test for https://github.com/trufflesecurity/trufflehog/issues/1876
func TestHandleFileLineNumbers(t *testing.T) {
t.Run("single chunk starts at line 1", func(t *testing.T) {
// Create a mock chunk reader with one chunk containing 3 lines.
chunks := []sources.ChunkResult{
sources.NewChunkResult([]byte("line1\nline2\nline3\n"), 18),
}
handler := newDefaultHandler(defaultHandlerType, withChunkReader(mockChunkReader(chunks)))
reader, err := newFileReader(context.Background(), strings.NewReader("ignored"))
require.NoError(t, err)
var results []DataOrErr
for dataOrErr := range handler.HandleFile(context.Background(), reader) {
results = append(results, dataOrErr)
}
require.Len(t, results, 1)
assert.Equal(t, int64(1), results[0].LineNumber, "first chunk should start at line 1")
})
t.Run("multiple chunks track line numbers correctly", func(t *testing.T) {
// Create mock chunks with known newline counts.
// Chunk 1: 10 lines (contentSize covers all data)
// Chunk 2: 5 lines
// Chunk 3: 3 lines
chunk1Data := []byte(strings.Repeat("line\n", 10)) // 10 newlines
chunk2Data := []byte(strings.Repeat("line\n", 5)) // 5 newlines
chunk3Data := []byte(strings.Repeat("line\n", 3)) // 3 newlines
chunks := []sources.ChunkResult{
sources.NewChunkResult(chunk1Data, len(chunk1Data)),
sources.NewChunkResult(chunk2Data, len(chunk2Data)),
sources.NewChunkResult(chunk3Data, len(chunk3Data)),
}
handler := newDefaultHandler(defaultHandlerType, withChunkReader(mockChunkReader(chunks)))
reader, err := newFileReader(context.Background(), strings.NewReader("ignored"))
require.NoError(t, err)
var results []DataOrErr
for dataOrErr := range handler.HandleFile(context.Background(), reader) {
results = append(results, dataOrErr)
}
require.Len(t, results, 3)
assert.Equal(t, int64(1), results[0].LineNumber, "chunk 1 should start at line 1")
assert.Equal(t, int64(11), results[1].LineNumber, "chunk 2 should start at line 11 (1 + 10)")
assert.Equal(t, int64(16), results[2].LineNumber, "chunk 3 should start at line 16 (11 + 5)")
})
t.Run("contentSize excludes peek data from line counting", func(t *testing.T) {
// Simulate peek overlap: chunk has 15 lines total but only 10 are content.
// The remaining 5 are "peek" data that shouldn't be counted.
fullData := []byte(strings.Repeat("line\n", 15)) // 15 newlines in data
contentSize := len([]byte(strings.Repeat("line\n", 10))) // Only 10 are content
chunks := []sources.ChunkResult{
sources.NewChunkResult(fullData, contentSize),
sources.NewChunkResult([]byte("final\n"), 6),
}
handler := newDefaultHandler(defaultHandlerType, withChunkReader(mockChunkReader(chunks)))
reader, err := newFileReader(context.Background(), strings.NewReader("ignored"))
require.NoError(t, err)
var results []DataOrErr
for dataOrErr := range handler.HandleFile(context.Background(), reader) {
results = append(results, dataOrErr)
}
require.Len(t, results, 2)
assert.Equal(t, int64(1), results[0].LineNumber)
// Second chunk should start at line 11 (only 10 lines counted from first chunk's content)
assert.Equal(t, int64(11), results[1].LineNumber, "peek data should not be counted")
})
}