From 42f9d91594abc0ad31bfaf3081dbfaf42a229468 Mon Sep 17 00:00:00 2001 From: Jeremy Long Date: Sun, 19 Oct 2025 15:04:33 -0400 Subject: [PATCH] feat: usage telemetry via scarf --- Dockerfile | 1 + .../owasp/dependencycheck/taskdefs/Check.java | 2 + ant/src/main/resources/task.properties | 1 + .../java/org/owasp/dependencycheck/App.java | 23 ++++- .../agent/DependencyCheckScanAgent.java | 3 + .../analyzer/NodePackageAnalyzer.java | 2 +- .../analyzer/OssIndexAnalyzer.java | 15 +-- .../dependency/VulnerableSoftware.java | 5 + .../dependencycheck/maven/AggregateMojo.java | 2 + .../dependencycheck/maven/CheckMojo.java | 2 + maven/src/main/resources/mojo.properties | 1 + src/site/markdown/data/index.md | 4 + src/site/markdown/general/telemetry.md | 5 + .../utils/scarf/TelemetryCollector.java | 93 +++++++++++++++++++ .../utils/scarf/TelemetryCollectorTest.java | 33 +++++++ 15 files changed, 179 insertions(+), 13 deletions(-) create mode 100644 src/site/markdown/general/telemetry.md create mode 100644 utils/src/main/java/org/owasp/dependencycheck/utils/scarf/TelemetryCollector.java create mode 100644 utils/src/test/java/org/owasp/dependencycheck/utils/scarf/TelemetryCollectorTest.java diff --git a/Dockerfile b/Dockerfile index bce18e1cdf3..6ddbdbf108b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,7 @@ ARG GID=1000 ENV user=dependencycheck ENV JAVA_HOME=/opt/jdk ENV JAVA_OPTS="-Danalyzer.assembly.dotnet.path=/usr/bin/dotnet -Danalyzer.bundle.audit.path=/usr/bin/bundle-audit -Danalyzer.golang.path=/usr/local/go/bin/go" +ENV ODC_NAME=dependency-check-docker COPY --from=jlink /jlinked /opt/jdk/ COPY --from=go /usr/local/go/ /usr/local/go/ 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 97268391d73..e531f3e928b 100644 --- a/ant/src/main/java/org/owasp/dependencycheck/taskdefs/Check.java +++ b/ant/src/main/java/org/owasp/dependencycheck/taskdefs/Check.java @@ -45,6 +45,7 @@ import org.owasp.dependencycheck.utils.InvalidSettingException; import org.owasp.dependencycheck.utils.Settings; import org.owasp.dependencycheck.utils.SeverityUtil; +import org.owasp.dependencycheck.utils.scarf.TelemetryCollector; import org.slf4j.impl.StaticLoggerBinder; //CSOFF: MethodCount @@ -1335,6 +1336,7 @@ protected void executeWithContextClassloader() throws BuildException { } catch (InvalidSettingException e) { throw new BuildException(e); } + TelemetryCollector.send(getSettings()); try (Engine engine = new Engine(Check.class.getClassLoader(), getSettings())) { for (Resource resource : getPath()) { final FileProvider provider = resource.as(FileProvider.class); diff --git a/ant/src/main/resources/task.properties b/ant/src/main/resources/task.properties index 89625759080..313178481cd 100644 --- a/ant/src/main/resources/task.properties +++ b/ant/src/main/resources/task.properties @@ -1,2 +1,3 @@ # the path to the data directory data.directory=data/11.0 +odc.application.name=dependency-check-ant diff --git a/cli/src/main/java/org/owasp/dependencycheck/App.java b/cli/src/main/java/org/owasp/dependencycheck/App.java index 57a0355bf45..ec9de612c54 100644 --- a/cli/src/main/java/org/owasp/dependencycheck/App.java +++ b/cli/src/main/java/org/owasp/dependencycheck/App.java @@ -26,6 +26,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; + import org.apache.commons.cli.ParseException; import org.apache.tools.ant.DirectoryScanner; import org.owasp.dependencycheck.data.nvdcve.DatabaseException; @@ -39,6 +40,7 @@ import org.owasp.dependencycheck.utils.Downloader; import org.owasp.dependencycheck.utils.InvalidSettingException; import org.owasp.dependencycheck.utils.Settings; +import org.owasp.dependencycheck.utils.scarf.TelemetryCollector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,7 +51,9 @@ import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import io.github.jeremylong.jcs3.slf4j.Slf4jAdapter; + import java.util.TreeSet; + import org.owasp.dependencycheck.utils.SeverityUtil; /** @@ -189,6 +193,7 @@ public int run(String[] args) { try { populateSettings(cli); Downloader.getInstance().configure(settings); + TelemetryCollector.send(settings); } catch (InvalidSettingException ex) { LOGGER.error(ex.getMessage(), ex); LOGGER.debug(ERROR_LOADING_PROPERTIES_FILE, ex); @@ -254,7 +259,7 @@ public int run(String[] args) { * collection. */ private int runScan(String reportDirectory, String[] outputFormats, String applicationName, String[] files, - String[] excludes, int symLinkDepth, float cvssFailScore) throws DatabaseException, + String[] excludes, int symLinkDepth, float cvssFailScore) throws DatabaseException, ExceptionCollection, ReportException { Engine engine = null; try { @@ -341,10 +346,10 @@ private int determineReturnCode(Engine engine, float cvssFailScore) { if (addName) { addName = false; ids.append(NEW_LINE).append(d.getFileName()).append(" (") - .append(Stream.concat(d.getSoftwareIdentifiers().stream(), d.getVulnerableSoftwareIdentifiers().stream()) - .map(Identifier::getValue) - .collect(Collectors.joining(", "))) - .append("): "); + .append(Stream.concat(d.getSoftwareIdentifiers().stream(), d.getVulnerableSoftwareIdentifiers().stream()) + .map(Identifier::getValue) + .collect(Collectors.joining(", "))) + .append("): "); ids.append(v.getName()).append('(').append(score).append(')'); } else { ids.append(", ").append(v.getName()).append('(').append(score).append(')'); @@ -450,6 +455,7 @@ private void runUpdateOnly() throws UpdateException, DatabaseException { } //CSOFF: MethodLength + /** * Updates the global Settings. * @@ -459,6 +465,12 @@ private void runUpdateOnly() throws UpdateException, DatabaseException { * file is unable to be loaded. */ protected void populateSettings(CliParser cli) throws InvalidSettingException { + String name = System.getenv("ODC_NAME") != null ? System.getenv("ODC_NAME") : "dependency-check-cli"; + if (name.isBlank()) { + name = "dependency-check-cli"; + } + name = name.replace("/", "-").replace(" ", "_"); + settings.setString(Settings.KEYS.APPLICATION_NAME, name); final File propertiesFile = cli.getFileArgument(CliParser.ARGUMENT.PROP); if (propertiesFile != null) { try { @@ -731,6 +743,7 @@ protected void populateSettings(CliParser cli) throws InvalidSettingException { } //CSON: MethodLength + /** * Creates a file appender and adds it to logback. * diff --git a/core/src/main/java/org/owasp/dependencycheck/agent/DependencyCheckScanAgent.java b/core/src/main/java/org/owasp/dependencycheck/agent/DependencyCheckScanAgent.java index 598c342f343..0269c6a6210 100644 --- a/core/src/main/java/org/owasp/dependencycheck/agent/DependencyCheckScanAgent.java +++ b/core/src/main/java/org/owasp/dependencycheck/agent/DependencyCheckScanAgent.java @@ -35,6 +35,7 @@ import org.owasp.dependencycheck.reporting.ReportGenerator; import org.owasp.dependencycheck.utils.Settings; import org.owasp.dependencycheck.utils.SeverityUtil; +import org.owasp.dependencycheck.utils.scarf.TelemetryCollector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -888,6 +889,8 @@ public void setPropertiesFilePath(String propertiesFilePath) { @SuppressWarnings("squid:S2095") private Engine executeDependencyCheck() throws ExceptionCollection { populateSettings(); + String version = settings.getString(Settings.KEYS.APPLICATION_VERSION, "Unknown"); + TelemetryCollector.send(settings, "dependency-check-scan-agent", version); final Engine engine; try { engine = new Engine(settings); diff --git a/core/src/main/java/org/owasp/dependencycheck/analyzer/NodePackageAnalyzer.java b/core/src/main/java/org/owasp/dependencycheck/analyzer/NodePackageAnalyzer.java index 650bcf9ce8c..56916566389 100644 --- a/core/src/main/java/org/owasp/dependencycheck/analyzer/NodePackageAnalyzer.java +++ b/core/src/main/java/org/owasp/dependencycheck/analyzer/NodePackageAnalyzer.java @@ -196,7 +196,7 @@ private boolean isNodeAuditEnabled(Engine engine) { try { ((AbstractNpmAnalyzer) a).prepareFileTypeAnalyzer(engine); } catch (InitializationException ex) { - String message = "Error initializing the " + a.getName(); + final String message = "Error initializing the " + a.getName(); LOGGER.debug(message, ex); } } diff --git a/core/src/main/java/org/owasp/dependencycheck/analyzer/OssIndexAnalyzer.java b/core/src/main/java/org/owasp/dependencycheck/analyzer/OssIndexAnalyzer.java index cae89d79467..b7ba5c3ad77 100644 --- a/core/src/main/java/org/owasp/dependencycheck/analyzer/OssIndexAnalyzer.java +++ b/core/src/main/java/org/owasp/dependencycheck/analyzer/OssIndexAnalyzer.java @@ -59,10 +59,10 @@ import java.net.SocketTimeoutException; import javax.annotation.Nullable; + import org.apache.commons.lang3.StringUtils; import org.owasp.dependencycheck.utils.CvssUtil; import org.sonatype.goodies.packageurl.InvalidException; -import org.sonatype.ossindex.service.client.transport.Transport.TransportException; /** * Enrich dependency information from Sonatype OSS index. @@ -131,13 +131,14 @@ protected void closeAnalyzer() throws Exception { @Override protected void prepareAnalyzer(Engine engine) throws InitializationException { - synchronized (FETCH_MUTIX) { - if (StringUtils.isEmpty(getSettings().getString(KEYS.ANALYZER_OSSINDEX_USER, StringUtils.EMPTY)) || - StringUtils.isEmpty(getSettings().getString(KEYS.ANALYZER_OSSINDEX_PASSWORD, StringUtils.EMPTY))) { - LOG.warn("Disabling OSS Index analyzer due to missing user/password credentials. Authentication is now required: https://ossindex.sonatype.org/doc/auth-required"); - setEnabled(false); + synchronized (FETCH_MUTIX) { + if (StringUtils.isEmpty(getSettings().getString(KEYS.ANALYZER_OSSINDEX_USER, StringUtils.EMPTY)) + || StringUtils.isEmpty(getSettings().getString(KEYS.ANALYZER_OSSINDEX_PASSWORD, StringUtils.EMPTY))) { + LOG.warn("Disabling OSS Index analyzer due to missing user/password credentials. Authentication is now " + + "required: https://ossindex.sonatype.org/doc/auth-required"); + setEnabled(false); + } } - } } @Override diff --git a/core/src/main/java/org/owasp/dependencycheck/dependency/VulnerableSoftware.java b/core/src/main/java/org/owasp/dependencycheck/dependency/VulnerableSoftware.java index dce7e828968..6b13ca830e0 100644 --- a/core/src/main/java/org/owasp/dependencycheck/dependency/VulnerableSoftware.java +++ b/core/src/main/java/org/owasp/dependencycheck/dependency/VulnerableSoftware.java @@ -528,6 +528,11 @@ public String toString() { return sb.toString(); } + /** + * Returns the NVD search URL for this vulnerable software. + * + * @return the NVD search URL + */ public String toNvdSearchUrl() { return CpeIdentifier.nvdSearchUrlFor(this); } diff --git a/maven/src/main/java/org/owasp/dependencycheck/maven/AggregateMojo.java b/maven/src/main/java/org/owasp/dependencycheck/maven/AggregateMojo.java index bc8bd135fcd..b9f4b4133b7 100644 --- a/maven/src/main/java/org/owasp/dependencycheck/maven/AggregateMojo.java +++ b/maven/src/main/java/org/owasp/dependencycheck/maven/AggregateMojo.java @@ -36,6 +36,7 @@ import org.codehaus.plexus.util.xml.Xpp3Dom; import org.owasp.dependencycheck.Engine; import org.owasp.dependencycheck.exception.ExceptionCollection; +import org.owasp.dependencycheck.utils.scarf.TelemetryCollector; /** * Maven Plugin that checks project dependencies and the dependencies of all @@ -69,6 +70,7 @@ public class AggregateMojo extends BaseDependencyCheckMojo { */ @Override protected ExceptionCollection scanDependencies(final Engine engine) throws MojoExecutionException { + TelemetryCollector.send(getSettings()); ExceptionCollection exCol = scanArtifacts(getProject(), engine, true); for (MavenProject childProject : getDescendants(this.getProject())) { //TODO consider the following as to whether a child should be skipped per #2152 diff --git a/maven/src/main/java/org/owasp/dependencycheck/maven/CheckMojo.java b/maven/src/main/java/org/owasp/dependencycheck/maven/CheckMojo.java index bda0ac408d2..a03c82e0e2a 100644 --- a/maven/src/main/java/org/owasp/dependencycheck/maven/CheckMojo.java +++ b/maven/src/main/java/org/owasp/dependencycheck/maven/CheckMojo.java @@ -27,6 +27,7 @@ import org.apache.maven.plugins.annotations.ResolutionScope; import org.owasp.dependencycheck.Engine; import org.owasp.dependencycheck.exception.ExceptionCollection; +import org.owasp.dependencycheck.utils.scarf.TelemetryCollector; /** * Maven Plugin that checks the project dependencies to see if they have any @@ -106,6 +107,7 @@ public String getDescription(Locale locale) { */ @Override protected ExceptionCollection scanDependencies(final Engine engine) throws MojoExecutionException { + TelemetryCollector.send(getSettings()); return scanArtifacts(getProject(), engine); } diff --git a/maven/src/main/resources/mojo.properties b/maven/src/main/resources/mojo.properties index 6a247b004a5..e2d35f90d27 100644 --- a/maven/src/main/resources/mojo.properties +++ b/maven/src/main/resources/mojo.properties @@ -5,3 +5,4 @@ #### data.directory=[JAR]/../../dependency-check-data/11.0 analyzer.central.enabled=false +odc.application.name=dependency-check-maven diff --git a/src/site/markdown/data/index.md b/src/site/markdown/data/index.md index bac58658c96..f832b8b3f15 100644 --- a/src/site/markdown/data/index.md +++ b/src/site/markdown/data/index.md @@ -72,3 +72,7 @@ OWASP dependency-check includes support to consult the [Sonatype OSS Index](http to enrich the report with supplemental vulnerability information. For more details on this integration see [Sonatype OSS Index](./ossindex.html). + +## Telemetry + +See the [telemetry documentation](../general/telemetry.html) for more information about telemetry data collection. diff --git a/src/site/markdown/general/telemetry.md b/src/site/markdown/general/telemetry.md new file mode 100644 index 00000000000..80761e20144 --- /dev/null +++ b/src/site/markdown/general/telemetry.md @@ -0,0 +1,5 @@ +# Telemetry + +## Scarf + +OWASP dependency-check uses [Scarf](https://about.scarf.sh/) to collect anonymous usage data to help us understand how the software is being used and how we can improve it. You can opt out of telemetry collection by setting the environment variable `SCARF_NO_ANALYTICS` or `DO_NOT_TRACK` to `true`. diff --git a/utils/src/main/java/org/owasp/dependencycheck/utils/scarf/TelemetryCollector.java b/utils/src/main/java/org/owasp/dependencycheck/utils/scarf/TelemetryCollector.java new file mode 100644 index 00000000000..f1a940577f6 --- /dev/null +++ b/utils/src/main/java/org/owasp/dependencycheck/utils/scarf/TelemetryCollector.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.owasp.dependencycheck.utils.scarf; + +import org.owasp.dependencycheck.utils.Downloader; +import org.owasp.dependencycheck.utils.Settings; + +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; + + +/** + * A utility class to collect and send telemetry data to scarf. + *

