Skip to content

Commit b2f851d

Browse files
Generalize check for suspicious dirs (#4)
1 parent 22b663e commit b2f851d

File tree

4 files changed

+187
-15
lines changed

4 files changed

+187
-15
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@
1010

1111
# Output of the go coverage tool, specifically when used with LiteIDE
1212
*.out
13-
.idea
13+
.idea
14+
15+
.vscode

pkg/assessor/cache/cache.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,29 @@ func (a CacheAssessor) Assess(fileMap deckodertypes.FileMap) ([]*types.Assessmen
3131
dirName := filepath.Dir(filename)
3232
dirBase := filepath.Base(dirName)
3333

34-
// match Directory
35-
if utils.StringInSlice(dirBase+"/", reqDirs) || utils.StringInSlice(dirName+"/", reqDirs) {
36-
if _, ok := detectedDir[dirName]; ok {
34+
// match Directory - check if any component of the directory path matches a required dir
35+
matched := utils.StringInSlice(dirBase+"/", reqDirs) || utils.StringInSlice(dirName+"/", reqDirs)
36+
reportDir := dirName
37+
38+
if !matched {
39+
// Check if any directory component in the path matches a required directory
40+
// and find the top-level occurrence to report
41+
parts := strings.Split(dirName, "/")
42+
for i, part := range parts {
43+
if utils.StringInSlice(part+"/", reqDirs) {
44+
matched = true
45+
// Report only up to and including the first required directory component
46+
reportDir = strings.Join(parts[:i+1], "/")
47+
break
48+
}
49+
}
50+
}
51+
52+
if matched {
53+
if _, ok := detectedDir[reportDir]; ok {
3754
continue
3855
}
39-
detectedDir[dirName] = struct{}{}
56+
detectedDir[reportDir] = struct{}{}
4057

4158
// Skip uncontrollable dependency directory e.g) npm : node_modules, php: composer
4259
if inIgnoreDir(filename) {
@@ -47,8 +64,8 @@ func (a CacheAssessor) Assess(fileMap deckodertypes.FileMap) ([]*types.Assessmen
4764
assesses,
4865
&types.Assessment{
4966
Code: types.InfoDeletableFiles,
50-
Filename: dirName,
51-
Desc: fmt.Sprintf("Suspicious directory : %s ", dirName),
67+
Filename: reportDir,
68+
Desc: fmt.Sprintf("Suspicious directory : %s ", reportDir),
5269
})
5370

5471
}

pkg/assessor/cache/cache_test.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package cache
2+
3+
import (
4+
"testing"
5+
6+
"github.com/SpazioDati/dockle/pkg/log"
7+
"github.com/SpazioDati/dockle/pkg/types"
8+
deckodertypes "github.com/goodwithtech/deckoder/types"
9+
)
10+
11+
func init() {
12+
// Initialize logger for tests
13+
log.InitLogger(false, false)
14+
}
15+
16+
func TestAssess(t *testing.T) {
17+
tests := []struct {
18+
name string
19+
fileMap deckodertypes.FileMap
20+
expectedCount int
21+
expectedFiles []string
22+
}{
23+
{
24+
name: "detects suspicious directories anywhere in filesystem",
25+
fileMap: deckodertypes.FileMap{
26+
"root/.cache/pip/http-v2/1/2/3/file": {},
27+
"root/.aws/credentials": {},
28+
"/root/.git/config": {},
29+
"home/user/.npm/registry": {},
30+
".cache/direct": {},
31+
},
32+
expectedCount: 5,
33+
expectedFiles: []string{"root/.cache", "root/.aws", "/root/.git", "home/user/.npm", ".cache"},
34+
},
35+
{
36+
name: "detects required files by basename",
37+
fileMap: deckodertypes.FileMap{
38+
"root/Dockerfile": {},
39+
"app/docker-compose.yml": {},
40+
"home/.vimrc": {},
41+
"project/.DS_Store": {},
42+
},
43+
expectedCount: 4,
44+
expectedFiles: []string{"root/Dockerfile", "app/docker-compose.yml", "home/.vimrc", "project/.DS_Store"},
45+
},
46+
{
47+
name: "reports only top-level suspicious directory once",
48+
fileMap: deckodertypes.FileMap{
49+
"root/.cache/file1": {},
50+
"root/.cache/pip/file2": {},
51+
"root/.cache/pip/http-v2/deep/file": {},
52+
},
53+
expectedCount: 1,
54+
expectedFiles: []string{"root/.cache"},
55+
},
56+
{
57+
name: "ignores uncontrollable directories",
58+
fileMap: deckodertypes.FileMap{
59+
"app/node_modules/.cache/file": {},
60+
"lib/vendor/.git/config": {},
61+
},
62+
expectedCount: 0,
63+
expectedFiles: []string{},
64+
},
65+
{
66+
name: "handles edge cases",
67+
fileMap: deckodertypes.FileMap{
68+
"usr/bin/app": {},
69+
"etc/config.conf": {},
70+
},
71+
expectedCount: 0,
72+
expectedFiles: []string{},
73+
},
74+
}
75+
76+
for _, tt := range tests {
77+
t.Run(tt.name, func(t *testing.T) {
78+
// Reset detectedDir map before each test
79+
detectedDir = map[string]struct{}{}
80+
81+
assessor := CacheAssessor{}
82+
assessments, err := assessor.Assess(tt.fileMap)
83+
84+
if err != nil {
85+
t.Fatalf("unexpected error: %v", err)
86+
}
87+
88+
if len(assessments) != tt.expectedCount {
89+
t.Errorf("expected %d assessments, got %d", tt.expectedCount, len(assessments))
90+
for i, a := range assessments {
91+
t.Logf(" [%d] %s", i, a.Filename)
92+
}
93+
}
94+
95+
// Verify expected files are present
96+
assessmentMap := make(map[string]*types.Assessment)
97+
for _, a := range assessments {
98+
assessmentMap[a.Filename] = a
99+
}
100+
101+
for _, expectedFile := range tt.expectedFiles {
102+
if _, found := assessmentMap[expectedFile]; !found {
103+
t.Errorf("expected assessment for '%s' not found", expectedFile)
104+
}
105+
}
106+
107+
// Verify all have correct code
108+
for _, a := range assessments {
109+
if a.Code != types.InfoDeletableFiles {
110+
t.Errorf("wrong code for %s: got %v, want %v", a.Filename, a.Code, types.InfoDeletableFiles)
111+
}
112+
}
113+
})
114+
}
115+
}
116+
117+
func TestInIgnoreDir(t *testing.T) {
118+
tests := []struct {
119+
name string
120+
filename string
121+
expected bool
122+
}{
123+
{
124+
name: "File in node_modules",
125+
filename: "app/node_modules/.cache/file",
126+
expected: true,
127+
},
128+
{
129+
name: "File in vendor",
130+
filename: "lib/vendor/.git/config",
131+
expected: true,
132+
},
133+
{
134+
name: "File not in ignore dir",
135+
filename: "app/.cache/file",
136+
expected: false,
137+
},
138+
{
139+
name: "File with node_modules in name but not as directory",
140+
filename: "app/my-node_modules-file.txt",
141+
expected: false, // Only matches "node_modules/" with trailing slash
142+
},
143+
}
144+
145+
for _, tt := range tests {
146+
t.Run(tt.name, func(t *testing.T) {
147+
result := inIgnoreDir(tt.filename)
148+
if result != tt.expected {
149+
t.Errorf("inIgnoreDir(%s) = %v, expected %v", tt.filename, result, tt.expected)
150+
}
151+
})
152+
}
153+
}

pkg/scanner/scan.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"os"
77
"path/filepath"
8+
"strings"
89

910
"github.com/goodwithtech/deckoder/analyzer"
1011
"github.com/goodwithtech/deckoder/extractor"
@@ -105,14 +106,13 @@ func createPathPermissionFilterFunc(filenames, extensions []string, permissions
105106
return true, nil
106107
}
107108

108-
// Check with file directory name
109-
fileDir := filepath.Dir(filePath)
110-
if _, ok := requiredDirNames[fileDir]; ok {
111-
return true, nil
112-
}
113-
fileDirBase := filepath.Base(fileDir)
114-
if _, ok := requiredDirNames[fileDirBase]; ok {
115-
return true, nil
109+
// Check if any directory component in the path matches a required directory
110+
// This catches .cache/, .git/, etc. anywhere in the filesystem tree
111+
parts := strings.Split(filePath, "/")
112+
for _, part := range parts {
113+
if _, ok := requiredDirNames[part]; ok {
114+
return true, nil
115+
}
116116
}
117117

118118
fi := h.FileInfo()

0 commit comments

Comments
 (0)