diff --git a/.gitignore b/.gitignore index d5108ea..f87ba09 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out -.idea \ No newline at end of file +.idea + +.vscode diff --git a/pkg/assessor/cache/cache.go b/pkg/assessor/cache/cache.go index d6102c1..0104924 100644 --- a/pkg/assessor/cache/cache.go +++ b/pkg/assessor/cache/cache.go @@ -31,12 +31,29 @@ func (a CacheAssessor) Assess(fileMap deckodertypes.FileMap) ([]*types.Assessmen dirName := filepath.Dir(filename) dirBase := filepath.Base(dirName) - // match Directory - if utils.StringInSlice(dirBase+"/", reqDirs) || utils.StringInSlice(dirName+"/", reqDirs) { - if _, ok := detectedDir[dirName]; ok { + // match Directory - check if any component of the directory path matches a required dir + matched := utils.StringInSlice(dirBase+"/", reqDirs) || utils.StringInSlice(dirName+"/", reqDirs) + reportDir := dirName + + if !matched { + // Check if any directory component in the path matches a required directory + // and find the top-level occurrence to report + parts := strings.Split(dirName, "/") + for i, part := range parts { + if utils.StringInSlice(part+"/", reqDirs) { + matched = true + // Report only up to and including the first required directory component + reportDir = strings.Join(parts[:i+1], "/") + break + } + } + } + + if matched { + if _, ok := detectedDir[reportDir]; ok { continue } - detectedDir[dirName] = struct{}{} + detectedDir[reportDir] = struct{}{} // Skip uncontrollable dependency directory e.g) npm : node_modules, php: composer if inIgnoreDir(filename) { @@ -47,8 +64,8 @@ func (a CacheAssessor) Assess(fileMap deckodertypes.FileMap) ([]*types.Assessmen assesses, &types.Assessment{ Code: types.InfoDeletableFiles, - Filename: dirName, - Desc: fmt.Sprintf("Suspicious directory : %s ", dirName), + Filename: reportDir, + Desc: fmt.Sprintf("Suspicious directory : %s ", reportDir), }) } diff --git a/pkg/assessor/cache/cache_test.go b/pkg/assessor/cache/cache_test.go new file mode 100644 index 0000000..68888f2 --- /dev/null +++ b/pkg/assessor/cache/cache_test.go @@ -0,0 +1,153 @@ +package cache + +import ( + "testing" + + "github.com/SpazioDati/dockle/pkg/log" + "github.com/SpazioDati/dockle/pkg/types" + deckodertypes "github.com/goodwithtech/deckoder/types" +) + +func init() { + // Initialize logger for tests + log.InitLogger(false, false) +} + +func TestAssess(t *testing.T) { + tests := []struct { + name string + fileMap deckodertypes.FileMap + expectedCount int + expectedFiles []string + }{ + { + name: "detects suspicious directories anywhere in filesystem", + fileMap: deckodertypes.FileMap{ + "root/.cache/pip/http-v2/1/2/3/file": {}, + "root/.aws/credentials": {}, + "/root/.git/config": {}, + "home/user/.npm/registry": {}, + ".cache/direct": {}, + }, + expectedCount: 5, + expectedFiles: []string{"root/.cache", "root/.aws", "/root/.git", "home/user/.npm", ".cache"}, + }, + { + name: "detects required files by basename", + fileMap: deckodertypes.FileMap{ + "root/Dockerfile": {}, + "app/docker-compose.yml": {}, + "home/.vimrc": {}, + "project/.DS_Store": {}, + }, + expectedCount: 4, + expectedFiles: []string{"root/Dockerfile", "app/docker-compose.yml", "home/.vimrc", "project/.DS_Store"}, + }, + { + name: "reports only top-level suspicious directory once", + fileMap: deckodertypes.FileMap{ + "root/.cache/file1": {}, + "root/.cache/pip/file2": {}, + "root/.cache/pip/http-v2/deep/file": {}, + }, + expectedCount: 1, + expectedFiles: []string{"root/.cache"}, + }, + { + name: "ignores uncontrollable directories", + fileMap: deckodertypes.FileMap{ + "app/node_modules/.cache/file": {}, + "lib/vendor/.git/config": {}, + }, + expectedCount: 0, + expectedFiles: []string{}, + }, + { + name: "handles edge cases", + fileMap: deckodertypes.FileMap{ + "usr/bin/app": {}, + "etc/config.conf": {}, + }, + expectedCount: 0, + expectedFiles: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset detectedDir map before each test + detectedDir = map[string]struct{}{} + + assessor := CacheAssessor{} + assessments, err := assessor.Assess(tt.fileMap) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(assessments) != tt.expectedCount { + t.Errorf("expected %d assessments, got %d", tt.expectedCount, len(assessments)) + for i, a := range assessments { + t.Logf(" [%d] %s", i, a.Filename) + } + } + + // Verify expected files are present + assessmentMap := make(map[string]*types.Assessment) + for _, a := range assessments { + assessmentMap[a.Filename] = a + } + + for _, expectedFile := range tt.expectedFiles { + if _, found := assessmentMap[expectedFile]; !found { + t.Errorf("expected assessment for '%s' not found", expectedFile) + } + } + + // Verify all have correct code + for _, a := range assessments { + if a.Code != types.InfoDeletableFiles { + t.Errorf("wrong code for %s: got %v, want %v", a.Filename, a.Code, types.InfoDeletableFiles) + } + } + }) + } +} + +func TestInIgnoreDir(t *testing.T) { + tests := []struct { + name string + filename string + expected bool + }{ + { + name: "File in node_modules", + filename: "app/node_modules/.cache/file", + expected: true, + }, + { + name: "File in vendor", + filename: "lib/vendor/.git/config", + expected: true, + }, + { + name: "File not in ignore dir", + filename: "app/.cache/file", + expected: false, + }, + { + name: "File with node_modules in name but not as directory", + filename: "app/my-node_modules-file.txt", + expected: false, // Only matches "node_modules/" with trailing slash + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := inIgnoreDir(tt.filename) + if result != tt.expected { + t.Errorf("inIgnoreDir(%s) = %v, expected %v", tt.filename, result, tt.expected) + } + }) + } +} diff --git a/pkg/scanner/scan.go b/pkg/scanner/scan.go index 62d32de..f5f2285 100644 --- a/pkg/scanner/scan.go +++ b/pkg/scanner/scan.go @@ -5,6 +5,7 @@ import ( "context" "os" "path/filepath" + "strings" "github.com/goodwithtech/deckoder/analyzer" "github.com/goodwithtech/deckoder/extractor" @@ -105,14 +106,13 @@ func createPathPermissionFilterFunc(filenames, extensions []string, permissions return true, nil } - // Check with file directory name - fileDir := filepath.Dir(filePath) - if _, ok := requiredDirNames[fileDir]; ok { - return true, nil - } - fileDirBase := filepath.Base(fileDir) - if _, ok := requiredDirNames[fileDirBase]; ok { - return true, nil + // Check if any directory component in the path matches a required directory + // This catches .cache/, .git/, etc. anywhere in the filesystem tree + parts := strings.Split(filePath, "/") + for _, part := range parts { + if _, ok := requiredDirNames[part]; ok { + return true, nil + } } fi := h.FileInfo()