@@ -281,6 +281,108 @@ func TestEngine_DuplicateSecrets(t *testing.T) {
281281 assert .Equal (t , want , e .GetMetrics ().UnverifiedSecretsFound )
282282}
283283
284+ // lineCaptureDispatcher is a test dispatcher that captures the line number
285+ // of detected secrets. It implements the Dispatcher interface and is used
286+ // to verify that the Engine correctly identifies and reports the line numbers
287+ // where secrets are found in the source code.
288+ type lineCaptureDispatcher struct { line int64 }
289+
290+ func (d * lineCaptureDispatcher ) Dispatch (_ context.Context , result detectors.ResultWithMetadata ) error {
291+ d .line = result .SourceMetadata .GetFilesystem ().GetLine ()
292+ return nil
293+ }
294+
295+ func TestEngineLineVariations (t * testing.T ) {
296+ tests := []struct {
297+ name string
298+ content string
299+ expectedLine int64
300+ }{
301+ {
302+ name : "secret on first line" ,
303+ content : `AKIA2OGYBAH6STMMNXNN
304+ aws_secret_access_key = 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` ,
305+ expectedLine : 1 ,
306+ },
307+ {
308+ name : "secret after multiple newlines" ,
309+ content : `
310+
311+
312+ AKIA2OGYBAH6STMMNXNN
313+ aws_secret_access_key = 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` ,
314+ expectedLine : 4 ,
315+ },
316+ {
317+ name : "secret with mixed whitespace before" ,
318+ content : `first line
319+
320+
321+ AKIA2OGYBAH6STMMNXNN
322+ aws_secret_access_key = 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` ,
323+ expectedLine : 4 ,
324+ },
325+ {
326+ name : "secret with content after" ,
327+ content : `[default]
328+ region = us-east-1
329+ AKIA2OGYBAH6STMMNXNN
330+ aws_secret_access_key = 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
331+ more content
332+ even more` ,
333+ expectedLine : 3 ,
334+ },
335+ }
336+
337+ for _ , tt := range tests {
338+ t .Run (tt .name , func (t * testing.T ) {
339+ t .Parallel ()
340+
341+ ctx , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
342+ defer cancel ()
343+
344+ tmpFile , err := os .CreateTemp ("" , "test_aws_credentials" )
345+ assert .NoError (t , err )
346+ defer os .Remove (tmpFile .Name ())
347+
348+ err = os .WriteFile (tmpFile .Name (), []byte (tt .content ), os .ModeAppend )
349+ assert .NoError (t , err )
350+
351+ const defaultOutputBufferSize = 64
352+ opts := []func (* sources.SourceManager ){
353+ sources .WithSourceUnits (),
354+ sources .WithBufferedOutput (defaultOutputBufferSize ),
355+ }
356+
357+ sourceManager := sources .NewManager (opts ... )
358+ lineCapturer := new (lineCaptureDispatcher )
359+
360+ conf := Config {
361+ Concurrency : 1 ,
362+ Decoders : decoders .DefaultDecoders (),
363+ Detectors : DefaultDetectors (),
364+ Verify : false ,
365+ SourceManager : sourceManager ,
366+ Dispatcher : lineCapturer ,
367+ }
368+
369+ eng , err := NewEngine (ctx , & conf )
370+ assert .NoError (t , err )
371+
372+ eng .Start (ctx )
373+
374+ cfg := sources.FilesystemConfig {Paths : []string {tmpFile .Name ()}}
375+ err = eng .ScanFileSystem (ctx , cfg )
376+ assert .NoError (t , err )
377+
378+ assert .NoError (t , eng .Finish (ctx ))
379+ want := uint64 (1 )
380+ assert .Equal (t , want , eng .GetMetrics ().UnverifiedSecretsFound )
381+ assert .Equal (t , tt .expectedLine , lineCapturer .line )
382+ })
383+ }
384+ }
385+
284386// TestEngine_VersionedDetectorsVerifiedSecrets is a test that detects ALL verified secrets across
285387// versioned detectors.
286388func TestEngine_VersionedDetectorsVerifiedSecrets (t * testing.T ) {
@@ -637,6 +739,20 @@ func TestFragmentFirstLineAndLink(t *testing.T) {
637739 expectedLine : 5 ,
638740 expectedLink : "https://example.azure.com" ,
639741 },
742+ {
743+ name : "Line number not set" ,
744+ chunk : & sources.Chunk {
745+ SourceMetadata : & source_metadatapb.MetaData {
746+ Data : & source_metadatapb.MetaData_Github {
747+ Github : & source_metadatapb.Github {
748+ Link : "https://example.github.com" ,
749+ },
750+ },
751+ },
752+ },
753+ expectedLine : 1 ,
754+ expectedLink : "https://example.github.com" ,
755+ },
640756 {
641757 name : "Unsupported Type" ,
642758 chunk : & sources.Chunk {},
0 commit comments