diff --git a/commons/pom.xml b/commons/pom.xml
index 066d0274f..590da24a6 100644
--- a/commons/pom.xml
+++ b/commons/pom.xml
@@ -51,6 +51,10 @@
io.quarkus
quarkus-micrometer-registry-prometheus
+
+ io.quarkus
+ quarkus-jaxb
+
io.github.mweirauch
diff --git a/commons/src/main/java/org/dependencytrack/commonutil/DateUtil.java b/commons/src/main/java/org/dependencytrack/commonutil/DateUtil.java
index 1b3c8a3ff..bf4bf2350 100644
--- a/commons/src/main/java/org/dependencytrack/commonutil/DateUtil.java
+++ b/commons/src/main/java/org/dependencytrack/commonutil/DateUtil.java
@@ -57,4 +57,11 @@ public static String toISO8601(final Date date) {
df.setTimeZone(tz);
return df.format(date);
}
+
+ public static Date fromISO8601(final String dateString) {
+ if (dateString == null) {
+ return null;
+ }
+ return jakarta.xml.bind.DatatypeConverter.parseDateTime(dateString).getTime();
+ }
}
diff --git a/repository-meta-analyzer/src/main/java/org/dependencytrack/repometaanalyzer/repositories/CargoMetaAnalyzer.java b/repository-meta-analyzer/src/main/java/org/dependencytrack/repometaanalyzer/repositories/CargoMetaAnalyzer.java
new file mode 100644
index 000000000..eb310ca2f
--- /dev/null
+++ b/repository-meta-analyzer/src/main/java/org/dependencytrack/repometaanalyzer/repositories/CargoMetaAnalyzer.java
@@ -0,0 +1,119 @@
+/*
+ * This file is part of Dependency-Track.
+ *
+ * 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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright (c) OWASP Foundation. All Rights Reserved.
+ */
+
+package org.dependencytrack.repometaanalyzer.repositories;
+
+import com.github.packageurl.PackageURL;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.util.EntityUtils;
+import org.dependencytrack.persistence.model.Component;
+import org.dependencytrack.persistence.model.RepositoryType;
+import org.dependencytrack.repometaanalyzer.model.MetaAnalyzerException;
+import org.dependencytrack.repometaanalyzer.model.MetaModel;
+import org.dependencytrack.commonutil.DateUtil;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * An IMetaAnalyzer implementation that supports Cargo via crates.io compatible repos
+ *
+ * @author Steve Springett
+ * @since 4.1.0
+ */
+
+public class CargoMetaAnalyzer extends AbstractMetaAnalyzer {
+ private static final Logger LOGGER = LoggerFactory.getLogger(CargoMetaAnalyzer.class);
+ private static final String DEFAULT_BASE_URL = "https://crates.io";
+ private static final String API_URL = "/api/v1/crates/%s";
+
+ CargoMetaAnalyzer() {
+ this.baseUrl = DEFAULT_BASE_URL;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public RepositoryType supportedRepositoryType() {
+ return RepositoryType.CARGO;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean isApplicable(final Component component) {
+ return component.getPurl() != null && PackageURL.StandardTypes.CARGO.equals(component.getPurl().getType());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public MetaModel analyze(final Component component) {
+ final MetaModel meta = new MetaModel(component);
+ if (component.getPurl() != null) {
+ final String url = String.format(baseUrl + API_URL, component.getPurl().getName());
+ try (final CloseableHttpResponse response = processHttpRequest(url)) {
+ if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ final HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ String responseString = EntityUtils.toString(entity);
+ var jsonObject = new JSONObject(responseString);
+ final JSONObject crate = jsonObject.optJSONObject("crate");
+ if (crate != null) {
+ final String latest = crate.getString("newest_version");
+ meta.setLatestVersion(latest);
+ }
+ final JSONArray versions = jsonObject.optJSONArray("versions");
+ if (versions != null) {
+ for (int i = 0; i < versions.length(); i++) {
+ final JSONObject version = versions.getJSONObject(i);
+ final String versionString = version.optString("num");
+ if (meta.getLatestVersion() != null && meta.getLatestVersion().equals(versionString)) {
+ final String publishedTimestamp = version.optString("created_at");
+ try {
+ meta.setPublishedTimestamp(DateUtil.fromISO8601(publishedTimestamp));
+ } catch (IllegalArgumentException e) {
+ LOGGER.warn("An error occurred while parsing published time", e);
+ }
+ }
+ }
+ }
+ }
+ } else {
+ handleUnexpectedHttpResponse(LOGGER, url, response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(), component);
+ }
+ } catch (IOException ex) {
+ handleRequestException(LOGGER, ex);
+ } catch (Exception ex) {
+ throw new MetaAnalyzerException(ex);
+ }
+ }
+ return meta;
+ }
+
+ @Override
+ public String getName() {
+ return this.getClass().getSimpleName();
+ }
+}
diff --git a/repository-meta-analyzer/src/main/java/org/dependencytrack/repometaanalyzer/repositories/RepositoryAnalyzerFactory.java b/repository-meta-analyzer/src/main/java/org/dependencytrack/repometaanalyzer/repositories/RepositoryAnalyzerFactory.java
index 0a4508da1..b9b428cfa 100644
--- a/repository-meta-analyzer/src/main/java/org/dependencytrack/repometaanalyzer/repositories/RepositoryAnalyzerFactory.java
+++ b/repository-meta-analyzer/src/main/java/org/dependencytrack/repometaanalyzer/repositories/RepositoryAnalyzerFactory.java
@@ -39,6 +39,7 @@ public class RepositoryAnalyzerFactory {
PackageURL.StandardTypes.NPM, NpmMetaAnalyzer::new,
PackageURL.StandardTypes.NUGET, NugetMetaAnalyzer::new,
PackageURL.StandardTypes.PYPI, PypiMetaAnalyzer::new,
+ PackageURL.StandardTypes.CARGO, CargoMetaAnalyzer::new,
"cpan", CpanMetaAnalyzer::new
);
diff --git a/repository-meta-analyzer/src/test/java/org/dependencytrack/repometaanalyzer/repositories/CargoMetaAnalyzerTest.java b/repository-meta-analyzer/src/test/java/org/dependencytrack/repometaanalyzer/repositories/CargoMetaAnalyzerTest.java
new file mode 100644
index 000000000..c22f582be
--- /dev/null
+++ b/repository-meta-analyzer/src/test/java/org/dependencytrack/repometaanalyzer/repositories/CargoMetaAnalyzerTest.java
@@ -0,0 +1,42 @@
+/*
+ * This file is part of Dependency-Track.
+ *
+ * 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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright (c) OWASP Foundation. All Rights Reserved.
+ */
+
+package org.dependencytrack.repometaanalyzer.repositories;
+
+import com.github.packageurl.PackageURL;
+import org.dependencytrack.persistence.model.Component;
+import org.dependencytrack.persistence.model.RepositoryType;
+import org.dependencytrack.repometaanalyzer.model.MetaModel;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class CargoMetaAnalyzerTest {
+ @Test
+ public void testAnalyzer() throws Exception {
+ Component component = new Component();
+ component.setPurl(new PackageURL("pkg:cargo/rand@0.7.2"));
+
+ CargoMetaAnalyzer analyzer = new CargoMetaAnalyzer();
+ Assert.assertTrue(analyzer.isApplicable(component));
+ Assert.assertEquals(RepositoryType.CARGO, analyzer.supportedRepositoryType());
+ MetaModel metaModel = analyzer.analyze(component);
+ Assert.assertNotNull(metaModel.getLatestVersion());
+ Assert.assertNotNull(metaModel.getPublishedTimestamp());
+ }
+}
diff --git a/repository-meta-analyzer/src/test/java/org/dependencytrack/repometaanalyzer/repositories/RepositoryAnalyzerFactoryTest.java b/repository-meta-analyzer/src/test/java/org/dependencytrack/repometaanalyzer/repositories/RepositoryAnalyzerFactoryTest.java
index 9e8e68220..61fb5a2f4 100644
--- a/repository-meta-analyzer/src/test/java/org/dependencytrack/repometaanalyzer/repositories/RepositoryAnalyzerFactoryTest.java
+++ b/repository-meta-analyzer/src/test/java/org/dependencytrack/repometaanalyzer/repositories/RepositoryAnalyzerFactoryTest.java
@@ -38,7 +38,7 @@ class RepositoryAnalyzerFactoryTest {
@ParameterizedTest
@CsvSource(value = {
"pkg:foo/bar, false",
- "pkg:cargo/foo, false",
+ "pkg:cargo/foo, true",
"pkg:cocoapods/foo, false",
"pkg:composer/foo, true",
"pkg:deb/foo, false",
@@ -63,6 +63,7 @@ void testCreateAnalyzerWithUnsupportedPurl() throws MalformedPackageURLException
@ParameterizedTest
@ValueSource(strings = {
+ "pkg:cargo/foo",
"pkg:composer/foo/bar",
"pkg:gem/foo/bar",
"pkg:golang/foo/bar",