diff --git a/ant/src/main/java/org/owasp/dependencycheck/taskdefs/Check.java b/ant/src/main/java/org/owasp/dependencycheck/taskdefs/Check.java index a5e2cc29e4e..191cc1513c2 100644 --- a/ant/src/main/java/org/owasp/dependencycheck/taskdefs/Check.java +++ b/ant/src/main/java/org/owasp/dependencycheck/taskdefs/Check.java @@ -168,6 +168,15 @@ public class Check extends Update { * installed. Default is true. */ private Boolean golangModEnabled; + /** + * Sets whether the Golang Vulncheck Analyzer is enabled; this requires `go` + * and `govulncheck` to be installed. Default is false. + */ + private Boolean golangVulncheckEnabled; + /** + * Sets the path to `govulncheck`. + */ + private String pathToGovulncheck; /** * Sets the path to `go`. */ @@ -1138,6 +1147,24 @@ public void setGolangModEnabled(Boolean golangModEnabled) { this.golangModEnabled = golangModEnabled; } + /** + * Set the value of golangVulncheckEnabled. + * + * @param golangVulncheckEnabled new value of golangVulncheckEnabled + */ + public void setGolangVulncheckEnabled(Boolean golangVulncheckEnabled) { + this.golangVulncheckEnabled = golangVulncheckEnabled; + } + + /** + * Set the value of pathToGovulncheck. + * + * @param pathToGovulncheck new value of pathToGovulncheck + */ + public void setPathToGovulncheck(String pathToGovulncheck) { + this.pathToGovulncheck = pathToGovulncheck; + } + /** * Set the value of dartAnalyzerEnabled. * @@ -1600,8 +1627,10 @@ protected void populateSettings() throws BuildException { getSettings().setArrayIfNotEmpty(Settings.KEYS.ANALYZER_RETIREJS_FILTERS, retireJsFilters); getSettings().setBooleanIfNotNull(Settings.KEYS.ANALYZER_GOLANG_DEP_ENABLED, golangDepEnabled); getSettings().setBooleanIfNotNull(Settings.KEYS.ANALYZER_GOLANG_MOD_ENABLED, golangModEnabled); + getSettings().setBooleanIfNotNull(Settings.KEYS.ANALYZER_GOLANG_VULNCHECK_ENABLED, golangVulncheckEnabled); getSettings().setBooleanIfNotNull(Settings.KEYS.ANALYZER_DART_ENABLED, dartAnalyzerEnabled); getSettings().setStringIfNotNull(Settings.KEYS.ANALYZER_GOLANG_PATH, pathToGo); + getSettings().setStringIfNotNull(Settings.KEYS.ANALYZER_GOLANG_VULNCHECK_PATH, pathToGovulncheck); getSettings().setStringIfNotNull(Settings.KEYS.ANALYZER_YARN_PATH, pathToYarn); getSettings().setStringIfNotNull(Settings.KEYS.ANALYZER_PNPM_PATH, pathToPnpm); getSettings().setBooleanIfNotNull(Settings.KEYS.ANALYZER_MIX_AUDIT_ENABLED, mixAuditAnalyzerEnabled); diff --git a/ant/src/site/markdown/configuration.md b/ant/src/site/markdown/configuration.md index 874214f4ec0..1e779be38b3 100644 --- a/ant/src/site/markdown/configuration.md +++ b/ant/src/site/markdown/configuration.md @@ -143,7 +143,9 @@ be needed. | pathToCore | The path to dotnet core .NET assembly analysis on non-windows systems. |   | | golangDepEnabled | Sets whether the [experimental](../analyzers/index.html) Golang Dependency Analyzer should be used. `enableExperimental` must be set to true. | true | | golangModEnabled | Sets whether the [experimental](../analyzers/index.html) Goland Module Analyzer should be used; requires `go` to be installed. `enableExperimental` must be set to true. | true | +| golangVulncheckEnabled | Sets whether the [experimental](../analyzers/index.html) Golang Vulncheck Analyzer should be used; requires `go` and `govulncheck` to be installed. `enableExperimental` must be set to true. | false | | pathToGo | The path to `go`. |   | +| pathToGovulncheck | The path to `govulncheck`. |   | | versionCheckEnabled | Whether dependency-check should check if a new version of dependency-check-maven exists. | true | Advanced Configuration diff --git a/cli/src/main/java/org/owasp/dependencycheck/App.java b/cli/src/main/java/org/owasp/dependencycheck/App.java index 672e74dbb24..f64da623fdc 100644 --- a/cli/src/main/java/org/owasp/dependencycheck/App.java +++ b/cli/src/main/java/org/owasp/dependencycheck/App.java @@ -523,6 +523,8 @@ protected void populateSettings(CliParser cli) throws InvalidSettingException { cli.hasOption(CliParser.ARGUMENT.RETIRED)); settings.setStringIfNotNull(Settings.KEYS.ANALYZER_GOLANG_PATH, cli.getStringArgument(CliParser.ARGUMENT.PATH_TO_GO)); + settings.setStringIfNotNull(Settings.KEYS.ANALYZER_GOLANG_VULNCHECK_PATH, + cli.getStringArgument(CliParser.ARGUMENT.PATH_TO_GOVULNCHECK)); settings.setStringIfNotNull(Settings.KEYS.ANALYZER_YARN_PATH, cli.getStringArgument(CliParser.ARGUMENT.PATH_TO_YARN)); settings.setStringIfNotNull(Settings.KEYS.ANALYZER_PNPM_PATH, @@ -617,6 +619,8 @@ protected void populateSettings(CliParser cli) throws InvalidSettingException { !cli.isDisabled(CliParser.ARGUMENT.DISABLE_GO_DEP, Settings.KEYS.ANALYZER_GOLANG_DEP_ENABLED)); settings.setBoolean(Settings.KEYS.ANALYZER_GOLANG_MOD_ENABLED, !cli.isDisabled(CliParser.ARGUMENT.DISABLE_GOLANG_MOD, Settings.KEYS.ANALYZER_GOLANG_MOD_ENABLED)); + settings.setBooleanIfNotNull(Settings.KEYS.ANALYZER_GOLANG_VULNCHECK_ENABLED, + cli.hasOption(CliParser.ARGUMENT.ENABLE_GOLANG_VULNCHECK) ? true : null); settings.setBoolean(Settings.KEYS.ANALYZER_DART_ENABLED, !cli.isDisabled(CliParser.ARGUMENT.DISABLE_DART, Settings.KEYS.ANALYZER_DART_ENABLED)); settings.setBoolean(Settings.KEYS.ANALYZER_NODE_PACKAGE_ENABLED, diff --git a/cli/src/main/java/org/owasp/dependencycheck/CliParser.java b/cli/src/main/java/org/owasp/dependencycheck/CliParser.java index 6260c10e040..654261392bc 100644 --- a/cli/src/main/java/org/owasp/dependencycheck/CliParser.java +++ b/cli/src/main/java/org/owasp/dependencycheck/CliParser.java @@ -472,6 +472,8 @@ private void addAdvancedOptions(final Options options) { "The Artifactory URL.")) .addOption(newOptionWithArg(ARGUMENT.PATH_TO_GO, "path", "The path to the `go` executable.")) + .addOption(newOptionWithArg(ARGUMENT.PATH_TO_GOVULNCHECK, "path", + "The path to the `govulncheck` executable.")) .addOption(newOptionWithArg(ARGUMENT.PATH_TO_YARN, "path", "The path to the `yarn` executable.")) .addOption(newOptionWithArg(ARGUMENT.PATH_TO_PNPM, "path", @@ -551,6 +553,8 @@ private void addAdvancedOptions(final Options options) { .addOption(newOption(ARGUMENT.DISABLE_NODE_AUDIT_SKIPDEV, "Configures the Node Audit Analyzer to skip devDependencies")) .addOption(newOption(ARGUMENT.DISABLE_RETIRE_JS, "Disable the RetireJS Analyzer.")) .addOption(newOption(ARGUMENT.ENABLE_NEXUS, "Enable the Nexus Analyzer.")) + .addOption(newOption(ARGUMENT.ENABLE_GOLANG_VULNCHECK, + "Enable the Golang Vulncheck Analyzer; requires `go` and `govulncheck` to be installed.")) .addOption(newOption(ARGUMENT.ARTIFACTORY_ENABLED, "Whether the Artifactory Analyzer should be enabled.")) .addOption(newOption(ARGUMENT.PURGE_NVD, "Purges the local NVD data cache")) .addOption(newOption(ARGUMENT.DISABLE_HOSTED_SUPPRESSIONS, "Disable retrieval of the hosted suppressions from the configured URL.")) @@ -1349,6 +1353,10 @@ public static class ARGUMENT { * The CLI argument name for setting the path to `go`. */ public static final String PATH_TO_GO = "go"; + /** + * The CLI argument name for setting the path to `govulncheck`. + */ + public static final String PATH_TO_GOVULNCHECK = "govulncheck"; /** * The CLI argument name for setting the path to `yarn`. */ @@ -1453,6 +1461,10 @@ public static class ARGUMENT { * Disables the Nexus Analyzer. */ public static final String ENABLE_NEXUS = "enableNexus"; + /** + * Enables the Golang Vulncheck Analyzer. + */ + public static final String ENABLE_GOLANG_VULNCHECK = "enableGolangVulncheck"; /** * Disables the Sonatype OSS Index Analyzer. */ diff --git a/cli/src/main/resources/completion-for-dependency-check.sh b/cli/src/main/resources/completion-for-dependency-check.sh index 95b8d5ea1d0..3aeeec92e64 100755 --- a/cli/src/main/resources/completion-for-dependency-check.sh +++ b/cli/src/main/resources/completion-for-dependency-check.sh @@ -72,12 +72,14 @@ _odc_completions() --dotnet --enableArtifactory --enableExperimental + --enableGolangVulncheck --enableNexus --enableRetired --exclude -f --format --failOnCVSS --go + --govulncheck -h --help --hints --hostedSuppressionsForceUpdate @@ -130,7 +132,7 @@ _odc_completions() case "${prev}" in - -s|--scan|-o|--out|-d|--data|--bundleAudit|--bundleAuditWorkingDirectory|--dbDriverPath|--dotnet|--go|-P|--propertyfile|--suppression|--hint|-l|--log|--yarn) + -s|--scan|-o|--out|-d|--data|--bundleAudit|--bundleAuditWorkingDirectory|--dbDriverPath|--dotnet|--go|--govulncheck|-P|--propertyfile|--suppression|--hint|-l|--log|--yarn) COMPREPLY=( $(compgen -f -o default -- ${cur}) ) return 0 ;; diff --git a/cli/src/site/markdown/arguments.md b/cli/src/site/markdown/arguments.md index 2bb8d1b0ac7..c045ab7ff3e 100644 --- a/cli/src/site/markdown/arguments.md +++ b/cli/src/site/markdown/arguments.md @@ -113,11 +113,13 @@ Advanced Options | | \-\-dotnet | \ | The path to dotnet core for .NET Assembly analysis on non-windows systems. |   | | | \-\-disableGolangDep | | Sets whether the [experimental](../analyzers/index.html) Go Dependency Analyzer should be used. |   | | | \-\-disableGolangMod | | Sets whether the [experimental](../analyzers/index.html) Go Mod Analyzer should be used. |   | +| | \-\-enableGolangVulncheck | | Sets whether the [experimental](../analyzers/index.html) Golang Vulncheck Analyzer should be used; requires `go` and `govulncheck` to be installed. `enableExperimental` must also be set. |   | | | \-\-disableMixAudit | | Sets whether the [experimental](../analyzers/index.html) Elixir mix audit Analyze should be used. |   | | | \-\-disablePE | | Sets whether the [experimental](../analyzers/index.html) PE Analyzer should be used. |   | | | \-\-disablePoetry | | Sets whether the [experimental](../analyzers/index.html) Poetry Analyzer should be used. |   | | | \-\-disableVersionCheck | | Sets whether dependency-check should check if a new version is available. |   | | | \-\-go | \ | The path to `go` executable for the Go Mode Analyzer; only necessary if `go` is not on the path. |   | +| | \-\-govulncheck | \ | The path to the `govulncheck` executable for the Golang Vulncheck Analyzer; only necessary if `govulncheck` is not on the path. |   | | | \-\-bundleAudit | | The path to the bundle-audit executable. |   | | | \-\-bundleAuditWorkingDirectory | \ | The path to working directory that the bundle-audit command should be executed from when doing Gem bundle analysis. |   | | | \-\-proxyserver | \ | The proxy server to use when downloading resources; see the [proxy configuration](../data/proxy.html) page for more information. |   | diff --git a/core/src/main/java/org/owasp/dependencycheck/analyzer/GolangVulncheckAnalyzer.java b/core/src/main/java/org/owasp/dependencycheck/analyzer/GolangVulncheckAnalyzer.java new file mode 100644 index 00000000000..081a7910a62 --- /dev/null +++ b/core/src/main/java/org/owasp/dependencycheck/analyzer/GolangVulncheckAnalyzer.java @@ -0,0 +1,231 @@ +/* + * This file is part of dependency-check-core. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2026 The OWASP Foundation. All Rights Reserved. + */ +package org.owasp.dependencycheck.analyzer; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.owasp.dependencycheck.Engine; +import org.owasp.dependencycheck.analyzer.exception.AnalysisException; +import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem; +import org.owasp.dependencycheck.dependency.Dependency; +import org.owasp.dependencycheck.exception.InitializationException; +import org.owasp.dependencycheck.processing.GovulncheckProcessor; +import org.owasp.dependencycheck.utils.FileFilterBuilder; +import org.owasp.dependencycheck.utils.Settings; +import org.owasp.dependencycheck.utils.processing.ProcessReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import us.springett.parsers.cpe.exceptions.CpeValidationException; + +/** + * Go vulnerability analyzer that runs the Go team's + * govulncheck tool against a Go + * module and reports the vulnerabilities it finds. + *

+ * Unlike the CPE-matching {@link GolangModAnalyzer}, govulncheck consults the + * curated Go vulnerability database and performs reachability (call-graph) + * analysis, so it reports only the vulnerabilities that are actually used by the + * scanned code. This requires the {@code govulncheck} executable (and the Go + * toolchain) to be installed; the analyzer disables itself when they are + * missing.

+ * + * @author Srinivas Chippagiri + */ +@Experimental +public class GolangVulncheckAnalyzer extends AbstractFileTypeAnalyzer { + + /** + * The logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(GolangVulncheckAnalyzer.class); + + /** + * A descriptor for the type of dependencies processed or added by this + * analyzer. + */ + public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.GOLANG; + + /** + * The name of the analyzer. + */ + private static final String ANALYZER_NAME = "Golang Vulncheck Analyzer"; + + /** + * The phase that this analyzer runs in. + */ + private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.PRE_INFORMATION_COLLECTION; + + /** + * The file filter for go.mod - govulncheck is run against the module rooted + * at the go.mod file. + */ + private static final FileFilter GO_MOD_FILTER = FileFilterBuilder.newInstance() + .addFilenames(GolangModAnalyzer.GO_MOD) + .build(); + + /** + * The path to the govulncheck executable, resolved once. + */ + private String govulncheckPath = null; + + @Override + public String getName() { + return ANALYZER_NAME; + } + + @Override + public AnalysisPhase getAnalysisPhase() { + return ANALYSIS_PHASE; + } + + @Override + protected String getAnalyzerEnabledSettingKey() { + return Settings.KEYS.ANALYZER_GOLANG_VULNCHECK_ENABLED; + } + + @Override + protected FileFilter getFileFilter() { + return GO_MOD_FILTER; + } + + /** + * Resolves the path to the govulncheck executable, honoring the configured + * path when supplied. + * + * @return the path to govulncheck + */ + private String getGovulncheck() { + synchronized (this) { + if (govulncheckPath == null) { + final String path = getSettings().getString(Settings.KEYS.ANALYZER_GOLANG_VULNCHECK_PATH); + if (path == null) { + govulncheckPath = "govulncheck"; + } else { + final File exe = new File(path); + if (exe.isFile()) { + govulncheckPath = exe.getAbsolutePath(); + } else { + LOGGER.warn("Provided path to `govulncheck` is invalid. Trying the default location. " + + "If you do want to set it, please set the `{}` property", + Settings.KEYS.ANALYZER_GOLANG_VULNCHECK_PATH); + govulncheckPath = "govulncheck"; + } + } + } + return govulncheckPath; + } + } + + /** + * Launches govulncheck with the given arguments in the given folder. + * + * @param folder the working directory + * @param arguments the arguments to pass to govulncheck + * @return a handle to the launched process + * @throws AnalysisException thrown if the process cannot be started + */ + private Process launchGovulncheck(File folder, List arguments) throws AnalysisException { + if (!folder.isDirectory()) { + throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath())); + } + final List args = new ArrayList<>(); + args.add(getGovulncheck()); + args.addAll(arguments); + final ProcessBuilder builder = new ProcessBuilder(args); + builder.directory(folder); + try { + LOGGER.debug("Launching: {} from {}", args, folder); + return builder.start(); + } catch (IOException ioe) { + throw new AnalysisException("govulncheck initialization failure; this error can be ignored if you are not " + + "analyzing Go. Otherwise ensure that govulncheck is installed and the path to govulncheck is " + + "correctly specified", ioe); + } + } + + @Override + protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException { + setEnabled(false); + final Process process; + try { + process = launchGovulncheck(getSettings().getTempDirectory(), Collections.singletonList("-version")); + } catch (AnalysisException ae) { + final String msg = String.format("Exception from govulncheck process: %s. Disabling %s", ae.getCause(), ANALYZER_NAME); + throw new InitializationException(msg, ae); + } catch (IOException ex) { + throw new InitializationException("Unable to create temporary file, the Golang Vulncheck Analyzer will be disabled", ex); + } + + try (ProcessReader processReader = new ProcessReader(process)) { + processReader.readAll(); + final int exitValue = process.exitValue(); + if (exitValue == 0) { + setEnabled(true); + LOGGER.debug("{} is enabled.", ANALYZER_NAME); + } else { + LOGGER.warn("Unexpected exit code ({}) from `govulncheck -version`. Disabling {}. {}", + exitValue, ANALYZER_NAME, StringUtils.trimToEmpty(processReader.getError())); + throw new InitializationException("Unexpected exit code from govulncheck. Disabling " + ANALYZER_NAME); + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new InitializationException("govulncheck process was interrupted. Disabling " + ANALYZER_NAME); + } catch (IOException ex) { + throw new InitializationException("IOException reading govulncheck output. Disabling " + ANALYZER_NAME, ex); + } + } + + @Override + protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException { + final File parentFile = dependency.getActualFile().getParentFile(); + final List arguments = new ArrayList<>(); + arguments.add("-json"); + arguments.add("./..."); + + final Process process = launchGovulncheck(parentFile, arguments); + String error = null; + try (GovulncheckProcessor processor = new GovulncheckProcessor(dependency, engine); + ProcessReader processReader = new ProcessReader(process, processor)) { + processReader.readAll(); + error = processReader.getError(); + if (!StringUtils.isBlank(error)) { + LOGGER.debug("While analyzing `{}` govulncheck produced the following messages:\n{}", + dependency.getFilePath(), error); + } + final int exitValue = process.exitValue(); + // govulncheck exits 0 in JSON mode even when vulnerabilities are found; a non-zero + // code generally indicates a run problem (e.g. the module failed to build). The + // output has already been parsed at this point, so log rather than fail the scan. + if (exitValue != 0) { + LOGGER.warn("govulncheck returned exit code {} while analyzing '{}'. Results may be incomplete. {}", + exitValue, dependency.getFilePath(), StringUtils.trimToEmpty(error)); + } + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new AnalysisException("govulncheck process interrupted while analyzing '" + dependency.getFilePath() + "'", ie); + } catch (IOException | CpeValidationException ex) { + throw new AnalysisException("Error while analyzing '" + dependency.getFilePath() + "' with govulncheck", ex); + } + } +} diff --git a/core/src/main/java/org/owasp/dependencycheck/data/golang/GovulncheckJsonParser.java b/core/src/main/java/org/owasp/dependencycheck/data/golang/GovulncheckJsonParser.java new file mode 100644 index 00000000000..0546bd1bad3 --- /dev/null +++ b/core/src/main/java/org/owasp/dependencycheck/data/golang/GovulncheckJsonParser.java @@ -0,0 +1,286 @@ +/* + * This file is part of dependency-check-core. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2026 The OWASP Foundation. All Rights Reserved. + */ +package org.owasp.dependencycheck.data.golang; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonException; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; +import jakarta.json.stream.JsonParsingException; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.concurrent.ThreadSafe; + +import org.owasp.dependencycheck.analyzer.exception.AnalysisException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Parses the streaming JSON output of govulncheck -json. + *

+ * govulncheck emits a stream of concatenated JSON objects, each wrapping a + * single message keyed by its type (config, SBOM, + * progress, osv, or finding); message + * types other than {@code osv} and {@code finding} are ignored. The {@code osv} messages carry the + * advisory metadata (id, aliases, description, references) while the + * {@code finding} messages describe where a vulnerability was located, including + * the module, version, and - when the vulnerable symbol is reachable - the + * calling package and function.

+ *

+ * This parser joins the two: it produces one {@link GovulncheckResult} per + * advisory, selecting the most precise finding for the advisory (a + * "called" finding is preferred over a module-only one).

+ * + * @author Srinivas Chippagiri + */ +@ThreadSafe +public final class GovulncheckJsonParser { + + /** + * The logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(GovulncheckJsonParser.class); + + private GovulncheckJsonParser() { + } + + /** + * Processes the govulncheck output stream into a list of results. + * + * @param inputStream the {@code govulncheck -json} output stream + * @return the list of vulnerabilities found + * @throws AnalysisException thrown if the output cannot be parsed + */ + public static List process(InputStream inputStream) throws AnalysisException { + LOGGER.debug("Beginning govulncheck output processing"); + + final Map advisories = new LinkedHashMap<>(); + // preserve advisory encounter order for a stable report + final Map bestFinding = new LinkedHashMap<>(); + + // govulncheck emits a stream of indented JSON objects (json.MarshalIndent), one + // message per object. Because string values never contain literal newlines, a line + // that is exactly "}" marks the end of a top-level message, which lets us frame and + // parse each message individually. + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + final StringBuilder current = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + current.append(line).append('\n'); + if ("}".equals(line)) { + handleMessage(current.toString(), advisories, bestFinding); + current.setLength(0); + } + } + } catch (JsonParsingException jsonpe) { + throw new AnalysisException("Error parsing output from `govulncheck -json`", jsonpe); + } catch (JsonException | IllegalStateException | ClassCastException ex) { + throw new AnalysisException("Error reading output from `govulncheck -json`", ex); + } catch (IOException ex) { + throw new AnalysisException("Error reading output of `govulncheck -json`", ex); + } + + final List results = new ArrayList<>(); + for (Map.Entry entry : bestFinding.entrySet()) { + final JsonObject advisory = advisories.get(entry.getKey()); + results.add(buildResult(entry.getKey(), advisory, entry.getValue())); + } + return results; + } + + /** + * Parses a single framed govulncheck message and records its advisory or + * finding. + * + * @param message the JSON text of a single message object + * @param advisories the map of advisory id to OSV record + * @param bestFinding the map of advisory id to the most precise finding + */ + private static void handleMessage(String message, Map advisories, Map bestFinding) { + try (JsonReader reader = Json.createReader(new StringReader(message))) { + final JsonObject wrapper = reader.readObject(); + if (wrapper.containsKey("osv")) { + final JsonObject osv = wrapper.getJsonObject("osv"); + final String id = osv.getString("id", null); + if (id != null) { + advisories.put(id, osv); + } + } else if (wrapper.containsKey("finding")) { + final JsonObject finding = wrapper.getJsonObject("finding"); + final String id = finding.getString("osv", null); + if (id != null && isMorePrecise(finding, bestFinding.get(id))) { + bestFinding.put(id, finding); + } + } + } + } + + /** + * Determines whether a newly seen finding is more precise than the one + * currently retained for the advisory. A finding whose vulnerable frame + * resolves a symbol (i.e. the vulnerability is reachable) is preferred over + * one that only resolves a package or module. + * + * @param candidate the finding just parsed + * @param current the finding currently retained (may be null) + * @return true if the candidate should replace the current + */ + private static boolean isMorePrecise(JsonObject candidate, JsonObject current) { + if (current == null) { + return true; + } + return precision(candidate) > precision(current); + } + + /** + * Scores the precision of a finding based on its most detailed trace frame: + * 2 when a vulnerable symbol is resolved, 1 when a package is resolved, and + * 0 when only the module is known. + * + * @param finding the finding to score + * @return the precision score + */ + private static int precision(JsonObject finding) { + final JsonObject frame = vulnerableFrame(finding); + if (frame == null) { + return 0; + } + if (frame.getString("function", null) != null) { + return 2; + } + if (frame.getString("package", null) != null) { + return 1; + } + return 0; + } + + /** + * Returns the vulnerable frame for a finding - the first element of the + * trace, which identifies the vulnerable module/package/symbol. + * + * @param finding the finding + * @return the vulnerable frame, or null if the trace is empty + */ + private static JsonObject vulnerableFrame(JsonObject finding) { + final JsonArray trace = finding.getJsonArray("trace"); + if (trace == null || trace.isEmpty()) { + return null; + } + return trace.getJsonObject(0); + } + + /** + * Builds a {@link GovulncheckResult} from an advisory record and its best + * finding. + * + * @param id the advisory id + * @param advisory the OSV advisory record (may be null if it was not seen) + * @param finding the finding for the advisory + * @return the assembled result + */ + private static GovulncheckResult buildResult(String id, JsonObject advisory, JsonObject finding) { + final JsonObject frame = vulnerableFrame(finding); + final String moduleName = frame == null ? null : frame.getString("module", null); + final String moduleVersion = frame == null ? null : frame.getString("version", null); + final String packageName = frame == null ? null : frame.getString("package", null); + final String symbol = frame == null ? null : frame.getString("function", null); + final String fixedVersion = finding.getString("fixed_version", null); + + List aliases = new ArrayList<>(); + List references = new ArrayList<>(); + String summary = null; + String details = null; + if (advisory != null) { + summary = advisory.getString("summary", null); + details = advisory.getString("details", null); + aliases = readStringArray(advisory, "aliases"); + references = readReferenceUrls(advisory); + } + return new GovulncheckResult(id, aliases, summary, details, references, + moduleName, stripLeadingV(moduleVersion), stripLeadingV(fixedVersion), packageName, symbol); + } + + /** + * Reads a JSON array of strings from the given object. + * + * @param object the containing object + * @param key the array key + * @return the list of strings (never null) + */ + private static List readStringArray(JsonObject object, String key) { + final List values = new ArrayList<>(); + final JsonArray array = object.getJsonArray(key); + if (array != null) { + for (JsonString value : array.getValuesAs(JsonString.class)) { + values.add(value.getString()); + } + } + return values; + } + + /** + * Reads the reference URLs from an OSV advisory's references + * array. + * + * @param advisory the OSV advisory record + * @return the list of reference URLs (never null) + */ + private static List readReferenceUrls(JsonObject advisory) { + final List urls = new ArrayList<>(); + final JsonArray references = advisory.getJsonArray("references"); + if (references != null) { + for (JsonValue value : references) { + if (value.getValueType() == JsonValue.ValueType.OBJECT) { + final String url = value.asJsonObject().getString("url", null); + if (url != null) { + urls.add(url); + } + } + } + } + return urls; + } + + /** + * Strips the leading v that Go module versions carry (e.g. + * v1.2.3 becomes 1.2.3) so the version aligns with + * the rest of dependency-check. + * + * @param version the raw version (may be null) + * @return the normalized version, or null if the input was null + */ + private static String stripLeadingV(String version) { + if (version != null && version.startsWith("v")) { + return version.substring(1); + } + return version; + } +} diff --git a/core/src/main/java/org/owasp/dependencycheck/data/golang/GovulncheckResult.java b/core/src/main/java/org/owasp/dependencycheck/data/golang/GovulncheckResult.java new file mode 100644 index 00000000000..697124a4621 --- /dev/null +++ b/core/src/main/java/org/owasp/dependencycheck/data/golang/GovulncheckResult.java @@ -0,0 +1,208 @@ +/* + * This file is part of dependency-check-core. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2026 The OWASP Foundation. All Rights Reserved. + */ +package org.owasp.dependencycheck.data.golang; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a single vulnerability reported by + * govulncheck -json. Each result joins the OSV advisory record + * (identified by its GO-YYYY-NNNN id) with the most precise + * finding emitted for it (the module, version, and - when the vulnerable symbol + * is reachable - the calling package and function). + * + * @author Srinivas Chippagiri + */ +public class GovulncheckResult { + + /** + * The Go vulnerability database id (e.g. GO-2022-0969). + */ + private final String id; + /** + * The aliases for the vulnerability (e.g. CVE and GHSA ids); used to + * de-duplicate against vulnerabilities reported by other data sources. + */ + private final List aliases; + /** + * A short summary of the vulnerability. + */ + private final String summary; + /** + * The detailed description of the vulnerability. + */ + private final String details; + /** + * The reference URLs for the vulnerability. + */ + private final List references; + /** + * The vulnerable module path (e.g. golang.org/x/net). + */ + private final String moduleName; + /** + * The version of the vulnerable module in use. + */ + private final String moduleVersion; + /** + * The first version in which the vulnerability is fixed; may be null. + */ + private final String fixedVersion; + /** + * The vulnerable package that is imported; may be null when only the module + * is known. + */ + private final String packageName; + /** + * The reachable vulnerable symbol; may be null when the vulnerability is not + * known to be called. + */ + private final String symbol; + + //CSOFF: ParameterNumber + /** + * Constructs a new govulncheck result. + * + * @param id the Go vulnerability database id + * @param aliases the CVE/GHSA aliases + * @param summary a short summary + * @param details the detailed description + * @param references the reference URLs + * @param moduleName the vulnerable module path + * @param moduleVersion the version of the module in use + * @param fixedVersion the first fixed version (may be null) + * @param packageName the vulnerable package that is imported (may be null) + * @param symbol the reachable vulnerable symbol (may be null) + */ + public GovulncheckResult(String id, List aliases, String summary, String details, List references, + String moduleName, String moduleVersion, String fixedVersion, String packageName, String symbol) { + this.id = id; + this.aliases = aliases == null ? new ArrayList<>() : aliases; + this.summary = summary; + this.details = details; + this.references = references == null ? new ArrayList<>() : references; + this.moduleName = moduleName; + this.moduleVersion = moduleVersion; + this.fixedVersion = fixedVersion; + this.packageName = packageName; + this.symbol = symbol; + } + //CSON: ParameterNumber + + /** + * Returns the Go vulnerability database id. + * + * @return the id + */ + public String getId() { + return id; + } + + /** + * Returns the CVE/GHSA aliases. + * + * @return the aliases + */ + public List getAliases() { + return aliases; + } + + /** + * Returns the short summary. + * + * @return the summary + */ + public String getSummary() { + return summary; + } + + /** + * Returns the detailed description. + * + * @return the details + */ + public String getDetails() { + return details; + } + + /** + * Returns the reference URLs. + * + * @return the references + */ + public List getReferences() { + return references; + } + + /** + * Returns the vulnerable module path. + * + * @return the module path + */ + public String getModuleName() { + return moduleName; + } + + /** + * Returns the version of the vulnerable module in use. + * + * @return the module version + */ + public String getModuleVersion() { + return moduleVersion; + } + + /** + * Returns the first version in which the vulnerability is fixed. + * + * @return the fixed version; may be null + */ + public String getFixedVersion() { + return fixedVersion; + } + + /** + * Returns the vulnerable package that is imported. + * + * @return the package name; may be null + */ + public String getPackageName() { + return packageName; + } + + /** + * Returns the reachable vulnerable symbol. + * + * @return the symbol; may be null + */ + public String getSymbol() { + return symbol; + } + + /** + * Indicates whether the vulnerable symbol is reachable (called) from the + * scanned code. When {@code false} the module is present but govulncheck did + * not observe a call into the vulnerable code. + * + * @return true if a vulnerable symbol is called + */ + public boolean isCalled() { + return symbol != null && !symbol.isEmpty(); + } +} diff --git a/core/src/main/java/org/owasp/dependencycheck/dependency/Vulnerability.java b/core/src/main/java/org/owasp/dependencycheck/dependency/Vulnerability.java index 118a2deb932..fcaf60d7485 100644 --- a/core/src/main/java/org/owasp/dependencycheck/dependency/Vulnerability.java +++ b/core/src/main/java/org/owasp/dependencycheck/dependency/Vulnerability.java @@ -69,7 +69,11 @@ public enum Source { /** * Vulnerability from Mix Audit. */ - MIXAUDIT + MIXAUDIT, + /** + * Vulnerability from the Go vulnerability database via `govulncheck`. + */ + GOVULNCHECK } /** diff --git a/core/src/main/java/org/owasp/dependencycheck/processing/GovulncheckProcessor.java b/core/src/main/java/org/owasp/dependencycheck/processing/GovulncheckProcessor.java new file mode 100644 index 00000000000..7bb6b13024b --- /dev/null +++ b/core/src/main/java/org/owasp/dependencycheck/processing/GovulncheckProcessor.java @@ -0,0 +1,305 @@ +/* + * This file is part of dependency-check-core. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2026 The OWASP Foundation. All Rights Reserved. + */ +package org.owasp.dependencycheck.processing; + +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; +import com.github.packageurl.PackageURLBuilder; + +import java.io.InputStream; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.owasp.dependencycheck.Engine; +import static org.owasp.dependencycheck.analyzer.GolangVulncheckAnalyzer.DEPENDENCY_ECOSYSTEM; +import org.owasp.dependencycheck.analyzer.exception.AnalysisException; +import org.owasp.dependencycheck.data.golang.GovulncheckJsonParser; +import org.owasp.dependencycheck.data.golang.GovulncheckResult; +import org.owasp.dependencycheck.data.nvdcve.CveDB; +import org.owasp.dependencycheck.data.nvdcve.DatabaseException; +import org.owasp.dependencycheck.dependency.Confidence; +import org.owasp.dependencycheck.dependency.Dependency; +import org.owasp.dependencycheck.dependency.EvidenceType; +import org.owasp.dependencycheck.dependency.Reference; +import org.owasp.dependencycheck.dependency.Vulnerability; +import org.owasp.dependencycheck.dependency.VulnerableSoftware; +import org.owasp.dependencycheck.dependency.VulnerableSoftwareBuilder; +import org.owasp.dependencycheck.dependency.naming.GenericIdentifier; +import org.owasp.dependencycheck.dependency.naming.PurlIdentifier; +import org.owasp.dependencycheck.utils.Checksum; +import org.owasp.dependencycheck.utils.processing.Processor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import us.springett.parsers.cpe.exceptions.CpeValidationException; +import us.springett.parsers.cpe.values.Part; + +/** + * Processor for the output of govulncheck -json. For each + * vulnerability reported it adds a synthetic dependency representing the + * vulnerable Go module and attaches the vulnerability to it. When an alias + * (CVE) is already known to dependency-check the existing vulnerability record + * is reused so the finding de-duplicates against other data sources. + * + * @author Srinivas Chippagiri + */ +public class GovulncheckProcessor extends Processor { + + /** + * The logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(GovulncheckProcessor.class); + /** + * Reference to the dependency-check engine. + */ + private final Engine engine; + /** + * Reference to the go.mod dependency that was scanned. + */ + private final Dependency goDependency; + /** + * Temporary storage for an exception if it occurs during processing. + */ + private AnalysisException analysisException; + /** + * Temporary storage for an exception if it occurs during processing. + */ + private CpeValidationException cpeException; + + /** + * Constructs a new processor to consume the output of `govulncheck`. + * + * @param goDependency a reference to the scanned `go.mod` dependency + * @param engine a reference to the dependency-check engine + */ + public GovulncheckProcessor(Dependency goDependency, Engine engine) { + this.engine = engine; + this.goDependency = goDependency; + } + + @Override + public void run() { + try { + final List results = GovulncheckJsonParser.process(getInput()); + for (GovulncheckResult result : results) { + processResult(result); + } + } catch (AnalysisException ex) { + this.analysisException = ex; + } catch (CpeValidationException ex) { + this.cpeException = ex; + } + } + + /** + * Throws any exceptions that occurred during processing. + * + * @throws AnalysisException thrown if an analysis error occurred + * @throws CpeValidationException thrown if a CPE validation error occurred + */ + @Override + public void close() throws AnalysisException, CpeValidationException { + if (analysisException != null) { + addSuppressedExceptions(analysisException, cpeException); + throw analysisException; + } + if (cpeException != null) { + throw cpeException; + } + } + + /** + * Adds a dependency and vulnerability for a single govulncheck result. + * + * @param result the parsed govulncheck result + * @throws CpeValidationException thrown if the vulnerable software cannot be + * built + */ + private void processResult(GovulncheckResult result) throws CpeValidationException { + if (StringUtils.isBlank(result.getModuleName())) { + LOGGER.debug("Skipping govulncheck result {} with no module information", result.getId()); + return; + } + final Dependency dependency = createDependency(result); + final Vulnerability vulnerability = resolveVulnerability(result); + dependency.addVulnerability(vulnerability); + engine.addDependency(dependency); + } + + /** + * Resolves the vulnerability for a result, reusing an existing record from + * the database when one of the aliases (typically a CVE) is already known, + * otherwise synthesizing one from the govulncheck advisory. + * + * @param result the parsed govulncheck result + * @return the vulnerability + * @throws CpeValidationException thrown if the vulnerable software cannot be + * built + */ + private Vulnerability resolveVulnerability(GovulncheckResult result) throws CpeValidationException { + final CveDB cvedb = engine.getDatabase(); + if (cvedb != null) { + for (String alias : result.getAliases()) { + if (alias != null && alias.startsWith("CVE-")) { + try { + final Vulnerability existing = cvedb.getVulnerability(alias); + if (existing != null) { + LOGGER.debug("Reusing {} from the database for govulncheck advisory {}", alias, result.getId()); + return existing; + } + } catch (DatabaseException ex) { + LOGGER.debug("Unable to look up alias {} for govulncheck advisory {}", alias, result.getId()); + } + } + } + } + return createVulnerability(result); + } + + /** + * Creates a synthetic dependency representing the vulnerable Go module. + * + * @param result the parsed govulncheck result + * @return the dependency + */ + private Dependency createDependency(GovulncheckResult result) { + final String moduleName = result.getModuleName(); + final String version = result.getModuleVersion(); + final Dependency dep = new Dependency(goDependency.getActualFile(), true); + + final String identifier = StringUtils.isBlank(version) ? moduleName : moduleName + ":" + version; + dep.setEcosystem(DEPENDENCY_ECOSYSTEM); + dep.setDisplayFileName(identifier); + dep.setName(moduleName); + if (StringUtils.isNotBlank(version)) { + dep.setVersion(version); + } + dep.setPackagePath(identifier); + dep.setSha1sum(Checksum.getSHA1Checksum(identifier)); + dep.setMd5sum(Checksum.getMD5Checksum(identifier)); + dep.setSha256sum(Checksum.getSHA256Checksum(identifier)); + + dep.addEvidence(EvidenceType.PRODUCT, "govulncheck", "module", moduleName, Confidence.HIGHEST); + dep.addEvidence(EvidenceType.VENDOR, "govulncheck", "module", moduleName, Confidence.HIGH); + if (StringUtils.isNotBlank(version)) { + dep.addEvidence(EvidenceType.VERSION, "govulncheck", "version", version, Confidence.HIGHEST); + } + + dep.addSoftwareIdentifier(buildPackageIdentifier(moduleName, version)); + return dep; + } + + /** + * Builds a Package-URL identifier for the given Go module, falling back to a + * generic identifier when the purl cannot be built. + * + * @param moduleName the module path + * @param version the module version + * @return the software identifier + */ + private org.owasp.dependencycheck.dependency.naming.Identifier buildPackageIdentifier(String moduleName, String version) { + final PackageURLBuilder builder = PackageURLBuilder.aPackageURL().withType("golang"); + final int lastSlash = moduleName.lastIndexOf('/'); + if (lastSlash > 0) { + builder.withNamespace(moduleName.substring(0, lastSlash)); + builder.withName(moduleName.substring(lastSlash + 1)); + } else { + builder.withName(moduleName); + } + if (StringUtils.isNotBlank(version)) { + builder.withVersion(version); + } + try { + final PackageURL purl = builder.build(); + return new PurlIdentifier(purl, Confidence.HIGHEST); + } catch (MalformedPackageURLException ex) { + LOGGER.debug("Unable to build package url for go module {}", moduleName, ex); + final StringBuilder value = new StringBuilder(moduleName); + if (StringUtils.isNotBlank(version)) { + value.append('@').append(version); + } + return new GenericIdentifier(value.toString(), Confidence.HIGHEST); + } + } + + /** + * Synthesizes a vulnerability from a govulncheck advisory when no matching + * record exists in the database. + * + * @param result the parsed govulncheck result + * @return the vulnerability + * @throws CpeValidationException thrown if the vulnerable software cannot be + * built + */ + private Vulnerability createVulnerability(GovulncheckResult result) throws CpeValidationException { + final Vulnerability vulnerability = new Vulnerability(); + vulnerability.setSource(Vulnerability.Source.GOVULNCHECK); + vulnerability.setName(result.getId()); + vulnerability.setUnscoredSeverity("UNKNOWN"); + vulnerability.setDescription(buildDescription(result)); + + final String product = result.getModuleName(); + final VulnerableSoftwareBuilder builder = new VulnerableSoftwareBuilder(); + final VulnerableSoftware vs = builder.part(Part.APPLICATION) + .vendor(String.format("%s_project", product)) + .product(product) + .version(StringUtils.defaultIfBlank(result.getModuleVersion(), "*")) + .build(); + vulnerability.addVulnerableSoftware(vs); + vulnerability.setMatchedVulnerableSoftware(vs); + + for (String url : result.getReferences()) { + final Reference ref = new Reference(); + ref.setName(result.getId()); + ref.setSource("govulncheck"); + ref.setUrl(url); + vulnerability.addReference(ref); + } + return vulnerability; + } + + /** + * Builds a human-readable description for a synthesized vulnerability, + * including reachability and remediation hints from govulncheck. + * + * @param result the parsed govulncheck result + * @return the description + */ + private String buildDescription(GovulncheckResult result) { + final StringBuilder sb = new StringBuilder(); + if (StringUtils.isNotBlank(result.getSummary())) { + sb.append(result.getSummary()).append("\n\n"); + } + if (StringUtils.isNotBlank(result.getDetails())) { + sb.append(result.getDetails()).append("\n\n"); + } + if (result.isCalled()) { + sb.append("Reachable: govulncheck determined a call to the vulnerable symbol `") + .append(result.getSymbol()).append("`.\n"); + } else { + sb.append("Not reported as called: the vulnerable module is present but govulncheck did not " + + "observe a call into the vulnerable code.\n"); + } + if (StringUtils.isNotBlank(result.getFixedVersion())) { + sb.append("Fixed in version ").append(result.getFixedVersion()).append(".\n"); + } + if (!result.getAliases().isEmpty()) { + sb.append("Aliases: ").append(String.join(", ", result.getAliases())).append('.'); + } + return sb.toString().trim(); + } +} diff --git a/core/src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer b/core/src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer index 3b56712f747..5e57a5ee3ef 100644 --- a/core/src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer +++ b/core/src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer @@ -31,6 +31,7 @@ org.owasp.dependencycheck.analyzer.NodeAuditAnalyzer org.owasp.dependencycheck.analyzer.YarnAuditAnalyzer org.owasp.dependencycheck.analyzer.PnpmAuditAnalyzer org.owasp.dependencycheck.analyzer.GolangModAnalyzer +org.owasp.dependencycheck.analyzer.GolangVulncheckAnalyzer org.owasp.dependencycheck.analyzer.GolangDepAnalyzer org.owasp.dependencycheck.analyzer.RetireJsAnalyzer org.owasp.dependencycheck.analyzer.RubyGemspecAnalyzer diff --git a/core/src/main/resources/dependencycheck.properties b/core/src/main/resources/dependencycheck.properties index b0d6516ee61..f0b7f73a348 100644 --- a/core/src/main/resources/dependencycheck.properties +++ b/core/src/main/resources/dependencycheck.properties @@ -118,6 +118,7 @@ analyzer.retirejs.repo.validforhours=24 analyzer.retirejs.repo.js.url=https://raw.githubusercontent.com/Retirejs/retire.js/master/repository/jsrepository.json analyzer.retirejs.filternonvulnerable=false analyzer.golang.mod.enabled=true +analyzer.golang.vulncheck.enabled=false analyzer.mix.audit.enabled=true analyzer.composer.lock.enabled=true analyzer.python.distribution.enabled=true diff --git a/core/src/test/java/org/owasp/dependencycheck/analyzer/GolangVulncheckAnalyzerIT.java b/core/src/test/java/org/owasp/dependencycheck/analyzer/GolangVulncheckAnalyzerIT.java new file mode 100644 index 00000000000..b46902ae802 --- /dev/null +++ b/core/src/test/java/org/owasp/dependencycheck/analyzer/GolangVulncheckAnalyzerIT.java @@ -0,0 +1,147 @@ +/* + * This file is part of dependency-check-core. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2026 The OWASP Foundation. All Rights Reserved. + */ +package org.owasp.dependencycheck.analyzer; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.owasp.dependencycheck.BaseDBTestCase; +import org.owasp.dependencycheck.BaseTest; +import org.owasp.dependencycheck.Engine; +import org.owasp.dependencycheck.analyzer.exception.AnalysisException; +import org.owasp.dependencycheck.data.nvdcve.DatabaseException; +import org.owasp.dependencycheck.dependency.Dependency; +import org.owasp.dependencycheck.dependency.Vulnerability; +import org.owasp.dependencycheck.exception.InitializationException; +import org.owasp.dependencycheck.utils.Settings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Integration tests for GolangVulncheckAnalyzer. These exercise the real + * {@code govulncheck} tool and are skipped when it (or {@code go}) is not + * installed. To run locally ensure {@code go} and {@code govulncheck} are on the + * PATH, or set the {@code analyzer.golang.vulncheck.path} property. + * + * @author Srinivas Chippagiri + */ +class GolangVulncheckAnalyzerIT extends BaseDBTestCase { + + private static final Logger LOGGER = LoggerFactory.getLogger(GolangVulncheckAnalyzerIT.class); + + private GolangVulncheckAnalyzer analyzer; + + @BeforeEach + @Override + public void setUp() throws Exception { + super.setUp(); + getSettings().setBoolean(Settings.KEYS.AUTO_UPDATE, false); + getSettings().setBoolean(Settings.KEYS.ANALYZER_NEXUS_ENABLED, false); + getSettings().setBoolean(Settings.KEYS.ANALYZER_CENTRAL_ENABLED, false); + getSettings().setBoolean(Settings.KEYS.ANALYZER_GOLANG_VULNCHECK_ENABLED, true); + analyzer = new GolangVulncheckAnalyzer(); + analyzer.initialize(getSettings()); + analyzer.setFilesMatched(true); + } + + @AfterEach + @Override + public void tearDown() throws Exception { + if (analyzer != null) { + analyzer.close(); + analyzer = null; + } + super.tearDown(); + } + + /** + * Runs govulncheck against a module that calls a known-vulnerable + * {@code golang.org/x/text} v0.3.5 symbol and verifies the reachable + * vulnerability (GO-2021-0113 / CVE-2021-38561) is reported. + * + * @throws DatabaseException thrown when the database cannot be opened + */ + @Test + void testAnalysis() throws DatabaseException { + try (Engine engine = new Engine(getSettings())) { + engine.openDatabase(); + analyzer.prepare(engine); + + final String resource = "golang/vulncheck/go.mod"; + final Dependency toScan = new Dependency(BaseTest.getResourceAsFile(this, resource)); + analyzer.analyze(toScan, engine); + + final Dependency[] dependencies = engine.getDependencies(); + assertTrue(dependencies.length > 0, "govulncheck should have reported at least one vulnerable module"); + + boolean foundTextModule = false; + boolean foundVulnerability = false; + for (Dependency d : dependencies) { + if ("golang.org/x/text".equals(d.getName())) { + foundTextModule = true; + assertTrue(d.isVirtual()); + assertEqualsIgnoringNull("0.3.5", d.getVersion()); + assertTrue(GolangVulncheckAnalyzer.DEPENDENCY_ECOSYSTEM.equals(d.getEcosystem())); + } + for (Vulnerability v : d.getVulnerabilities()) { + final String name = v.getName(); + // depending on the local DB the finding may be reused from NVD (CVE id) + // or synthesized from the govulncheck advisory (GO id) + if ("GO-2021-0113".equals(name) || "CVE-2021-38561".equals(name)) { + foundVulnerability = true; + } + } + } + assertTrue(foundTextModule, "expected a dependency for golang.org/x/text"); + assertTrue(foundVulnerability, "expected GO-2021-0113 / CVE-2021-38561 to be reported for x/text 0.3.5"); + } catch (InitializationException | AnalysisException e) { + LOGGER.warn("Skipping GolangVulncheckAnalyzerIT; ensure go and govulncheck are installed " + + "(or set the \"analyzer.golang.vulncheck.path\" property).", e); + assumeTrue(false, "govulncheck may not be installed: " + e); + } + } + + /** + * When govulncheck is misconfigured (the configured path is not a valid + * executable) the analyzer must disable itself. + */ + @Test + void testInvalidGovulncheckExecutable() { + final String path = BaseTest.getResourceAsFile(this, "golang/vulncheck/go.mod").getAbsolutePath(); + getSettings().setString(Settings.KEYS.ANALYZER_GOLANG_VULNCHECK_PATH, path); + analyzer.initialize(getSettings()); + try { + analyzer.prepare(null); + } catch (InitializationException e) { + assertNotNull(e); + } finally { + assertFalse(analyzer.isEnabled()); + } + } + + private static void assertEqualsIgnoringNull(String expected, String actual) { + if (actual != null) { + assertTrue(expected.equals(actual), "expected version " + expected + " but was " + actual); + } + } +} diff --git a/core/src/test/java/org/owasp/dependencycheck/analyzer/GolangVulncheckAnalyzerTest.java b/core/src/test/java/org/owasp/dependencycheck/analyzer/GolangVulncheckAnalyzerTest.java new file mode 100644 index 00000000000..180136f46e1 --- /dev/null +++ b/core/src/test/java/org/owasp/dependencycheck/analyzer/GolangVulncheckAnalyzerTest.java @@ -0,0 +1,77 @@ +/* + * This file is part of dependency-check-core. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2026 The OWASP Foundation. All Rights Reserved. + */ +package org.owasp.dependencycheck.analyzer; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.owasp.dependencycheck.BaseTest; +import org.owasp.dependencycheck.utils.Settings; + +import java.io.File; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Unit tests for GolangVulncheckAnalyzer. + * + * @author Srinivas Chippagiri + */ +class GolangVulncheckAnalyzerTest extends BaseTest { + + private GolangVulncheckAnalyzer analyzer; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + getSettings().setBoolean(Settings.KEYS.AUTO_UPDATE, false); + // the analyzer is disabled by default; enable it so accept() reports matches + getSettings().setBoolean(Settings.KEYS.ANALYZER_GOLANG_VULNCHECK_ENABLED, true); + analyzer = new GolangVulncheckAnalyzer(); + analyzer.initialize(getSettings()); + analyzer.setFilesMatched(true); + } + + @AfterEach + @Override + public void tearDown() throws Exception { + if (analyzer != null) { + analyzer.close(); + analyzer = null; + } + super.tearDown(); + } + + @Test + void testGetName() { + assertThat(analyzer.getName(), is("Golang Vulncheck Analyzer")); + } + + @Test + void testGetAnalysisPhase() { + assertThat(analyzer.getAnalysisPhase(), is(AnalysisPhase.PRE_INFORMATION_COLLECTION)); + } + + @Test + void testSupportsFiles() { + assertThat(analyzer.accept(new File("go.mod")), is(true)); + assertThat(analyzer.accept(new File("go.sum")), is(false)); + } +} diff --git a/core/src/test/java/org/owasp/dependencycheck/data/golang/GovulncheckJsonParserTest.java b/core/src/test/java/org/owasp/dependencycheck/data/golang/GovulncheckJsonParserTest.java new file mode 100644 index 00000000000..10c8f81691d --- /dev/null +++ b/core/src/test/java/org/owasp/dependencycheck/data/golang/GovulncheckJsonParserTest.java @@ -0,0 +1,77 @@ +/* + * This file is part of dependency-check-core. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2026 The OWASP Foundation. All Rights Reserved. + */ +package org.owasp.dependencycheck.data.golang; + +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for {@link GovulncheckJsonParser}. + * + * @author Srinivas Chippagiri + */ +class GovulncheckJsonParserTest { + + private GovulncheckResult findById(List results, String id) { + return results.stream().filter(r -> id.equals(r.getId())).findFirst().orElse(null); + } + + @Test + void testProcess() throws Exception { + try (InputStream in = getClass().getClassLoader().getResourceAsStream("golang/govulncheck.json")) { + assertNotNull(in, "test resource golang/govulncheck.json was not found"); + final List results = GovulncheckJsonParser.process(in); + + // one result per advisory, despite the config/progress messages and the + // duplicate (module-level and symbol-level) findings for GO-2022-0969 + assertEquals(2, results.size()); + + final GovulncheckResult called = findById(results, "GO-2022-0969"); + assertNotNull(called); + assertEquals("golang.org/x/net", called.getModuleName()); + // leading "v" is stripped from module and fixed versions + assertEquals("0.1.0", called.getModuleVersion()); + assertEquals("0.4.0", called.getFixedVersion()); + // the more precise (symbol-resolving) finding is retained + assertTrue(called.isCalled()); + assertEquals("canonicalHeader", called.getSymbol()); + assertEquals("golang.org/x/net/http2", called.getPackageName()); + assertTrue(called.getAliases().contains("CVE-2022-41717")); + assertTrue(called.getAliases().contains("GHSA-xrjj-mj9h-534m")); + assertEquals(2, called.getReferences().size()); + + final GovulncheckResult informational = findById(results, "GO-2021-0113"); + assertNotNull(informational); + assertEquals("golang.org/x/text", informational.getModuleName()); + assertEquals("0.3.5", informational.getModuleVersion()); + assertEquals("0.3.7", informational.getFixedVersion()); + // module is present but no vulnerable symbol is reported as called + assertFalse(informational.isCalled()); + assertNull(informational.getSymbol()); + assertTrue(informational.getAliases().contains("CVE-2021-38561")); + } + } +} diff --git a/core/src/test/resources/golang/govulncheck.json b/core/src/test/resources/golang/govulncheck.json new file mode 100644 index 00000000000..76bc3b1b90d --- /dev/null +++ b/core/src/test/resources/golang/govulncheck.json @@ -0,0 +1,140 @@ +{ + "config": { + "protocol_version": "v1.0.0", + "scanner_name": "govulncheck", + "scanner_version": "v1.1.3", + "db": "https://vuln.go.dev" + } +} +{ + "SBOM": { + "go_version": "go1.26.4", + "modules": [ + { + "path": "example.com/mymodule" + }, + { + "path": "golang.org/x/net", + "version": "v0.1.0" + } + ] + } +} +{ + "progress": { + "message": "Scanning your code and 245 packages across 12 dependent modules for known vulnerabilities..." + } +} +{ + "osv": { + "id": "GO-2022-0969", + "modified": "2023-06-12T18:45:41Z", + "published": "2022-12-14T22:26:07Z", + "aliases": [ + "CVE-2022-41717", + "GHSA-xrjj-mj9h-534m" + ], + "summary": "Excessive memory growth in net/http and golang.org/x/net/http2", + "details": "An attacker can cause excessive memory growth in a Go server accepting HTTP/2 requests.", + "affected": [ + { + "package": { + "name": "golang.org/x/net", + "ecosystem": "Go" + }, + "ranges": [ + { + "type": "SEMVER", + "events": [ + { + "introduced": "0" + }, + { + "fixed": "0.4.0" + } + ] + } + ] + } + ], + "references": [ + { + "type": "REPORT", + "url": "https://go.dev/issue/56350" + }, + { + "type": "WEB", + "url": "https://groups.google.com/g/golang-announce/c/L_3rmdT0BMU" + } + ], + "database_specific": { + "url": "https://pkg.go.dev/vuln/GO-2022-0969" + } + } +} +{ + "finding": { + "osv": "GO-2022-0969", + "fixed_version": "v0.4.0", + "trace": [ + { + "module": "golang.org/x/net", + "version": "v0.1.0" + } + ] + } +} +{ + "finding": { + "osv": "GO-2022-0969", + "fixed_version": "v0.4.0", + "trace": [ + { + "module": "golang.org/x/net", + "version": "v0.1.0", + "package": "golang.org/x/net/http2", + "function": "canonicalHeader", + "position": { + "filename": "headermap.go", + "line": 84 + } + }, + { + "module": "example.com/mymodule", + "package": "example.com/mymodule/server", + "function": "handle" + } + ] + } +} +{ + "osv": { + "id": "GO-2021-0113", + "aliases": [ + "CVE-2021-38561" + ], + "summary": "Out-of-bounds read in golang.org/x/text/language", + "details": "Due to improper index calculation an out-of-bounds read may occur.", + "references": [ + { + "type": "FIX", + "url": "https://go.dev/cl/340830" + } + ], + "database_specific": { + "url": "https://pkg.go.dev/vuln/GO-2021-0113" + } + } +} +{ + "finding": { + "osv": "GO-2021-0113", + "fixed_version": "v0.3.7", + "trace": [ + { + "module": "golang.org/x/text", + "version": "v0.3.5" + } + ] + } +} diff --git a/core/src/test/resources/golang/vulncheck/go.mod b/core/src/test/resources/golang/vulncheck/go.mod new file mode 100644 index 00000000000..985e9953c60 --- /dev/null +++ b/core/src/test/resources/golang/vulncheck/go.mod @@ -0,0 +1,5 @@ +module vulntest + +go 1.21 + +require golang.org/x/text v0.3.5 diff --git a/core/src/test/resources/golang/vulncheck/go.sum b/core/src/test/resources/golang/vulncheck/go.sum new file mode 100644 index 00000000000..bbd33e8920d --- /dev/null +++ b/core/src/test/resources/golang/vulncheck/go.sum @@ -0,0 +1,3 @@ +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/core/src/test/resources/golang/vulncheck/main.go b/core/src/test/resources/golang/vulncheck/main.go new file mode 100644 index 00000000000..e498ea4cc02 --- /dev/null +++ b/core/src/test/resources/golang/vulncheck/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + + "golang.org/x/text/language" +) + +// Calls into golang.org/x/text/language.Parse, which in v0.3.5 is affected by +// GO-2021-0113 (CVE-2021-38561) - so govulncheck reports a reachable ("called") +// finding for this module. Used by GolangVulncheckAnalyzerIT. +func main() { + tag, err := language.Parse("en-US") + if err != nil { + panic(err) + } + fmt.Println(tag) +} diff --git a/maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java b/maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java index f45828eb5fd..19278c64623 100644 --- a/maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java +++ b/maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java @@ -281,12 +281,25 @@ public abstract class BaseDependencyCheckMojo extends AbstractMojo implements Ma @SuppressWarnings("CanBeFinal") @Parameter(property = "golangModEnabled") private Boolean golangModEnabled; + /** + * Sets whether the Golang Vulncheck Analyzer is enabled; this requires `go` + * and `govulncheck` to be installed. Default is false. + */ + @SuppressWarnings("CanBeFinal") + @Parameter(property = "golangVulncheckEnabled") + private Boolean golangVulncheckEnabled; /** * Sets the path to `go`. */ @SuppressWarnings("CanBeFinal") @Parameter(property = "pathToGo") private String pathToGo; + /** + * Sets the path to `govulncheck`. + */ + @SuppressWarnings("CanBeFinal") + @Parameter(property = "pathToGovulncheck") + private String pathToGovulncheck; /** * Sets the path to `yarn`. @@ -2337,8 +2350,10 @@ protected void populateSettings() throws MojoFailureException, MojoExecutionExce settings.setBooleanIfNotNull(Settings.KEYS.ANALYZER_RETIRED_ENABLED, enableRetired); settings.setBooleanIfNotNull(Settings.KEYS.ANALYZER_GOLANG_DEP_ENABLED, golangDepEnabled); settings.setBooleanIfNotNull(Settings.KEYS.ANALYZER_GOLANG_MOD_ENABLED, golangModEnabled); + settings.setBooleanIfNotNull(Settings.KEYS.ANALYZER_GOLANG_VULNCHECK_ENABLED, golangVulncheckEnabled); settings.setBooleanIfNotNull(Settings.KEYS.ANALYZER_DART_ENABLED, dartAnalyzerEnabled); settings.setStringIfNotNull(Settings.KEYS.ANALYZER_GOLANG_PATH, pathToGo); + settings.setStringIfNotNull(Settings.KEYS.ANALYZER_GOLANG_VULNCHECK_PATH, pathToGovulncheck); settings.setStringIfNotNull(Settings.KEYS.ANALYZER_YARN_PATH, pathToYarn); settings.setStringIfNotNull(Settings.KEYS.ANALYZER_PNPM_PATH, pathToPnpm); diff --git a/maven/src/site/markdown/configuration.md b/maven/src/site/markdown/configuration.md index 68a79397c11..62c6d7496cf 100644 --- a/maven/src/site/markdown/configuration.md +++ b/maven/src/site/markdown/configuration.md @@ -120,7 +120,9 @@ be needed. | pathToCore | The path to dotnet core .NET assembly analysis on non-windows systems. |   | | golangDepEnabled | Sets whether or not the [experimental](../analyzers/index.html) Golang Dependency Analyzer should be used. `enableExperimental` must be set to true. | true | | golangModEnabled | Sets whether or not the [experimental](../analyzers/index.html) Goland Module Analyzer should be used; requires `go` to be installed. `enableExperimental` must be set to true. | true | +| golangVulncheckEnabled | Sets whether or not the [experimental](../analyzers/index.html) Golang Vulncheck Analyzer should be used; requires `go` and `govulncheck` to be installed. `enableExperimental` must be set to true. | false | | pathToGo | The path to `go`. |   | +| pathToGovulncheck | The path to `govulncheck`. |   | RetireJS Configuration ==================== diff --git a/src/site/markdown/analyzers/golang-vulncheck.md b/src/site/markdown/analyzers/golang-vulncheck.md new file mode 100644 index 00000000000..f152689b827 --- /dev/null +++ b/src/site/markdown/analyzers/golang-vulncheck.md @@ -0,0 +1,33 @@ +Golang Vulncheck Analyzer +============== + +*v1 of the scanner*: This is the first version (v1) of the govulncheck-based scanner +integration. It leverages govulncheck's curated Go vulnerability database and +call-graph reachability analysis to keep false positive and false negative rates low. + +OWASP dependency-check includes an analyzer that runs the Go team's +[govulncheck](https://go.dev/doc/security/vuln) tool against a Go module and reports +the vulnerabilities it finds. Unlike the [Golang Mod Analyzer](./golang-mod.html), +which relies on CPE matching against the NVD, govulncheck consults the curated Go +vulnerability database and performs reachability (call-graph) analysis, so it reports +primarily the vulnerabilities that are actually reachable from the scanned code. +This substantially reduces the false positive and false negative rates for Go modules. + +For each reported vulnerability the analyzer adds a synthetic dependency for the +vulnerable Go module and attaches the vulnerability to it. When a govulncheck advisory +carries a CVE alias that dependency-check already knows about, the existing record is +reused so the finding is de-duplicated against the other data sources. + +This analyzer requires that both `go` and `govulncheck` are installed and available. +`govulncheck` can be installed with: + +``` +go install golang.org/x/vuln/cmd/govulncheck@latest +``` + +The analyzer is disabled by default (in addition to requiring the _experimental_ +option). It may be enabled and the path to `govulncheck` configured; see the +documentation for the CLI, Ant, Maven, etc. for the relevant configuration options +(`analyzer.golang.vulncheck.enabled` and `analyzer.golang.vulncheck.path`). + +File names scanned: go.mod diff --git a/src/site/markdown/analyzers/index.md b/src/site/markdown/analyzers/index.md index f4b6f004c68..57af340e8cf 100644 --- a/src/site/markdown/analyzers/index.md +++ b/src/site/markdown/analyzers/index.md @@ -51,6 +51,7 @@ several teams have found them useful in their current state. | [Dart](./dart.html) | `pubspec.yaml`, `pubspec.lock` | Extracts dependency information from specification files. | | [Golang mod](./golang-mod.html) | `go.mod` | Uses `go mod` to determine exactly which dependencies are used. | | [Golang dep](./golang-dep.html) | `Gopkg.lock` | Analyzes the lock file directly to parse dependency information. | +| [Golang vulncheck](./golang-vulncheck.html) | `go.mod` | Runs `govulncheck` against the module and reports reachable vulnerabilities from the Go vulnerability database. | | [PE Analyzer](./pe-analyzer.html) | `PE DLL and EXE` | Analyzes the PE Headers to obtain dependency information. | | [Python](./python.html) | Python source files (\*.py); Package metadata files (PKG-INFO, METADATA); Package Distribution Files (\*.whl, \*.egg, \*.zip) | Regex scan of Python source files for setuptools metadata; Parse RFC822 header format for metadata in all other artifacts. | | [Pip](./pip.html) | Python Pip requirements.txt files | Regex scan of requirements.txt. | diff --git a/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java b/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java index b2bde73f331..dee33b4efba 100644 --- a/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java +++ b/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java @@ -534,6 +534,15 @@ public static final class KEYS { * The path to go, if available. */ public static final String ANALYZER_GOLANG_PATH = "analyzer.golang.path"; + /** + * The properties key for whether the Golang govulncheck analyzer is + * enabled. + */ + public static final String ANALYZER_GOLANG_VULNCHECK_ENABLED = "analyzer.golang.vulncheck.enabled"; + /** + * The path to govulncheck, if available. + */ + public static final String ANALYZER_GOLANG_VULNCHECK_PATH = "analyzer.golang.vulncheck.path"; /** * The path to go, if available. */