Skip to content

Commit fc1e46f

Browse files
authored
fix(secret): correctly skip secret-scanner config file from scanning (aquasecurity#10666)
1 parent a61feac commit fc1e46f

2 files changed

Lines changed: 106 additions & 28 deletions

File tree

pkg/fanal/analyzer/secret/secret.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"path/filepath"
99
"slices"
10+
"strings"
1011

1112
"github.com/samber/lo"
1213
"golang.org/x/xerrors"
@@ -45,7 +46,7 @@ type SecretAnalyzer struct {
4546
func NewSecretAnalyzer(s secret.Scanner, configPath string) *SecretAnalyzer {
4647
return &SecretAnalyzer{
4748
scanner: s,
48-
configPath: configPath,
49+
configPath: cleanPath(configPath),
4950
}
5051
}
5152

@@ -62,10 +63,17 @@ func (a *SecretAnalyzer) Init(opt analyzer.AnalyzerOptions) error {
6263
return xerrors.Errorf("secret config error: %w", err)
6364
}
6465
a.scanner = secret.NewScanner(c)
65-
a.configPath = configPath
66+
a.configPath = cleanPath(opt.SecretScannerOption.ConfigPath)
6667
return nil
6768
}
6869

70+
func cleanPath(p string) string {
71+
if p == "" {
72+
return ""
73+
}
74+
return filepath.ToSlash(filepath.Clean(p))
75+
}
76+
6977
func (a *SecretAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
7078
// Do not scan binaries
7179
binary, err := utils.IsBinary(input.Content, input.Info.Size())
@@ -114,9 +122,18 @@ func (a *SecretAnalyzer) Required(filePath string, fi os.FileInfo) bool {
114122
return false
115123
}
116124

117-
// Skip the config file for secret scanning
118-
if filepath.Base(a.configPath) == filePath {
119-
return false
125+
// Skip the secret-scanner config file itself.
126+
// a.configPath is already cleaned/slash-normalized in Init; filePath is scan-relative
127+
// from the walker but may carry native separators on Windows, so normalize it too.
128+
// We accept filePath as a path-boundary suffix of configPath to handle the common case
129+
// where --secret-config is given relative to CWD (so it carries a scan-root prefix that
130+
// the walker strips from filePath). This trades off a rare over-skip (same-basename
131+
// file elsewhere in the scan tree) for correctness in the common case.
132+
if a.configPath != "" {
133+
cleanFile := cleanPath(filePath)
134+
if a.configPath == cleanFile || strings.HasSuffix(a.configPath, "/"+cleanFile) {
135+
return false
136+
}
120137
}
121138

122139
if a.scanner.IsSkipped(filePath) {

pkg/fanal/analyzer/secret/secret_test.go

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package secret_test
22

33
import (
4+
"cmp"
45
"os"
56
"testing"
7+
"time"
68

79
"github.com/stretchr/testify/assert"
810
"github.com/stretchr/testify/require"
@@ -12,6 +14,17 @@ import (
1214
"github.com/aquasecurity/trivy/pkg/fanal/types"
1315
)
1416

17+
// fakeFileInfo lets Required tests use synthetic scan-relative paths without
18+
// needing the path to exist on disk; Required only reads Size().
19+
type fakeFileInfo struct{ size int64 }
20+
21+
func (f fakeFileInfo) Name() string { return "" }
22+
func (f fakeFileInfo) Size() int64 { return f.size }
23+
func (f fakeFileInfo) Mode() os.FileMode { return 0 }
24+
func (f fakeFileInfo) ModTime() time.Time { return time.Time{} }
25+
func (f fakeFileInfo) IsDir() bool { return false }
26+
func (f fakeFileInfo) Sys() any { return nil }
27+
1528
func TestSecretAnalyzer(t *testing.T) {
1629
wantFinding1 := types.SecretFinding{
1730
RuleID: "rule1",
@@ -208,35 +221,85 @@ func TestSecretAnalyzer(t *testing.T) {
208221
}
209222

210223
func TestSecretRequire(t *testing.T) {
224+
const defaultConfig = "testdata/skip-tests-config.yaml"
225+
211226
tests := []struct {
212-
name string
213-
filePath string
214-
want bool
227+
name string
228+
configPath string
229+
filePath string
230+
size int64
231+
want bool
215232
}{
216233
{
217-
name: "pass regular file",
218-
filePath: "testdata/secret.txt",
219-
want: true,
234+
name: "pass regular file",
235+
configPath: defaultConfig,
236+
filePath: "testdata/secret.txt",
237+
want: true,
220238
},
221239
{
222-
name: "skip small file",
223-
filePath: "testdata/emptyfile",
224-
want: false,
240+
name: "skip small file",
241+
configPath: defaultConfig,
242+
filePath: "testdata/emptyfile",
243+
size: 5,
244+
want: false,
225245
},
226246
{
227-
name: "skip folder",
228-
filePath: "testdata/node_modules/secret.txt",
229-
want: false,
247+
name: "skip folder",
248+
configPath: defaultConfig,
249+
filePath: "testdata/node_modules/secret.txt",
250+
want: false,
230251
},
231252
{
232-
name: "skip file",
233-
filePath: "testdata/package-lock.json",
234-
want: false,
253+
name: "skip file",
254+
configPath: defaultConfig,
255+
filePath: "testdata/package-lock.json",
256+
want: false,
235257
},
236258
{
237-
name: "skip extension",
238-
filePath: "testdata/secret.doc",
239-
want: false,
259+
name: "skip extension",
260+
configPath: defaultConfig,
261+
filePath: "testdata/secret.doc",
262+
want: false,
263+
},
264+
{
265+
name: "skip config file when configPath is a relative path matching filePath",
266+
configPath: "testdata/skip-tests-config.yaml",
267+
filePath: "testdata/skip-tests-config.yaml",
268+
want: false,
269+
},
270+
{
271+
name: "skip config file when configPath is a bare filename matching filePath",
272+
configPath: "skip-tests-config.yaml",
273+
filePath: "skip-tests-config.yaml",
274+
want: false,
275+
},
276+
{
277+
name: "skip config file when configPath has a scan-root prefix and filePath is the tail",
278+
configPath: "testdata/fixtures/repo/secrets/trivy-secret.yaml",
279+
filePath: "trivy-secret.yaml",
280+
want: false,
281+
},
282+
{
283+
name: "do not skip file whose basename matches but path boundary does not",
284+
configPath: "trivy-secret.yaml",
285+
filePath: "my-trivy-secret.yaml",
286+
want: true,
287+
},
288+
{
289+
// Known limitation of the path-suffix match: an unrelated file at the scan
290+
// root whose name equals the configPath's tail is also skipped. This locks
291+
// in the trade-off documented in Required so a refactor cannot silently
292+
// change it.
293+
name: "over-skip: configPath suffix matches unrelated file at scan root",
294+
configPath: "configs/myconfig.yaml",
295+
filePath: "myconfig.yaml",
296+
want: false,
297+
},
298+
{
299+
name: "do not skip file when configPath is empty",
300+
configPath: "",
301+
filePath: "src/myfile.yaml",
302+
want: true,
240303
},
241304
}
242305

@@ -245,15 +308,13 @@ func TestSecretRequire(t *testing.T) {
245308
a := secret.SecretAnalyzer{}
246309
err := a.Init(analyzer.AnalyzerOptions{
247310
SecretScannerOption: analyzer.SecretScannerOption{
248-
ConfigPath: "testdata/skip-tests-config.yaml",
311+
ConfigPath: tt.configPath,
249312
},
250313
})
251314
require.NoError(t, err)
252315

253-
fi, err := os.Stat(tt.filePath)
254-
require.NoError(t, err)
255-
256-
got := a.Required(tt.filePath, fi)
316+
size := cmp.Or(tt.size, 1024)
317+
got := a.Required(tt.filePath, fakeFileInfo{size: size})
257318
assert.Equal(t, tt.want, got)
258319
})
259320
}

0 commit comments

Comments
 (0)