+ * Originally from https://github.com/apache/sedona/blob/4e4791d08ddafcf0b46c3d2c092f750eb5dcf2ef/common/src/main/java/org/apache/sedona/common/utils/TelemetryCollector.java#L26 + */ +public class TelemetryCollector { + + private static final String BASE_URL = "https://dependency-check.gateway.scarf.sh/scan/"; + private static final AtomicBoolean telemetrySubmitted = new AtomicBoolean(false); + + public static void send(Settings settings) { + try { + String tool = settings.getString(Settings.KEYS.APPLICATION_NAME, "dependency-check"); + String version = settings.getString(Settings.KEYS.APPLICATION_VERSION, "Unknown"); + send(settings, tool, version); + } catch (Exception e) { + // Silent catch block + } + } + public static void send(Settings settings, String tool, String version) { + if (!telemetrySubmitted.compareAndSet(false, true)) { + return; + } + // Check for user opt-out + if (System.getenv("SCARF_NO_ANALYTICS") != null + && System.getenv("SCARF_NO_ANALYTICS").equalsIgnoreCase("true") + || System.getenv("DO_NOT_TRACK") != null + && System.getenv("DO_NOT_TRACK").equalsIgnoreCase("true") + || System.getProperty("SCARF_NO_ANALYTICS") != null + && System.getProperty("SCARF_NO_ANALYTICS").equalsIgnoreCase("true") + || System.getProperty("DO_NOT_TRACK") != null + && System.getProperty("DO_NOT_TRACK").equalsIgnoreCase("true")) { + return; + } + try { + URL telemetryUrl = new URL(BASE_URL + + URLEncoder.encode(tool, StandardCharsets.UTF_8) + + "/" + + URLEncoder.encode(version, StandardCharsets.UTF_8)); + Thread telemetryThread = createThread(settings, telemetryUrl); + telemetryThread.start(); + } catch (Exception e) { + // Silent catch block + } + } + + private static Thread createThread(Settings settings, URL url) { + Thread telemetryThread = + new Thread("telemetry-thread") { + @Override + public void run() { + try { + Downloader downloader = Downloader.getInstance(); + downloader.configure(settings); + downloader.fetchContent(url, StandardCharsets.UTF_8); + } catch (Exception e) { + // Silent catch block + } + } + }; + telemetryThread.setDaemon(true); + return telemetryThread; + } +} \ No newline at end of file diff --git a/utils/src/test/java/org/owasp/dependencycheck/utils/scarf/TelemetryCollectorTest.java b/utils/src/test/java/org/owasp/dependencycheck/utils/scarf/TelemetryCollectorTest.java new file mode 100644 index 00000000000..30d8e72987f --- /dev/null +++ b/utils/src/test/java/org/owasp/dependencycheck/utils/scarf/TelemetryCollectorTest.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.owasp.dependencycheck.utils.scarf; + +import org.junit.jupiter.api.Test; +import org.owasp.dependencycheck.utils.BaseTest; +import org.owasp.dependencycheck.utils.Settings; + +class TelemetryCollectorTest extends BaseTest { + + @Test + void testSendTelemetry() throws InterruptedException { + String version = getSettings().getString(Settings.KEYS.APPLICATION_VERSION, "Unknown"); + TelemetryCollector.send(getSettings(), "build", version); + Thread.sleep(1000); + } +}