diff --git a/cmd/analyze.go b/cmd/analyze.go index dfc256a0..f6e76a36 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -41,6 +41,7 @@ type LanguagesConfig struct { Name string `yaml:"name" json:"name"` Languages []string `yaml:"languages" json:"languages"` Extensions []string `yaml:"extensions" json:"extensions"` + Files []string `yaml:"files" json:"files"` } `yaml:"tools" json:"tools"` } @@ -90,7 +91,7 @@ func GetFileExtension(filePath string) string { return strings.ToLower(filepath.Ext(filePath)) } -// IsToolSupportedForFile checks if a tool supports a given file based on its extension +// IsToolSupportedForFile checks if a tool supports a given file based on its extension or filename func IsToolSupportedForFile(toolName string, filePath string, langConfig *LanguagesConfig) bool { if langConfig == nil { // If no language config is available, assume all tools are supported @@ -98,10 +99,7 @@ func IsToolSupportedForFile(toolName string, filePath string, langConfig *Langua } fileExt := GetFileExtension(filePath) - if fileExt == "" { - // If file has no extension, assume tool is supported - return true - } + fileName := filepath.Base(filePath) for _, tool := range langConfig.Tools { if tool.Name == toolName { @@ -117,6 +115,13 @@ func IsToolSupportedForFile(toolName string, filePath string, langConfig *Langua } } + // Check if filename is supported by this tool (exact match) + for _, file := range tool.Files { + if strings.EqualFold(file, fileName) { + return true + } + } + // Extension not found in tool's supported extensions return false } diff --git a/cmd/analyze_test.go b/cmd/analyze_test.go index e8de72ed..75ab3020 100644 --- a/cmd/analyze_test.go +++ b/cmd/analyze_test.go @@ -62,21 +62,31 @@ func TestIsToolSupportedForFile(t *testing.T) { Name string `yaml:"name" json:"name"` Languages []string `yaml:"languages" json:"languages"` Extensions []string `yaml:"extensions" json:"extensions"` + Files []string `yaml:"files" json:"files"` }{ { Name: "pylint", Languages: []string{"Python"}, Extensions: []string{".py"}, + Files: []string{}, + }, + { + Name: "eslint", + Languages: []string{"JavaScript", "TypeScript"}, + Extensions: []string{}, + Files: []string{}, }, { Name: "cppcheck", Languages: []string{"C", "CPP"}, Extensions: []string{".c", ".cpp", ".h", ".hpp"}, + Files: []string{}, }, { Name: "trivy", Languages: []string{"Multiple"}, - Extensions: []string{}, + Extensions: []string{".yaml", ".yml"}, + Files: []string{"requirements.txt"}, }, }, } @@ -111,7 +121,7 @@ func TestIsToolSupportedForFile(t *testing.T) { }, { name: "Tool with no extensions specified", - toolName: "trivy", + toolName: "eslint", filePath: "any.file", config: langConfig, want: true, @@ -130,6 +140,13 @@ func TestIsToolSupportedForFile(t *testing.T) { config: nil, want: true, }, + { + name: "Trivy with requirements.txt", + toolName: "trivy", + filePath: "requirements.txt", + config: langConfig, + want: true, + }, } for _, tt := range tests { diff --git a/codacy-client/client.go b/codacy-client/client.go index 5f7c7b56..f2934994 100644 --- a/codacy-client/client.go +++ b/codacy-client/client.go @@ -253,7 +253,7 @@ func GetToolsVersions() ([]domain.Tool, error) { } // GetRepositoryLanguages fetches the languages for a repository -func GetRepositoryLanguages(initFlags domain.InitFlags) ([]domain.Language, error) { +func GetRepositoryLanguages(initFlags domain.InitFlags) ([]domain.RepositoryLanguage, error) { baseURL := fmt.Sprintf("%s/api/v3/organizations/%s/%s/repositories/%s/settings/languages", CodacyApiBase, initFlags.Provider, diff --git a/domain/language.go b/domain/language.go index 6f9da01a..ee2a559b 100644 --- a/domain/language.go +++ b/domain/language.go @@ -1,23 +1,32 @@ package domain -// Language represents a language in the Codacy API -type Language struct { +// RepositoryLanguage represents a language in the Codacy API +type RepositoryLanguage struct { Name string `json:"name"` CodacyDefaults []string `json:"codacyDefaults"` Extensions []string `json:"extensions"` + DefaultFiles []string `json:"defaultFiles"` Enabled bool `json:"enabled"` Detected bool `json:"detected"` } // LanguagesResponse represents the structure of the languages response type LanguagesResponse struct { - Languages []Language `json:"languages"` + Languages []RepositoryLanguage `json:"languages"` +} + +// Language represents a processed language with combined extensions and files +type Language struct { + Name string + Extensions []string + Files []string } // LanguageTool represents a language tool with its file extensions from the API type LanguageTool struct { Name string `json:"name"` FileExtensions []string `json:"fileExtensions"` + Files []string `json:"files"` } // LanguageToolsResponse represents the structure of the language tools API response @@ -30,6 +39,7 @@ type ToolLanguageInfo struct { Name string `yaml:"name"` Languages []string `yaml:"languages,flow"` Extensions []string `yaml:"extensions,flow"` + Files []string `yaml:"files,flow"` } // LanguagesConfig represents the structure of the languages configuration file diff --git a/integration-tests/config-discover/expected/tools-configs/languages-config.yaml b/integration-tests/config-discover/expected/tools-configs/languages-config.yaml index 5d54c143..6e686c11 100644 --- a/integration-tests/config-discover/expected/tools-configs/languages-config.yaml +++ b/integration-tests/config-discover/expected/tools-configs/languages-config.yaml @@ -2,24 +2,32 @@ tools: - name: dartanalyzer languages: [Dart] extensions: [.dart] + files: [] - name: eslint languages: [Javascript, TypeScript] extensions: [.js, .jsm, .jsx, .mjs, .ts, .tsx, .vue] + files: [] - name: lizard languages: [C, CPP, CSharp, Erlang, Fortran, Go, Java, Javascript, Kotlin, Lua, Objective C, PHP, Python, Ruby, Rust, Scala, Solidity, Swift, TypeScript] extensions: [.c, .cc, .cpp, .cs, .cxx, .gemspec, .go, .h, .hpp, .ino, .java, .jbuilder, .js, .jsm, .jsx, .kt, .kts, .m, .mjs, .opal, .php, .podspec, .py, .rake, .rb, .rlib, .rs, .scala, .swift, .ts, .tsx, .vue] + files: [] - name: pmd languages: [Apex, JSP, Java, Javascript, PLSQL, SQL, Velocity, VisualForce, XML] extensions: [.cls, .component, .fnc, .java, .js, .jsm, .jsp, .jsx, .mjs, .page, .pck, .pkb, .pkh, .pks, .plb, .pld, .plh, .pls, .pom, .prc, .sql, .tpb, .tps, .trg, .trigger, .tyb, .typ, .vm, .vue, .wsdl, .xml, .xsl] + files: [] - name: pylint languages: [Python] extensions: [.py] + files: [] - name: revive languages: [Go] extensions: [.go] + files: [] - name: semgrep languages: [Apex, C, CPP, CSharp, Dockerfile, Go, Java, Javascript, Kotlin, PHP, PLSQL, Python, Ruby, Rust, SQL, Scala, Shell, Swift, Terraform, TypeScript, YAML] extensions: [.bash, .c, .cc, .cls, .cpp, .cs, .cxx, .dockerfile, .fnc, .gemspec, .go, .h, .hpp, .ino, .java, .jbuilder, .js, .jsm, .jsx, .kt, .kts, .mjs, .opal, .pck, .php, .pkb, .pkh, .pks, .plb, .pld, .plh, .pls, .podspec, .prc, .py, .rake, .rb, .rlib, .rs, .scala, .sh, .sql, .swift, .tf, .tpb, .tps, .trg, .trigger, .ts, .tsx, .tyb, .typ, .vue, .yaml, .yml] + files: [] - name: trivy languages: [C, CPP, CSharp, Dart, Dockerfile, Elixir, Go, JSON, Java, Javascript, PHP, Python, Ruby, Rust, Scala, Swift, Terraform, TypeScript, XML, YAML] extensions: [.c, .cc, .cpp, .cs, .cxx, .dart, .dockerfile, .ex, .exs, .gemspec, .go, .h, .hpp, .ino, .java, .jbuilder, .js, .jsm, .json, .jsx, .mjs, .opal, .php, .podspec, .pom, .py, .rake, .rb, .rlib, .rs, .scala, .swift, .tf, .ts, .tsx, .vue, .wsdl, .xml, .xsl, .yaml, .yml] + files: [.deps.json, Berksfile, Capfile, Cargo.lock, Cheffile, Directory.Packages.props, Dockerfile, Fastfile, Gemfile, Gemfile.lock, Guardfile, Package.resolved, Packages.props, Pipfile.lock, Podfile, Podfile.lock, Rakefile, Thorfile, Vagabondfile, Vagrantfile, build.sbt.lock, composer.lock, conan.lock, config.ru, go.mod, gradle.lockfile, mix.lock, package-lock.json, package.json, packages.config, packages.lock.json, pnpm-lock.yaml, poetry.lock, pom.xml, pubspec.lock, requirements.txt, uv.lock, yarn.lock] diff --git a/integration-tests/init-with-token/expected/tools-configs/languages-config.yaml b/integration-tests/init-with-token/expected/tools-configs/languages-config.yaml index 8c8d5243..57a16699 100644 --- a/integration-tests/init-with-token/expected/tools-configs/languages-config.yaml +++ b/integration-tests/init-with-token/expected/tools-configs/languages-config.yaml @@ -2,18 +2,24 @@ tools: - name: eslint languages: [Javascript] extensions: [.js, .jsm, .jsx, .mjs, .vue] + files: [] - name: lizard languages: [Java, Javascript, Python] extensions: [.java, .js, .jsm, .jsx, .mjs, .py, .vue] + files: [] - name: pmd languages: [Java, Javascript] extensions: [.java, .js, .jsm, .jsx, .mjs, .vue] + files: [] - name: pylint languages: [Python] extensions: [.py] + files: [] - name: semgrep languages: [Java, Javascript, Python] extensions: [.java, .js, .jsm, .jsx, .mjs, .py, .vue] + files: [] - name: trivy languages: [JSON, Java, Javascript, Python] - extensions: [.java, .js, .jsm, .json, .jsx, .mjs, .py, .vue] \ No newline at end of file + extensions: [.java, .js, .jsm, .json, .jsx, .mjs, .py, .vue] + files: [Pipfile.lock, gradle.lockfile, package-lock.json, package.json, pnpm-lock.yaml, poetry.lock, pom.xml, requirements.txt, uv.lock, yarn.lock] \ No newline at end of file diff --git a/integration-tests/init-without-token/expected/tools-configs/languages-config.yaml b/integration-tests/init-without-token/expected/tools-configs/languages-config.yaml index 385510f8..0998dce0 100644 --- a/integration-tests/init-without-token/expected/tools-configs/languages-config.yaml +++ b/integration-tests/init-without-token/expected/tools-configs/languages-config.yaml @@ -2,24 +2,32 @@ tools: - name: dartanalyzer languages: [Dart] extensions: [.dart] + files: [] - name: eslint languages: [Javascript, TypeScript] extensions: [.js, .jsm, .jsx, .mjs, .ts, .tsx, .vue] + files: [] - name: lizard languages: [C, CPP, CSharp, Erlang, Fortran, Go, Java, Javascript, Kotlin, Lua, Objective C, PHP, Python, Ruby, Rust, Scala, Solidity, Swift, TypeScript] extensions: [.c, .cc, .cpp, .cs, .cxx, .gemspec, .go, .h, .hpp, .ino, .java, .jbuilder, .js, .jsm, .jsx, .kt, .kts, .m, .mjs, .opal, .php, .podspec, .py, .rake, .rb, .rlib, .rs, .scala, .swift, .ts, .tsx, .vue] + files: [] - name: pmd languages: [Apex, JSP, Java, Javascript, PLSQL, SQL, Velocity, VisualForce, XML] extensions: [.cls, .component, .fnc, .java, .js, .jsm, .jsp, .jsx, .mjs, .page, .pck, .pkb, .pkh, .pks, .plb, .pld, .plh, .pls, .pom, .prc, .sql, .tpb, .tps, .trg, .trigger, .tyb, .typ, .vm, .vue, .wsdl, .xml, .xsl] + files: [] - name: pylint languages: [Python] extensions: [.py] + files: [] - name: revive languages: [Go] extensions: [.go] + files: [] - name: semgrep languages: [Apex, C, CPP, CSharp, Dockerfile, Go, Java, Javascript, Kotlin, PHP, PLSQL, Python, Ruby, Rust, SQL, Scala, Shell, Swift, Terraform, TypeScript, YAML] extensions: [.bash, .c, .cc, .cls, .cpp, .cs, .cxx, .dockerfile, .fnc, .gemspec, .go, .h, .hpp, .ino, .java, .jbuilder, .js, .jsm, .jsx, .kt, .kts, .mjs, .opal, .pck, .php, .pkb, .pkh, .pks, .plb, .pld, .plh, .pls, .podspec, .prc, .py, .rake, .rb, .rlib, .rs, .scala, .sh, .sql, .swift, .tf, .tpb, .tps, .trg, .trigger, .ts, .tsx, .tyb, .typ, .vue, .yaml, .yml] + files: [] - name: trivy languages: [C, CPP, CSharp, Dart, Dockerfile, Elixir, Go, JSON, Java, Javascript, PHP, Python, Ruby, Rust, Scala, Swift, Terraform, TypeScript, XML, YAML] - extensions: [.c, .cc, .cpp, .cs, .cxx, .dart, .dockerfile, .ex, .exs, .gemspec, .go, .h, .hpp, .ino, .java, .jbuilder, .js, .jsm, .json, .jsx, .mjs, .opal, .php, .podspec, .pom, .py, .rake, .rb, .rlib, .rs, .scala, .swift, .tf, .ts, .tsx, .vue, .wsdl, .xml, .xsl, .yaml, .yml] \ No newline at end of file + extensions: [.c, .cc, .cpp, .cs, .cxx, .dart, .dockerfile, .ex, .exs, .gemspec, .go, .h, .hpp, .ino, .java, .jbuilder, .js, .jsm, .json, .jsx, .mjs, .opal, .php, .podspec, .pom, .py, .rake, .rb, .rlib, .rs, .scala, .swift, .tf, .ts, .tsx, .vue, .wsdl, .xml, .xsl, .yaml, .yml] + files: [.deps.json, Berksfile, Capfile, Cargo.lock, Cheffile, Directory.Packages.props, Dockerfile, Fastfile, Gemfile, Gemfile.lock, Guardfile, Package.resolved, Packages.props, Pipfile.lock, Podfile, Podfile.lock, Rakefile, Thorfile, Vagabondfile, Vagrantfile, build.sbt.lock, composer.lock, conan.lock, config.ru, go.mod, gradle.lockfile, mix.lock, package-lock.json, package.json, packages.config, packages.lock.json, pnpm-lock.yaml, poetry.lock, pom.xml, pubspec.lock, requirements.txt, uv.lock, yarn.lock] \ No newline at end of file diff --git a/plugins/runtime-utils_test.go b/plugins/runtime-utils_test.go index 73532bbd..34d6f0d5 100644 --- a/plugins/runtime-utils_test.go +++ b/plugins/runtime-utils_test.go @@ -69,21 +69,21 @@ func TestProcessRuntimes(t *testing.T) { // Assert the download URL is correctly formatted expectedDownloadURL := "https://nodejs.org/dist/v18.17.1/" + expectedFileName + "." + expectedExtension assert.Equal(t, expectedDownloadURL, nodeInfo.DownloadURL) - + // Assert binary paths are correctly set assert.NotNil(t, nodeInfo.Binaries) assert.Greater(t, len(nodeInfo.Binaries), 0) - + // Check if node and npm binaries are present nodeBinary := nodeInfo.InstallDir + "/bin/node" npmBinary := nodeInfo.InstallDir + "/bin/npm" - + // Add .exe extension for Windows if runtime.GOOS == "windows" { nodeBinary += ".exe" npmBinary += ".exe" } - + assert.Equal(t, nodeBinary, nodeInfo.Binaries["node"]) assert.Equal(t, npmBinary, nodeInfo.Binaries["npm"]) } diff --git a/plugins/tool-utils.go b/plugins/tool-utils.go index 580158c5..17530679 100644 --- a/plugins/tool-utils.go +++ b/plugins/tool-utils.go @@ -55,18 +55,19 @@ type RuntimeBinaries struct { // ToolPluginConfig holds the structure of the tool plugin.yaml file type ToolPluginConfig struct { - Name string `yaml:"name"` - Description string `yaml:"description"` - DefaultVersion string `yaml:"default_version"` - Runtime string `yaml:"runtime"` - RuntimeBinaries RuntimeBinaries `yaml:"runtime_binaries"` - Installation InstallationConfig `yaml:"installation"` - Download DownloadConfig `yaml:"download"` - Environment map[string]string `yaml:"environment"` - Binaries []ToolBinary `yaml:"binaries"` - Formatters []Formatter `yaml:"formatters"` - OutputOptions OutputOptions `yaml:"output_options"` - AnalysisOptions AnalysisOptions `yaml:"analysis_options"` + Name string `yaml:"name"` + Description string `yaml:"description"` + DefaultVersion string `yaml:"default_version"` + SupportsSpecificFiles bool `yaml:"support_specific_files"` + Runtime string `yaml:"runtime"` + RuntimeBinaries RuntimeBinaries `yaml:"runtime_binaries"` + Installation InstallationConfig `yaml:"installation"` + Download DownloadConfig `yaml:"download"` + Environment map[string]string `yaml:"environment"` + Binaries []ToolBinary `yaml:"binaries"` + Formatters []Formatter `yaml:"formatters"` + OutputOptions OutputOptions `yaml:"output_options"` + AnalysisOptions AnalysisOptions `yaml:"analysis_options"` } // ToolConfig represents configuration for a tool @@ -78,15 +79,16 @@ type ToolConfig struct { // ToolInfo contains all processed information about a tool type ToolInfo struct { - Name string - Version string - Runtime string - InstallDir string - Binaries map[string]string // Map of binary name to full path - Formatters map[string]string // Map of formatter name to flag - OutputFlag string - AutofixFlag string - DefaultPath string + Name string + Version string + Runtime string + InstallDir string + Binaries map[string]string // Map of binary name to full path + Formatters map[string]string // Map of formatter name to flag + OutputFlag string + AutofixFlag string + DefaultPath string + SupportsSpecificFiles bool // Whether tool supports specific file analysis // Runtime binaries PackageManager string ExecutionBinary string @@ -135,15 +137,16 @@ func ProcessTools(configs []ToolConfig, toolDir string, runtimes map[string]*Run } // Create ToolInfo with basic information info := &ToolInfo{ - Name: config.Name, - Version: config.Version, - Runtime: toolRuntime, - InstallDir: installDir, - Binaries: make(map[string]string), - Formatters: make(map[string]string), - OutputFlag: pluginConfig.OutputOptions.FileFlag, - AutofixFlag: pluginConfig.AnalysisOptions.AutofixFlag, - DefaultPath: pluginConfig.AnalysisOptions.DefaultPath, + Name: config.Name, + Version: config.Version, + Runtime: toolRuntime, + InstallDir: installDir, + Binaries: make(map[string]string), + Formatters: make(map[string]string), + OutputFlag: pluginConfig.OutputOptions.FileFlag, + AutofixFlag: pluginConfig.AnalysisOptions.AutofixFlag, + DefaultPath: pluginConfig.AnalysisOptions.DefaultPath, + SupportsSpecificFiles: pluginConfig.SupportsSpecificFiles, // Store runtime binary information PackageManager: pluginConfig.RuntimeBinaries.PackageManager, ExecutionBinary: pluginConfig.RuntimeBinaries.Execution, diff --git a/plugins/tool-utils_test.go b/plugins/tool-utils_test.go index f459c06f..ee18ac30 100644 --- a/plugins/tool-utils_test.go +++ b/plugins/tool-utils_test.go @@ -66,6 +66,9 @@ func TestProcessTools(t *testing.T) { // Assert installation command templates are correctly set assert.Equal(t, "install --prefix {{.InstallDir}} {{.PackageName}}@{{.Version}} @microsoft/eslint-formatter-sarif", eslintInfo.InstallCommand) assert.Equal(t, "config set registry {{.Registry}}", eslintInfo.RegistryCommand) + + // Assert that eslint does not support specific files (default false) + assert.False(t, eslintInfo.SupportsSpecificFiles, "eslint should not support specific files") } func TestProcessToolsWithDownload(t *testing.T) { @@ -120,6 +123,9 @@ func TestProcessToolsWithDownload(t *testing.T) { trivyBinary := filepath.Join(expectedInstallDir, "trivy") assert.Equal(t, trivyBinary, trivyInfo.Binaries["trivy"]) + // Assert that trivy supports specific files (from plugin.yaml) + assert.True(t, trivyInfo.SupportsSpecificFiles, "trivy should support specific files") + // Verify URL components assert.Contains(t, trivyInfo.DownloadURL, "aquasecurity/trivy/releases/download") assert.Contains(t, trivyInfo.DownloadURL, trivyInfo.Version) @@ -199,3 +205,47 @@ func TestGetSupportedTools(t *testing.T) { }) } } + +func TestGetToolConfig(t *testing.T) { + pluginManager := GetPluginManager() + + tests := []struct { + name string + toolName string + expectedSupportsSpecificFiles bool + expectedError bool + }{ + { + name: "trivy should support specific files", + toolName: "trivy", + expectedSupportsSpecificFiles: true, + expectedError: false, + }, + { + name: "eslint should not support specific files", + toolName: "eslint", + expectedSupportsSpecificFiles: false, + expectedError: false, + }, + { + name: "non-existent tool should return error", + toolName: "non-existent-tool", + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := pluginManager.GetToolConfig(tt.toolName) + + if tt.expectedError { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.expectedSupportsSpecificFiles, config.SupportsSpecificFiles, + "SupportsSpecificFiles should match expected value for %s", tt.toolName) + }) + } +} diff --git a/plugins/tools/pylint/test/src/.codacy/tools-configs/languages-config.yaml b/plugins/tools/pylint/test/src/.codacy/tools-configs/languages-config.yaml new file mode 100644 index 00000000..2a134aed --- /dev/null +++ b/plugins/tools/pylint/test/src/.codacy/tools-configs/languages-config.yaml @@ -0,0 +1,4 @@ +tools: + - name: pylint + languages: [Python] + extensions: [.py] diff --git a/plugins/tools/pylint/test/src/.codacy/tools-configs/pylint.rc b/plugins/tools/pylint/test/src/.codacy/tools-configs/pylint.rc new file mode 100644 index 00000000..92f4aea4 --- /dev/null +++ b/plugins/tools/pylint/test/src/.codacy/tools-configs/pylint.rc @@ -0,0 +1,9 @@ +[MASTER] +ignore=CVS +persistent=yes +load-plugins= + +[MESSAGES CONTROL] +disable=all +enable=C0123,C0200,C0303,E0100,E0101,E0102,E0103,E0104,E0105,E0106,E0107,E0108,E0110,E0112,E0113,E0114,E0115,E0116,E0117,E0202,E0203,E0211,E0236,E0238,E0239,E0240,E0241,E0301,E0302,E0601,E0603,E0604,E0701,E0702,E0704,E0710,E0711,E0712,E1003,E1102,E1111,E1120,E1121,E1123,E1124,E1125,E1126,E1127,E1132,E1200,E1201,E1205,E1206,E1300,E1301,E1302,E1303,E1304,E1305,E1306,R0202,R0203,W0101,W0102,W0104,W0105,W0106,W0107,W0108,W0109,W0120,W0122,W0124,W0150,W0199,W0221,W0222,W0233,W0404,W0410,W0601,W0602,W0604,W0611,W0612,W0622,W0702,W0705,W0711,W1300,W1301,W1302,W1303,W1305,W1306,W1307 + diff --git a/plugins/tools/pylint/test/src/requirements.txt b/plugins/tools/pylint/test/src/requirements.txt new file mode 100644 index 00000000..fbde3519 --- /dev/null +++ b/plugins/tools/pylint/test/src/requirements.txt @@ -0,0 +1,2 @@ +# This file should be successfully ignored by Codacy CLI as pylint does not support it. +django==1.11.29 \ No newline at end of file diff --git a/plugins/tools/trivy/plugin.yaml b/plugins/tools/trivy/plugin.yaml index c4a1c169..8def664c 100644 --- a/plugins/tools/trivy/plugin.yaml +++ b/plugins/tools/trivy/plugin.yaml @@ -1,6 +1,7 @@ name: trivy description: Trivy is a comprehensive security scanner for containers and other artifacts. default_version: 0.59.1 +support_specific_files: true download: url_template: "https://github.com/aquasecurity/trivy/releases/download/v{{.Version}}/trivy_{{.Version}}_{{.OS}}-{{.Arch}}.{{.Extension}}" file_name_template: "trivy_{{.Version}}_{{.OS}}_{{.Arch}}" diff --git a/plugins/tools/trivy/test/expected.sarif b/plugins/tools/trivy/test/expected.sarif index 9507b051..562e72b3 100644 --- a/plugins/tools/trivy/test/expected.sarif +++ b/plugins/tools/trivy/test/expected.sarif @@ -9,6 +9,33 @@ } }, "results": [ + { + "level": "note", + "locations": [ + { + "message": { + "text": "package-lock.json: brace-expansion@1.1.11" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "package-lock.json", + "uriBaseId": "ROOTPATH" + }, + "region": { + "endColumn": 1, + "endLine": 357, + "startColumn": 1, + "startLine": 349 + } + } + } + ], + "message": { + "text": "Package: brace-expansion\nInstalled Version: 1.1.11\nVulnerability CVE-2025-5889\nSeverity: LOW\nFixed Version: 2.0.2, 1.1.12, 3.0.1, 4.0.1\nLink: [CVE-2025-5889](https://avd.aquasec.com/nvd/cve-2025-5889)" + }, + "ruleId": "CVE-2025-5889", + "ruleIndex": 0 + }, { "level": "error", "locations": [ @@ -37,31 +64,112 @@ "ruleIndex": 1 }, { - "level": "note", + "level": "error", "locations": [ { "message": { - "text": "package-lock.json: brace-expansion@1.1.11" + "text": "requirements.txt: django@1.11.29" }, "physicalLocation": { "artifactLocation": { - "uri": "package-lock.json", + "uri": "requirements.txt", "uriBaseId": "ROOTPATH" }, "region": { "endColumn": 1, - "endLine": 357, + "endLine": 1, "startColumn": 1, - "startLine": 349 + "startLine": 1 } } } ], "message": { - "text": "Package: brace-expansion\nInstalled Version: 1.1.11\nVulnerability CVE-2025-5889\nSeverity: LOW\nFixed Version: 2.0.2, 1.1.12, 3.0.1, 4.0.1\nLink: [CVE-2025-5889](https://avd.aquasec.com/nvd/cve-2025-5889)" + "text": "Package: django\nInstalled Version: 1.11.29\nVulnerability CVE-2022-36359\nSeverity: HIGH\nFixed Version: 3.2.15, 4.0.7\nLink: [CVE-2022-36359](https://avd.aquasec.com/nvd/cve-2022-36359)" }, - "ruleId": "CVE-2025-5889", - "ruleIndex": 0 + "ruleId": "CVE-2022-36359", + "ruleIndex": 2 + }, + { + "level": "warning", + "locations": [ + { + "message": { + "text": "requirements.txt: django@1.11.29" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "requirements.txt", + "uriBaseId": "ROOTPATH" + }, + "region": { + "endColumn": 1, + "endLine": 1, + "startColumn": 1, + "startLine": 1 + } + } + } + ], + "message": { + "text": "Package: django\nInstalled Version: 1.11.29\nVulnerability CVE-2021-33203\nSeverity: MEDIUM\nFixed Version: 2.2.24, 3.1.12, 3.2.4\nLink: [CVE-2021-33203](https://avd.aquasec.com/nvd/cve-2021-33203)" + }, + "ruleId": "CVE-2021-33203", + "ruleIndex": 3 + }, + { + "level": "warning", + "locations": [ + { + "message": { + "text": "requirements.txt: django@1.11.29" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "requirements.txt", + "uriBaseId": "ROOTPATH" + }, + "region": { + "endColumn": 1, + "endLine": 1, + "startColumn": 1, + "startLine": 1 + } + } + } + ], + "message": { + "text": "Package: django\nInstalled Version: 1.11.29\nVulnerability CVE-2024-45231\nSeverity: MEDIUM\nFixed Version: 5.1.1, 5.0.9, 4.2.16\nLink: [CVE-2024-45231](https://avd.aquasec.com/nvd/cve-2024-45231)" + }, + "ruleId": "CVE-2024-45231", + "ruleIndex": 4 + }, + { + "level": "warning", + "locations": [ + { + "message": { + "text": "requirements.txt: django@1.11.29" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "requirements.txt", + "uriBaseId": "ROOTPATH" + }, + "region": { + "endColumn": 1, + "endLine": 1, + "startColumn": 1, + "startLine": 1 + } + } + } + ], + "message": { + "text": "Package: django\nInstalled Version: 1.11.29\nVulnerability CVE-2025-48432\nSeverity: MEDIUM\nFixed Version: 5.2.2, 5.1.10, 4.2.22\nLink: [CVE-2025-48432](https://avd.aquasec.com/nvd/cve-2025-48432)" + }, + "ruleId": "CVE-2025-48432", + "ruleIndex": 5 } ], "tool": { diff --git a/plugins/tools/trivy/test/src/.codacy/tools-configs/languages-config.yaml b/plugins/tools/trivy/test/src/.codacy/tools-configs/languages-config.yaml new file mode 100644 index 00000000..648997a7 --- /dev/null +++ b/plugins/tools/trivy/test/src/.codacy/tools-configs/languages-config.yaml @@ -0,0 +1,5 @@ +tools: + - name: trivy + languages: [C, CPP, CSharp, Dart, Dockerfile, Elixir, Go, JSON, Java, Javascript, PHP, Python, Ruby, Rust, Scala, Swift, Terraform, TypeScript, XML, YAML] + extensions: [.c, .cc, .cpp, .cs, .cxx, .dart, .dockerfile, .ex, .exs, .gemspec, .go, .h, .hpp, .ino, .java, .jbuilder, .js, .jsm, .json, .jsx, .mjs, .opal, .php, .podspec, .pom, .py, .rake, .rb, .rlib, .rs, .scala, .swift, .tf, .ts, .tsx, .vue, .wsdl, .xml, .xsl, .yaml, .yml] + files: [.deps.json, Berksfile, Capfile, Cargo.lock, Cheffile, Directory.Packages.props, Dockerfile, Fastfile, Gemfile, Gemfile.lock, Guardfile, Package.resolved, Packages.props, Pipfile.lock, Podfile, Podfile.lock, Rakefile, Thorfile, Vagabondfile, Vagrantfile, build.sbt.lock, composer.lock, conan.lock, config.ru, go.mod, gradle.lockfile, mix.lock, package-lock.json, package.json, packages.config, packages.lock.json, pnpm-lock.yaml, poetry.lock, pom.xml, pubspec.lock, requirements.txt, uv.lock, yarn.lock] diff --git a/plugins/tools/trivy/test/src/.codacy/tools-configs/trivy.yaml b/plugins/tools/trivy/test/src/.codacy/tools-configs/trivy.yaml new file mode 100644 index 00000000..c785541c --- /dev/null +++ b/plugins/tools/trivy/test/src/.codacy/tools-configs/trivy.yaml @@ -0,0 +1,10 @@ +severity: + - LOW + - MEDIUM + - HIGH + - CRITICAL + +scan: + scanners: + - vuln + - secret diff --git a/plugins/tools/trivy/test/src/requirements.txt b/plugins/tools/trivy/test/src/requirements.txt new file mode 100644 index 00000000..fd925c78 --- /dev/null +++ b/plugins/tools/trivy/test/src/requirements.txt @@ -0,0 +1 @@ +django==1.11.29 \ No newline at end of file diff --git a/tools/language_config.go b/tools/language_config.go index d1aa10ef..e80b35f6 100644 --- a/tools/language_config.go +++ b/tools/language_config.go @@ -13,6 +13,7 @@ import ( "codacy/cli-v2/config" "codacy/cli-v2/constants" "codacy/cli-v2/domain" + "codacy/cli-v2/plugins" "codacy/cli-v2/utils/logger" "github.com/sirupsen/logrus" @@ -44,6 +45,12 @@ func buildToolLanguageInfoFromAPI() (map[string]domain.ToolLanguageInfo, error) languageExtensionsMap[strings.ToLower(langTool.Name)] = langTool.FileExtensions } + // Create map of language name to files + languageFilesMap := make(map[string][]string) + for _, langTool := range languageTools { + languageFilesMap[strings.ToLower(langTool.Name)] = langTool.Files + } + // Build tool language configurations from API data result := make(map[string]domain.ToolLanguageInfo) supportedToolNames := make(map[string]bool) @@ -70,10 +77,18 @@ func buildToolLanguageInfoFromAPI() (map[string]domain.ToolLanguageInfo, error) Name: toolName, Languages: tool.Languages, Extensions: []string{}, + Files: []string{}, } - // Build extensions from API language data + // Build extensions and files from API language data extensionsSet := make(map[string]struct{}) + filesSet := make(map[string]struct{}) + + // Check if this tool supports specific files + pluginManager := plugins.GetPluginManager() + pluginConfig, err := pluginManager.GetToolConfig(toolName) + supportsSpecificFiles := err == nil && pluginConfig.SupportsSpecificFiles + for _, apiLang := range tool.Languages { lowerLang := strings.ToLower(apiLang) if extensions, exists := languageExtensionsMap[lowerLang]; exists { @@ -81,13 +96,25 @@ func buildToolLanguageInfoFromAPI() (map[string]domain.ToolLanguageInfo, error) extensionsSet[ext] = struct{}{} } } + // Only populate files if the tool supports specific files + if supportsSpecificFiles { + if files, exists := languageFilesMap[lowerLang]; exists { + for _, file := range files { + filesSet[file] = struct{}{} + } + } + } } - // Convert set to sorted slice + // Convert sets to sorted slices for ext := range extensionsSet { configTool.Extensions = append(configTool.Extensions, ext) } slices.Sort(configTool.Extensions) + for file := range filesSet { + configTool.Files = append(configTool.Files, file) + } + slices.Sort(configTool.Files) // Sort languages alphabetically slices.Sort(configTool.Languages) @@ -201,18 +228,6 @@ func CreateLanguagesConfigFile(apiTools []domain.Tool, toolsConfigDir string, to // buildRemoteModeLanguagesConfig builds the languages config for remote mode using repository languages as source of truth func buildRemoteModeLanguagesConfig(apiTools []domain.Tool, toolIDMap map[string]string, initFlags domain.InitFlags) ([]domain.ToolLanguageInfo, error) { - // Get language file extensions from API - languageTools, err := codacyclient.GetLanguageTools() - if err != nil { - return nil, fmt.Errorf("failed to get language tools from API: %w", err) - } - - // Create map of language name to file extensions - languageExtensionsMap := make(map[string][]string) - for _, langTool := range languageTools { - languageExtensionsMap[strings.ToLower(langTool.Name)] = langTool.FileExtensions - } - // Get repository languages - this is the single source of truth for remote mode repositoryLanguages, err := getRepositoryLanguages(initFlags) if err != nil { @@ -232,18 +247,41 @@ func buildRemoteModeLanguagesConfig(apiTools []domain.Tool, toolIDMap map[string Name: shortName, Languages: []string{}, Extensions: []string{}, + Files: []string{}, } // Use only languages that exist in the repository extensionsSet := make(map[string]struct{}) + filesSet := make(map[string]struct{}) + + // Check if this tool supports specific files + pluginManager := plugins.GetPluginManager() + pluginConfig, err := pluginManager.GetToolConfig(shortName) + supportsSpecificFiles := err == nil && pluginConfig.SupportsSpecificFiles for _, lang := range tool.Languages { lowerLang := strings.ToLower(lang) - if repoExts, exists := repositoryLanguages[lowerLang]; exists && len(repoExts) > 0 { - configTool.Languages = append(configTool.Languages, lang) - // Add repository-specific extensions - for _, ext := range repoExts { - extensionsSet[ext] = struct{}{} + if repoLang, exists := repositoryLanguages[lowerLang]; exists { + // Check if this language has either extensions or files + hasExtensions := len(repoLang.Extensions) > 0 + hasFiles := len(repoLang.Files) > 0 && supportsSpecificFiles + + if hasExtensions || hasFiles { + configTool.Languages = append(configTool.Languages, lang) + + // Add repository-specific extensions if they exist + if hasExtensions { + for _, ext := range repoLang.Extensions { + extensionsSet[ext] = struct{}{} + } + } + + // Add repository-specific files if they exist (only if tool supports it) + if hasFiles { + for _, file := range repoLang.Files { + filesSet[file] = struct{}{} + } + } } } } @@ -254,6 +292,12 @@ func buildRemoteModeLanguagesConfig(apiTools []domain.Tool, toolIDMap map[string } slices.Sort(configTool.Extensions) + // Convert files set to sorted slice + for file := range filesSet { + configTool.Files = append(configTool.Files, file) + } + slices.Sort(configTool.Files) + // Sort languages alphabetically slices.Sort(configTool.Languages) @@ -264,14 +308,14 @@ func buildRemoteModeLanguagesConfig(apiTools []domain.Tool, toolIDMap map[string return configTools, nil } -func getRepositoryLanguages(initFlags domain.InitFlags) (map[string][]string, error) { +func getRepositoryLanguages(initFlags domain.InitFlags) (map[string]domain.Language, error) { response, err := codacyclient.GetRepositoryLanguages(initFlags) if err != nil { return nil, fmt.Errorf("failed to get repository languages: %w", err) } - // Create map to store language name -> combined extensions - result := make(map[string][]string) + // Create map to store language name -> Language struct + result := make(map[string]domain.Language) // Filter and process languages for _, lang := range response { @@ -285,17 +329,32 @@ func getRepositoryLanguages(initFlags domain.InitFlags) (map[string][]string, er extensions[ext] = struct{}{} } - // Convert map to slice + // Combine and deduplicate files + files := make(map[string]struct{}) + for _, file := range lang.DefaultFiles { + files[file] = struct{}{} + } + + // Convert extension map to slice extSlice := make([]string, 0, len(extensions)) for ext := range extensions { extSlice = append(extSlice, ext) } - - // Sort extensions for consistent ordering in the config file slices.Sort(extSlice) + // Convert files map to slice + fileSlice := make([]string, 0, len(files)) + for file := range files { + fileSlice = append(fileSlice, file) + } + slices.Sort(fileSlice) + // Add to result map with lowercase key for case-insensitive matching - result[strings.ToLower(lang.Name)] = extSlice + result[strings.ToLower(lang.Name)] = domain.Language{ + Name: lang.Name, + Extensions: extSlice, + Files: fileSlice, + } } }