Thread original chunk data through engine pipeline (#4780)

* [secret-storage] Thread original chunk data through engine pipeline

Adds OriginalData/ChunkData fields to preserve pre-decode source data
through the scan pipeline:

1. Chunk.OriginalData: captures chunk.Data before iterativeDecode
2. engine.go: sets chunk.OriginalData = chunk.Data before decode
3. ResultWithMetadata.ChunkData: populated by CopyMetadata from
   OriginalData (falls back to Data when nil)

This enables downstream consumers (e.g. the dispatcher in thog) to
access the original source data for secret storage encryption.

* Update TestChunkSize for OriginalData field addition

Chunk struct grew from 80 to 104 bytes with the OriginalData []byte
slice header (24 bytes). Field placement is already optimal (adjacent
to Data []byte).

* fix: preserve OriginalData field in EscapedUnicode decoder

The EscapedUnicode decoder constructed a new sources.Chunk manually
copying fields but omitted OriginalData. This caused CopyMetadata to
fall back to the decoded Data instead of the original pre-decode content,
defeating the purpose of preserving original chunk data for secret
storage encryption.

* Address PR review feedback: use testify/assert, add nil-guard comment, remove stale alignment comment

---------

Co-authored-by: Cursor Agent <[email protected]>
This commit is contained in:
Dustin Decker
2026-03-03 14:34:05 -05:00
committed by GitHub
co-authored by Cursor Agent
parent 8df943f829
commit 648aca62d5
6 changed files with 60 additions and 7 deletions
+1
View File
@@ -105,6 +105,7 @@ func (d *EscapedUnicode) FromChunk(chunk *sources.Chunk) *DecodableChunk {
DecoderType: d.Type(),
Chunk: &sources.Chunk{
Data: chunkData,
OriginalData: chunk.OriginalData,
SourceName: chunk.SourceName,
SourceID: chunk.SourceID,
JobID: chunk.JobID,
+40
View File
@@ -0,0 +1,40 @@
package detectors
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
func TestCopyMetadata_ChunkDataFromOriginalData(t *testing.T) {
chunk := &sources.Chunk{
Data: []byte("decoded-data"),
OriginalData: []byte("original-source-data"),
SourceName: "test-source",
}
result := Result{
DetectorType: 1,
Raw: []byte("secret"),
}
rwm := CopyMetadata(chunk, result)
assert.Equal(t, "original-source-data", string(rwm.ChunkData))
}
func TestCopyMetadata_ChunkDataFallsBackToData(t *testing.T) {
chunk := &sources.Chunk{
Data: []byte("only-data"),
SourceName: "test-source",
}
result := Result{
DetectorType: 1,
Raw: []byte("secret"),
}
rwm := CopyMetadata(chunk, result)
assert.Equal(t, "only-data", string(rwm.ChunkData))
}
+10
View File
@@ -217,10 +217,19 @@ type ResultWithMetadata struct {
DetectorDescription string
// DecoderType is the type of decoder that was used to generate this result's data.
DecoderType detectorspb.DecoderType
// ChunkData holds the original pre-decode source chunk data, preserved
// for secret storage encryption in the dispatcher.
ChunkData []byte
}
// CopyMetadata returns a detector result with included metadata from the source chunk.
func CopyMetadata(chunk *sources.Chunk, result Result) ResultWithMetadata {
// OriginalData may be nil when CopyMetadata is called outside the engine
// pipeline (e.g., in tests or external consumers that construct chunks directly).
chunkData := chunk.OriginalData
if chunkData == nil {
chunkData = chunk.Data
}
return ResultWithMetadata{
SourceMetadata: chunk.SourceMetadata,
SourceID: chunk.SourceID,
@@ -229,6 +238,7 @@ func CopyMetadata(chunk *sources.Chunk, result Result) ResultWithMetadata {
SourceType: chunk.SourceType,
SourceName: chunk.SourceName,
Result: result,
ChunkData: chunkData,
}
}
+1
View File
@@ -854,6 +854,7 @@ func (e *Engine) scannerWorker(ctx context.Context) {
startTime := time.Now()
sourceVerify := chunk.SourceVerify
chunk.OriginalData = chunk.Data
decoded := iterativeDecode(chunk, e.decoders, e.maxDecodeDepth)
for _, d := range decoded {
+4 -5
View File
@@ -21,14 +21,13 @@ type (
)
// Chunk contains data to be decoded and scanned along with context on where it came from.
//
// **Important:** The order of the fields in this struct is specifically designed to optimize
// struct alignment and minimize memory usage. Do not change the field order without carefully considering
// the potential impact on memory consumption.
// Ex: https://go.dev/play/p/Azf4a7O-DhC
type Chunk struct {
// Data is the data to decode and scan.
Data []byte
// OriginalData holds the pre-decode source data, preserved for secret
// storage. Set before iterative decoding so it retains the original
// content even after Data is replaced with decoded forms.
OriginalData []byte
// SourceName is the name of the Source that produced the chunk.
SourceName string
+4 -2
View File
@@ -7,8 +7,10 @@ import (
"github.com/stretchr/testify/assert"
)
// TestChunkSize ensures that the Chunk struct does not exceed 80 bytes.
// TestChunkSize ensures that the Chunk struct does not exceed 104 bytes.
// Size increased from 80 to 104 with the addition of OriginalData []byte
// (24-byte slice header) for secret storage chunk threading.
func TestChunkSize(t *testing.T) {
t.Parallel()
assert.Equal(t, unsafe.Sizeof(Chunk{}), uintptr(80), "Chunk struct size exceeds 80 bytes")
assert.Equal(t, unsafe.Sizeof(Chunk{}), uintptr(104), "Chunk struct size exceeds 104 bytes")
}