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",