From 50049b40b91befdb76b3ee37a16d088e0173fbfb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 08:09:20 +0000 Subject: [PATCH 001/412] Bump debian from `435ba09` to `d10f054` in /src/main/docker Bumps debian from `435ba09` to `d10f054`. --- updated-dependencies: - dependency-name: debian dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- src/main/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docker/Dockerfile b/src/main/docker/Dockerfile index 948eccb9e9..208895f76b 100644 --- a/src/main/docker/Dockerfile +++ b/src/main/docker/Dockerfile @@ -1,6 +1,6 @@ FROM eclipse-temurin:21.0.2_13-jre-jammy@sha256:d9f7b8326b9d396d070432982a998015a04ffb8885b145e97777a5ae324a8df1 AS jre-build -FROM debian:stable-slim@sha256:435ba09b2e259426c0552a041eef609b01d4655d9c8467d75be390801068baf3 +FROM debian:stable-slim@sha256:d10f0545d14bad5f4d230301f7e4fd904384f2dd16fda16d708f936c2fa1db3e # Arguments that can be passed at build time # Directory names must end with / to avoid errors when ADDing and COPYing From 551e632ff4aeec91a479999ebb00ae8499877fe7 Mon Sep 17 00:00:00 2001 From: Magnus Viernickel Date: Tue, 12 Mar 2024 18:41:54 +0100 Subject: [PATCH 002/412] [feat] add the hackage meta analyzer Signed-off-by: Magnus Viernickel --- .gitignore | 6 +- .../dependencytrack/model/RepositoryType.java | 5 ++ .../repositories/HackageMetaAnalyzer.java | 74 +++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java diff --git a/.gitignore b/.gitignore index 6d373cb939..c121e5a8a1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,8 @@ target/ # IntelliJ .idea/* !.idea/icon.svg -!.idea/runConfigurations/ \ No newline at end of file +!.idea/runConfigurations/ + +# nix +.direnv +.pre-commit-config.yaml diff --git a/src/main/java/org/dependencytrack/model/RepositoryType.java b/src/main/java/org/dependencytrack/model/RepositoryType.java index 978f61cd18..a52ad4a1eb 100644 --- a/src/main/java/org/dependencytrack/model/RepositoryType.java +++ b/src/main/java/org/dependencytrack/model/RepositoryType.java @@ -39,10 +39,13 @@ public enum RepositoryType { CARGO, GO_MODULES, GITHUB, + HACKAGE, + NIXPKGS, UNSUPPORTED; /** * Returns a RepositoryType for the specified PackageURL. + * * @param packageURL a package URL * @return a RepositoryType */ @@ -70,6 +73,8 @@ public static RepositoryType resolve(PackageURL packageURL) { return GO_MODULES; } else if (PackageURL.StandardTypes.GITHUB.equals(type)) { return GITHUB; + } else if ("hackage".equals(type)) { + return HACKAGE; } return UNSUPPORTED; } diff --git a/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java new file mode 100644 index 0000000000..bf980740d0 --- /dev/null +++ b/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java @@ -0,0 +1,74 @@ +package org.dependencytrack.tasks.repositories; + +import alpine.common.logging.Logger; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.util.EntityUtils; +import org.dependencytrack.exception.MetaAnalyzerException; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.RepositoryType; +import org.json.JSONObject; + +import java.io.IOException; + +public class HackageMetaAnalyzer extends AbstractMetaAnalyzer { + private static final Logger LOGGER = Logger.getLogger(HackageMetaAnalyzer.class); + private static final String DEFAULT_BASE_URL = "https://hackage.haskell.org/"; + + HackageMetaAnalyzer() { + this.baseUrl = DEFAULT_BASE_URL; + } + + /** + * {@inheritDoc} + */ + public RepositoryType supportedRepositoryType() { + return RepositoryType.HACKAGE; + } + + /** + * {@inheritDoc} + */ + public boolean isApplicable(Component component) { + // FUTUREWORK(mangoiv): add nixpkgs and hackage to https://github.com/package-url/packageurl-java/blob/master/src/main/java/com/github/packageurl/PackageURL.java + var purl = component.getPurl(); + return purl != null && "hackage".equals(purl.getType()); + } + + /** + * {@inheritDoc} + */ + public MetaModel analyze(final Component component) { + final var meta = new MetaModel(component); + var purl = component.getPurl(); + if (purl != null) { + final var url = baseUrl + "/package/" + purl.getName() + "/preferred"; + try (final CloseableHttpResponse response = processHttpRequest(url)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + final var entity = response.getEntity(); + if (entity != null) { + String responseString = EntityUtils.toString(entity); + final var deserialized = new JSONObject(responseString); + final var preferred = deserialized.getJSONArray("normal-version"); + // the latest version is the first in the list + if (preferred != null) { + final var latest = preferred.getString(0); + meta.setLatestVersion(latest); + } + // the hackage API doesn't expose the "published_at" information + // we could use https://flora.pm/experimental/packages/{namespace}/{packageName} + // but it appears this isn't reliable yet + } + } else { + var statusLine = response.getStatusLine(); + handleUnexpectedHttpResponse(LOGGER, url, statusLine.getStatusCode(), statusLine.getReasonPhrase(), component); + } + } catch (IOException ex) { + handleRequestException(LOGGER, ex); + } catch (Exception ex) { + throw new MetaAnalyzerException(ex); + } + } + return meta; + } +} From ba13d15c57a235779e99a11a1ea7f5953bcdd5a8 Mon Sep 17 00:00:00 2001 From: Magnus Viernickel Date: Tue, 12 Mar 2024 18:54:39 +0100 Subject: [PATCH 003/412] [chore] add hackage meta analyzer test Signed-off-by: Magnus Viernickel --- .../repositories/HackageMetaAnalyzerTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/test/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzerTest.java diff --git a/src/test/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzerTest.java b/src/test/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzerTest.java new file mode 100644 index 0000000000..0fb68b0c3c --- /dev/null +++ b/src/test/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzerTest.java @@ -0,0 +1,21 @@ +package org.dependencytrack.tasks.repositories; + +import com.github.packageurl.PackageURL; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.RepositoryType; +import org.junit.Assert; +import org.junit.Test; + +public class HackageMetaAnalyzerTest { + @Test + public void testAnalyzer() throws Exception { + Component component = new Component(); + component.setPurl(new PackageURL("pkg:hackage/singletons-th@3.1")); + + HackageMetaAnalyzer analyzer = new HackageMetaAnalyzer(); + Assert.assertTrue(analyzer.isApplicable(component)); + Assert.assertEquals(RepositoryType.HACKAGE, analyzer.supportedRepositoryType()); + MetaModel metaModel = analyzer.analyze(component); + Assert.assertNotNull(metaModel.getLatestVersion()); + } +} From 6f071856027b81c6093743470b5dd391c0635d82 Mon Sep 17 00:00:00 2001 From: Magnus Viernickel Date: Tue, 12 Mar 2024 19:47:39 +0100 Subject: [PATCH 004/412] [fix] actually run the meta analyzer when applicable Signed-off-by: Magnus Viernickel --- src/main/java/org/dependencytrack/model/RepositoryType.java | 1 - .../tasks/repositories/HackageMetaAnalyzer.java | 6 +++--- .../dependencytrack/tasks/repositories/IMetaAnalyzer.java | 5 +++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/RepositoryType.java b/src/main/java/org/dependencytrack/model/RepositoryType.java index a52ad4a1eb..0f32dcd250 100644 --- a/src/main/java/org/dependencytrack/model/RepositoryType.java +++ b/src/main/java/org/dependencytrack/model/RepositoryType.java @@ -40,7 +40,6 @@ public enum RepositoryType { GO_MODULES, GITHUB, HACKAGE, - NIXPKGS, UNSUPPORTED; /** diff --git a/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java index bf980740d0..9032da2fcc 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java @@ -30,8 +30,8 @@ public RepositoryType supportedRepositoryType() { * {@inheritDoc} */ public boolean isApplicable(Component component) { - // FUTUREWORK(mangoiv): add nixpkgs and hackage to https://github.com/package-url/packageurl-java/blob/master/src/main/java/com/github/packageurl/PackageURL.java - var purl = component.getPurl(); + // FUTUREWORK(mangoiv): add hackage to https://github.com/package-url/packageurl-java/blob/master/src/main/java/com/github/packageurl/PackageURL.java + final var purl = component.getPurl(); return purl != null && "hackage".equals(purl.getType()); } @@ -40,7 +40,7 @@ public boolean isApplicable(Component component) { */ public MetaModel analyze(final Component component) { final var meta = new MetaModel(component); - var purl = component.getPurl(); + final var purl = component.getPurl(); if (purl != null) { final var url = baseUrl + "/package/" + purl.getName() + "/preferred"; try (final CloseableHttpResponse response = processHttpRequest(url)) { diff --git a/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java index b8e057e175..97570456df 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java @@ -135,6 +135,11 @@ static IMetaAnalyzer build(Component component) { if (analyzer.isApplicable(component)) { return analyzer; } + } else if ("hackage".equals(component.getPurl().getType())) { + IMetaAnalyzer analyzer = new HackageMetaAnalyzer(); + if (analyzer.isApplicable(component)) { + return analyzer; + } } } From 1bf8993e45369bad79246194a40ebec47302196a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 08:58:48 +0000 Subject: [PATCH 005/412] Bump com.google.cloud.sql:cloud-sql-connector-jdbc-sqlserver Bumps com.google.cloud.sql:cloud-sql-connector-jdbc-sqlserver from 1.16.0 to 1.17.0. --- updated-dependencies: - dependency-name: com.google.cloud.sql:cloud-sql-connector-jdbc-sqlserver dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 538365180b..bd5133effb 100644 --- a/pom.xml +++ b/pom.xml @@ -85,7 +85,7 @@ ${project.parent.version} 4.2.0 10.12.5 - 1.16.0 + 1.17.0 1.16.0 1.16.0 2.1.0 From 5b5168173d43b29454960c26f97fcfa0ddd2dc8d Mon Sep 17 00:00:00 2001 From: Magnus Viernickel Date: Wed, 13 Mar 2024 18:08:08 +0100 Subject: [PATCH 006/412] [wip] start implementing Nixpkgs Meta Analyzer Signed-off-by: Magnus Viernickel --- .../dependencytrack/model/RepositoryType.java | 3 + .../tasks/repositories/IMetaAnalyzer.java | 5 + .../repositories/NixpkgsMetaAnalyzer.java | 118 ++++++++++++++++++ .../repositories/NixpkgsMetaAnalyzerTest.java | 23 ++++ 4 files changed, 149 insertions(+) create mode 100644 src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java create mode 100644 src/test/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzerTest.java diff --git a/src/main/java/org/dependencytrack/model/RepositoryType.java b/src/main/java/org/dependencytrack/model/RepositoryType.java index 0f32dcd250..8c7002b334 100644 --- a/src/main/java/org/dependencytrack/model/RepositoryType.java +++ b/src/main/java/org/dependencytrack/model/RepositoryType.java @@ -40,6 +40,7 @@ public enum RepositoryType { GO_MODULES, GITHUB, HACKAGE, + NIXPKGS, UNSUPPORTED; /** @@ -74,6 +75,8 @@ public static RepositoryType resolve(PackageURL packageURL) { return GITHUB; } else if ("hackage".equals(type)) { return HACKAGE; + } else if ("nixpkgs".equals(type)) { + return NIXPKGS; } return UNSUPPORTED; } diff --git a/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java index 97570456df..b165c5d4bb 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java @@ -140,6 +140,11 @@ static IMetaAnalyzer build(Component component) { if (analyzer.isApplicable(component)) { return analyzer; } + } else if ("nixpkgs".equals(component.getPurl().getType())) { + IMetaAnalyzer analyzer = NixpkgsMetaAnalyzer.getNixpkgsMetaAnalyzer(); + if (analyzer.isApplicable(component)) { + return analyzer; + } } } diff --git a/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java new file mode 100644 index 0000000000..8a162a5423 --- /dev/null +++ b/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java @@ -0,0 +1,118 @@ +package org.dependencytrack.tasks.repositories; + +import alpine.common.logging.Logger; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.entity.BrotliInputStreamFactory; +import org.apache.hc.client5.http.entity.DecompressingEntity; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.http.client.utils.URIBuilder; +import org.dependencytrack.exception.MetaAnalyzerException; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.RepositoryType; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.HashMap; + +public class NixpkgsMetaAnalyzer extends AbstractMetaAnalyzer { + private static final Logger LOGGER = Logger.getLogger(NixpkgsMetaAnalyzer.class); + private static final String DEFAULT_CHANNEL_URL = "https://channels.nixos.org/nixpkgs-unstable/packages.json.br"; + private static NixpkgsMetaAnalyzer nixpkgsMetaAnalyzer = new NixpkgsMetaAnalyzer(); + // this doesn't really make sense wrt the "AbstractMetaAnalyzer" + // because this statically known url will just redirect us to + // the actual URL + private final HashMap latestVersion; + + private NixpkgsMetaAnalyzer() { + this.baseUrl = DEFAULT_CHANNEL_URL; + HashMap newLatestVersion = new HashMap<>(); + + try (final CloseableHttpResponse packagesResponse = processHttpRequest5()) { + if (packagesResponse != null && packagesResponse.getCode() == HttpStatus.SC_OK) { + final var entity = packagesResponse.getEntity(); + if (entity != null) { + // TODO(mangoiv): is this the fastest way we can do this? + final var entityString = EntityUtils.toString(new DecompressingEntity(entity, new BrotliInputStreamFactory())); + final var packages = new JSONObject(entityString).getJSONObject("packages").toMap().values(); + packages.forEach(pkg -> { + // FUTUREWORK(mangoiv): there are potentially packages with the same pname + if (pkg instanceof JSONObject jsonPkg) { + final var pname = jsonPkg.getString("pname"); + final var version = jsonPkg.getString("version"); + newLatestVersion.putIfAbsent(pname, version); + } + }); + } + + } + } catch (IOException ex) { + handleRequestException(LOGGER, ex); + } catch (Exception ex) { + throw new MetaAnalyzerException(ex); + } + this.latestVersion = newLatestVersion; + LOGGER.info("finished updating the nixpkgs meta analyzer"); + } + + public static NixpkgsMetaAnalyzer getNixpkgsMetaAnalyzer() { + return nixpkgsMetaAnalyzer; + } + + private CloseableHttpResponse processHttpRequest5() throws IOException { + try { + URIBuilder uriBuilder = new URIBuilder(baseUrl); + final HttpGet request = new HttpGet(uriBuilder.build().toString()); + request.addHeader("accept", "application/json"); + try (final CloseableHttpClient client = HttpClients.createDefault()) { + return client.execute(request); + } + } catch (URISyntaxException ex) { + handleRequestException(LOGGER, ex); + return null; + } + } + + /** + * updates the NixpkgsMetaAnalyzer asynchronously by fetching a new version + * of the standard channel + */ + public void updateNixpkgsMetaAnalyzer() { + new Thread(() -> nixpkgsMetaAnalyzer = new NixpkgsMetaAnalyzer()).start(); + } + + /** + * {@inheritDoc} + */ + public RepositoryType supportedRepositoryType() { + return RepositoryType.NIXPKGS; + } + + /** + * {@inheritDoc} + */ + public boolean isApplicable(Component component) { + // FUTUREWORK(mangoiv): add nixpkgs to https://github.com/package-url/packageurl-java/blob/master/src/main/java/com/github/packageurl/PackageURL.java + final var purl = component.getPurl(); + return purl != null && "nixpkgs".equals(purl.getType()); + } + + /** + * {@inheritDoc} + */ + public MetaModel analyze(Component component) { + final var meta = new MetaModel(component); + final var purl = component.getPurl(); + if (purl != null) { + final var newerVersion = latestVersion.get(purl.getName()); + if (newerVersion != null) { + meta.setLatestVersion(newerVersion); + } + } + return meta; + } +} diff --git a/src/test/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzerTest.java b/src/test/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzerTest.java new file mode 100644 index 0000000000..a33bde76a5 --- /dev/null +++ b/src/test/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzerTest.java @@ -0,0 +1,23 @@ +package org.dependencytrack.tasks.repositories; + +import com.github.packageurl.PackageURL; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.RepositoryType; +import org.junit.Assert; +import org.junit.Test; + +public class NixpkgsMetaAnalyzerTest { + @Test + public void testAnalyzer() throws Exception { + final var component1 = new Component(); + final var component2 = new Component(); + component1.setPurl(new PackageURL("pkg:nixpkgs/SDL_sound@1.0.3")); + component2.setPurl(new PackageURL("pkg:nixpkgs/amarok@2.9.71")); + final var analyzer = NixpkgsMetaAnalyzer.getNixpkgsMetaAnalyzer(); + Assert.assertTrue(analyzer.isApplicable(component1)); + Assert.assertTrue(analyzer.isApplicable(component2)); + Assert.assertEquals(RepositoryType.NIXPKGS, analyzer.supportedRepositoryType()); + Assert.assertNotNull(analyzer.analyze(component1).getLatestVersion()); + Assert.assertNotNull(analyzer.analyze(component2).getLatestVersion()); + } +} From c5096b25253eb117d508b5ea1a3a451efb839aed Mon Sep 17 00:00:00 2001 From: Magnus Viernickel Date: Thu, 14 Mar 2024 13:08:19 +0100 Subject: [PATCH 007/412] [feat] remove brotli stuff; for some reason http client 5 does something different than the older http client Signed-off-by: Magnus Viernickel --- .../repositories/NixpkgsMetaAnalyzer.java | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java index 8a162a5423..64e8ba2b98 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java @@ -2,8 +2,6 @@ import alpine.common.logging.Logger; import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.entity.BrotliInputStreamFactory; -import org.apache.hc.client5.http.entity.DecompressingEntity; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; @@ -32,27 +30,31 @@ private NixpkgsMetaAnalyzer() { this.baseUrl = DEFAULT_CHANNEL_URL; HashMap newLatestVersion = new HashMap<>(); - try (final CloseableHttpResponse packagesResponse = processHttpRequest5()) { - if (packagesResponse != null && packagesResponse.getCode() == HttpStatus.SC_OK) { - final var entity = packagesResponse.getEntity(); - if (entity != null) { - // TODO(mangoiv): is this the fastest way we can do this? - final var entityString = EntityUtils.toString(new DecompressingEntity(entity, new BrotliInputStreamFactory())); - final var packages = new JSONObject(entityString).getJSONObject("packages").toMap().values(); - packages.forEach(pkg -> { - // FUTUREWORK(mangoiv): there are potentially packages with the same pname - if (pkg instanceof JSONObject jsonPkg) { - final var pname = jsonPkg.getString("pname"); - final var version = jsonPkg.getString("version"); - newLatestVersion.putIfAbsent(pname, version); - } - }); - } + try (final CloseableHttpClient client = HttpClients.createDefault()) { + try (final CloseableHttpResponse packagesResponse = processHttpRequest5(client)) { + if (packagesResponse != null && packagesResponse.getCode() == HttpStatus.SC_OK) { + final var entity = packagesResponse.getEntity(); + if (entity != null) { + // TODO(mangoiv): is this the fastest way we can do this? + final var entityString = EntityUtils.toString(entity); + final var packages = new JSONObject(entityString).getJSONObject("packages").toMap().values(); + packages.forEach(pkg -> { + // FUTUREWORK(mangoiv): there are potentially packages with the same pname + if (pkg instanceof HashMap jsonPkg) { + final var pname = jsonPkg.get("pname"); + final var version = jsonPkg.get("version"); + newLatestVersion.putIfAbsent((String)pname, (String)version); + } + }); + } + } } } catch (IOException ex) { + LOGGER.debug(ex.toString()); handleRequestException(LOGGER, ex); } catch (Exception ex) { + LOGGER.debug(ex.toString()); throw new MetaAnalyzerException(ex); } this.latestVersion = newLatestVersion; @@ -63,14 +65,14 @@ public static NixpkgsMetaAnalyzer getNixpkgsMetaAnalyzer() { return nixpkgsMetaAnalyzer; } - private CloseableHttpResponse processHttpRequest5() throws IOException { + private CloseableHttpResponse processHttpRequest5(CloseableHttpClient client) throws IOException { try { URIBuilder uriBuilder = new URIBuilder(baseUrl); final HttpGet request = new HttpGet(uriBuilder.build().toString()); request.addHeader("accept", "application/json"); - try (final CloseableHttpClient client = HttpClients.createDefault()) { - return client.execute(request); - } + + return client.execute(request); + } catch (URISyntaxException ex) { handleRequestException(LOGGER, ex); return null; From 1e008571697f7ae93bb83e3c5f19995384266d1f Mon Sep 17 00:00:00 2001 From: Magnus Viernickel Date: Thu, 14 Mar 2024 14:25:22 +0100 Subject: [PATCH 008/412] [chore] add header lines Signed-off-by: Magnus Viernickel --- .../repositories/HackageMetaAnalyzer.java | 18 +++++++++++++++++ .../repositories/NixpkgsMetaAnalyzer.java | 20 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java index 9032da2fcc..b6b74cce45 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java @@ -1,3 +1,21 @@ +/* + * 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) Steve Springett. All Rights Reserved. + */ package org.dependencytrack.tasks.repositories; import alpine.common.logging.Logger; diff --git a/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java index 64e8ba2b98..c884aea58e 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java @@ -1,3 +1,21 @@ +/* + * 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) Steve Springett. All Rights Reserved. + */ package org.dependencytrack.tasks.repositories; import alpine.common.logging.Logger; @@ -43,7 +61,7 @@ private NixpkgsMetaAnalyzer() { if (pkg instanceof HashMap jsonPkg) { final var pname = jsonPkg.get("pname"); final var version = jsonPkg.get("version"); - newLatestVersion.putIfAbsent((String)pname, (String)version); + newLatestVersion.putIfAbsent((String) pname, (String) version); } }); } From b89f8c6ea97e3f87db191c6f01943a63d86eb93a Mon Sep 17 00:00:00 2001 From: Aravind Parappil Date: Sat, 16 Mar 2024 00:53:35 -0400 Subject: [PATCH 009/412] Perform License Resolution On Name Field During SBOM Import On import of SBOMs with SPDX compliant license in the name field and with no id field, license resolution should first occur against standard licenses. If none found, only then should license resolution occur against custom licenses Signed-off-by: Aravind Parappil --- .../parser/cyclonedx/util/ModelConverter.java | 7 +++- .../tasks/BomUploadProcessingTaskV2.java | 8 ++++ .../tasks/BomUploadProcessingTaskTest.java | 42 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java index 441a72de58..7f5ad510b1 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -526,9 +526,14 @@ public static Component convert(final QueryManager qm, final org.cyclonedx.model } else if (StringUtils.isNotBlank(cycloneLicense.getName())) { - final License license = qm.getCustomLicense(StringUtils.trimToNull(cycloneLicense.getName())); + final License license = qm.getLicense(StringUtils.trimToNull(cycloneLicense.getName())); if (license != null) { component.setResolvedLicense(license); + } else { + final License customLicense = qm.getCustomLicense(StringUtils.trimToNull(cycloneLicense.getName())); + if (customLicense != null) { + component.setResolvedLicense(customLicense); + } } } component.setLicense(StringUtils.trimToNull(cycloneLicense.getName())); diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java index 91bcdb16d1..eff1327548 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java @@ -672,6 +672,14 @@ private static void resolveAndApplyLicense(final QueryManager qm, } if (isNotBlank(licenseCandidate.getName())) { + final License resolvedLicense = licenseCache.computeIfAbsent(licenseCandidate.getName(), + licenseName -> resolveLicense(qm, licenseName)); + if (resolvedLicense != License.UNRESOLVED) { + component.setResolvedLicense(resolvedLicense); + component.setLicenseUrl(trimToNull(licenseCandidate.getUrl())); + break; + } + final License resolvedCustomLicense = customLicenseCache.computeIfAbsent(licenseCandidate.getName(), licenseName -> resolveCustomLicense(qm, licenseName)); if (resolvedCustomLicense != License.UNRESOLVED) { diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index 15820854e0..9161faa8a3 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -676,6 +676,48 @@ public void informWithBomContainingInvalidLicenseExpressionTest() { }); } + @Test // https://github.com/DependencyTrack/dependency-track/issues/3433 + public void informIssue3433Test() { + final var license = new License(); + license.setLicenseId("GPL-3.0-or-later"); + license.setName("GPL-3.0-or-later"); + qm.persist(license); + + final var project = new Project(); + project.setName("acme-license-app"); + qm.persist(project); + + final byte[] bomBytes = """ + { + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b80", + "version": 1, + "components": [ + { + "type": "library", + "name": "acme-lib-x", + "licenses": [ + { + "license": { + "name": "GPL-3.0-or-later" + } + } + ] + } + ] + } + """.getBytes(StandardCharsets.UTF_8); + + bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes)); + awaitBomProcessedNotification(); + + assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { + assertThat(component.getResolvedLicense()).isNotNull(); + assertThat(component.getResolvedLicense().getLicenseId()).isEqualTo("GPL-3.0-or-later"); + }); + } + @Test public void informWithBomContainingServiceTest() throws Exception { final Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); From e55e95011a4e26a0ccb485b91a70539bcde9ebd2 Mon Sep 17 00:00:00 2001 From: Aravind Parappil Date: Sat, 16 Mar 2024 20:58:36 -0400 Subject: [PATCH 010/412] Update License Of Existing Components On BOM Upload When an SBOM is re-uploaded with different license information on an already imported component, the component should be updated with the new license. Similarly, if license information previously existed on a component but not on a subsequent SBOM upload, the resolved license should be deleted from the component Signed-off-by: Aravind Parappil --- .../parser/cyclonedx/util/ModelConverter.java | 46 ++--- .../tasks/BomUploadProcessingTaskTest.java | 162 ++++++++++++++++++ 2 files changed, 186 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java index 7f5ad510b1..0b3d40f40a 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -513,34 +513,36 @@ public static Component convert(final QueryManager qm, final org.cyclonedx.model licenseOptions.addAll(licenseChoice.getLicenses()); } - // try to find a license in the database among the license options, but only if none has been - // selected previously. - if (component.getResolvedLicense() == null) { - for (final org.cyclonedx.model.License cycloneLicense : licenseOptions) { - if (cycloneLicense != null) { - if (StringUtils.isNotBlank(cycloneLicense.getId())) { - final License license = qm.getLicense(StringUtils.trimToNull(cycloneLicense.getId())); - if (license != null) { - component.setResolvedLicense(license); - } + // try to find a license in the database among the license options + for (final org.cyclonedx.model.License cycloneLicense : licenseOptions) { + if (cycloneLicense != null) { + if (StringUtils.isNotBlank(cycloneLicense.getId())) { + final License license = qm.getLicense(StringUtils.trimToNull(cycloneLicense.getId())); + if (license != null) { + component.setResolvedLicense(license); } - else if (StringUtils.isNotBlank(cycloneLicense.getName())) - { - final License license = qm.getLicense(StringUtils.trimToNull(cycloneLicense.getName())); - if (license != null) { - component.setResolvedLicense(license); - } else { - final License customLicense = qm.getCustomLicense(StringUtils.trimToNull(cycloneLicense.getName())); - if (customLicense != null) { - component.setResolvedLicense(customLicense); - } + } + else if (StringUtils.isNotBlank(cycloneLicense.getName())) + { + final License license = qm.getLicense(StringUtils.trimToNull(cycloneLicense.getName())); + if (license != null) { + component.setResolvedLicense(license); + } else { + final License customLicense = qm.getCustomLicense(StringUtils.trimToNull(cycloneLicense.getName())); + if (customLicense != null) { + component.setResolvedLicense(customLicense); } } - component.setLicense(StringUtils.trimToNull(cycloneLicense.getName())); - component.setLicenseUrl(StringUtils.trimToNull(cycloneLicense.getUrl())); } + component.setLicense(StringUtils.trimToNull(cycloneLicense.getName())); + component.setLicenseUrl(StringUtils.trimToNull(cycloneLicense.getUrl())); } } + } else { + component.setLicense(null); + component.setLicenseUrl(null); + component.setLicenseExpression(null); + component.setResolvedLicense(null); } if (cycloneDxComponent.getExternalReferences() != null && !cycloneDxComponent.getExternalReferences().isEmpty()) { diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index 9161faa8a3..7c39f7c26d 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -718,6 +718,168 @@ public void informIssue3433Test() { }); } + @Test // https://github.com/DependencyTrack/dependency-track/issues/3498 + public void informUpdateExistingLicenseTest() { + final var existingLicense = new License(); + existingLicense.setLicenseId("GPL-3.0-or-later"); + existingLicense.setName("GPL-3.0-or-later"); + qm.persist(existingLicense); + + final var updatedLicense = new License(); + updatedLicense.setLicenseId("Apache-2.0"); + updatedLicense.setName("Apache-2.0"); + qm.persist(updatedLicense); + + final var project = new Project(); + project.setName("acme-update-license-app"); + qm.persist(project); + + final byte[] existingBomBytes = """ + { + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "components": [ + { + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "acme-lib-y", + "version": "2.0.0", + "licenses": [ + { + "license": { + "name": "GPL-3.0-or-later" + } + } + ] + } + ] + } + """.getBytes(StandardCharsets.UTF_8); + + bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), existingBomBytes)); + awaitBomProcessedNotification(); + + assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { + assertThat(component.getResolvedLicense()).isNotNull(); + assertThat(component.getResolvedLicense().getLicenseId()).isEqualTo(existingLicense.getLicenseId()); + }); + + // Upload bom again but with new license + final byte[] updatedBomBytes = """ + { + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "components": [ + { + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "acme-lib-y", + "version": "2.0.0", + "licenses": [ + { + "license": { + "name": "Apache-2.0" + } + } + ] + } + ] + } + """.getBytes(StandardCharsets.UTF_8); + + bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), updatedBomBytes)); + awaitBomProcessedNotification(); + qm.getPersistenceManager().evictAll(); + + assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { + assertThat(component.getResolvedLicense()).isNotNull(); + assertThat(component.getResolvedLicense().getLicenseId()).isEqualTo(updatedLicense.getLicenseId()); + }); + + } + + @Test // https://github.com/DependencyTrack/dependency-track/issues/3498 + public void informDeleteExistingLicenseTest() { + final var existingLicense = new License(); + existingLicense.setLicenseId("GPL-3.0-or-later"); + existingLicense.setName("GPL-3.0-or-later"); + qm.persist(existingLicense); + + final var project = new Project(); + project.setName("acme-update-license-app"); + qm.persist(project); + + final byte[] existingBomBytes = """ + { + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "components": [ + { + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "acme-lib-y", + "version": "2.0.0", + "licenses": [ + { + "license": { + "name": "GPL-3.0-or-later" + } + } + ] + } + ] + } + """.getBytes(StandardCharsets.UTF_8); + + bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), existingBomBytes)); + awaitBomProcessedNotification(); + + assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { + assertThat(component.getResolvedLicense()).isNotNull(); + assertThat(component.getResolvedLicense().getLicenseId()).isEqualTo(existingLicense.getLicenseId()); + }); + + // Upload bom again but with license deleted + final byte[] updatedBomBytes = """ + { + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "components": [ + { + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "acme-lib-y", + "version": "2.0.0", + "licenses": [] + } + ] + } + """.getBytes(StandardCharsets.UTF_8); + + bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), updatedBomBytes)); + awaitBomProcessedNotification(); + qm.getPersistenceManager().evictAll(); + + assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { + assertThat(component.getResolvedLicense()).isNull(); + assertThat(component.getLicense()).isNull(); + assertThat(component.getLicenseUrl()).isNull(); + assertThat(component.getLicenseExpression()).isNull(); + }); + } + @Test public void informWithBomContainingServiceTest() throws Exception { final Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); From 71e62957f79f16ca41113f1ce5f892bbe64f9c6a Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 17 Mar 2024 15:49:43 +0100 Subject: [PATCH 011/412] Remove unused `usedBy` field from `Component`; Hide internal properties from OpenAPI Signed-off-by: nscuro --- .../org/dependencytrack/model/Component.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/Component.java b/src/main/java/org/dependencytrack/model/Component.java index ef689b08f3..fcba320057 100644 --- a/src/main/java/org/dependencytrack/model/Component.java +++ b/src/main/java/org/dependencytrack/model/Component.java @@ -367,7 +367,6 @@ public enum FetchGroup { private transient DependencyMetrics metrics; private transient RepositoryMetaComponent repositoryMeta; private transient boolean isNew; - private transient int usedBy; private transient JsonObject cacheResult; private transient Set dependencyGraph; private transient boolean expandDependencyGraph; @@ -581,6 +580,7 @@ public void setPurl(String purl) { } @JsonSerialize(using = CustomPackageURLSerializer.class) + @ApiModelProperty(dataType = "string", accessMode = ApiModelProperty.AccessMode.READ_ONLY) public PackageURL getPurlCoordinates() { if (purlCoordinates == null) { return null; @@ -769,10 +769,13 @@ public void setRepositoryMeta(RepositoryMetaComponent repositoryMeta) { this.repositoryMeta = repositoryMeta; } + @JsonIgnore + @ApiModelProperty(hidden = true) public boolean isNew() { return isNew; } + @JsonIgnore public void setNew(final boolean aNew) { isNew = aNew; } @@ -785,31 +788,30 @@ public void setLastInheritedRiskScore(Double lastInheritedRiskScore) { this.lastInheritedRiskScore = lastInheritedRiskScore; } + @JsonIgnore + @ApiModelProperty(hidden = true) public String getBomRef() { return bomRef; } + @JsonIgnore public void setBomRef(String bomRef) { this.bomRef = bomRef; } + @JsonIgnore + @ApiModelProperty(hidden = true) public List getLicenseCandidates() { return licenseCandidates; } + @JsonIgnore public void setLicenseCandidates(final List licenseCandidates) { this.licenseCandidates = licenseCandidates; } - public int getUsedBy() { - return usedBy; - } - - public void setUsedBy(int usedBy) { - this.usedBy = usedBy; - } - @JsonIgnore + @ApiModelProperty(hidden = true) public JsonObject getCacheResult() { return cacheResult; } From 4448ada94110736b08510429fae2372bdd733fb3 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 17 Mar 2024 15:50:57 +0100 Subject: [PATCH 012/412] Add OpenAPI `notification` tag to notification resources Previously, these endpoints were listed under `default` in the OpenAPI spec. Signed-off-by: nscuro --- .../resources/v1/NotificationPublisherResource.java | 2 +- .../dependencytrack/resources/v1/NotificationRuleResource.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java index eaecceb6fc..63ae0e077c 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java @@ -68,7 +68,7 @@ * @since 3.2.0 */ @Path("/v1/notification/publisher") -@Api(authorizations = @Authorization(value = "X-Api-Key")) +@Api(value = "notification", authorizations = @Authorization(value = "X-Api-Key")) public class NotificationPublisherResource extends AlpineResource { private static final Logger LOGGER = Logger.getLogger(NotificationPublisherResource.class); diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java index 3ad09ec1e2..92db0e0e7e 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java @@ -59,7 +59,7 @@ * @since 3.2.0 */ @Path("/v1/notification/rule") -@Api(authorizations = @Authorization(value = "X-Api-Key")) +@Api(value = "notification", authorizations = @Authorization(value = "X-Api-Key")) public class NotificationRuleResource extends AlpineResource { private static final Logger LOGGER = Logger.getLogger(NotificationRuleResource.class); From cb07d292727077928f67d4964b1a031670a6763a Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 17 Mar 2024 15:58:08 +0100 Subject: [PATCH 013/412] `OsvEcosystemResource` -> `IntegrationResource` Signed-off-by: nscuro --- ...OsvEcosytemResource.java => IntegrationResource.java} | 9 +++++---- ...temResourceTest.java => IntegrationResourceTest.java} | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) rename src/main/java/org/dependencytrack/resources/v1/{OsvEcosytemResource.java => IntegrationResource.java} (92%) rename src/test/java/org/dependencytrack/resources/v1/{OsvEcosystemResourceTest.java => IntegrationResourceTest.java} (96%) diff --git a/src/main/java/org/dependencytrack/resources/v1/OsvEcosytemResource.java b/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java similarity index 92% rename from src/main/java/org/dependencytrack/resources/v1/OsvEcosytemResource.java rename to src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java index ea6f0faf8a..db701209c0 100644 --- a/src/main/java/org/dependencytrack/resources/v1/OsvEcosytemResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java @@ -36,11 +36,12 @@ import java.util.List; import java.util.stream.Collectors; -@Path("/v1/integration/osv/ecosystem") -@Api(value = "ecosystem", authorizations = @Authorization(value = "X-Api-Key")) -public class OsvEcosytemResource extends AlpineResource { +@Path("/v1/integration") +@Api(value = "integration", authorizations = @Authorization(value = "X-Api-Key")) +public class IntegrationResource extends AlpineResource { @GET + @Path("/osv/ecosystem") @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns a list of all ecosystems in OSV", @@ -58,7 +59,7 @@ public Response getAllEcosystems() { } @GET - @Path("/inactive") + @Path("/osv/ecosystem/inactive") @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns a list of available inactive ecosystems in OSV to be selected by user", diff --git a/src/test/java/org/dependencytrack/resources/v1/OsvEcosystemResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/IntegrationResourceTest.java similarity index 96% rename from src/test/java/org/dependencytrack/resources/v1/OsvEcosystemResourceTest.java rename to src/test/java/org/dependencytrack/resources/v1/IntegrationResourceTest.java index 4d6a14b21b..51bbbb8e47 100644 --- a/src/test/java/org/dependencytrack/resources/v1/OsvEcosystemResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/IntegrationResourceTest.java @@ -36,12 +36,12 @@ import static org.dependencytrack.model.ConfigPropertyConstants.VULNERABILITY_SOURCE_GOOGLE_OSV_BASE_URL; import static org.dependencytrack.model.ConfigPropertyConstants.VULNERABILITY_SOURCE_GOOGLE_OSV_ENABLED; -public class OsvEcosystemResourceTest extends ResourceTest { +public class IntegrationResourceTest extends ResourceTest { @Override protected DeploymentContext configureDeployment() { return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(OsvEcosytemResource.class) + new ResourceConfig(IntegrationResource.class) .register(ApiFilter.class) .register(AuthenticationFilter.class))) .build(); From b5efff7291df9ca20dd134d79a2249c092e5f5b0 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 17 Mar 2024 17:42:53 +0100 Subject: [PATCH 014/412] Add required permissions to OpenAPI spec Fixes #3113 Closes #3504 Co-authored-by: Florian Signed-off-by: nscuro --- .../resources/v1/AccessControlResource.java | 9 ++- .../resources/v1/AnalysisResource.java | 6 +- .../resources/v1/BomResource.java | 76 +++++++++++++------ .../resources/v1/ComponentResource.java | 32 +++++--- .../resources/v1/ConfigPropertyResource.java | 9 ++- .../resources/v1/DependencyGraphResource.java | 6 +- .../resources/v1/EventResource.java | 25 ++++-- .../resources/v1/FindingResource.java | 16 ++-- .../resources/v1/IntegrationResource.java | 6 +- .../resources/v1/LdapResource.java | 16 +++- .../resources/v1/LicenseGroupResource.java | 21 +++-- .../resources/v1/LicenseResource.java | 10 ++- .../resources/v1/MetricsResource.java | 42 ++++++---- .../v1/NotificationPublisherResource.java | 18 +++-- .../v1/NotificationRuleResource.java | 24 ++++-- .../resources/v1/OidcResource.java | 24 ++++-- .../resources/v1/PermissionResource.java | 16 ++-- .../resources/v1/PolicyConditionResource.java | 8 +- .../resources/v1/PolicyResource.java | 27 ++++--- .../resources/v1/PolicyViolationResource.java | 9 ++- .../resources/v1/ProjectPropertyResource.java | 15 ++-- .../resources/v1/ProjectResource.java | 70 ++++++++++++----- .../resources/v1/RepositoryResource.java | 15 ++-- .../resources/v1/SearchResource.java | 18 +++-- .../resources/v1/ServiceResource.java | 15 ++-- .../resources/v1/TagResource.java | 5 +- .../resources/v1/TeamResource.java | 30 +++++--- .../resources/v1/UserResource.java | 36 ++++++--- .../resources/v1/VexResource.java | 38 ++++++---- .../v1/ViolationAnalysisResource.java | 6 +- .../resources/v1/VulnerabilityResource.java | 42 ++++++---- 31 files changed, 465 insertions(+), 225 deletions(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index 7bff4876af..b2f6317f87 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -65,7 +65,8 @@ public class AccessControlResource extends AlpineResource { @ApiOperation( value = "Returns the projects assigned to the specified team", response = String.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -94,7 +95,8 @@ public Response retrieveProjects (@ApiParam(value = "The UUID of the team to ret @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Adds an ACL mapping", - response = AclMappingRequest.class + response = AclMappingRequest.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -130,7 +132,8 @@ public Response addMapping(AclMappingRequest request) { @Path("/mapping/team/{teamUuid}/project/{projectUuid}") @Produces(MediaType.APPLICATION_JSON) @ApiOperation( - value = "Removes an ACL mapping" + value = "Removes an ACL mapping", + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java b/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java index 6b9d16a7d6..d18cfe4028 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java @@ -68,7 +68,8 @@ public class AnalysisResource extends AlpineResource { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Retrieves an analysis trail", - response = Analysis.class + response = Analysis.class, + notes = "

Requires permission VIEW_VULNERABILITY

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -115,7 +116,8 @@ public Response retrieveAnalysis(@ApiParam(value = "The UUID of the project") @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Records an analysis decision", - response = Analysis.class + response = Analysis.class, + notes = "

Requires permission VULNERABILITY_ANALYSIS

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/src/main/java/org/dependencytrack/resources/v1/BomResource.java index 1f1c2eee77..37b22a5f74 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -50,7 +50,6 @@ import org.dependencytrack.resources.v1.vo.IsTokenBeingProcessedResponse; import org.glassfish.jersey.media.multipart.BodyPartEntity; import org.glassfish.jersey.media.multipart.FormDataBodyPart; -import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.glassfish.jersey.media.multipart.FormDataParam; import javax.validation.Validator; @@ -92,7 +91,8 @@ public class BomResource extends AlpineResource { @Produces({CycloneDxMediaType.APPLICATION_CYCLONEDX_XML, CycloneDxMediaType.APPLICATION_CYCLONEDX_JSON, MediaType.APPLICATION_OCTET_STREAM}) @ApiOperation( value = "Returns dependency metadata for a project in CycloneDX format", - response = String.class + response = String.class, + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -161,7 +161,8 @@ public Response exportProjectAsCycloneDx ( @Produces(CycloneDxMediaType.APPLICATION_CYCLONEDX_XML) @ApiOperation( value = "Returns dependency metadata for a specific component in CycloneDX format", - response = String.class + response = String.class, + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -207,14 +208,20 @@ public Response exportComponentAsCycloneDx ( @ApiOperation( value = "Upload a supported bill of material format document", notes = """ - Expects CycloneDX and a valid project UUID. If a UUID is not specified, \ - then the projectName and projectVersion must be specified. \ - Optionally, if autoCreate is specified and 'true' and the project does not exist, \ - the project will be created. In this scenario, the principal making the request will \ - additionally need the PORTFOLIO_MANAGEMENT or PROJECT_CREATION_UPLOAD permission. - The BOM will be validated against the CycloneDX schema. If schema validation fails, \ - a response with problem details in RFC 9457 format will be returned. In this case, \ - the response's content type will be application/problem+json.""", +

+ Expects CycloneDX and a valid project UUID. If a UUID is not specified, + then the projectName and projectVersion must be specified. + Optionally, if autoCreate is specified and true and the project does not exist, + the project will be created. In this scenario, the principal making the request will + additionally need the PORTFOLIO_MANAGEMENT or + PROJECT_CREATION_UPLOAD permission. +

+

+ The BOM will be validated against the CycloneDX schema. If schema validation fails, + a response with problem details in RFC 9457 format will be returned. In this case, + the response's content type will be application/problem+json. +

+

Requires permission BOM_UPLOAD

""", response = BomUploadResponse.class, nickname = "UploadBomBase64Encoded" ) @@ -225,7 +232,7 @@ public Response exportComponentAsCycloneDx ( @ApiResponse(code = 404, message = "The project could not be found") }) @PermissionRequired(Permissions.Constants.BOM_UPLOAD) - public Response uploadBom(BomSubmitRequest request) { + public Response uploadBom(@ApiParam(required = true) BomSubmitRequest request) { final Validator validator = getValidator(); if (request.getProject() != null) { // behavior in v3.0.0 failOnValidationError( @@ -286,14 +293,20 @@ public Response uploadBom(BomSubmitRequest request) { @ApiOperation( value = "Upload a supported bill of material format document", notes = """ - Expects CycloneDX and a valid project UUID. If a UUID is not specified, \ - then the projectName and projectVersion must be specified. \ - Optionally, if autoCreate is specified and 'true' and the project does not exist, \ - the project will be created. In this scenario, the principal making the request will \ - additionally need the PORTFOLIO_MANAGEMENT or PROJECT_CREATION_UPLOAD permission. - The BOM will be validated against the CycloneDX schema. If schema validation fails, \ - a response with problem details in RFC 9457 format will be returned. In this case, \ - the response's content type will be application/problem+json.""", +

+ Expects CycloneDX and a valid project UUID. If a UUID is not specified, + then the projectName and projectVersion must be specified. + Optionally, if autoCreate is specified and true and the project does not exist, + the project will be created. In this scenario, the principal making the request will + additionally need the PORTFOLIO_MANAGEMENT or + PROJECT_CREATION_UPLOAD permission. +

+

+ The BOM will be validated against the CycloneDX schema. If schema validation fails, + a response with problem details in RFC 9457 format will be returned. In this case, + the response's content type will be application/problem+json. +

+

Requires permission BOM_UPLOAD

""", response = BomUploadResponse.class, nickname = "UploadBom" ) @@ -311,9 +324,7 @@ public Response uploadBom(@FormDataParam("project") String projectUuid, @FormDataParam("parentName") String parentName, @FormDataParam("parentVersion") String parentVersion, @FormDataParam("parentUUID") String parentUUID, - final FormDataMultiPart multiPart) { - - final List artifactParts = multiPart.getFields("bom"); + @ApiParam(type = "string") @FormDataParam("bom") final List artifactParts) { if (projectUuid != null) { // behavior in v3.0.0 try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, projectUuid); @@ -358,7 +369,24 @@ public Response uploadBom(@FormDataParam("project") String projectUuid, @GET @Path("/token/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation(value = "Determines if there are any tasks associated with the token that are being processed, or in the queue to be processed.", notes = "Deprecated. Use /v1/event/token/{uuid} instead.", response = IsTokenBeingProcessedResponse.class) + @ApiOperation( + value = "Determines if there are any tasks associated with the token that are being processed, or in the queue to be processed.", + notes = """ +

+ This endpoint is intended to be used in conjunction with uploading a supported BOM document. + Upon upload, a token will be returned. The token can then be queried using this endpoint to + determine if any tasks (such as vulnerability analysis) is being performed on the BOM: +

    +
  • A value of true indicates processing is occurring.
  • +
  • A value of false indicates that no processing is occurring for the specified token.
  • +
+ However, a value of false also does not confirm the token is valid, + only that no processing is associated with the specified token. +

+

Requires permission BOM_UPLOAD

+

Deprecated. Use /v1/event/token/{uuid} instead.

""", + response = IsTokenBeingProcessedResponse.class + ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java index 1a92757b79..599f241914 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java @@ -45,6 +45,7 @@ import org.dependencytrack.model.RepositoryType; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.util.InternalComponentIdentificationUtil; + import javax.validation.Validator; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -78,7 +79,8 @@ public class ComponentResource extends AlpineResource { value = "Returns a list of all components for a given project", response = Component.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of components") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of components"), + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -113,7 +115,8 @@ public Response getAllComponents( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns a specific component", - response = Component.class + response = Component.class, + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -155,7 +158,8 @@ public Response getComponentByUuid( @ApiOperation( value = "Returns a list of components that have the specified component identity. This resource accepts coordinates (group, name, version) or purl, cpe, or swidTagId", responseContainer = "List", - response = Component.class + response = Component.class, + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -213,7 +217,8 @@ public Response getComponentByIdentity(@ApiParam(value = "The group of the compo @ApiOperation( value = "Returns a list of components that have the specified hash value", responseContainer = "List", - response = Component.class + response = Component.class, + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -235,7 +240,8 @@ public Response getComponentByHash( @ApiOperation( value = "Creates a new component", response = Component.class, - code = 201 + code = 201, + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -342,7 +348,8 @@ public Response createComponent(@PathParam("uuid") String uuid, Component jsonCo @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Updates a component", - response = Component.class + response = Component.class, + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -449,7 +456,8 @@ public Response updateComponent(Component jsonComponent) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a component", - code = 204 + code = 204, + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -478,7 +486,11 @@ public Response deleteComponent( @GET @Path("/internal/identify") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation(value = "Requests the identification of internal components in the portfolio", code = 204) + @ApiOperation( + value = "Requests the identification of internal components in the portfolio", + code = 204, + notes = "

Requires permission SYSTEM_CONFIGURATION

" + ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), }) @@ -494,7 +506,9 @@ public Response identifyInternalComponents() { @ApiOperation( value = "Returns the expanded dependency graph to every occurrence of a component", response = Component.class, - responseContainer = "Map") + responseContainer = "Map", + notes = "

Requires permission VIEW_PORTFOLIO

" + ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), diff --git a/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java index b3e0283c54..8b32390c9c 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java @@ -54,7 +54,8 @@ public class ConfigPropertyResource extends AbstractConfigPropertyResource { @ApiOperation( value = "Returns a list of all ConfigProperties for the specified groupName", response = ConfigProperty.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -82,7 +83,8 @@ public Response getConfigProperties() { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Updates a config property", - response = ConfigProperty.class + response = ConfigProperty.class, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -109,7 +111,8 @@ public Response updateConfigProperty(ConfigProperty json) { @ApiOperation( value = "Updates an array of config properties", response = ConfigProperty.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java b/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java index 72dd3bfa2c..a42b5fb7d5 100644 --- a/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java @@ -70,7 +70,8 @@ public class DependencyGraphResource extends AlpineResource { @ApiOperation( value = "Returns a list of specific components and services from project UUID", response = DependencyGraphResponse.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -108,7 +109,8 @@ public Response getComponentsAndServicesByProjectUuid(final @PathParam("uuid") S @ApiOperation( value = "Returns a list of specific components and services from component UUID", response = DependencyGraphResponse.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/EventResource.java b/src/main/java/org/dependencytrack/resources/v1/EventResource.java index 4c3cb4e1d8..9da4e56da0 100644 --- a/src/main/java/org/dependencytrack/resources/v1/EventResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/EventResource.java @@ -22,10 +22,10 @@ import alpine.server.resources.AlpineResource; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; import org.dependencytrack.resources.v1.vo.IsTokenBeingProcessedResponse; import javax.ws.rs.GET; @@ -49,12 +49,21 @@ public class EventResource extends AlpineResource { @GET @Path("/token/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation(value = "Determines if there are any tasks associated with the token that are being processed, or in the queue to be processed.", - notes = "This endpoint is intended to be used in conjunction with other API calls which return a token for asynchronous tasks. " + - "The token can then be queried using this endpoint to determine if the task is complete. " + - "A value of true indicates processing is occurring. A value of false indicates that no processing is " + - "occurring for the specified token. However, a value of false also does not confirm the token is valid, " + - "only that no processing is associated with the specified token.", response = IsTokenBeingProcessedResponse.class) + @ApiOperation( + value = "Determines if there are any tasks associated with the token that are being processed, or in the queue to be processed.", + response = IsTokenBeingProcessedResponse.class, + notes = """ +

+ This endpoint is intended to be used in conjunction with other API calls which return a token for asynchronous tasks. + The token can then be queried using this endpoint to determine if the task is complete: +

    +
  • A value of true indicates processing is occurring.
  • +
  • A value of false indicates that no processing is occurring for the specified token.
  • +
+ However, a value of false also does not confirm the token is valid, + only that no processing is associated with the specified token. +

""" + ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) diff --git a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index cec9c4cecb..dfbb162e67 100644 --- a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -41,6 +41,7 @@ import org.dependencytrack.model.Project; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.persistence.QueryManager; + import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; @@ -75,7 +76,8 @@ public class FindingResource extends AlpineResource { value = "Returns a list of all findings for a specific project", response = Finding.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of findings") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of findings"), + notes = "

Requires permission VIEW_VULNERABILITY

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -113,7 +115,8 @@ public Response getFindingsByProject(@PathParam("uuid") String uuid, @Path("/project/{uuid}/export") @Produces(MediaType.APPLICATION_JSON) @ApiOperation( - value = "Returns the findings for the specified project as FPF" + value = "Returns the findings for the specified project as FPF", + notes = "

Requires permission VIEW_VULNERABILITY

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -145,7 +148,8 @@ public Response exportFindingsByProject(@PathParam("uuid") String uuid) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Triggers Vulnerability Analysis on a specific project", - response = Project.class + response = Project.class, + notes = "

Requires permission VIEW_VULNERABILITY

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -187,7 +191,8 @@ public Response analyzeProject( value = "Returns a list of all findings", response = Finding.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of findings") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of findings"), + notes = "

Requires permission VIEW_VULNERABILITY

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -250,7 +255,8 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") value = "Returns a list of all findings grouped by vulnerability", response = GroupedFinding.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of findings") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of findings"), + notes = "

Requires permission VIEW_VULNERABILITY

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java b/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java index db701209c0..3d42f95736 100644 --- a/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java @@ -46,7 +46,8 @@ public class IntegrationResource extends AlpineResource { @ApiOperation( value = "Returns a list of all ecosystems in OSV", response = String.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -64,7 +65,8 @@ public Response getAllEcosystems() { @ApiOperation( value = "Returns a list of available inactive ecosystems in OSV to be selected by user", response = String.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") diff --git a/src/main/java/org/dependencytrack/resources/v1/LdapResource.java b/src/main/java/org/dependencytrack/resources/v1/LdapResource.java index 9c913db6ab..25c3edf894 100644 --- a/src/main/java/org/dependencytrack/resources/v1/LdapResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/LdapResource.java @@ -72,7 +72,12 @@ public class LdapResource extends AlpineResource { response = String.class, responseContainer = "List", responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of ldap groups that match the specified search criteria"), - notes = "This API performs a pass-thru query to the configured LDAP server. Search criteria results are cached using default Alpine CacheManager policy" + notes = """ +

+ This API performs a pass-through query to the configured LDAP server. + Search criteria results are cached using default Alpine CacheManager policy. +

+

Requires permission ACCESS_MANAGEMENT

""" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -116,7 +121,8 @@ public Response retrieveLdapGroups () { @ApiOperation( value = "Returns the DNs of all groups mapped to the specified team", response = String.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -141,7 +147,8 @@ public Response retrieveLdapGroups (@ApiParam(value = "The UUID of the team to r @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Adds a mapping", - response = MappedLdapGroup.class + response = MappedLdapGroup.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -175,7 +182,8 @@ public Response addMapping(MappedLdapGroupRequest request) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Removes a mapping", - response = MappedLdapGroup.class + response = MappedLdapGroup.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java b/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java index 4fa96d76f6..c18b1e5301 100644 --- a/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java @@ -63,7 +63,8 @@ public class LicenseGroupResource extends AlpineResource { value = "Returns a list of all license groups", response = LicenseGroup.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of license groups") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of license groups"), + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -81,7 +82,8 @@ public Response getLicenseGroups() { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns a specific license group", - response = License.class + response = License.class, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -107,7 +109,8 @@ public Response getLicenseGroup( @ApiOperation( value = "Creates a new license group", response = LicenseGroup.class, - code = 201 + code = 201, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -136,7 +139,8 @@ public Response createLicenseGroup(LicenseGroup jsonLicenseGroup) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Updates a license group", - response = LicenseGroup.class + response = LicenseGroup.class, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -166,7 +170,8 @@ public Response updateLicenseGroup(LicenseGroup jsonLicenseGroup) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a license group", - code = 204 + code = 204, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -193,7 +198,8 @@ public Response deleteLicenseGroup( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Adds the license to the specified license group.", - response = LicenseGroup.class + response = LicenseGroup.class, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The license group already has the specified license assigned"), @@ -232,7 +238,8 @@ public Response addLicenseToLicenseGroup( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Removes the license from the license group.", - response = LicenseGroup.class + response = LicenseGroup.class, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The license is not a member with the license group"), diff --git a/src/main/java/org/dependencytrack/resources/v1/LicenseResource.java b/src/main/java/org/dependencytrack/resources/v1/LicenseResource.java index c6b85b4817..3c9a132169 100644 --- a/src/main/java/org/dependencytrack/resources/v1/LicenseResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/LicenseResource.java @@ -18,6 +18,7 @@ */ package org.dependencytrack.resources.v1; +import alpine.common.logging.Logger; import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; @@ -33,16 +34,15 @@ import org.dependencytrack.persistence.QueryManager; import javax.validation.Validator; +import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.PUT; -import javax.ws.rs.DELETE; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.List; -import alpine.common.logging.Logger; /** * JAX-RS resources for processing licenses. @@ -120,7 +120,8 @@ public Response getLicense( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Creates a new custom license", - response = License.class + response = License.class, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -150,7 +151,8 @@ public Response createLicense(License jsonLicense) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a custom license", - code = 204 + code = 204, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java b/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java index 3b3fa99fc3..9fb04d702b 100644 --- a/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java @@ -66,7 +66,8 @@ public class MetricsResource extends AlpineResource { @ApiOperation( value = "Returns the sum of all vulnerabilities in the database by year and month", response = VulnerabilityMetrics.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -84,7 +85,8 @@ public Response getVulnerabilityMetrics() { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns current metrics for the entire portfolio", - response = PortfolioMetrics.class + response = PortfolioMetrics.class, + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -102,7 +104,9 @@ public Response getPortfolioCurrentMetrics() { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns historical metrics for the entire portfolio from a specific date", - notes = "Date format must be YYYYMMDD", + notes = """ +

Date format must be YYYYMMDD

+

Requires permission VIEW_PORTFOLIO

""", response = PortfolioMetrics.class, responseContainer = "List" ) @@ -130,7 +134,8 @@ public Response getPortfolioMetricsSince( @ApiOperation( value = "Returns X days of historical metrics for the entire portfolio", response = PortfolioMetrics.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -152,7 +157,8 @@ public Response getPortfolioMetricsXDays( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Requests a refresh of the portfolio metrics", - response = PortfolioMetrics.class + response = PortfolioMetrics.class, + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -168,7 +174,8 @@ public Response RefreshPortfolioMetrics() { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns current metrics for a specific project", - response = ProjectMetrics.class + response = ProjectMetrics.class, + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -199,7 +206,9 @@ public Response getProjectCurrentMetrics( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns historical metrics for a specific project from a specific date", - notes = "Date format must be YYYYMMDD", + notes = """ +

Date format must be YYYYMMDD

+

Requires permission VIEW_PORTFOLIO

""", response = ProjectMetrics.class, responseContainer = "List" ) @@ -225,7 +234,8 @@ public Response getProjectMetricsSince( @ApiOperation( value = "Returns X days of historical metrics for a specific project", response = ProjectMetrics.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -247,7 +257,8 @@ public Response getProjectMetricsXDays( @Path("/project/{uuid}/refresh") @Produces(MediaType.APPLICATION_JSON) @ApiOperation( - value = "Requests a refresh of a specific projects metrics" + value = "Requests a refresh of a specific projects metrics", + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -278,7 +289,8 @@ public Response RefreshProjectMetrics( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns current metrics for a specific component", - response = DependencyMetrics.class + response = DependencyMetrics.class, + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -309,7 +321,9 @@ public Response getComponentCurrentMetrics( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns historical metrics for a specific component from a specific date", - notes = "Date format must be YYYYMMDD", + notes = """ +

Date format must be YYYYMMDD

+

Requires permission VIEW_PORTFOLIO

""", response = DependencyMetrics.class, responseContainer = "List" ) @@ -338,7 +352,8 @@ public Response getComponentMetricsSince( @ApiOperation( value = "Returns X days of historical metrics for a specific component", response = DependencyMetrics.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -360,7 +375,8 @@ public Response getComponentMetricsXDays( @Path("/component/{uuid}/refresh") @Produces(MediaType.APPLICATION_JSON) @ApiOperation( - value = "Requests a refresh of a specific components metrics" + value = "Requests a refresh of a specific components metrics", + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java index 63ae0e077c..66e3d11b96 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java @@ -78,7 +78,8 @@ public class NotificationPublisherResource extends AlpineResource { @ApiOperation( value = "Returns a list of all notification publishers", response = NotificationPublisher.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -97,7 +98,8 @@ public Response getAllNotificationPublishers() { @ApiOperation( value = "Creates a new notification publisher", response = NotificationPublisher.class, - code = 201 + code = 201, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 400, message = "Invalid notification class or trying to modify a default publisher"), @@ -149,7 +151,8 @@ public Response createNotificationPublisher(NotificationPublisher jsonNotificati @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Updates a notification publisher", - response = NotificationRule.class + response = NotificationRule.class, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 400, message = "Invalid notification class or trying to modify a default publisher"), @@ -211,7 +214,8 @@ public Response updateNotificationPublisher(NotificationPublisher jsonNotificati @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a notification publisher and all related notification rules", - code = 204 + code = 204, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 400, message = "Deleting a default notification publisher is forbidden"), @@ -241,7 +245,8 @@ public Response deleteNotificationPublisher(@ApiParam(value = "The UUID of the n @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @ApiOperation( - value = "Restore the default notification publisher templates using the ones in the solution classpath" + value = "Restore the default notification publisher templates using the ones in the solution classpath", + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -268,7 +273,8 @@ public Response restoreDefaultTemplates() { @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.APPLICATION_JSON) @ApiOperation( - value = "Dispatches a SMTP notification test" + value = "Dispatches a SMTP notification test", + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java index 92db0e0e7e..1a2bc3deb3 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java @@ -70,7 +70,8 @@ public class NotificationRuleResource extends AlpineResource { value = "Returns a list of all notification rules", response = NotificationRule.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of notification rules") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of notification rules"), + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @@ -90,7 +91,8 @@ public Response getAllNotificationRules() { @ApiOperation( value = "Creates a new notification rule", response = NotificationRule.class, - code = 201 + code = 201, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -126,7 +128,8 @@ public Response createNotificationRule(NotificationRule jsonRule) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Updates a notification rule", - response = NotificationRule.class + response = NotificationRule.class, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -157,7 +160,8 @@ public Response updateNotificationRule(NotificationRule jsonRule) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a notification rule", - code = 204 + code = 204, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -182,7 +186,8 @@ public Response deleteNotificationRule(NotificationRule jsonRule) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Adds a project to a notification rule", - response = NotificationRule.class + response = NotificationRule.class, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The rule already has the specified project assigned"), @@ -223,7 +228,8 @@ public Response addProjectToRule( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Removes a project from a notification rule", - response = NotificationRule.class + response = NotificationRule.class, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The rule does not have the specified project assigned"), @@ -264,7 +270,8 @@ public Response removeProjectFromRule( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Adds a team to a notification rule", - response = NotificationRule.class + response = NotificationRule.class, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The rule already has the specified team assigned"), @@ -305,7 +312,8 @@ public Response addTeamToRule( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Removes a team from a notification rule", - response = NotificationRule.class + response = NotificationRule.class, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The rule does not have the specified team assigned"), diff --git a/src/main/java/org/dependencytrack/resources/v1/OidcResource.java b/src/main/java/org/dependencytrack/resources/v1/OidcResource.java index 2430a118c2..9415ad221a 100644 --- a/src/main/java/org/dependencytrack/resources/v1/OidcResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/OidcResource.java @@ -80,7 +80,8 @@ public Response isAvailable() { @ApiOperation( value = "Returns a list of all groups", response = OidcGroup.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -100,7 +101,8 @@ public Response retrieveGroups() { @ApiOperation( value = "Creates group", response = OidcGroup.class, - code = 201 + code = 201, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -129,7 +131,8 @@ public Response createGroup(final OidcGroup jsonGroup) { @Consumes(MediaType.APPLICATION_JSON) @ApiOperation( value = "Updates group", - response = OidcGroup.class + response = OidcGroup.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -160,7 +163,8 @@ public Response updateGroup(final OidcGroup jsonGroup) { @Consumes(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a group", - code = 204 + code = 204, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -188,7 +192,8 @@ public Response deleteGroup(@ApiParam(value = "The UUID of the group to delete", @ApiOperation( value = "Returns a list of teams associated with the specified group", response = Team.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -216,7 +221,8 @@ public Response retrieveTeamsMappedToGroup(@ApiParam(value = "The UUID of the ma @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Adds a mapping", - response = MappedOidcGroup.class + response = MappedOidcGroup.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -257,7 +263,8 @@ public Response addMapping(final MappedOidcGroupRequest request) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a mapping", - code = 204 + code = 204, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -283,7 +290,8 @@ public Response deleteMappingByUuid(@ApiParam(value = "The UUID of the mapping t @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a mapping", - code = 204 + code = 204, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java b/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java index a1f1b08f22..4f8db7595a 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java @@ -62,7 +62,8 @@ public class PermissionResource extends AlpineResource { @ApiOperation( value = "Returns a list of all permissions", response = alpine.model.Permission.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -81,7 +82,8 @@ public Response getAllPermissions() { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Adds the permission to the specified username.", - response = UserPrincipal.class + response = UserPrincipal.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The user already has the specified permission assigned"), @@ -121,7 +123,8 @@ public Response addPermissionToUser( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Removes the permission from the user.", - response = UserPrincipal.class + response = UserPrincipal.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The user already has the specified permission assigned"), @@ -161,8 +164,8 @@ public Response removePermissionFromUser( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Adds the permission to the specified team.", - notes = "Requires 'manage users' permission.", - response = Team.class + response = Team.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The team already has the specified permission assigned"), @@ -202,7 +205,8 @@ public Response addPermissionToTeam( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Removes the permission from the team.", - response = Team.class + response = Team.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The team already has the specified permission assigned"), diff --git a/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java b/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java index 6d30a684bb..f91e41fd12 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java @@ -61,7 +61,8 @@ public class PolicyConditionResource extends AlpineResource { @ApiOperation( value = "Creates a new policy condition", response = PolicyCondition.class, - code = 201 + code = 201, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -95,7 +96,7 @@ public Response createPolicyCondition( @ApiOperation( value = "Updates a policy condition", response = PolicyCondition.class, - code = 200 + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -124,7 +125,8 @@ public Response updatePolicyCondition(PolicyCondition jsonPolicyCondition) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a policy condition", - code = 204 + code = 204, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java b/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java index d098366bc5..84c55c3128 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java @@ -64,7 +64,8 @@ public class PolicyResource extends AlpineResource { value = "Returns a list of all policies", response = Policy.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policies") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policies"), + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -82,7 +83,8 @@ public Response getPolicies() { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns a specific policy", - response = Policy.class + response = Policy.class, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -108,7 +110,8 @@ public Response getPolicy( @ApiOperation( value = "Creates a new policy", response = Policy.class, - code = 201 + code = 201, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -147,7 +150,8 @@ public Response createPolicy(Policy jsonPolicy) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Updates a policy", - response = Policy.class + response = Policy.class, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -180,7 +184,8 @@ public Response updatePolicy(Policy jsonPolicy) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a policy", - code = 204 + code = 204, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -207,7 +212,8 @@ public Response deletePolicy( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Adds a project to a policy", - response = Policy.class + response = Policy.class, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The policy already has the specified project assigned"), @@ -245,7 +251,8 @@ public Response addProjectToPolicy( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Removes a project from a policy", - response = Policy.class + response = Policy.class, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The policy does not have the specified project assigned"), @@ -283,7 +290,8 @@ public Response removeProjectFromPolicy( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Adds a tag to a policy", - response = Policy.class + response = Policy.class, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The policy already has the specified tag assigned"), @@ -322,7 +330,8 @@ public Response addTagToPolicy( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Removes a tag from a policy", - response = Policy.class + response = Policy.class, + notes = "

Requires permission POLICY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The policy does not have the specified tag assigned"), diff --git a/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java b/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java index 84d03a10d1..37889f19d5 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java @@ -61,7 +61,8 @@ public class PolicyViolationResource extends AlpineResource { value = "Returns a list of all policy violations for the entire portfolio", response = PolicyViolation.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policy violations") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policy violations"), + notes = "

Requires permission VIEW_POLICY_VIOLATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -84,7 +85,8 @@ public Response getViolations(@ApiParam(value = "Optionally includes suppressed value = "Returns a list of all policy violations for a specific project", response = PolicyViolation.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policy violations") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policy violations"), + notes = "

Requires permission VIEW_POLICY_VIOLATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -119,7 +121,8 @@ public Response getViolationsByProject(@PathParam("uuid") String uuid, value = "Returns a list of all policy violations for a specific component", response = PolicyViolation.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policy violations") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policy violations"), + notes = "

Requires permission VIEW_POLICY_VIOLATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java index 895018cf91..56c72603c3 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java @@ -18,7 +18,6 @@ */ package org.dependencytrack.resources.v1; -import alpine.common.logging.Logger; import alpine.server.auth.PermissionRequired; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -55,14 +54,13 @@ @Api(value = "projectProperty", authorizations = @Authorization(value = "X-Api-Key")) public class ProjectPropertyResource extends AbstractConfigPropertyResource { - private static final Logger LOGGER = Logger.getLogger(ProjectPropertyResource.class); - @GET @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns a list of all ProjectProperties for the specified project", response = ProjectProperty.class, - responseContainer = "List" + responseContainer = "List", + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -104,7 +102,8 @@ public Response getProperties( @ApiOperation( value = "Creates a new project property", response = ProjectProperty.class, - code = 201 + code = 201, + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -160,7 +159,8 @@ public Response createProperty( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Updates a project property", - response = ProjectProperty.class + response = ProjectProperty.class, + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -202,7 +202,8 @@ public Response updateProperty( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a config property", - response = ProjectProperty.class + response = ProjectProperty.class, + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index ba238f0667..38c5dae39b 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -80,7 +80,8 @@ public class ProjectResource extends AlpineResource { value = "Returns a list of all projects", response = Project.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects"), + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -113,7 +114,8 @@ public Response getProjects(@ApiParam(value = "The optional name of the project @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns a specific project", - response = Project.class + response = Project.class, + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -141,7 +143,12 @@ public Response getProject( @GET @Path("/lookup") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation(value = "Returns a specific project by its name and version", response = Project.class, nickname = "getProjectByNameAndVersion") + @ApiOperation( + value = "Returns a specific project by its name and version", + response = Project.class, + nickname = "getProjectByNameAndVersion", + notes = "

Requires permission VIEW_PORTFOLIO

" + ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), @@ -174,7 +181,8 @@ public Response getProject( value = "Returns a list of all projects by tag", response = Project.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects with the tag") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects with the tag"), + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -201,7 +209,8 @@ public Response getProjectsByTag( value = "Returns a list of all projects by classifier", response = Project.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects of the specified classifier") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects of the specified classifier"), + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -228,13 +237,20 @@ public Response getProjectsByClassifier( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Creates a new project", - notes = "If a parent project exists, the UUID of the parent project is required ", + notes = """ +

If a parent project exists, parent.uuid is required

+

Requires permission PORTFOLIO_MANAGEMENT

+ """, response = Project.class, code = 201 ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 409, message = "- An inactive Parent cannot be selected as parent\n- A project with the specified name already exists"), + @ApiResponse(code = 409, message = """ +
    +
  • An inactive Parent cannot be selected as parent, or
  • +
  • A project with the specified name already exists
  • +
"""), }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response createProject(Project jsonProject) { @@ -282,12 +298,19 @@ public Response createProject(Project jsonProject) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Updates a project", - response = Project.class + response = Project.class, + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 404, message = "The UUID of the project could not be found"), - @ApiResponse(code = 409, message = "- An inactive Parent cannot be selected as parent\n- Project cannot be set to inactive if active children are present\n- A project with the specified name already exists\n- A project cannot select itself as a parent") + @ApiResponse(code = 409, message = """ +
    +
  • An inactive Parent cannot be selected as parent, or
  • +
  • Project cannot be set to inactive if active children are present, or
  • +
  • A project with the specified name already exists, or
  • +
  • A project cannot select itself as a parent
  • +
""") }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response updateProject(Project jsonProject) { @@ -344,12 +367,19 @@ public Response updateProject(Project jsonProject) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Partially updates a project", - response = Project.class + response = Project.class, + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 404, message = "The UUID of the project could not be found"), - @ApiResponse(code = 409, message = "- An inactive Parent cannot be selected as parent\n- Project cannot be set to inactive if active children are present\n- A project with the specified name already exists\n- A project cannot select itself as a parent") + @ApiResponse(code = 409, message = """ +
    +
  • An inactive Parent cannot be selected as parent, or
  • +
  • Project cannot be set to inactive if active children are present, or
  • +
  • A project with the specified name already exists, or
  • +
  • A project cannot select itself as a parent
  • +
""") }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response patchProject( @@ -470,7 +500,8 @@ private boolean setIfDifferent(final Project source, final Project target, f @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a project", - code = 204 + code = 204, + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -503,7 +534,8 @@ public Response deleteProject( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Clones a project", - response = Project.class + response = Project.class, + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -544,7 +576,8 @@ public Response cloneProject(CloneProjectRequest jsonRequest) { value = "Returns a list of all children for a project", response = Project.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects"), + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -578,7 +611,8 @@ public Response getChildrenProjects(@ApiParam(value = "The UUID of the project t value = "Returns a list of all children for a project by classifier", response = Project.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects"), + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -616,7 +650,8 @@ public Response getChildrenProjectsByClassifier( value = "Returns a list of all children for a project by tag", response = Project.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects"), + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -654,7 +689,8 @@ public Response getChildrenProjectsByTag( value = "Returns a list of all projects without the descendants of the selected project", response = Project.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects"), + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java b/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java index 0f1219af4a..b0404d66b1 100644 --- a/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java @@ -69,7 +69,8 @@ public class RepositoryResource extends AlpineResource { value = "Returns a list of all repositories", response = Repository.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of repositories") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of repositories"), + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -89,7 +90,8 @@ public Response getRepositories() { value = "Returns repositories that support the specific type", response = Repository.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of repositories") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of repositories"), + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @@ -148,7 +150,8 @@ public Response getRepositoryMetaComponent( @ApiOperation( value = "Creates a new repository", response = Repository.class, - code = 201 + code = 201, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -186,7 +189,8 @@ public Response createRepository(Repository jsonRepository) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Updates a repository", - response = Repository.class + response = Repository.class, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -227,7 +231,8 @@ public Response updateRepository(Repository jsonRepository) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a repository", - code = 204 + code = 204, + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/SearchResource.java b/src/main/java/org/dependencytrack/resources/v1/SearchResource.java index 2a7df3d964..ed68045798 100644 --- a/src/main/java/org/dependencytrack/resources/v1/SearchResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/SearchResource.java @@ -55,7 +55,8 @@ public class SearchResource extends AlpineResource { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Processes and returns search results", - response = SearchResult.class + response = SearchResult.class, + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -73,7 +74,7 @@ public Response aggregateSearch(@QueryParam("query") String query) { @ApiOperation( value = "Processes and returns search results", response = SearchResult.class, - notes = "Preferred search endpoint" + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -91,7 +92,7 @@ public Response projectSearch(@QueryParam("query") String query) { @ApiOperation( value = "Processes and returns search results", response = SearchResult.class, - notes = "Preferred search endpoint" + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -109,7 +110,7 @@ public Response componentSearch(@QueryParam("query") String query) { @ApiOperation( value = "Processes and returns search results", response = SearchResult.class, - notes = "Preferred search endpoint" + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -127,7 +128,7 @@ public Response serviceSearch(@QueryParam("query") String query) { @ApiOperation( value = "Processes and returns search results", response = SearchResult.class, - notes = "Preferred search endpoint" + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -145,7 +146,7 @@ public Response licenseSearch(@QueryParam("query") String query) { @ApiOperation( value = "Processes and returns search results", response = SearchResult.class, - notes = "Preferred search endpoint" + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -163,7 +164,7 @@ public Response vulnerabilitySearch(@QueryParam("query") String query) { @ApiOperation( value = "Processes and returns search results", response = SearchResult.class, - notes = "Preferred search endpoint" + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -185,7 +186,8 @@ public Response vulnerableSoftwareSearch(@QueryParam("query") String query, @Que @POST @Produces(MediaType.APPLICATION_JSON) @ApiOperation( - value = "Rebuild lucene indexes for search operations" + value = "Rebuild lucene indexes for search operations", + notes = "

Requires permission SYSTEM_CONFIGURATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java b/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java index 1304b2214f..9693b2b74b 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java @@ -63,7 +63,8 @@ public class ServiceResource extends AlpineResource { value = "Returns a list of all services for a given project", response = ServiceComponent.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of services") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of services"), + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -92,7 +93,8 @@ public Response getAllServices(@PathParam("uuid") String uuid) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns a specific service", - response = ServiceComponent.class + response = ServiceComponent.class, + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -126,7 +128,8 @@ public Response getServiceByUuid( @ApiOperation( value = "Creates a new service", response = ServiceComponent.class, - code = 201 + code = 201, + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -177,7 +180,8 @@ public Response createService(@PathParam("uuid") String uuid, ServiceComponent j @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Updates a service", - response = ServiceComponent.class + response = ServiceComponent.class, + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -227,7 +231,8 @@ public Response updateService(ServiceComponent jsonService) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a service", - code = 204 + code = 204, + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/TagResource.java b/src/main/java/org/dependencytrack/resources/v1/TagResource.java index d0b58883ec..6779e9d7ae 100644 --- a/src/main/java/org/dependencytrack/resources/v1/TagResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/TagResource.java @@ -34,8 +34,8 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; -import javax.ws.rs.Produces; import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -50,7 +50,8 @@ public class TagResource extends AlpineResource { value = "Returns a list of all tags", response = Tag.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of tags") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of tags"), + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") diff --git a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java index 60f94d765d..2bb2389219 100644 --- a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java @@ -69,7 +69,8 @@ public class TeamResource extends AlpineResource { value = "Returns a list of all teams", response = Team.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of teams") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of teams"), + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -88,7 +89,8 @@ public Response getTeams() { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns a specific team", - response = Team.class + response = Team.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -114,7 +116,8 @@ public Response getTeam( @ApiOperation( value = "Creates a new team along with an associated API key", response = Team.class, - code = 201 + code = 201, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -140,7 +143,8 @@ public Response createTeam(Team jsonTeam) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Updates a team's fields including", - response = Team.class + response = Team.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -171,7 +175,8 @@ public Response updateTeam(Team jsonTeam) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a team", - code = 204 + code = 204, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -198,7 +203,8 @@ public Response deleteTeam(Team jsonTeam) { @ApiOperation( value = "Generates an API key and returns its value", response = ApiKey.class, - code = 201 + code = 201, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -224,7 +230,8 @@ public Response generateApiKey( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Regenerates an API key by removing the specified key, generating a new one and returning its value", - response = ApiKey.class + response = ApiKey.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -249,7 +256,11 @@ public Response regenerateApiKey( @Path("/key/{key}/comment") @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation(value = "Updates an API key's comment", response = ApiKey.class) + @ApiOperation( + value = "Updates an API key's comment", + response = ApiKey.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" + ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 404, message = "The API key could not be found") @@ -279,7 +290,8 @@ public Response updateApiKeyComment(@PathParam("key") final String key, @Path("/key/{apikey}") @ApiOperation( value = "Deletes the specified API key", - code = 204 + code = 204, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/src/main/java/org/dependencytrack/resources/v1/UserResource.java index a427e9bdb6..c027e653e8 100644 --- a/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -227,7 +227,8 @@ public Response forceChangePassword(@FormParam("username") String username, @For value = "Returns a list of all managed users", response = ManagedUser.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of managed users") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of managed users"), + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -248,7 +249,8 @@ public Response getManagedUsers() { value = "Returns a list of all LDAP users", response = LdapUser.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of LDAP users") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of LDAP users"), + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -272,7 +274,8 @@ public Response getLdapUsers() { value = "Returns a list of all OIDC users", response = OidcUser.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of OIDC users") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of OIDC users"), + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -371,7 +374,8 @@ public Response updateSelf(ManagedUser jsonUser) { @ApiOperation( value = "Creates a new user that references an existing LDAP object.", response = LdapUser.class, - code = 201 + code = 201, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 400, message = "Username cannot be null or blank."), @@ -408,7 +412,8 @@ public Response createLdapUser(LdapUser jsonUser) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a user.", - code = 204 + code = 204, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -443,7 +448,8 @@ public Response deleteLdapUser(LdapUser jsonUser) { @ApiOperation( value = "Creates a new user.", response = ManagedUser.class, - code = 201 + code = 201, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 400, message = "Missing required field"), @@ -496,7 +502,8 @@ public Response createManagedUser(ManagedUser jsonUser) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Updates a managed user.", - response = ManagedUser.class + response = ManagedUser.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 400, message = "Missing required field"), @@ -538,7 +545,8 @@ public Response updateManagedUser(ManagedUser jsonUser) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a user.", - code = 204 + code = 204, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -573,7 +581,8 @@ public Response deleteManagedUser(ManagedUser jsonUser) { @ApiOperation( value = "Creates a new user that references an existing OpenID Connect user.", response = OidcUser.class, - code = 201 + code = 201, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 400, message = "Username cannot be null or blank."), @@ -610,7 +619,8 @@ public Response createOidcUser(final OidcUser jsonUser) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes an OpenID Connect user.", - code = 204 + code = 204, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -644,7 +654,8 @@ public Response deleteOidcUser(final OidcUser jsonUser) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Adds the username to the specified team.", - response = UserPrincipal.class + response = UserPrincipal.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The user is already a member of the specified team"), @@ -683,7 +694,8 @@ public Response addTeamToUser( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Removes the username from the specified team.", - response = UserPrincipal.class + response = UserPrincipal.class, + notes = "

Requires permission ACCESS_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 304, message = "The user was not a member of the specified team"), diff --git a/src/main/java/org/dependencytrack/resources/v1/VexResource.java b/src/main/java/org/dependencytrack/resources/v1/VexResource.java index a07463a590..aa47764584 100644 --- a/src/main/java/org/dependencytrack/resources/v1/VexResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/VexResource.java @@ -42,7 +42,6 @@ import org.dependencytrack.resources.v1.vo.VexSubmitRequest; import org.glassfish.jersey.media.multipart.BodyPartEntity; import org.glassfish.jersey.media.multipart.FormDataBodyPart; -import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.glassfish.jersey.media.multipart.FormDataParam; import javax.validation.Validator; @@ -79,7 +78,8 @@ public class VexResource extends AlpineResource { @Produces({CycloneDxMediaType.APPLICATION_CYCLONEDX_JSON, MediaType.APPLICATION_OCTET_STREAM}) @ApiOperation( value = "Returns a VEX for a project in CycloneDX format", - response = String.class + response = String.class, + notes = "

Requires permission VULNERABILITY_ANALYSIS

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -124,11 +124,16 @@ public Response exportProjectAsCycloneDx ( @ApiOperation( value = "Upload a supported VEX document", notes = """ - Expects CycloneDX and a valid project UUID. If a UUID is not specified, \ - then the projectName and projectVersion must be specified. - The VEX will be validated against the CycloneDX schema. If schema validation fails, \ - a response with problem details in RFC 9457 format will be returned. In this case, \ - the response's content type will be application/problem+json.""" +

+ Expects CycloneDX and a valid project UUID. If a UUID is not specified, + then the projectName and projectVersion must be specified. +

+

+ The VEX will be validated against the CycloneDX schema. If schema validation fails, + a response with problem details in RFC 9457 format will be returned. In this case, + the response's content type will be application/problem+json. +

+

Requires permission VULNERABILITY_ANALYSIS

""" ) @ApiResponses(value = { @ApiResponse(code = 400, message = "Invalid VEX", response = InvalidBomProblemDetails.class), @@ -167,11 +172,16 @@ public Response uploadVex(VexSubmitRequest request) { @ApiOperation( value = "Upload a supported VEX document", notes = """ - Expects CycloneDX along and a valid project UUID. If a UUID is not specified, \ - then the projectName and projectVersion must be specified. - The VEX will be validated against the CycloneDX schema. If schema validation fails, \ - a response with problem details in RFC 9457 format will be returned. In this case, \ - the response's content type will be application/problem+json.""" +

+ Expects CycloneDX and a valid project UUID. If a UUID is not specified, + then the projectName and projectVersion must be specified. +

+

+ The VEX will be validated against the CycloneDX schema. If schema validation fails, + a response with problem details in RFC 9457 format will be returned. In this case, + the response's content type will be application/problem+json. +

+

Requires permission VULNERABILITY_ANALYSIS

""" ) @ApiResponses(value = { @ApiResponse(code = 400, message = "Invalid VEX", response = InvalidBomProblemDetails.class), @@ -183,9 +193,7 @@ public Response uploadVex(VexSubmitRequest request) { public Response uploadVex(@FormDataParam("project") String projectUuid, @FormDataParam("projectName") String projectName, @FormDataParam("projectVersion") String projectVersion, - final FormDataMultiPart multiPart) { - - final List artifactParts = multiPart.getFields("vex"); + @ApiParam(type = "string") @FormDataParam("vex") final List artifactParts) { if (projectUuid != null) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, projectUuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/ViolationAnalysisResource.java b/src/main/java/org/dependencytrack/resources/v1/ViolationAnalysisResource.java index ff01201fa4..8c4df02d4c 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ViolationAnalysisResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ViolationAnalysisResource.java @@ -66,7 +66,8 @@ public class ViolationAnalysisResource extends AlpineResource { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Retrieves a violation analysis trail", - response = ViolationAnalysis.class + response = ViolationAnalysis.class, + notes = "

Requires permission VIEW_POLICY_VIOLATION

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -100,7 +101,8 @@ public Response retrieveAnalysis(@ApiParam(value = "The UUID of the component", @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Records a violation analysis decision", - response = ViolationAnalysis.class + response = ViolationAnalysis.class, + notes = "

Requires permission POLICY_VIOLATION_ANALYSIS

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), diff --git a/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java b/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java index 5116150781..dac3bbdfc1 100644 --- a/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java @@ -79,7 +79,8 @@ public class VulnerabilityResource extends AlpineResource { value = "Returns a list of all vulnerabilities for a specific component", response = Vulnerability.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of vulnerabilities") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of vulnerabilities"), + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -112,7 +113,8 @@ public Response getVulnerabilitiesByComponent(@PathParam("uuid") String uuid, value = "Returns a list of all vulnerabilities for a specific project", response = Vulnerability.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of vulnerabilities") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of vulnerabilities"), + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -143,7 +145,8 @@ public Response getVulnerabilitiesByProject(@PathParam("uuid") String uuid, @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns a specific vulnerability", - response = Vulnerability.class + response = Vulnerability.class, + notes = "

Requires permission VULNERABILITY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -167,7 +170,8 @@ public Response getVulnerabilityByUuid(@ApiParam(value = "The UUID of the vulner @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Returns a specific vulnerability", - response = Vulnerability.class + response = Vulnerability.class, + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -201,7 +205,8 @@ public Response getVulnerabilityByVulnId(@PathParam("source") String source, value = "Returns a list of all projects affected by a specific vulnerability", response = Project.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects"), + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -235,7 +240,8 @@ public Response getAffectedProject(@PathParam("source") String source, value = "Returns a list of all vulnerabilities", response = Vulnerability.class, responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of vulnerabilities") + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of vulnerabilities"), + notes = "

Requires permission VIEW_PORTFOLIO

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -254,7 +260,8 @@ public Response getAllVulnerabilities() { @ApiOperation( value = "Creates a new vulnerability", response = Vulnerability.class, - code = 201 + code = 201, + notes = "

Requires permission VULNERABILITY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -325,7 +332,8 @@ public Response createVulnerability(Vulnerability jsonVulnerability) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Updates an internal vulnerability", - response = Project.class + response = Project.class, + notes = "

Requires permission VULNERABILITY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -400,7 +408,8 @@ public Response updateVulnerability(Vulnerability jsonVuln) { @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a vulnerability", - code = 204 + code = 204, + notes = "

Requires permission VULNERABILITY_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -437,7 +446,8 @@ public Response deleteVulnerability( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Generates an internal vulnerability identifier", - response = String.class + response = String.class, + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") @@ -493,7 +503,8 @@ public void recalculateScoresAndSeverityFromVectors(Vulnerability vuln) throws M @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @ApiOperation( - value = "Assigns a vulnerability to a component" + value = "Assigns a vulnerability to a component", + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -531,7 +542,8 @@ public Response assignVulnerability(@ApiParam(value = "The vulnerability source" @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @ApiOperation( - value = "Assigns a vulnerability to a component" + value = "Assigns a vulnerability to a component", + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -567,7 +579,8 @@ public Response assignVulnerability(@ApiParam(value = "The UUID of the vulnerabi @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @ApiOperation( - value = "Removes assignment of a vulnerability from a component" + value = "Removes assignment of a vulnerability from a component", + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -605,7 +618,8 @@ public Response unassignVulnerability(@ApiParam(value = "The vulnerability sourc @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @ApiOperation( - value = "Removes assignment of a vulnerability from a component" + value = "Removes assignment of a vulnerability from a component", + notes = "

Requires permission PORTFOLIO_MANAGEMENT

" ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), From 753924d050aa928e8698b27b5a433ef7583703a7 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 17 Mar 2024 17:43:46 +0100 Subject: [PATCH 015/412] Add OpenAPI example values to request objects Signed-off-by: nscuro --- docs/_posts/2024-xx-xx-v4.11.0.md | 6 +++- .../resources/v1/problems/ProblemDetails.java | 6 ++-- .../resources/v1/vo/BomSubmitRequest.java | 30 ++++++++++++++----- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index fb5c51d3a8..91402695c7 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -47,6 +47,7 @@ environment variable `BOM_VALIDATION_ENABLED` to `false`. * Add auto-generated changelog to GitHub releases - [apiserver/#3502] * Bump SPDX license list to v3.23 - [apiserver/#3508] * Validate uploaded BOMs against CycloneDX schema prior to processing them - [apiserver/#3522] +* Add *required permissions* to OpenAPI descriptions of endpoints - [apiserver/#3557] * Show component count in projects list - [frontend/#683] * Add current *fail*, *warn*, and *info* values to bottom of policy violation metrics - [frontend/#707] * Remove unused policy violation widget - [frontend/#710] @@ -76,6 +77,7 @@ environment variable `BOM_VALIDATION_ENABLED` to `false`. * Fix Cargo repository metadata analyzer not being invoked - [apiserver/#3511] * Fix type of `purl` fields in Swagger docs - [apiserver/#3512] * Fix CI build status badge - [apiserver/#3513] +* Fix `bom` and `vex` request fields not being visible in OpenAPI spec - [apiserver/#3557] * Fix `VUE_APP_SERVER_URL` being ignored - [frontend/#682] * Fix visibility of "Vulnerabilities" and "Policy Violations" columns not being toggle-able individually - [frontend/#686] * Fix finding search routes - [frontend/#689] @@ -117,7 +119,7 @@ For a complete list of changes, refer to the respective GitHub milestones: We thank all organizations and individuals who contributed to this release, from logging issues to taking part in discussions on GitHub & Slack to testing of fixes. Special thanks to everyone who contributed code to implement enhancements and fix defects: -[@acdha], [@AnthonyMastrean], [@LaVibeX], [@VithikaS], [@baburkin], [@fnxpt], [@kepten], [@leec94], +[@a5a351e7], [@acdha], [@AnthonyMastrean], [@LaVibeX], [@VithikaS], [@baburkin], [@fnxpt], [@kepten], [@leec94], [@lukas-braune], [@malice00], [@mehab], [@mge-mm], [@mikkeschiren], [@mykter], [@rbt-mm], [@rkg-mm], [@sahibamittal], [@sebD], [@setchy] @@ -177,6 +179,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [apiserver/#3512]: https://github.com/DependencyTrack/dependency-track/pull/3512 [apiserver/#3513]: https://github.com/DependencyTrack/dependency-track/pull/3513 [apiserver/#3522]: https://github.com/DependencyTrack/dependency-track/pull/3522 +[apiserver/#3557]: https://github.com/DependencyTrack/dependency-track/pull/3557 [frontend/#682]: https://github.com/DependencyTrack/frontend/pull/682 [frontend/#683]: https://github.com/DependencyTrack/frontend/pull/683 @@ -202,6 +205,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [frontend/#752]: https://github.com/DependencyTrack/frontend/pull/752 [frontend/#768]: https://github.com/DependencyTrack/frontend/pull/768 +[@a5a351e7]: https://github.com/a5a351e7 [@acdha]: https://github.com/acdha [@AnthonyMastrean]: https://github.com/AnthonyMastrean [@LaVibeX]: https://github.com/LaVibeX diff --git a/src/main/java/org/dependencytrack/resources/v1/problems/ProblemDetails.java b/src/main/java/org/dependencytrack/resources/v1/problems/ProblemDetails.java index b8ce606da4..acb7f88b36 100644 --- a/src/main/java/org/dependencytrack/resources/v1/problems/ProblemDetails.java +++ b/src/main/java/org/dependencytrack/resources/v1/problems/ProblemDetails.java @@ -45,7 +45,8 @@ public class ProblemDetails { @ApiModelProperty( value = "HTTP status code generated by the origin server for this occurrence of the problem", - example = "400" + example = "400", + required = true ) private Integer status; @@ -58,7 +59,8 @@ public class ProblemDetails { @ApiModelProperty( value = "Human-readable explanation specific to this occurrence of the problem", - example = "Example detail" + example = "Example detail", + required = true ) private String detail; diff --git a/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java b/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java index 7021bca27c..0f6b8b9109 100644 --- a/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java +++ b/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.swagger.annotations.ApiModelProperty; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; @@ -76,13 +77,13 @@ public BomSubmitRequest(String project, } @JsonCreator - public BomSubmitRequest(@JsonProperty(value = "project", required = false) String project, - @JsonProperty(value = "projectName", required = false) String projectName, - @JsonProperty(value = "projectVersion", required = false) String projectVersion, - @JsonProperty(value = "autoCreate", required = false) boolean autoCreate, - @JsonProperty(value = "parentUUID", required = false) String parentUUID, - @JsonProperty(value = "parentName", required = false) String parentName, - @JsonProperty(value = "parentVersion", required = false) String parentVersion, + public BomSubmitRequest(@JsonProperty(value = "project") String project, + @JsonProperty(value = "projectName") String projectName, + @JsonProperty(value = "projectVersion") String projectVersion, + @JsonProperty(value = "autoCreate") boolean autoCreate, + @JsonProperty(value = "parentUUID") String parentUUID, + @JsonProperty(value = "parentName") String parentName, + @JsonProperty(value = "parentVersion") String parentVersion, @JsonProperty(value = "bom", required = true) String bom) { this.project = project; this.projectName = projectName; @@ -94,26 +95,32 @@ public BomSubmitRequest(@JsonProperty(value = "project", required = false) Strin this.bom = bom; } + @ApiModelProperty(example = "38640b33-4ba9-4733-bdab-cbfc40c6f8aa") public String getProject() { return project; } + @ApiModelProperty(example = "Example Application") public String getProjectName() { return projectName; } + @ApiModelProperty(example = "1.0.0") public String getProjectVersion() { return projectVersion; } + @ApiModelProperty(example = "5341f53c-611b-4388-9d9c-731026dc5eec") public String getParentUUID() { return parentUUID; } + @ApiModelProperty(example = "Example Application Parent") public String getParentName() { return parentName; } + @ApiModelProperty(example = "1.0.0") public String getParentVersion() { return parentVersion; } @@ -122,6 +129,15 @@ public boolean isAutoCreate() { return autoCreate; } + @ApiModelProperty( + value = "Base64 encoded BOM", + required = true, + example = """ + ewogICJib21Gb3JtYXQiOiAiQ3ljbG9uZURYIiwKICAic3BlY1ZlcnNpb24iOiAi\ + MS40IiwKICAiY29tcG9uZW50cyI6IFsKICAgIHsKICAgICAgInR5cGUiOiAibGli\ + cmFyeSIsCiAgICAgICJuYW1lIjogImFjbWUtbGliIiwKICAgICAgInZlcnNpb24i\ + OiAiMS4wLjAiCiAgICB9CiAgXQp9""" + ) public String getBom() { return bom; } From a6804a49960ada72bc38cee4b36d52d949d4e6f3 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 17 Mar 2024 19:36:13 +0100 Subject: [PATCH 016/412] Provide meaningful error message for `bom` and `vex` exceeding Jackson's character limit Also document the limitation in the OpenAPI spec of the respective `PUT` methods that accept JSON payloads. Fixes #3182 Signed-off-by: nscuro --- docs/_posts/2024-xx-xx-v4.11.0.md | 2 + .../resources/v1/BomResource.java | 5 ++ .../resources/v1/VexResource.java | 5 ++ .../exception/JsonMappingExceptionMapper.java | 86 +++++++++++++++++++ .../resources/v1/BomResourceTest.java | 36 +++++++- .../resources/v1/VexResourceTest.java | 35 +++++++- 6 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/dependencytrack/resources/v1/exception/JsonMappingExceptionMapper.java diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index 91402695c7..36612d2fb3 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -78,6 +78,7 @@ environment variable `BOM_VALIDATION_ENABLED` to `false`. * Fix type of `purl` fields in Swagger docs - [apiserver/#3512] * Fix CI build status badge - [apiserver/#3513] * Fix `bom` and `vex` request fields not being visible in OpenAPI spec - [apiserver/#3557] +* Fix unclear error response when base64 encoded `bom` and `vex` values exceed character limit - [apiserver/#3558] * Fix `VUE_APP_SERVER_URL` being ignored - [frontend/#682] * Fix visibility of "Vulnerabilities" and "Policy Violations" columns not being toggle-able individually - [frontend/#686] * Fix finding search routes - [frontend/#689] @@ -180,6 +181,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [apiserver/#3513]: https://github.com/DependencyTrack/dependency-track/pull/3513 [apiserver/#3522]: https://github.com/DependencyTrack/dependency-track/pull/3522 [apiserver/#3557]: https://github.com/DependencyTrack/dependency-track/pull/3557 +[apiserver/#3558]: https://github.com/DependencyTrack/dependency-track/pull/3558 [frontend/#682]: https://github.com/DependencyTrack/frontend/pull/682 [frontend/#683]: https://github.com/DependencyTrack/frontend/pull/683 diff --git a/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/src/main/java/org/dependencytrack/resources/v1/BomResource.java index 37b22a5f74..0170cdd07c 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -221,6 +221,11 @@ public Response exportComponentAsCycloneDx ( a response with problem details in RFC 9457 format will be returned. In this case, the response's content type will be application/problem+json.

+

+ The maximum allowed length of the bom value is 20'000'000 characters. + When uploading large BOMs, the POST endpoint is preferred, + as it does not have this limit. +

Requires permission BOM_UPLOAD

""", response = BomUploadResponse.class, nickname = "UploadBomBase64Encoded" diff --git a/src/main/java/org/dependencytrack/resources/v1/VexResource.java b/src/main/java/org/dependencytrack/resources/v1/VexResource.java index aa47764584..beb76c6667 100644 --- a/src/main/java/org/dependencytrack/resources/v1/VexResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/VexResource.java @@ -133,6 +133,11 @@ public Response exportProjectAsCycloneDx ( a response with problem details in RFC 9457 format will be returned. In this case, the response's content type will be application/problem+json.

+

+ The maximum allowed length of the vex value is 20'000'000 characters. + When uploading large VEX files, the POST endpoint is preferred, + as it does not have this limit. +

Requires permission VULNERABILITY_ANALYSIS

""" ) @ApiResponses(value = { diff --git a/src/main/java/org/dependencytrack/resources/v1/exception/JsonMappingExceptionMapper.java b/src/main/java/org/dependencytrack/resources/v1/exception/JsonMappingExceptionMapper.java new file mode 100644 index 0000000000..d431391478 --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/exception/JsonMappingExceptionMapper.java @@ -0,0 +1,86 @@ +/* + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.resources.v1.exception; + +import com.fasterxml.jackson.core.exc.StreamConstraintsException; +import com.fasterxml.jackson.databind.JsonMappingException; +import org.dependencytrack.resources.v1.problems.ProblemDetails; +import org.dependencytrack.resources.v1.vo.BomSubmitRequest; +import org.dependencytrack.resources.v1.vo.VexSubmitRequest; + +import javax.annotation.Priority; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import java.util.Objects; + +/** + * @since 4.11.0 + */ +@Provider +@Priority(1) +public class JsonMappingExceptionMapper implements ExceptionMapper { + + @Context + private HttpServletRequest request; + + @Context + private ResourceInfo resourceInfo; + + @Override + public Response toResponse(final JsonMappingException exception) { + final var problemDetails = new ProblemDetails(); + problemDetails.setStatus(400); + problemDetails.setTitle("The provided JSON payload could not be mapped"); + problemDetails.setDetail(createDetail(exception)); + + return Response + .status(Response.Status.BAD_REQUEST) + .type(ProblemDetails.MEDIA_TYPE_JSON) + .entity(problemDetails) + .build(); + } + + private static String createDetail(final JsonMappingException exception) { + if (!(exception.getCause() instanceof StreamConstraintsException)) { + return exception.getMessage(); + } + + final JsonMappingException.Reference reference = exception.getPath().get(0); + if (Objects.equals(reference.getFrom(), BomSubmitRequest.class) + && "bom".equals(reference.getFieldName())) { + return """ + The BOM is too large to be transmitted safely via Base64 encoded JSON value. \ + Please use the "POST /api/v1/bom" endpoint with Content-Type "multipart/form-data" instead. \ + Original cause: %s""".formatted(exception.getMessage()); + } else if (Objects.equals(reference.getFrom(), VexSubmitRequest.class) + && "vex".equals(reference.getFieldName())) { + return """ + The VEX is too large to be transmitted safely via Base64 encoded JSON value. \ + Please use the "POST /api/v1/vex" endpoint with Content-Type "multipart/form-data" instead. \ + Original cause: %s""".formatted(exception.getMessage()); + } + + return exception.getMessage(); + } + +} diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index dbea0b351b..6f817307fb 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -21,6 +21,7 @@ import alpine.common.util.UuidUtil; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import com.fasterxml.jackson.core.StreamReadConstraints; import org.apache.http.HttpStatus; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; @@ -35,6 +36,7 @@ import org.dependencytrack.model.Severity; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.parser.cyclonedx.CycloneDxValidator; +import org.dependencytrack.resources.v1.exception.JsonMappingExceptionMapper; import org.dependencytrack.resources.v1.vo.BomSubmitRequest; import org.dependencytrack.tasks.scanners.AnalyzerIdentity; import org.glassfish.jersey.media.multipart.MultiPartFeature; @@ -68,7 +70,8 @@ protected DeploymentContext configureDeployment() { new ResourceConfig(BomResource.class) .register(ApiFilter.class) .register(AuthenticationFilter.class) - .register(MultiPartFeature.class))) + .register(MultiPartFeature.class) + .register(JsonMappingExceptionMapper.class))) .build(); } @@ -930,4 +933,35 @@ public void uploadBomInvalidXmlTest() { """); } + @Test + public void uploadBomTooLargeViaPutTest() { + initializeWithPermissions(Permissions.BOM_UPLOAD); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + final String bom = "a".repeat(StreamReadConstraints.DEFAULT_MAX_STRING_LEN + 1); + + final Response response = target(V1_BOM).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "projectName": "acme-app", + "projectVersion": "1.0.0", + "bom": "%s" + } + """.formatted(bom), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.getHeaderString("Content-Type")).isEqualTo("application/problem+json"); + assertThatJson(getPlainTextBody(response)).isEqualTo(""" + { + "status": 400, + "title": "The provided JSON payload could not be mapped", + "detail": "The BOM is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/bom\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String length (20000001) exceeds the maximum length (20000000) (through reference chain: org.dependencytrack.resources.v1.vo.BomSubmitRequest[\\"bom\\"])" + } + """); + } + } diff --git a/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java index f15fcbe067..089c744ad6 100644 --- a/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java @@ -20,6 +20,7 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import com.fasterxml.jackson.core.StreamReadConstraints; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.AnalysisResponse; @@ -30,6 +31,7 @@ import org.dependencytrack.model.Severity; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.parser.cyclonedx.CycloneDxValidator; +import org.dependencytrack.resources.v1.exception.JsonMappingExceptionMapper; import org.dependencytrack.tasks.scanners.AnalyzerIdentity; import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.server.ResourceConfig; @@ -41,7 +43,6 @@ import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; - import java.util.Base64; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; @@ -57,7 +58,8 @@ protected DeploymentContext configureDeployment() { new ResourceConfig(VexResource.class) .register(ApiFilter.class) .register(AuthenticationFilter.class) - .register(MultiPartFeature.class))) + .register(MultiPartFeature.class) + .register(JsonMappingExceptionMapper.class))) .build(); } @@ -306,4 +308,33 @@ public void uploadVexInvalidXmlTest() { """); } + @Test + public void uploadVexTooLargeViaPutTest() { + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + final String vex = "a".repeat(StreamReadConstraints.DEFAULT_MAX_STRING_LEN + 1); + + final Response response = target(V1_VEX).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "projectName": "acme-app", + "projectVersion": "1.0.0", + "vex": "%s" + } + """.formatted(vex), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.getHeaderString("Content-Type")).isEqualTo("application/problem+json"); + assertThatJson(getPlainTextBody(response)).isEqualTo(""" + { + "status": 400, + "title": "The provided JSON payload could not be mapped", + "detail": "The VEX is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/vex\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String length (20000001) exceeds the maximum length (20000000) (through reference chain: org.dependencytrack.resources.v1.vo.VexSubmitRequest[\\"vex\\"])" + } + """); + } + } \ No newline at end of file From d97856eedb7ddab59a46ed5e0215ebff65bf96bc Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 17 Mar 2024 19:49:03 +0100 Subject: [PATCH 017/412] Fix unhandled `NotFoundException`s causing a `HTTP 500` response Previously, requests to routes that had no explicit servlet mapping ended up being handled by the `GlobalExceptionHandler` of Alpine, which logs the exception as `error`, and returns a `HTTP 500 Internal Server Error` response. This behavior was misleading and could flood the logs unnecessarily. We now only return a `HTTP 404 Not Found` response, without emitting any logs. Signed-off-by: nscuro --- docs/_posts/2024-xx-xx-v4.11.0.md | 2 + .../v1/exception/NotFoundExceptionMapper.java | 42 +++++++++++++++++++ .../NotFoundExceptionMapperTest.java | 38 +++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 src/main/java/org/dependencytrack/resources/v1/exception/NotFoundExceptionMapper.java create mode 100644 src/test/java/org/dependencytrack/resources/v1/exception/NotFoundExceptionMapperTest.java diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index 36612d2fb3..4077ff63c5 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -79,6 +79,7 @@ environment variable `BOM_VALIDATION_ENABLED` to `false`. * Fix CI build status badge - [apiserver/#3513] * Fix `bom` and `vex` request fields not being visible in OpenAPI spec - [apiserver/#3557] * Fix unclear error response when base64 encoded `bom` and `vex` values exceed character limit - [apiserver/#3558] +* Fix unhandled `NotFoundException`s causing a `HTTP 500` response - [apiserver/#3559] * Fix `VUE_APP_SERVER_URL` being ignored - [frontend/#682] * Fix visibility of "Vulnerabilities" and "Policy Violations" columns not being toggle-able individually - [frontend/#686] * Fix finding search routes - [frontend/#689] @@ -182,6 +183,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [apiserver/#3522]: https://github.com/DependencyTrack/dependency-track/pull/3522 [apiserver/#3557]: https://github.com/DependencyTrack/dependency-track/pull/3557 [apiserver/#3558]: https://github.com/DependencyTrack/dependency-track/pull/3558 +[apiserver/#3559]: https://github.com/DependencyTrack/dependency-track/pull/3559 [frontend/#682]: https://github.com/DependencyTrack/frontend/pull/682 [frontend/#683]: https://github.com/DependencyTrack/frontend/pull/683 diff --git a/src/main/java/org/dependencytrack/resources/v1/exception/NotFoundExceptionMapper.java b/src/main/java/org/dependencytrack/resources/v1/exception/NotFoundExceptionMapper.java new file mode 100644 index 0000000000..0bd2fcff08 --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/exception/NotFoundExceptionMapper.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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.resources.v1.exception; + +import alpine.server.resources.GlobalExceptionHandler; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * An {@link ExceptionMapper} to handle {@link NotFoundException}, that would otherwise be + * handled by Alpine's {@link GlobalExceptionHandler}, resulting in a misleading {@code HTTP 500} response. + * + * @since 4.11.0 + */ +@Provider +public class NotFoundExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(final NotFoundException exception) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + +} diff --git a/src/test/java/org/dependencytrack/resources/v1/exception/NotFoundExceptionMapperTest.java b/src/test/java/org/dependencytrack/resources/v1/exception/NotFoundExceptionMapperTest.java new file mode 100644 index 0000000000..3230c080d6 --- /dev/null +++ b/src/test/java/org/dependencytrack/resources/v1/exception/NotFoundExceptionMapperTest.java @@ -0,0 +1,38 @@ +/* + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.resources.v1.exception; + +import org.junit.Test; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.Response; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NotFoundExceptionMapperTest { + + @Test + @SuppressWarnings("resource") + public void testToResponse() { + final Response response = new NotFoundExceptionMapper().toResponse(new NotFoundException()); + assertThat(response.getStatus()).isEqualTo(404); + assertThat(response.getEntity()).isNull(); + } + +} \ No newline at end of file From 72121d6680d2ee553792ea7b5f83d1eecb7f87a3 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 17 Mar 2024 21:27:19 +0100 Subject: [PATCH 018/412] Extend length of `PURL` and `PURLCOORDINATES` columns from 255 to 786 Because PURLs are also used to populate the `TARGET` column of `COMPONENTANALYSISCACHE`, that column's length also has to be extended. Fixes #2076 Signed-off-by: nscuro --- docs/_posts/2024-xx-xx-v4.11.0.md | 7 +++ .../org/dependencytrack/model/Component.java | 6 +- .../model/ComponentAnalysisCache.java | 2 +- .../org/dependencytrack/model/Project.java | 3 +- .../upgrade/v4110/v4110Updater.java | 56 ++++++++++++++++++- 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index 4077ff63c5..7c91914d7d 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -80,6 +80,7 @@ environment variable `BOM_VALIDATION_ENABLED` to `false`. * Fix `bom` and `vex` request fields not being visible in OpenAPI spec - [apiserver/#3557] * Fix unclear error response when base64 encoded `bom` and `vex` values exceed character limit - [apiserver/#3558] * Fix unhandled `NotFoundException`s causing a `HTTP 500` response - [apiserver/#3559] +* Fix inability to store PURLs longer than 255 characters - [apiserver/#3560] * Fix `VUE_APP_SERVER_URL` being ignored - [frontend/#682] * Fix visibility of "Vulnerabilities" and "Policy Violations" columns not being toggle-able individually - [frontend/#686] * Fix finding search routes - [frontend/#689] @@ -112,6 +113,11 @@ and updated automatically upon upgrade, based on CVSSv2, CVSSv3, and OWASP Risk * The following default values for configuration properties have changed: * `ossindex.retry.backoff.max.duration.ms`: 600000ms (10min) → 60000ms (1min) * The `name` tag of the `resilience4j_retry_calls_total` for OSS Index has changed from `ossIndexRetryer` to `ossindex-api` +* The types of the following columns are changed from `VARCHAR(255)` to `VARCHAR(786)` automatically upon upgrade: + * `COMPONENT.PURL` + * `COMPONENT.PURLCOORDINATES` + * `COMPONENTANALYSISCACHE.TARGET` + * `PROJECT.PURL` For a complete list of changes, refer to the respective GitHub milestones: @@ -184,6 +190,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [apiserver/#3557]: https://github.com/DependencyTrack/dependency-track/pull/3557 [apiserver/#3558]: https://github.com/DependencyTrack/dependency-track/pull/3558 [apiserver/#3559]: https://github.com/DependencyTrack/dependency-track/pull/3559 +[apiserver/#3560]: https://github.com/DependencyTrack/dependency-track/pull/3560 [frontend/#682]: https://github.com/DependencyTrack/frontend/pull/682 [frontend/#683]: https://github.com/DependencyTrack/frontend/pull/683 diff --git a/src/main/java/org/dependencytrack/model/Component.java b/src/main/java/org/dependencytrack/model/Component.java index fcba320057..efe02b6149 100644 --- a/src/main/java/org/dependencytrack/model/Component.java +++ b/src/main/java/org/dependencytrack/model/Component.java @@ -248,7 +248,8 @@ public enum FetchGroup { @Persistent(defaultFetchGroup = "true") @Index(name = "COMPONENT_PURL_IDX") - @Size(max = 255) + @Column(name = "PURL", length = 786) + @Size(max = 786) @com.github.packageurl.validator.PackageURL @JsonDeserialize(using = TrimmedStringDeserializer.class) @ApiModelProperty(dataType = "string") @@ -256,7 +257,8 @@ public enum FetchGroup { @Persistent(defaultFetchGroup = "true") @Index(name = "COMPONENT_PURL_COORDINATES_IDX") - @Size(max = 255) + @Column(name = "PURLCOORDINATES", length = 786) + @Size(max = 786) @com.github.packageurl.validator.PackageURL @JsonDeserialize(using = TrimmedStringDeserializer.class) private String purlCoordinates; // Field should contain only type, namespace, name, and version. Everything up to the qualifiers diff --git a/src/main/java/org/dependencytrack/model/ComponentAnalysisCache.java b/src/main/java/org/dependencytrack/model/ComponentAnalysisCache.java index f935eb0ad3..96e7383d32 100644 --- a/src/main/java/org/dependencytrack/model/ComponentAnalysisCache.java +++ b/src/main/java/org/dependencytrack/model/ComponentAnalysisCache.java @@ -84,7 +84,7 @@ public enum CacheType { private String targetType; @Persistent - @Column(name = "TARGET", allowsNull = "false") + @Column(name = "TARGET", allowsNull = "false", length = 786) @NotNull private String target; diff --git a/src/main/java/org/dependencytrack/model/Project.java b/src/main/java/org/dependencytrack/model/Project.java index 1f8964b9b4..b234845f10 100644 --- a/src/main/java/org/dependencytrack/model/Project.java +++ b/src/main/java/org/dependencytrack/model/Project.java @@ -195,7 +195,8 @@ public enum FetchGroup { @Persistent @Index(name = "PROJECT_PURL_IDX") - @Size(max = 255) + @Column(name = "PURL", length = 786) + @Size(max = 786) @com.github.packageurl.validator.PackageURL @JsonDeserialize(using = TrimmedStringDeserializer.class) @ApiModelProperty(dataType = "string") diff --git a/src/main/java/org/dependencytrack/upgrade/v4110/v4110Updater.java b/src/main/java/org/dependencytrack/upgrade/v4110/v4110Updater.java index aed0b8c6a8..66ee37efc3 100644 --- a/src/main/java/org/dependencytrack/upgrade/v4110/v4110Updater.java +++ b/src/main/java/org/dependencytrack/upgrade/v4110/v4110Updater.java @@ -21,6 +21,7 @@ import alpine.common.logging.Logger; import alpine.persistence.AlpineQueryManager; import alpine.server.upgrade.AbstractUpgradeItem; +import alpine.server.util.DbUtil; import org.dependencytrack.model.Severity; import org.dependencytrack.util.VulnerabilityUtil; @@ -42,6 +43,7 @@ public String getSchemaVersion() { public void executeUpgrade(final AlpineQueryManager qm, final Connection connection) throws Exception { dropCweTable(connection); computeVulnerabilitySeverities(connection); + extendPurlColumnLengths(connection); } private static void dropCweTable(final Connection connection) throws Exception { @@ -59,10 +61,17 @@ private static void dropCweTable(final Connection connection) throws Exception { ALTER TABLE "VULNERABILITY" DROP CONSTRAINT IF EXISTS "VULNERABILITY_FK1" """); - LOGGER.info("Dropping index \"VULNERABILITY\".\"VULNERABILITY_CWE_IDX\""); - stmt.executeUpdate(""" + if (DbUtil.isH2()) { + LOGGER.info("Dropping index \"VULNERABILITY_CWE_IDX\""); + stmt.executeUpdate(""" + DROP INDEX IF EXISTS "VULNERABILITY_CWE_IDX" + """); + } else { + LOGGER.info("Dropping index \"VULNERABILITY\".\"VULNERABILITY_CWE_IDX\""); + stmt.executeUpdate(""" DROP INDEX IF EXISTS "VULNERABILITY"."VULNERABILITY_CWE_IDX" """); + } LOGGER.info("Dropping column \"VULNERABILITY\".\"CWE\""); stmt.executeUpdate(""" @@ -139,4 +148,47 @@ private static void computeVulnerabilitySeverities(final Connection connection) } } + private static void extendPurlColumnLengths(final Connection connection) throws Exception { + LOGGER.info("Extending length of PURL and PURLCOORDINATES columns from 255 to 786"); + if (DbUtil.isH2() || DbUtil.isPostgreSQL()) { + try (final Statement statement = connection.createStatement()) { + statement.addBatch(""" + ALTER TABLE "COMPONENT" ALTER COLUMN "PURL" SET DATA TYPE VARCHAR(786)"""); + statement.addBatch(""" + ALTER TABLE "COMPONENT" ALTER COLUMN "PURLCOORDINATES" SET DATA TYPE VARCHAR(786)"""); + statement.addBatch(""" + ALTER TABLE "COMPONENTANALYSISCACHE" ALTER COLUMN "TARGET" SET DATA TYPE VARCHAR(786)"""); + statement.addBatch(""" + ALTER TABLE "PROJECT" ALTER COLUMN "PURL" SET DATA TYPE VARCHAR(786)"""); + statement.executeBatch(); + } + } else if (DbUtil.isMssql()) { + try (final Statement statement = connection.createStatement()) { + statement.addBatch(""" + ALTER TABLE "COMPONENT" ALTER COLUMN "PURL" VARCHAR(786) NULL"""); + statement.addBatch(""" + ALTER TABLE "COMPONENT" ALTER COLUMN "PURLCOORDINATES" VARCHAR(786) NULL"""); + statement.addBatch(""" + ALTER TABLE "COMPONENTANALYSISCACHE" ALTER COLUMN "TARGET" VARCHAR(786) NOT NULL"""); + statement.addBatch(""" + ALTER TABLE "PROJECT" ALTER COLUMN "PURL" VARCHAR(786) NULL"""); + statement.executeBatch(); + } + } else if (DbUtil.isMysql()) { + try (final Statement statement = connection.createStatement()) { + statement.addBatch(""" + ALTER TABLE "COMPONENT" MODIFY COLUMN "PURL" VARCHAR(786)"""); + statement.addBatch(""" + ALTER TABLE "COMPONENT" MODIFY COLUMN "PURLCOORDINATES" VARCHAR(786)"""); + statement.addBatch(""" + ALTER TABLE "COMPONENTANALYSISCACHE" MODIFY COLUMN "TARGET" VARCHAR(786)"""); + statement.addBatch(""" + ALTER TABLE "PROJECT" MODIFY COLUMN "PURL" VARCHAR(786)"""); + statement.executeBatch(); + } + } else { + throw new IllegalStateException("Unrecognized database type"); + } + } + } From 8069b36742dcca64a63ef134019428b9338d57e4 Mon Sep 17 00:00:00 2001 From: Aravind Parappil Date: Mon, 18 Mar 2024 01:14:53 -0400 Subject: [PATCH 019/412] Generate SARIF File Of Project Vulnerability Findings If request header `Accept: application/sarif+json` is provided to the GET Finding By Project UUID API, it will now return a SARIF file with the vulnerability findings in that project. SARIF file is generated based on a pebble template Signed-off-by: Aravind Parappil --- .../resources/v1/FindingResource.java | 41 ++++++++++++-- .../resources/templates/findings/sarif.peb | 54 +++++++++++++++++++ .../resources/v1/FindingResourceTest.java | 43 +++++++++++++++ 3 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/templates/findings/sarif.peb diff --git a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index cec9c4cecb..b76526526f 100644 --- a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -20,9 +20,12 @@ import alpine.common.logging.Logger; import alpine.event.framework.Event; +import alpine.model.About; import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; +import io.pebbletemplates.pebble.PebbleEngine; +import io.pebbletemplates.pebble.template.PebbleTemplate; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -30,6 +33,11 @@ import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; import io.swagger.annotations.ResponseHeader; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.core.Response.Status; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.PolicyEvaluationEvent; import org.dependencytrack.event.RepositoryMetaEvent; @@ -67,12 +75,13 @@ public class FindingResource extends AlpineResource { private static final Logger LOGGER = Logger.getLogger(FindingResource.class); + public static final String MEDIA_TYPE_SARIF_JSON = "application/sarif+json"; @GET @Path("/project/{uuid}") - @Produces(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON, MEDIA_TYPE_SARIF_JSON}) @ApiOperation( - value = "Returns a list of all findings for a specific project", + value = "Returns a list of all findings for a specific project or generates SARIF file if Accept: application/sarif+json header is provided", response = Finding.class, responseContainer = "List", responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of findings") @@ -87,13 +96,24 @@ public Response getFindingsByProject(@PathParam("uuid") String uuid, @ApiParam(value = "Optionally includes suppressed findings") @QueryParam("suppressed") boolean suppressed, @ApiParam(value = "Optionally limit findings to specific sources of vulnerability intelligence") - @QueryParam("source") Vulnerability.Source source) { + @QueryParam("source") Vulnerability.Source source, + @HeaderParam("accept") String acceptHeader) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { if (qm.hasAccess(super.getPrincipal(), project)) { //final long totalCount = qm.getVulnerabilityCount(project, suppressed); final List findings = qm.getFindings(project, suppressed); + if (acceptHeader != null && acceptHeader.contains(MEDIA_TYPE_SARIF_JSON)) { + try { + return Response.ok(generateSARIF(findings), MEDIA_TYPE_SARIF_JSON) + .header("content-disposition","attachment; filename=\"findings-" + uuid + ".sarif\"") + .build(); + } catch (IOException ioException) { + LOGGER.error(ioException.getMessage(), ioException); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("An error occurred while generating SARIF file").build(); + } + } if (source != null) { final List filteredList = findings.stream().filter(finding -> source.name().equals(finding.getVulnerability().get("source"))).collect(Collectors.toList()); return Response.ok(filteredList).header(TOTAL_COUNT_HEADER, filteredList.size()).build(); @@ -298,4 +318,19 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") } } + private String generateSARIF(List findings) throws IOException { + final PebbleEngine engine = new PebbleEngine.Builder().newLineTrimming(false).build(); + final PebbleTemplate sarifTemplate = engine.getTemplate("templates/findings/sarif.peb"); + + final Map context = new HashMap<>(); + final About about = new About(); + context.put("findings", findings); + context.put("dependencyTrackVersion", about.getVersion()); + + try (final Writer writer = new StringWriter()) { + sarifTemplate.evaluate(writer, context); + return writer.toString(); + } + } + } diff --git a/src/main/resources/templates/findings/sarif.peb b/src/main/resources/templates/findings/sarif.peb new file mode 100644 index 0000000000..6c4e600841 --- /dev/null +++ b/src/main/resources/templates/findings/sarif.peb @@ -0,0 +1,54 @@ +{ + "version": "2.1.0", + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "name": "OWASP Dependency-Track", + "version": "{{ dependencyTrackVersion }}", + "informationUri": "https://dependencytrack.org/", + "rules": [{% for finding in findings %} + { + "id": "{{ finding.vulnerability.vulnId }}", + "name": "{{ finding.component.name }} - {{ finding.vulnerability.cweName }}", + "shortDescription": { + "text": "{{ finding.vulnerability.description | split('\n') | join(' ') | trim }}" + } + }{% if not loop.last %},{% endif %}{% endfor %} + ] + } + }, + "results": [{% for finding in findings %} + { + "ruleId": "{{ finding.vulnerability.vulnId }}", + "message": { + "text": "{{ finding.vulnerability.description | split('\n') | join(' ') | trim }}" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "{{ finding.component.purl }}" + } + } + } + ], + "level": {% if ['LOW', 'INFO'] contains finding.vulnerability.severity %}"note",{% elseif finding.vulnerability.severity == 'MEDIUM' %}"warning",{% elseif ['HIGH', 'CRITICAL'] contains finding.vulnerability.severity %}"error",{% else %}"none",{% endif %} + "properties": { + "name": "{{ finding.component.name }}", + "group": "{{ finding.component.group }}", + "version": "{{ finding.component.version }}", + "source": "{{ finding.vulnerability.source }}", + "cweId": "{{ finding.vulnerability.cweId }}", + "cvssV3BaseScore": "{{ finding.vulnerability.cvssV3BaseScore }}", + "epssScore": "{{ finding.vulnerability.epssScore }}", + "epssPercentile": "{{ finding.vulnerability.epssPercentile }}", + "severityRank": "{{ finding.vulnerability.severityRank }}", + "recommendation": "{{ finding.vulnerability.recommendation | split('\n') | join(' ') | trim }}" + } + }{% if not loop.last %},{% endif %}{% endfor %} + ] + } + ] +} diff --git a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java index 278a190268..96079b4304 100644 --- a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java @@ -18,11 +18,15 @@ */ package org.dependencytrack.resources.v1; +import static org.dependencytrack.resources.v1.FindingResource.MEDIA_TYPE_SARIF_JSON; + import alpine.Config; +import alpine.model.About; import alpine.model.ConfigProperty; import alpine.model.Team; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import javax.ws.rs.core.HttpHeaders; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; @@ -589,6 +593,45 @@ public void getAllFindingsGroupedByVulnerabilityWithAclEnabled() { Assert.assertEquals(1, json.getJsonObject(2).getJsonObject("vulnerability").getInt("affectedProjectCount")); } + @Test + public void getSARIFFindingsByProjectTest() { + Project p1 = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + Component c1 = createComponent(p1, "Component A", "1.0"); + Component c2 = createComponent(p1, "Component B", "1.0"); + Component c3 = createComponent(p1, "Component C", "1.0"); + Vulnerability v1 = createVulnerability("Vuln-1", Severity.CRITICAL); + Vulnerability v2 = createVulnerability("Vuln-2", Severity.HIGH); + Vulnerability v3 = createVulnerability("Vuln-3", Severity.MEDIUM); + Vulnerability v4 = createVulnerability("Vuln-4", Severity.LOW); + qm.addVulnerability(v1, c1, AnalyzerIdentity.NONE); + qm.addVulnerability(v2, c1, AnalyzerIdentity.NONE); + qm.addVulnerability(v3, c2, AnalyzerIdentity.NONE); + qm.addVulnerability(v4, c3, AnalyzerIdentity.NONE); + + Response response = target(V1_FINDING + "/project/" + p1.getUuid().toString()).request() + .header(HttpHeaders.ACCEPT, MEDIA_TYPE_SARIF_JSON) + .header(X_API_KEY, apiKey) + .get(Response.class); + + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals(MEDIA_TYPE_SARIF_JSON, response.getHeaderString(HttpHeaders.CONTENT_TYPE)); + + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + + JsonArray runs = json.getJsonArray("runs"); + Assert.assertNotNull(runs); + JsonObject runsJson = runs.getJsonObject(0); + Assert.assertNotNull(runsJson); + Assert.assertEquals("OWASP Dependency-Track", runsJson.getJsonObject("tool").getJsonObject("driver").getString("name")); + Assert.assertEquals(new About().getVersion(), runsJson.getJsonObject("tool").getJsonObject("driver").getString("version")); + Assert.assertNotNull(runsJson.getJsonObject("tool").getJsonObject("driver").getJsonArray("rules")); + Assert.assertEquals("Vuln-1", runsJson.getJsonObject("tool").getJsonObject("driver").getJsonArray("rules").getJsonObject(0).getString("id")); + Assert.assertNotNull(runsJson.getJsonArray("results")); + Assert.assertEquals("error", runsJson.getJsonArray("results").getJsonObject(0).getString("level")); + Assert.assertEquals("Vuln-1", runsJson.getJsonArray("results").getJsonObject(0).getString("ruleId")); + } + private Component createComponent(Project project, String name, String version) { Component component = new Component(); component.setProject(project); From c8db7f30169e350e200d3974bdbfc20ea6370d08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 08:03:20 +0000 Subject: [PATCH 020/412] Bump docker/setup-buildx-action from 3.1.0 to 3.2.0 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.1.0 to 3.2.0. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/0d103c3126aa41d772a8362f6aa67afac040f80c...2b51285047da1547ffb1b2203d8be4c0af6b1f20) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index f19f5ff0c3..4cf069eb86 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -86,7 +86,7 @@ jobs: uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # tag=v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@0d103c3126aa41d772a8362f6aa67afac040f80c # tag=v3.1.0 + uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20 # tag=v3.2.0 id: buildx with: install: true From 73c0d40ebbd123d288ee3dce2e6f486558ee0cab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 08:03:24 +0000 Subject: [PATCH 021/412] Bump actions/checkout from 4.1.1 to 4.1.2 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.1 to 4.1.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/b4ffde65f46336ab88eb53be808477a3936bae11...9bb56186c3b09b4f86b1c65136769dd318469633) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 4 ++-- .github/workflows/ci-publish.yaml | 4 ++-- .github/workflows/ci-release.yaml | 6 +++--- .github/workflows/ci-test.yaml | 2 +- .github/workflows/dependency-review.yaml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index f19f5ff0c3..e033bb2d82 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4.1.1 + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 - name: Set up JDK uses: actions/setup-java@9704b39bf258b59bc04b50fa2dd55e9ed76b47a8 # tag=v4.1.0 @@ -74,7 +74,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4.1.1 + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 - name: Download Artifacts uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # tag=v4.1.4 diff --git a/.github/workflows/ci-publish.yaml b/.github/workflows/ci-publish.yaml index 89ebf9c36a..326f46ec7f 100644 --- a/.github/workflows/ci-publish.yaml +++ b/.github/workflows/ci-publish.yaml @@ -23,7 +23,7 @@ jobs: exit 1 fi - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4.1.1 + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 - name: Parse Version from POM id: parse @@ -51,7 +51,7 @@ jobs: - call-build steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4.1.1 + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 - name: Download Artifacts uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # tag=v4.1.4 diff --git a/.github/workflows/ci-release.yaml b/.github/workflows/ci-release.yaml index d25a97a1cf..cc4847816b 100644 --- a/.github/workflows/ci-release.yaml +++ b/.github/workflows/ci-release.yaml @@ -20,7 +20,7 @@ jobs: release-branch: ${{ steps.variables.outputs.release-branch }} steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4.1.1 + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 - name: Setup Environment id: variables @@ -51,7 +51,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4.1.1 + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 - name: Set up JDK uses: actions/setup-java@9704b39bf258b59bc04b50fa2dd55e9ed76b47a8 # tag=v4.1.0 @@ -118,7 +118,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4.1.1 + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 with: ref: ${{ needs.prepare-release.outputs.release-branch }} diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index d8aa8b2039..b044c613bb 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4.1.1 + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 - name: Set up JDK uses: actions/setup-java@9704b39bf258b59bc04b50fa2dd55e9ed76b47a8 # tag=v4.1.0 diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index 330a7a6d67..3a321cedbc 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4.1.1 + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 - name: Dependency Review uses: actions/dependency-review-action@9129d7d40b8c12c1ed0f60400d00c92d437adcce # tag=v4.1.3 From 939e634ddeaea8a0d74d1e33533f6d5fbd22a608 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 08:03:27 +0000 Subject: [PATCH 022/412] Bump docker/build-push-action from 5.2.0 to 5.3.0 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5.2.0 to 5.3.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/af5a7ed5ba88268d5278f7203fb52cd833f66d6e...2cdde995de11925a030ce8070c3d77a52ffcf1c0) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index f19f5ff0c3..359706c128 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -109,7 +109,7 @@ jobs: echo "tags=${TAGS}" >> $GITHUB_OUTPUT - name: Build multi-arch Container Image - uses: docker/build-push-action@af5a7ed5ba88268d5278f7203fb52cd833f66d6e # tag=v5.2.0 + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # tag=v5.3.0 with: tags: ${{ steps.tags.outputs.tags }} build-args: |- From aed6f46a09a86f701bb37a64a1ae7ab2a680cd7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 09:42:47 +0000 Subject: [PATCH 023/412] Bump actions/setup-java from 4.1.0 to 4.2.1 Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4.1.0 to 4.2.1. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/9704b39bf258b59bc04b50fa2dd55e9ed76b47a8...99b8673ff64fbf99d8d325f52d9a5bdedb8483e9) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- .github/workflows/ci-release.yaml | 2 +- .github/workflows/ci-test.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index ab6eef0753..0f45dfb3a4 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 - name: Set up JDK - uses: actions/setup-java@9704b39bf258b59bc04b50fa2dd55e9ed76b47a8 # tag=v4.1.0 + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v4.2.1 with: distribution: 'temurin' java-version: '17' diff --git a/.github/workflows/ci-release.yaml b/.github/workflows/ci-release.yaml index cc4847816b..626bfe8eb6 100644 --- a/.github/workflows/ci-release.yaml +++ b/.github/workflows/ci-release.yaml @@ -54,7 +54,7 @@ jobs: uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 - name: Set up JDK - uses: actions/setup-java@9704b39bf258b59bc04b50fa2dd55e9ed76b47a8 # tag=v4.1.0 + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v4.2.1 with: distribution: 'temurin' java-version: '17' diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index b044c613bb..318967a927 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 - name: Set up JDK - uses: actions/setup-java@9704b39bf258b59bc04b50fa2dd55e9ed76b47a8 # tag=v4.1.0 + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v4.2.1 with: distribution: 'temurin' java-version: '17' From 965fbc3025bcbcd0a4da918c9b1953d5520eb8de Mon Sep 17 00:00:00 2001 From: Magnus Viernickel Date: Mon, 18 Mar 2024 11:51:07 +0100 Subject: [PATCH 024/412] [fix] fix test Signed-off-by: Magnus Viernickel --- pom.xml | 8 ++++- .../repositories/NixpkgsMetaAnalyzer.java | 31 ++++++++++--------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index 538365180b..cdcf8f110f 100644 --- a/pom.xml +++ b/pom.xml @@ -267,6 +267,13 @@ compile + + + org.brotli + dec + 0.1.2 + + org.apache.httpcomponents httpmime @@ -410,7 +417,6 @@ 2.35.2 test - com.github.stefanbirkner system-rules diff --git a/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java index c884aea58e..7a67fc8659 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java @@ -24,16 +24,20 @@ import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.core5.http.HttpStatus; -import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.http.client.utils.URIBuilder; +import org.brotli.dec.BrotliInputStream; import org.dependencytrack.exception.MetaAnalyzerException; import org.dependencytrack.model.Component; import org.dependencytrack.model.RepositoryType; import org.json.JSONObject; +import org.json.JSONTokener; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; import java.net.URISyntaxException; import java.util.HashMap; +import java.util.stream.Collectors; public class NixpkgsMetaAnalyzer extends AbstractMetaAnalyzer { private static final Logger LOGGER = Logger.getLogger(NixpkgsMetaAnalyzer.class); @@ -51,20 +55,17 @@ private NixpkgsMetaAnalyzer() { try (final CloseableHttpClient client = HttpClients.createDefault()) { try (final CloseableHttpResponse packagesResponse = processHttpRequest5(client)) { if (packagesResponse != null && packagesResponse.getCode() == HttpStatus.SC_OK) { - final var entity = packagesResponse.getEntity(); - if (entity != null) { - // TODO(mangoiv): is this the fastest way we can do this? - final var entityString = EntityUtils.toString(entity); - final var packages = new JSONObject(entityString).getJSONObject("packages").toMap().values(); - packages.forEach(pkg -> { - // FUTUREWORK(mangoiv): there are potentially packages with the same pname - if (pkg instanceof HashMap jsonPkg) { - final var pname = jsonPkg.get("pname"); - final var version = jsonPkg.get("version"); - newLatestVersion.putIfAbsent((String) pname, (String) version); - } - }); - } + var reader = new BufferedReader(new InputStreamReader(packagesResponse.getEntity().getContent())); + var packages = new JSONObject(new JSONTokener(reader)).getJSONObject("packages").toMap().values(); + packages.forEach(pkg -> { + // FUTUREWORK(mangoiv): there are potentially packages with the same pname + if (pkg instanceof HashMap jsonPkg) { + final var pname = jsonPkg.get("pname"); + final var version = jsonPkg.get("version"); + newLatestVersion.putIfAbsent((String) pname, (String) version); + } + }); + } } From 93573e0663f5bc9e6a6b2370e96524b21b0d78f7 Mon Sep 17 00:00:00 2001 From: Magnus Viernickel Date: Mon, 18 Mar 2024 11:58:18 +0100 Subject: [PATCH 025/412] [chore] reformat Signed-off-by: Magnus Viernickel --- .../dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java index 7a67fc8659..0825937bbb 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java @@ -25,7 +25,6 @@ import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.core5.http.HttpStatus; import org.apache.http.client.utils.URIBuilder; -import org.brotli.dec.BrotliInputStream; import org.dependencytrack.exception.MetaAnalyzerException; import org.dependencytrack.model.Component; import org.dependencytrack.model.RepositoryType; @@ -37,7 +36,6 @@ import java.io.InputStreamReader; import java.net.URISyntaxException; import java.util.HashMap; -import java.util.stream.Collectors; public class NixpkgsMetaAnalyzer extends AbstractMetaAnalyzer { private static final Logger LOGGER = Logger.getLogger(NixpkgsMetaAnalyzer.class); From ec636cb951869a5eaa84a061eb682f8aca80c306 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 08:29:36 +0000 Subject: [PATCH 026/412] Bump com.google.cloud.sql:cloud-sql-connector-jdbc-sqlserver Bumps com.google.cloud.sql:cloud-sql-connector-jdbc-sqlserver from 1.17.0 to 1.17.1. --- updated-dependencies: - dependency-name: com.google.cloud.sql:cloud-sql-connector-jdbc-sqlserver dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bd5133effb..4d31f6842a 100644 --- a/pom.xml +++ b/pom.xml @@ -85,7 +85,7 @@ ${project.parent.version} 4.2.0 10.12.5 - 1.17.0 + 1.17.1 1.16.0 1.16.0 2.1.0 From c5eda9271e3b55e7a55b8d60615816cdfe585916 Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 20 Mar 2024 18:39:35 +0100 Subject: [PATCH 027/412] Transfer copyright from Steve Springett to OWASP Foundation Signed-off-by: nscuro --- .checkstyle-header | 2 +- README.md | 2 +- dev/docker-compose.monitoring.yml | 2 +- dev/docker-compose.mssql.yml | 2 +- dev/docker-compose.postgres.yml | 2 +- dev/docker-compose.yml | 2 +- dev/scripts/cwe-dictionary-generate.py | 2 +- docs/images/dt-logo.svg | 2 +- pom.xml | 2 +- src/main/java/org/dependencytrack/RequirementsVerifier.java | 2 +- src/main/java/org/dependencytrack/auth/Permissions.java | 2 +- src/main/java/org/dependencytrack/auth/package-info.java | 2 +- .../org/dependencytrack/common/AlpineHttpProxySelector.java | 2 +- src/main/java/org/dependencytrack/common/ConfigKey.java | 2 +- src/main/java/org/dependencytrack/common/HttpClientPool.java | 2 +- src/main/java/org/dependencytrack/common/ManagedHttpClient.java | 2 +- .../org/dependencytrack/common/ManagedHttpClientFactory.java | 2 +- src/main/java/org/dependencytrack/common/MdcKeys.java | 2 +- .../event/AbstractVulnerabilityManagementUploadEvent.java | 2 +- src/main/java/org/dependencytrack/event/BomUploadEvent.java | 2 +- src/main/java/org/dependencytrack/event/CallbackEvent.java | 2 +- .../dependencytrack/event/ClearComponentAnalysisCacheEvent.java | 2 +- src/main/java/org/dependencytrack/event/CloneProjectEvent.java | 2 +- .../org/dependencytrack/event/ComponentMetricsUpdateEvent.java | 2 +- .../dependencytrack/event/DefectDojoUploadEventAbstract.java | 2 +- src/main/java/org/dependencytrack/event/EpssMirrorEvent.java | 2 +- .../org/dependencytrack/event/EventSubsystemInitializer.java | 2 +- .../dependencytrack/event/FortifySscUploadEventAbstract.java | 2 +- .../org/dependencytrack/event/GitHubAdvisoryMirrorEvent.java | 2 +- src/main/java/org/dependencytrack/event/IndexEvent.java | 2 +- .../java/org/dependencytrack/event/InternalAnalysisEvent.java | 2 +- .../event/InternalComponentIdentificationEvent.java | 2 +- .../dependencytrack/event/KennaSecurityUploadEventAbstract.java | 2 +- .../event/NewVulnerableDependencyAnalysisEvent.java | 2 +- src/main/java/org/dependencytrack/event/NistApiMirrorEvent.java | 2 +- src/main/java/org/dependencytrack/event/NistMirrorEvent.java | 2 +- .../java/org/dependencytrack/event/OssIndexAnalysisEvent.java | 2 +- src/main/java/org/dependencytrack/event/OsvMirrorEvent.java | 2 +- .../java/org/dependencytrack/event/PolicyEvaluationEvent.java | 2 +- .../org/dependencytrack/event/PortfolioMetricsUpdateEvent.java | 2 +- .../event/PortfolioVulnerabilityAnalysisEvent.java | 2 +- .../org/dependencytrack/event/ProjectMetricsUpdateEvent.java | 2 +- .../java/org/dependencytrack/event/RepositoryMetaEvent.java | 2 +- src/main/java/org/dependencytrack/event/SnykAnalysisEvent.java | 2 +- src/main/java/org/dependencytrack/event/TrivyAnalysisEvent.java | 2 +- src/main/java/org/dependencytrack/event/VexUploadEvent.java | 2 +- .../java/org/dependencytrack/event/VulnDbAnalysisEvent.java | 2 +- src/main/java/org/dependencytrack/event/VulnDbSyncEvent.java | 2 +- .../org/dependencytrack/event/VulnerabilityAnalysisEvent.java | 2 +- .../dependencytrack/event/VulnerabilityMetricsUpdateEvent.java | 2 +- src/main/java/org/dependencytrack/event/package-info.java | 2 +- .../org/dependencytrack/exception/MetaAnalyzerException.java | 2 +- src/main/java/org/dependencytrack/exception/ParseException.java | 2 +- .../java/org/dependencytrack/exception/PolicyException.java | 2 +- .../java/org/dependencytrack/exception/PublisherException.java | 2 +- .../org/dependencytrack/exception/RequirementsException.java | 2 +- src/main/java/org/dependencytrack/exception/package-info.java | 2 +- .../java/org/dependencytrack/health/HealthCheckInitializer.java | 2 +- .../dependencytrack/integrations/AbstractIntegrationPoint.java | 2 +- .../dependencytrack/integrations/FindingPackagingFormat.java | 2 +- .../java/org/dependencytrack/integrations/FindingUploader.java | 2 +- .../java/org/dependencytrack/integrations/IntegrationPoint.java | 2 +- .../dependencytrack/integrations/PortfolioFindingUploader.java | 2 +- .../dependencytrack/integrations/ProjectFindingUploader.java | 2 +- .../integrations/defectdojo/DefectDojoClient.java | 2 +- .../integrations/defectdojo/DefectDojoUploader.java | 2 +- .../integrations/fortifyssc/FortifySscClient.java | 2 +- .../integrations/fortifyssc/FortifySscUploader.java | 2 +- .../integrations/kenna/KennaDataTransformer.java | 2 +- .../integrations/kenna/KennaSecurityUploader.java | 2 +- src/main/java/org/dependencytrack/metrics/Metrics.java | 2 +- src/main/java/org/dependencytrack/metrics/package-info.java | 2 +- .../org/dependencytrack/model/AffectedVersionAttribution.java | 2 +- src/main/java/org/dependencytrack/model/Analysis.java | 2 +- src/main/java/org/dependencytrack/model/AnalysisComment.java | 2 +- .../java/org/dependencytrack/model/AnalysisJustification.java | 2 +- src/main/java/org/dependencytrack/model/AnalysisResponse.java | 2 +- src/main/java/org/dependencytrack/model/AnalysisState.java | 2 +- src/main/java/org/dependencytrack/model/Bom.java | 2 +- src/main/java/org/dependencytrack/model/Classifier.java | 2 +- src/main/java/org/dependencytrack/model/Component.java | 2 +- .../java/org/dependencytrack/model/ComponentAnalysisCache.java | 2 +- src/main/java/org/dependencytrack/model/ComponentIdentity.java | 2 +- .../java/org/dependencytrack/model/ConfigPropertyConstants.java | 2 +- src/main/java/org/dependencytrack/model/Coordinates.java | 2 +- src/main/java/org/dependencytrack/model/Cwe.java | 2 +- src/main/java/org/dependencytrack/model/DataClassification.java | 2 +- src/main/java/org/dependencytrack/model/DependencyMetrics.java | 2 +- src/main/java/org/dependencytrack/model/ExternalReference.java | 2 +- src/main/java/org/dependencytrack/model/Finding.java | 2 +- src/main/java/org/dependencytrack/model/FindingAttribution.java | 2 +- src/main/java/org/dependencytrack/model/GroupedFinding.java | 2 +- src/main/java/org/dependencytrack/model/ICpe.java | 2 +- src/main/java/org/dependencytrack/model/IdentifiableObject.java | 2 +- src/main/java/org/dependencytrack/model/License.java | 2 +- src/main/java/org/dependencytrack/model/LicenseGroup.java | 2 +- .../java/org/dependencytrack/model/NotificationPublisher.java | 2 +- src/main/java/org/dependencytrack/model/NotificationRule.java | 2 +- .../java/org/dependencytrack/model/OrganizationalContact.java | 2 +- .../java/org/dependencytrack/model/OrganizationalEntity.java | 2 +- src/main/java/org/dependencytrack/model/Policy.java | 2 +- src/main/java/org/dependencytrack/model/PolicyCondition.java | 2 +- src/main/java/org/dependencytrack/model/PolicyViolation.java | 2 +- src/main/java/org/dependencytrack/model/PortfolioMetrics.java | 2 +- src/main/java/org/dependencytrack/model/Project.java | 2 +- src/main/java/org/dependencytrack/model/ProjectMetadata.java | 2 +- src/main/java/org/dependencytrack/model/ProjectMetrics.java | 2 +- src/main/java/org/dependencytrack/model/ProjectProperty.java | 2 +- src/main/java/org/dependencytrack/model/ProjectVersion.java | 2 +- src/main/java/org/dependencytrack/model/Repository.java | 2 +- .../java/org/dependencytrack/model/RepositoryMetaComponent.java | 2 +- src/main/java/org/dependencytrack/model/RepositoryType.java | 2 +- src/main/java/org/dependencytrack/model/ServiceComponent.java | 2 +- src/main/java/org/dependencytrack/model/Severity.java | 2 +- src/main/java/org/dependencytrack/model/SnykCvssSource.java | 2 +- src/main/java/org/dependencytrack/model/Tag.java | 2 +- src/main/java/org/dependencytrack/model/Vex.java | 2 +- src/main/java/org/dependencytrack/model/ViolationAnalysis.java | 2 +- .../org/dependencytrack/model/ViolationAnalysisComment.java | 2 +- .../java/org/dependencytrack/model/ViolationAnalysisState.java | 2 +- src/main/java/org/dependencytrack/model/Vulnerability.java | 2 +- src/main/java/org/dependencytrack/model/VulnerabilityAlias.java | 2 +- .../org/dependencytrack/model/VulnerabilityAnalysisLevel.java | 2 +- .../java/org/dependencytrack/model/VulnerabilityMetrics.java | 2 +- src/main/java/org/dependencytrack/model/VulnerableSoftware.java | 2 +- src/main/java/org/dependencytrack/model/package-info.java | 2 +- .../model/validation/SpdxExpressionValidator.java | 2 +- .../dependencytrack/model/validation/ValidSpdxExpression.java | 2 +- .../org/dependencytrack/notification/NotificationConstants.java | 2 +- .../org/dependencytrack/notification/NotificationGroup.java | 2 +- .../org/dependencytrack/notification/NotificationRouter.java | 2 +- .../org/dependencytrack/notification/NotificationScope.java | 2 +- .../notification/NotificationSubsystemInitializer.java | 2 +- .../notification/publisher/AbstractWebhookPublisher.java | 2 +- .../notification/publisher/ConsolePublisher.java | 2 +- .../notification/publisher/CsWebexPublisher.java | 2 +- .../notification/publisher/DefaultNotificationPublishers.java | 2 +- .../dependencytrack/notification/publisher/JiraPublisher.java | 2 +- .../notification/publisher/MattermostPublisher.java | 2 +- .../notification/publisher/MsTeamsPublisher.java | 2 +- .../dependencytrack/notification/publisher/PublishContext.java | 2 +- .../org/dependencytrack/notification/publisher/Publisher.java | 2 +- .../notification/publisher/SendMailPublisher.java | 2 +- .../dependencytrack/notification/publisher/SlackPublisher.java | 2 +- .../notification/publisher/WebhookPublisher.java | 2 +- .../dependencytrack/notification/vo/AnalysisDecisionChange.java | 2 +- .../dependencytrack/notification/vo/BomConsumedOrProcessed.java | 2 +- .../dependencytrack/notification/vo/BomProcessingFailed.java | 2 +- .../notification/vo/NewVulnerabilityIdentified.java | 2 +- .../notification/vo/NewVulnerableDependency.java | 2 +- .../notification/vo/PolicyViolationIdentified.java | 2 +- .../dependencytrack/notification/vo/VexConsumedOrProcessed.java | 2 +- .../notification/vo/ViolationAnalysisDecisionChange.java | 2 +- .../dependencytrack/parser/common/resolver/CweDictionary.java | 2 +- .../org/dependencytrack/parser/common/resolver/CweResolver.java | 2 +- .../org/dependencytrack/parser/cyclonedx/CycloneDXExporter.java | 2 +- .../dependencytrack/parser/cyclonedx/CycloneDXVexImporter.java | 2 +- .../dependencytrack/parser/cyclonedx/CycloneDxValidator.java | 2 +- .../dependencytrack/parser/cyclonedx/InvalidBomException.java | 2 +- .../java/org/dependencytrack/parser/cyclonedx/package-info.java | 2 +- .../dependencytrack/parser/cyclonedx/util/ModelConverter.java | 2 +- src/main/java/org/dependencytrack/parser/epss/EpssParser.java | 2 +- .../parser/github/graphql/GitHubSecurityAdvisoryParser.java | 2 +- .../parser/github/graphql/model/GitHubSecurityAdvisory.java | 2 +- .../parser/github/graphql/model/GitHubVulnerability.java | 2 +- .../parser/github/graphql/model/PageableList.java | 2 +- .../java/org/dependencytrack/parser/nvd/ModelConverter.java | 2 +- src/main/java/org/dependencytrack/parser/nvd/NvdParser.java | 2 +- .../org/dependencytrack/parser/nvd/api20/ModelConverter.java | 2 +- src/main/java/org/dependencytrack/parser/nvd/package-info.java | 2 +- .../org/dependencytrack/parser/ossindex/OssIndexParser.java | 2 +- .../dependencytrack/parser/ossindex/model/ComponentReport.java | 2 +- .../parser/ossindex/model/ComponentReportVulnerability.java | 2 +- .../java/org/dependencytrack/parser/osv/OsvAdvisoryParser.java | 2 +- .../java/org/dependencytrack/parser/osv/model/OsvAdvisory.java | 2 +- .../dependencytrack/parser/osv/model/OsvAffectedPackage.java | 2 +- src/main/java/org/dependencytrack/parser/package-info.java | 2 +- src/main/java/org/dependencytrack/parser/snyk/SnykParser.java | 2 +- .../java/org/dependencytrack/parser/snyk/model/SnykError.java | 2 +- .../parser/spdx/expression/SpdxExpressionParser.java | 2 +- .../parser/spdx/expression/model/SpdxExpression.java | 2 +- .../parser/spdx/expression/model/SpdxExpressionOperation.java | 2 +- .../parser/spdx/expression/model/SpdxOperator.java | 2 +- .../dependencytrack/parser/spdx/expression/package-info.java | 2 +- .../parser/spdx/json/SpdxLicenseDetailParser.java | 2 +- .../java/org/dependencytrack/parser/spdx/json/package-info.java | 2 +- src/main/java/org/dependencytrack/parser/spdx/package-info.java | 2 +- src/main/java/org/dependencytrack/parser/trivy/TrivyParser.java | 2 +- .../org/dependencytrack/parser/trivy/model/Application.java | 2 +- .../java/org/dependencytrack/parser/trivy/model/BlobInfo.java | 2 +- src/main/java/org/dependencytrack/parser/trivy/model/CVSS.java | 2 +- .../java/org/dependencytrack/parser/trivy/model/DataSource.java | 2 +- .../org/dependencytrack/parser/trivy/model/DeleteRequest.java | 2 +- src/main/java/org/dependencytrack/parser/trivy/model/Layer.java | 2 +- .../java/org/dependencytrack/parser/trivy/model/Library.java | 2 +- src/main/java/org/dependencytrack/parser/trivy/model/OS.java | 2 +- .../java/org/dependencytrack/parser/trivy/model/Options.java | 2 +- .../java/org/dependencytrack/parser/trivy/model/Package.java | 2 +- .../org/dependencytrack/parser/trivy/model/PackageInfo.java | 2 +- .../java/org/dependencytrack/parser/trivy/model/PurlType.java | 2 +- .../java/org/dependencytrack/parser/trivy/model/PutRequest.java | 2 +- .../java/org/dependencytrack/parser/trivy/model/Result.java | 2 +- .../org/dependencytrack/parser/trivy/model/ScanRequest.java | 2 +- .../org/dependencytrack/parser/trivy/model/TrivyResponse.java | 2 +- .../org/dependencytrack/parser/trivy/model/Vulnerability.java | 2 +- .../java/org/dependencytrack/parser/vulndb/ModelConverter.java | 2 +- .../java/org/dependencytrack/parser/vulndb/VulnDbClient.java | 2 +- .../java/org/dependencytrack/parser/vulndb/VulnDbParser.java | 2 +- .../java/org/dependencytrack/parser/vulndb/model/ApiObject.java | 2 +- .../java/org/dependencytrack/parser/vulndb/model/Author.java | 2 +- .../org/dependencytrack/parser/vulndb/model/Classification.java | 2 +- src/main/java/org/dependencytrack/parser/vulndb/model/Cpe.java | 2 +- .../org/dependencytrack/parser/vulndb/model/CvssV2Metric.java | 2 +- .../org/dependencytrack/parser/vulndb/model/CvssV3Metric.java | 2 +- .../dependencytrack/parser/vulndb/model/ExternalReference.java | 2 +- .../org/dependencytrack/parser/vulndb/model/ExternalText.java | 2 +- .../dependencytrack/parser/vulndb/model/NvdAdditionalInfo.java | 2 +- .../java/org/dependencytrack/parser/vulndb/model/Product.java | 2 +- .../java/org/dependencytrack/parser/vulndb/model/Results.java | 2 +- .../java/org/dependencytrack/parser/vulndb/model/Status.java | 2 +- .../java/org/dependencytrack/parser/vulndb/model/Vendor.java | 2 +- .../java/org/dependencytrack/parser/vulndb/model/Version.java | 2 +- .../org/dependencytrack/parser/vulndb/model/Vulnerability.java | 2 +- .../java/org/dependencytrack/persistence/BomQueryManager.java | 2 +- .../java/org/dependencytrack/persistence/CacheQueryManager.java | 2 +- .../dependencytrack/persistence/CollectionIntegerConverter.java | 2 +- .../org/dependencytrack/persistence/ComponentQueryManager.java | 2 +- .../org/dependencytrack/persistence/DefaultObjectGenerator.java | 2 +- .../org/dependencytrack/persistence/FindingsQueryManager.java | 2 +- .../dependencytrack/persistence/FindingsSearchQueryManager.java | 2 +- .../dependencytrack/persistence/H2WebConsoleInitializer.java | 2 +- .../java/org/dependencytrack/persistence/IQueryManager.java | 2 +- .../org/dependencytrack/persistence/LicenseQueryManager.java | 2 +- .../org/dependencytrack/persistence/MetricsQueryManager.java | 2 +- .../dependencytrack/persistence/NotificationQueryManager.java | 2 +- .../dependencytrack/persistence/PackageURLStringConverter.java | 2 +- .../org/dependencytrack/persistence/PolicyQueryManager.java | 2 +- .../dependencytrack/persistence/ProjectQueryFilterBuilder.java | 2 +- .../org/dependencytrack/persistence/ProjectQueryManager.java | 2 +- src/main/java/org/dependencytrack/persistence/QueryManager.java | 2 +- .../org/dependencytrack/persistence/RepositoryQueryManager.java | 2 +- .../persistence/ServiceComponentQueryManager.java | 2 +- .../java/org/dependencytrack/persistence/TagQueryManager.java | 2 +- .../java/org/dependencytrack/persistence/VexQueryManager.java | 2 +- .../dependencytrack/persistence/VulnerabilityQueryManager.java | 2 +- .../persistence/VulnerableSoftwareQueryManager.java | 2 +- .../persistence/converter/AbstractJsonConverter.java | 2 +- .../converter/OrganizationalContactsJsonConverter.java | 2 +- .../converter/OrganizationalEntityJsonConverter.java | 2 +- .../persistence/defaults/DefaultLicenseGroupImporter.java | 2 +- .../persistence/defaults/IDefaultObjectImporter.java | 2 +- .../persistence/listener/IndexingInstanceLifecycleListener.java | 2 +- .../listener/L2CacheEvictingInstanceLifecycleListener.java | 2 +- src/main/java/org/dependencytrack/persistence/package-info.java | 2 +- .../org/dependencytrack/policy/AbstractPolicyEvaluator.java | 2 +- .../org/dependencytrack/policy/ComponentAgePolicyEvaluator.java | 2 +- .../dependencytrack/policy/ComponentHashPolicyEvaluator.java | 2 +- .../org/dependencytrack/policy/CoordinatesPolicyEvaluator.java | 2 +- .../java/org/dependencytrack/policy/CpePolicyEvaluator.java | 2 +- .../java/org/dependencytrack/policy/CwePolicyEvaluator.java | 2 +- .../org/dependencytrack/policy/LicenseGroupPolicyEvaluator.java | 2 +- .../java/org/dependencytrack/policy/LicensePolicyEvaluator.java | 2 +- src/main/java/org/dependencytrack/policy/Matcher.java | 2 +- .../org/dependencytrack/policy/PackageURLPolicyEvaluator.java | 2 +- .../org/dependencytrack/policy/PolicyConditionViolation.java | 2 +- src/main/java/org/dependencytrack/policy/PolicyEngine.java | 2 +- src/main/java/org/dependencytrack/policy/PolicyEvaluator.java | 2 +- .../org/dependencytrack/policy/SeverityPolicyEvaluator.java | 2 +- .../org/dependencytrack/policy/SwidTagIdPolicyEvaluator.java | 2 +- .../dependencytrack/policy/VersionDistancePolicyEvaluator.java | 2 +- .../java/org/dependencytrack/policy/VersionPolicyEvaluator.java | 2 +- .../dependencytrack/policy/VulnerabilityIdPolicyEvaluator.java | 2 +- src/main/java/org/dependencytrack/resources/package-info.java | 2 +- .../resources/v1/AbstractConfigPropertyResource.java | 2 +- .../org/dependencytrack/resources/v1/AccessControlResource.java | 2 +- .../java/org/dependencytrack/resources/v1/AnalysisResource.java | 2 +- .../java/org/dependencytrack/resources/v1/BadgeResource.java | 2 +- src/main/java/org/dependencytrack/resources/v1/BomResource.java | 2 +- .../org/dependencytrack/resources/v1/CalculatorResource.java | 2 +- .../org/dependencytrack/resources/v1/ComponentResource.java | 2 +- .../dependencytrack/resources/v1/ConfigPropertyResource.java | 2 +- src/main/java/org/dependencytrack/resources/v1/CweResource.java | 2 +- .../dependencytrack/resources/v1/DependencyGraphResource.java | 2 +- .../java/org/dependencytrack/resources/v1/EventResource.java | 2 +- .../java/org/dependencytrack/resources/v1/FindingResource.java | 2 +- .../org/dependencytrack/resources/v1/IntegrationResource.java | 2 +- .../java/org/dependencytrack/resources/v1/LdapResource.java | 2 +- .../org/dependencytrack/resources/v1/LicenseGroupResource.java | 2 +- .../java/org/dependencytrack/resources/v1/LicenseResource.java | 2 +- .../java/org/dependencytrack/resources/v1/MetricsResource.java | 2 +- .../resources/v1/NotificationPublisherResource.java | 2 +- .../dependencytrack/resources/v1/NotificationRuleResource.java | 2 +- .../java/org/dependencytrack/resources/v1/OidcResource.java | 2 +- .../org/dependencytrack/resources/v1/PermissionResource.java | 2 +- .../dependencytrack/resources/v1/PolicyConditionResource.java | 2 +- .../java/org/dependencytrack/resources/v1/PolicyResource.java | 2 +- .../dependencytrack/resources/v1/PolicyViolationResource.java | 2 +- .../dependencytrack/resources/v1/ProjectPropertyResource.java | 2 +- .../java/org/dependencytrack/resources/v1/ProjectResource.java | 2 +- .../org/dependencytrack/resources/v1/RepositoryResource.java | 2 +- .../java/org/dependencytrack/resources/v1/SearchResource.java | 2 +- .../java/org/dependencytrack/resources/v1/ServiceResource.java | 2 +- src/main/java/org/dependencytrack/resources/v1/TagResource.java | 2 +- .../java/org/dependencytrack/resources/v1/TeamResource.java | 2 +- .../java/org/dependencytrack/resources/v1/UserResource.java | 2 +- src/main/java/org/dependencytrack/resources/v1/VexResource.java | 2 +- .../dependencytrack/resources/v1/ViolationAnalysisResource.java | 2 +- .../org/dependencytrack/resources/v1/VulnerabilityResource.java | 2 +- .../resources/v1/exception/JsonMappingExceptionMapper.java | 2 +- .../resources/v1/exception/NotFoundExceptionMapper.java | 2 +- src/main/java/org/dependencytrack/resources/v1/misc/Badger.java | 2 +- .../java/org/dependencytrack/resources/v1/package-info.java | 2 +- .../resources/v1/problems/InvalidBomProblemDetails.java | 2 +- .../dependencytrack/resources/v1/problems/ProblemDetails.java | 2 +- .../resources/v1/serializers/CustomPackageURLSerializer.java | 2 +- .../resources/v1/serializers/CweDeserializer.java | 2 +- .../dependencytrack/resources/v1/serializers/CweSerializer.java | 2 +- .../resources/v1/serializers/Iso8601DateSerializer.java | 2 +- .../org/dependencytrack/resources/v1/vo/AclMappingRequest.java | 2 +- .../org/dependencytrack/resources/v1/vo/AffectedComponent.java | 2 +- .../org/dependencytrack/resources/v1/vo/AffectedProject.java | 2 +- .../org/dependencytrack/resources/v1/vo/AnalysisRequest.java | 2 +- .../org/dependencytrack/resources/v1/vo/BomSubmitRequest.java | 2 +- .../org/dependencytrack/resources/v1/vo/BomUploadResponse.java | 2 +- .../dependencytrack/resources/v1/vo/CloneProjectRequest.java | 2 +- .../resources/v1/vo/DependencyGraphResponse.java | 2 +- .../org/dependencytrack/resources/v1/vo/DependencyRequest.java | 2 +- .../resources/v1/vo/IsTokenBeingProcessedResponse.java | 2 +- .../dependencytrack/resources/v1/vo/MappedLdapGroupRequest.java | 2 +- .../dependencytrack/resources/v1/vo/MappedOidcGroupRequest.java | 2 +- .../org/dependencytrack/resources/v1/vo/TeamSelfResponse.java | 2 +- .../org/dependencytrack/resources/v1/vo/VexSubmitRequest.java | 2 +- .../resources/v1/vo/ViolationAnalysisRequest.java | 2 +- .../java/org/dependencytrack/resources/v1/vo/package-info.java | 2 +- src/main/java/org/dependencytrack/search/ComponentIndexer.java | 2 +- .../search/FuzzyVulnerableSoftwareSearchManager.java | 2 +- src/main/java/org/dependencytrack/search/IndexConstants.java | 2 +- src/main/java/org/dependencytrack/search/IndexManager.java | 2 +- .../java/org/dependencytrack/search/IndexManagerFactory.java | 2 +- .../org/dependencytrack/search/IndexSubsystemInitializer.java | 2 +- src/main/java/org/dependencytrack/search/LicenseIndexer.java | 2 +- src/main/java/org/dependencytrack/search/ObjectIndexer.java | 2 +- src/main/java/org/dependencytrack/search/ProjectIndexer.java | 2 +- src/main/java/org/dependencytrack/search/SearchManager.java | 2 +- src/main/java/org/dependencytrack/search/SearchResult.java | 2 +- .../org/dependencytrack/search/ServiceComponentIndexer.java | 2 +- .../java/org/dependencytrack/search/VulnerabilityIndexer.java | 2 +- .../org/dependencytrack/search/VulnerableSoftwareIndexer.java | 2 +- .../org/dependencytrack/search/document/ComponentDocument.java | 2 +- .../java/org/dependencytrack/search/document/DummyDocument.java | 2 +- .../org/dependencytrack/search/document/LicenseDocument.java | 2 +- .../org/dependencytrack/search/document/ProjectDocument.java | 2 +- .../org/dependencytrack/search/document/SearchDocument.java | 2 +- .../search/document/ServiceComponentDocument.java | 2 +- .../dependencytrack/search/document/VulnerabilityDocument.java | 2 +- .../search/document/VulnerableSoftwareDocument.java | 2 +- src/main/java/org/dependencytrack/search/package-info.java | 2 +- .../java/org/dependencytrack/servlets/NvdMirrorServlet.java | 2 +- .../java/org/dependencytrack/tasks/BomUploadProcessingTask.java | 2 +- .../org/dependencytrack/tasks/BomUploadProcessingTaskV2.java | 2 +- src/main/java/org/dependencytrack/tasks/CallbackTask.java | 2 +- .../dependencytrack/tasks/ClearComponentAnalysisCacheTask.java | 2 +- src/main/java/org/dependencytrack/tasks/CloneProjectTask.java | 2 +- .../java/org/dependencytrack/tasks/DefectDojoUploadTask.java | 2 +- src/main/java/org/dependencytrack/tasks/EpssMirrorTask.java | 2 +- .../java/org/dependencytrack/tasks/FortifySscUploadTask.java | 2 +- .../org/dependencytrack/tasks/GitHubAdvisoryMirrorTask.java | 2 +- src/main/java/org/dependencytrack/tasks/IndexTask.java | 2 +- .../tasks/InternalComponentIdentificationTask.java | 2 +- .../java/org/dependencytrack/tasks/KennaSecurityUploadTask.java | 2 +- .../tasks/NewVulnerableDependencyAnalysisTask.java | 2 +- src/main/java/org/dependencytrack/tasks/NistApiMirrorTask.java | 2 +- src/main/java/org/dependencytrack/tasks/NistMirrorTask.java | 2 +- src/main/java/org/dependencytrack/tasks/OsvDownloadTask.java | 2 +- .../java/org/dependencytrack/tasks/PolicyEvaluationTask.java | 2 +- src/main/java/org/dependencytrack/tasks/TaskScheduler.java | 2 +- .../java/org/dependencytrack/tasks/VexUploadProcessingTask.java | 2 +- src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java | 2 +- .../org/dependencytrack/tasks/VulnerabilityAnalysisTask.java | 2 +- .../tasks/VulnerabilityManagementUploadTask.java | 2 +- .../tasks/metrics/ComponentMetricsUpdateTask.java | 2 +- src/main/java/org/dependencytrack/tasks/metrics/Counters.java | 2 +- .../tasks/metrics/PortfolioMetricsUpdateTask.java | 2 +- .../dependencytrack/tasks/metrics/ProjectMetricsUpdateTask.java | 2 +- .../tasks/metrics/VulnerabilityMetricsUpdateTask.java | 2 +- .../java/org/dependencytrack/tasks/metrics/YearMonthMetric.java | 2 +- src/main/java/org/dependencytrack/tasks/package-info.java | 2 +- .../tasks/repositories/AbstractMetaAnalyzer.java | 2 +- .../dependencytrack/tasks/repositories/CargoMetaAnalyzer.java | 2 +- .../tasks/repositories/ComposerMetaAnalyzer.java | 2 +- .../dependencytrack/tasks/repositories/CpanMetaAnalyzer.java | 2 +- .../org/dependencytrack/tasks/repositories/GemMetaAnalyzer.java | 2 +- .../dependencytrack/tasks/repositories/GithubMetaAnalyzer.java | 2 +- .../tasks/repositories/GoModulesMetaAnalyzer.java | 2 +- .../org/dependencytrack/tasks/repositories/HexMetaAnalyzer.java | 2 +- .../org/dependencytrack/tasks/repositories/IMetaAnalyzer.java | 2 +- .../dependencytrack/tasks/repositories/MavenMetaAnalyzer.java | 2 +- .../java/org/dependencytrack/tasks/repositories/MetaModel.java | 2 +- .../org/dependencytrack/tasks/repositories/NpmMetaAnalyzer.java | 2 +- .../dependencytrack/tasks/repositories/NugetMetaAnalyzer.java | 2 +- .../dependencytrack/tasks/repositories/PypiMetaAnalyzer.java | 2 +- .../tasks/repositories/RepositoryMetaAnalyzerTask.java | 2 +- .../tasks/scanners/AbstractVulnerableSoftwareAnalysisTask.java | 2 +- .../org/dependencytrack/tasks/scanners/AnalyzerIdentity.java | 2 +- .../tasks/scanners/BaseComponentAnalyzerTask.java | 2 +- .../org/dependencytrack/tasks/scanners/CacheableScanTask.java | 2 +- .../dependencytrack/tasks/scanners/InternalAnalysisTask.java | 2 +- .../dependencytrack/tasks/scanners/OssIndexAnalysisTask.java | 2 +- src/main/java/org/dependencytrack/tasks/scanners/ScanTask.java | 2 +- .../org/dependencytrack/tasks/scanners/SnykAnalysisTask.java | 2 +- .../org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java | 2 +- .../org/dependencytrack/tasks/scanners/VulnDbAnalysisTask.java | 2 +- .../java/org/dependencytrack/upgrade/UpgradeInitializer.java | 2 +- src/main/java/org/dependencytrack/upgrade/UpgradeItems.java | 2 +- src/main/java/org/dependencytrack/upgrade/v400/v400Updater.java | 2 +- src/main/java/org/dependencytrack/upgrade/v410/v410Updater.java | 2 +- .../java/org/dependencytrack/upgrade/v4100/v4100Updater.java | 2 +- .../java/org/dependencytrack/upgrade/v4110/v4110Updater.java | 2 +- src/main/java/org/dependencytrack/upgrade/v420/v420Updater.java | 2 +- src/main/java/org/dependencytrack/upgrade/v440/v440Updater.java | 2 +- src/main/java/org/dependencytrack/upgrade/v450/v450Updater.java | 2 +- src/main/java/org/dependencytrack/upgrade/v460/v460Updater.java | 2 +- src/main/java/org/dependencytrack/upgrade/v463/v463Updater.java | 2 +- src/main/java/org/dependencytrack/upgrade/v470/v470Updater.java | 2 +- src/main/java/org/dependencytrack/upgrade/v480/v480Updater.java | 2 +- src/main/java/org/dependencytrack/upgrade/v490/v490Updater.java | 2 +- src/main/java/org/dependencytrack/util/AnalysisCommentUtil.java | 2 +- .../java/org/dependencytrack/util/CacheStampedeBlocker.java | 2 +- .../org/dependencytrack/util/ComponentIdentificationUtil.java | 2 +- src/main/java/org/dependencytrack/util/ComponentVersion.java | 2 +- src/main/java/org/dependencytrack/util/CompressUtil.java | 2 +- src/main/java/org/dependencytrack/util/DateUtil.java | 2 +- src/main/java/org/dependencytrack/util/HashUtil.java | 2 +- src/main/java/org/dependencytrack/util/HttpUtil.java | 2 +- .../util/InternalComponentIdentificationUtil.java | 2 +- .../org/dependencytrack/util/InternalComponentIdentifier.java | 2 +- src/main/java/org/dependencytrack/util/JsonUtil.java | 2 +- src/main/java/org/dependencytrack/util/NotificationUtil.java | 2 +- src/main/java/org/dependencytrack/util/PersistenceUtil.java | 2 +- src/main/java/org/dependencytrack/util/PurlUtil.java | 2 +- src/main/java/org/dependencytrack/util/RetryUtil.java | 2 +- src/main/java/org/dependencytrack/util/RoundRobinAccessor.java | 2 +- src/main/java/org/dependencytrack/util/VersionDistance.java | 2 +- src/main/java/org/dependencytrack/util/VulnerabilityUtil.java | 2 +- src/main/java/org/dependencytrack/util/XmlUtil.java | 2 +- src/main/resources/META-INF/persistence.xml | 2 +- src/main/webapp/WEB-INF/web.xml | 2 +- src/test/java/org/dependencytrack/PersistenceCapableTest.java | 2 +- src/test/java/org/dependencytrack/ResourceTest.java | 2 +- src/test/java/org/dependencytrack/auth/PermissionsTest.java | 2 +- .../org/dependencytrack/common/AlpineHttpProxySelectorTest.java | 2 +- .../java/org/dependencytrack/common/HttpClientPoolTest.java | 2 +- .../dependencytrack/common/ManagedHttpClientFactoryTest.java | 2 +- .../java/org/dependencytrack/common/ManagedHttpClientTest.java | 2 +- src/test/java/org/dependencytrack/event/BomUploadEventTest.java | 2 +- .../java/org/dependencytrack/event/CloneProjectEventTest.java | 2 +- .../org/dependencytrack/event/FortifySscUploadEventTest.java | 2 +- src/test/java/org/dependencytrack/event/IndexEventTest.java | 2 +- .../org/dependencytrack/event/InternalAnalysisEventTest.java | 2 +- .../org/dependencytrack/event/KennaSecurityUploadEventTest.java | 2 +- .../java/org/dependencytrack/event/NistMirrorEventTest.java | 2 +- .../org/dependencytrack/event/OssIndexAnalysisEventTest.java | 2 +- .../org/dependencytrack/event/PolicyEvaluationEventTest.java | 2 +- .../java/org/dependencytrack/event/RepositoryMetaEventTest.java | 2 +- .../java/org/dependencytrack/event/VulnDbSyncEventTest.java | 2 +- .../dependencytrack/event/VulnerabilityAnalysisEventTest.java | 2 +- .../java/org/dependencytrack/exception/ParseExceptionTest.java | 2 +- .../integrations/AbstractIntegrationPointTest.java | 2 +- .../integrations/FindingPackagingFormatTest.java | 2 +- .../org/dependencytrack/integrations/FindingUploaderTest.java | 2 +- .../org/dependencytrack/integrations/IntegrationPointTest.java | 2 +- .../integrations/PortfolioFindingUploaderTest.java | 2 +- .../integrations/ProjectFindingUploaderTest.java | 2 +- .../integrations/defectdojo/DefectDojoUploaderTest.java | 2 +- .../integrations/fortifyssc/FortifySscClientTest.java | 2 +- .../integrations/fortifyssc/FortifySscUploaderTest.java | 2 +- .../integrations/kenna/KennaSecurityUploaderTest.java | 2 +- src/test/java/org/dependencytrack/metrics/MetricsTest.java | 2 +- .../java/org/dependencytrack/model/AnalysisCommentTest.java | 2 +- src/test/java/org/dependencytrack/model/AnalysisStateTest.java | 2 +- src/test/java/org/dependencytrack/model/AnalysisTest.java | 2 +- src/test/java/org/dependencytrack/model/BomTest.java | 2 +- src/test/java/org/dependencytrack/model/ClassifierTest.java | 2 +- .../java/org/dependencytrack/model/ComponentIdentityTest.java | 2 +- src/test/java/org/dependencytrack/model/ComponentTest.java | 2 +- src/test/java/org/dependencytrack/model/CweTest.java | 2 +- .../java/org/dependencytrack/model/DependencyMetricsTest.java | 2 +- src/test/java/org/dependencytrack/model/FindingTest.java | 2 +- src/test/java/org/dependencytrack/model/GroupedFindingTest.java | 2 +- src/test/java/org/dependencytrack/model/LicenseGroupTest.java | 2 +- src/test/java/org/dependencytrack/model/LicenseTest.java | 2 +- .../org/dependencytrack/model/NotificationPublisherTest.java | 2 +- .../java/org/dependencytrack/model/NotificationRuleTest.java | 2 +- .../java/org/dependencytrack/model/PolicyConditionTest.java | 2 +- src/test/java/org/dependencytrack/model/PolicyTest.java | 2 +- .../java/org/dependencytrack/model/PortfolioMetricsTest.java | 2 +- src/test/java/org/dependencytrack/model/ProjectMetricsTest.java | 2 +- .../java/org/dependencytrack/model/ProjectPropertyTest.java | 2 +- src/test/java/org/dependencytrack/model/ProjectTest.java | 2 +- .../org/dependencytrack/model/RepositoryMetaComponentTest.java | 2 +- src/test/java/org/dependencytrack/model/RepositoryTest.java | 2 +- src/test/java/org/dependencytrack/model/RepositoryTypeTest.java | 2 +- src/test/java/org/dependencytrack/model/SeverityTest.java | 2 +- src/test/java/org/dependencytrack/model/TagTest.java | 2 +- .../org/dependencytrack/model/VulnerabilityMetricsTest.java | 2 +- src/test/java/org/dependencytrack/model/VulnerabilityTest.java | 2 +- .../java/org/dependencytrack/model/VulnerableSoftwareTest.java | 2 +- .../model/validation/SpdxExpressionValidatorTest.java | 2 +- .../dependencytrack/notification/NotificationConstantsTest.java | 2 +- .../org/dependencytrack/notification/NotificationGroupTest.java | 2 +- .../dependencytrack/notification/NotificationRouterTest.java | 2 +- .../org/dependencytrack/notification/NotificationScopeTest.java | 2 +- .../notification/NotificationSubsystemInitializerTest.java | 2 +- .../notification/publisher/AbstractPublisherTest.java | 2 +- .../notification/publisher/AbstractWebhookPublisherTest.java | 2 +- .../notification/publisher/ConsolePublisherTest.java | 2 +- .../notification/publisher/CsWebexPublisherTest.java | 2 +- .../publisher/DefaultNotificationPublishersTest.java | 2 +- .../notification/publisher/JiraPublisherTest.java | 2 +- .../notification/publisher/MattermostPublisherTest.java | 2 +- .../notification/publisher/MsTeamsPublisherTest.java | 2 +- .../notification/publisher/NotificationTestConfigProvider.java | 2 +- .../notification/publisher/SlackPublisherTest.java | 2 +- .../notification/publisher/WebhookPublisherTest.java | 2 +- .../notification/vo/AnalysisDecisionChangeTest.java | 2 +- .../notification/vo/NewVulnerabilityIdentifiedTest.java | 2 +- .../notification/vo/NewVulnerableDependencyTest.java | 2 +- .../dependencytrack/parser/common/resolver/CweResolverTest.java | 2 +- .../parser/cyclonedx/CycloneDxValidatorTest.java | 2 +- .../java/org/dependencytrack/parser/snyk/SnykParserTest.java | 2 +- .../dependencytrack/persistence/DefaultObjectGeneratorTest.java | 2 +- .../persistence/PackageURLStringConverterTest.java | 2 +- .../org/dependencytrack/persistence/PolicyQueryManagerTest.java | 2 +- .../dependencytrack/persistence/ProjectQueryManagerTest.java | 2 +- .../persistence/VulnerabilityQueryManagerTest.java | 2 +- .../converter/OrganizationalContactsJsonConverterTest.java | 2 +- .../converter/OrganizationalEntityJsonConverterTest.java | 2 +- .../dependencytrack/policy/ComponentAgePolicyEvaluatorTest.java | 2 +- .../policy/ComponentHashPolicyEvaluatorTest.java | 2 +- .../dependencytrack/policy/CoordinatesPolicyEvaluatorTest.java | 2 +- .../java/org/dependencytrack/policy/CpePolicyEvaluatorTest.java | 2 +- .../java/org/dependencytrack/policy/CwePolicyEvaluatorTest.java | 2 +- .../dependencytrack/policy/LicenseGroupPolicyEvaluatorTest.java | 2 +- .../org/dependencytrack/policy/LicensePolicyEvaluatorTest.java | 2 +- src/test/java/org/dependencytrack/policy/MatcherTest.java | 2 +- .../dependencytrack/policy/PackageURLPolicyEvaluatorTest.java | 2 +- src/test/java/org/dependencytrack/policy/PolicyEngineTest.java | 2 +- .../org/dependencytrack/policy/SeverityPolicyEvaluatorTest.java | 2 +- .../dependencytrack/policy/SwidTagIdPolicyEvaluatorTest.java | 2 +- .../policy/VersionDistancePolicyEvaluatorTest.java | 2 +- .../policy/VulnerabilityIdPolicyEvaluatorTest.java | 2 +- .../org/dependencytrack/resources/v1/AnalysisResourceTest.java | 2 +- .../org/dependencytrack/resources/v1/BadgeResourceTest.java | 2 +- .../java/org/dependencytrack/resources/v1/BomResourceTest.java | 2 +- .../dependencytrack/resources/v1/CalculatorResourceTest.java | 2 +- .../org/dependencytrack/resources/v1/ComponentResourceTest.java | 2 +- .../resources/v1/ConfigPropertyResourceTest.java | 2 +- .../java/org/dependencytrack/resources/v1/CweResourceTest.java | 2 +- .../resources/v1/DependencyGraphResourceTest.java | 2 +- .../org/dependencytrack/resources/v1/FindingResourceTest.java | 2 +- .../dependencytrack/resources/v1/IntegrationResourceTest.java | 2 +- .../java/org/dependencytrack/resources/v1/LdapResourceTest.java | 2 +- .../dependencytrack/resources/v1/LicenseGroupResourceTest.java | 2 +- .../org/dependencytrack/resources/v1/LicenseResourceTest.java | 2 +- .../resources/v1/NotificationPublisherResourceTest.java | 2 +- .../resources/v1/NotificationRuleResourceTest.java | 2 +- .../dependencytrack/resources/v1/PermissionResourceTest.java | 2 +- .../org/dependencytrack/resources/v1/PolicyResourceTest.java | 2 +- .../resources/v1/PolicyViolationResourceTest.java | 2 +- .../resources/v1/ProjectPropertyResourceTest.java | 2 +- .../org/dependencytrack/resources/v1/ProjectResourceTest.java | 2 +- .../dependencytrack/resources/v1/RepositoryResourceTest.java | 2 +- .../org/dependencytrack/resources/v1/SearchResourceTest.java | 2 +- .../java/org/dependencytrack/resources/v1/TeamResourceTest.java | 2 +- .../resources/v1/UserResourceAuthenticatedTest.java | 2 +- .../resources/v1/UserResourceUnauthenticatedTest.java | 2 +- .../java/org/dependencytrack/resources/v1/VexResourceTest.java | 2 +- .../resources/v1/ViolationAnalysisResourceTest.java | 2 +- .../dependencytrack/resources/v1/VulnerabilityResourceTest.java | 2 +- .../resources/v1/exception/NotFoundExceptionMapperTest.java | 2 +- .../java/org/dependencytrack/resources/v1/misc/BadgerTest.java | 2 +- .../java/org/dependencytrack/search/ComponentIndexerTest.java | 2 +- .../java/org/dependencytrack/search/LicenseIndexerTest.java | 2 +- .../java/org/dependencytrack/search/ProjectIndexerTest.java | 2 +- .../org/dependencytrack/search/ServiceComponentIndexerTest.java | 2 +- .../org/dependencytrack/search/VulnerabilityIndexerTest.java | 2 +- .../dependencytrack/search/VulnerableSoftwareIndexerTest.java | 2 +- .../java/org/dependencytrack/servlet/NvdMirrorServletTest.java | 2 +- .../org/dependencytrack/tasks/BomUploadProcessingTaskTest.java | 2 +- .../org/dependencytrack/tasks/GitHubAdvisoryMirrorTaskTest.java | 2 +- .../tasks/InternalComponentIdentificationTaskTest.java | 2 +- .../java/org/dependencytrack/tasks/NistApiMirrorTaskTest.java | 2 +- .../tasks/metrics/AbstractMetricsUpdateTaskTest.java | 2 +- .../tasks/metrics/ComponentMetricsUpdateTaskTest.java | 2 +- .../tasks/metrics/PortfolioMetricsUpdateTaskTest.java | 2 +- .../tasks/metrics/ProjectMetricsUpdateTaskTest.java | 2 +- .../tasks/metrics/VulnerabilityMetricsUpdateTaskTest.java | 2 +- .../tasks/repositories/CargoMetaAnalyzerTest.java | 2 +- .../tasks/repositories/ComposerMetaAnalyzerTest.java | 2 +- .../tasks/repositories/CpanMetaAnalyzerTest.java | 2 +- .../dependencytrack/tasks/repositories/GemMetaAnalyzerTest.java | 2 +- .../tasks/repositories/GitHubMetaAnalyzerTest.java | 2 +- .../tasks/repositories/GoModulesMetaAnalyzerTest.java | 2 +- .../dependencytrack/tasks/repositories/HexMetaAnalyzerTest.java | 2 +- .../tasks/repositories/MavenMetaAnalyzerTest.java | 2 +- .../dependencytrack/tasks/repositories/NpmMetaAnalyzerTest.java | 2 +- .../tasks/repositories/NugetMetaAnalyzerTest.java | 2 +- .../tasks/repositories/PypiMetaAnalyzerTest.java | 2 +- .../tasks/scanners/InternalAnalysisTaskCpeMatchingTest.java | 2 +- .../dependencytrack/tasks/scanners/SnykAnalysisTaskTest.java | 2 +- .../tasks/scanners/TrivyAnalysisTaskIntegrationTest.java | 2 +- .../dependencytrack/tasks/scanners/TrivyAnalysisTaskTest.java | 2 +- .../dependencytrack/tasks/scanners/VulnDBAnalysisTaskTest.java | 2 +- src/test/java/org/dependencytrack/util/DateUtilTest.java | 2 +- src/test/java/org/dependencytrack/util/HashUtilTest.java | 2 +- src/test/java/org/dependencytrack/util/HttpUtilTest.java | 2 +- src/test/java/org/dependencytrack/util/PersistenceUtilTest.java | 2 +- .../java/org/dependencytrack/util/RoundRobinAccessorTest.java | 2 +- 618 files changed, 618 insertions(+), 618 deletions(-) diff --git a/.checkstyle-header b/.checkstyle-header index c773577a1f..a89578d349 100644 --- a/.checkstyle-header +++ b/.checkstyle-header @@ -14,5 +14,5 @@ * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) Steve Springett. All Rights Reserved. + * Copyright (c) OWASP Foundation. All Rights Reserved. */ \ No newline at end of file diff --git a/README.md b/README.md index b49618376c..9a76912cf9 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ Interested in contributing to Dependency-Track? Please check [`CONTRIBUTING.md`] * Discussion (Groups.io): ## Copyright & License -Dependency-Track is Copyright (c) Steve Springett. All Rights Reserved. +Dependency-Track is Copyright (c) OWASP Foundation. All Rights Reserved. Permission to modify and redistribute is granted under the terms of the [Apache License 2.0]. diff --git a/dev/docker-compose.monitoring.yml b/dev/docker-compose.monitoring.yml index 1c2c5874b1..872f2a19e6 100644 --- a/dev/docker-compose.monitoring.yml +++ b/dev/docker-compose.monitoring.yml @@ -13,7 +13,7 @@ # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 -# Copyright (c) Steve Springett. All Rights Reserved. +# Copyright (c) OWASP Foundation. All Rights Reserved. services: apiserver: environment: diff --git a/dev/docker-compose.mssql.yml b/dev/docker-compose.mssql.yml index a7d354ee80..1257b99c13 100644 --- a/dev/docker-compose.mssql.yml +++ b/dev/docker-compose.mssql.yml @@ -13,7 +13,7 @@ # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 -# Copyright (c) Steve Springett. All Rights Reserved. +# Copyright (c) OWASP Foundation. All Rights Reserved. services: apiserver: depends_on: diff --git a/dev/docker-compose.postgres.yml b/dev/docker-compose.postgres.yml index 561247a195..28fe4592b2 100644 --- a/dev/docker-compose.postgres.yml +++ b/dev/docker-compose.postgres.yml @@ -13,7 +13,7 @@ # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 -# Copyright (c) Steve Springett. All Rights Reserved. +# Copyright (c) OWASP Foundation. All Rights Reserved. services: apiserver: depends_on: diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 20343ee6dd..91e8abc42c 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -13,7 +13,7 @@ # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 -# Copyright (c) Steve Springett. All Rights Reserved. +# Copyright (c) OWASP Foundation. All Rights Reserved. name: "dependency-track" services: diff --git a/dev/scripts/cwe-dictionary-generate.py b/dev/scripts/cwe-dictionary-generate.py index 3eea75aa47..c9ee30d205 100644 --- a/dev/scripts/cwe-dictionary-generate.py +++ b/dev/scripts/cwe-dictionary-generate.py @@ -33,7 +33,7 @@ * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) Steve Springett. All Rights Reserved. + * Copyright (c) OWASP Foundation. All Rights Reserved. */ package {{ package }}; diff --git a/docs/images/dt-logo.svg b/docs/images/dt-logo.svg index 0b6bebfa1a..234369bea2 100644 --- a/docs/images/dt-logo.svg +++ b/docs/images/dt-logo.svg @@ -15,7 +15,7 @@ - limitations under the License. - - SPDX-License-Identifier: Apache-2.0 - - Copyright (c) Steve Springett. All Rights Reserved. + - Copyright (c) OWASP Foundation. All Rights Reserved. --> diff --git a/pom.xml b/pom.xml index 4d31f6842a..218cc9f77e 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ ~ limitations under the License. ~ ~ SPDX-License-Identifier: Apache-2.0 - ~ Copyright (c) Steve Springett. All Rights Reserved. + ~ Copyright (c) OWASP Foundation. All Rights Reserved. --> Date: Wed, 20 Mar 2024 20:43:07 -0400 Subject: [PATCH 028/412] Keep Only Unique Rules In SARIF And Other Minor Changes The rules array in the generated SARIF should contain only unique values, so that the same rule can be referenced by multiple results. Rule name in the SARIF should be PascalCased. Using WordUtils from apache commons library to convert cweName to PascalCase. Set default escape strategy to the Pebble Engine to json, to escape linebreaks and double quotes in vulnerability description. Update test case to assert whole SARIF json. Signed-off-by: Aravind Parappil --- .../resources/v1/FindingResource.java | 29 +- .../resources/templates/findings/sarif.peb | 20 +- .../resources/v1/FindingResourceTest.java | 247 ++++++++++++++++-- 3 files changed, 260 insertions(+), 36 deletions(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index b76526526f..73ef81a39c 100644 --- a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -38,6 +38,7 @@ import java.io.Writer; import javax.ws.rs.HeaderParam; import javax.ws.rs.core.Response.Status; +import org.apache.commons.lang3.text.WordUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.PolicyEvaluationEvent; import org.dependencytrack.event.RepositoryMetaEvent; @@ -319,13 +320,29 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") } private String generateSARIF(List findings) throws IOException { - final PebbleEngine engine = new PebbleEngine.Builder().newLineTrimming(false).build(); + final PebbleEngine engine = new PebbleEngine.Builder() + .newLineTrimming(false) + .defaultEscapingStrategy("json") + .build(); final PebbleTemplate sarifTemplate = engine.getTemplate("templates/findings/sarif.peb"); final Map context = new HashMap<>(); final About about = new About(); + + // Using "vulnId" as key, forming a list of unique vulnerabilities across all findings + // Also converts cweName to PascalCase, since it will be used as rule.name in the SARIF file + List> uniqueVulnerabilities = findings.stream() + .collect(Collectors.toMap( + finding -> finding.getVulnerability().get("vulnId"), + FindingResource::convertCweNameToPascalCase, + (existingVuln, replacementVuln) -> existingVuln)) + .values() + .stream() + .toList(); + context.put("findings", findings); context.put("dependencyTrackVersion", about.getVersion()); + context.put("uniqueVulnerabilities", uniqueVulnerabilities); try (final Writer writer = new StringWriter()) { sarifTemplate.evaluate(writer, context); @@ -333,4 +350,14 @@ private String generateSARIF(List findings) throws IOException { } } + private static Map convertCweNameToPascalCase(Finding finding) { + final Object cweName = finding.getVulnerability() + .get("cweName"); + if (cweName != null) { + final String pascalCasedCweName = WordUtils.capitalizeFully(cweName.toString()).replaceAll("\\s", ""); + finding.getVulnerability().put("cweName", pascalCasedCweName); + } + return finding.getVulnerability(); + } + } diff --git a/src/main/resources/templates/findings/sarif.peb b/src/main/resources/templates/findings/sarif.peb index 6c4e600841..8fb65f196a 100644 --- a/src/main/resources/templates/findings/sarif.peb +++ b/src/main/resources/templates/findings/sarif.peb @@ -8,12 +8,12 @@ "name": "OWASP Dependency-Track", "version": "{{ dependencyTrackVersion }}", "informationUri": "https://dependencytrack.org/", - "rules": [{% for finding in findings %} + "rules": [{% for vuln in uniqueVulnerabilities %} { - "id": "{{ finding.vulnerability.vulnId }}", - "name": "{{ finding.component.name }} - {{ finding.vulnerability.cweName }}", + "id": "{{ vuln.vulnId }}", + "name": "{{ vuln.cweName }}", "shortDescription": { - "text": "{{ finding.vulnerability.description | split('\n') | join(' ') | trim }}" + "text": "{{ vuln.description | trim }}" } }{% if not loop.last %},{% endif %}{% endfor %} ] @@ -23,15 +23,15 @@ { "ruleId": "{{ finding.vulnerability.vulnId }}", "message": { - "text": "{{ finding.vulnerability.description | split('\n') | join(' ') | trim }}" + "text": "{{ finding.vulnerability.description | trim }}" }, "locations": [ { - "physicalLocation": { - "artifactLocation": { - "uri": "{{ finding.component.purl }}" + "logicalLocations": [ + { + "fullyQualifiedName": "{{ finding.component.purl }}" } - } + ] } ], "level": {% if ['LOW', 'INFO'] contains finding.vulnerability.severity %}"note",{% elseif finding.vulnerability.severity == 'MEDIUM' %}"warning",{% elseif ['HIGH', 'CRITICAL'] contains finding.vulnerability.severity %}"error",{% else %}"none",{% endif %} @@ -45,7 +45,7 @@ "epssScore": "{{ finding.vulnerability.epssScore }}", "epssPercentile": "{{ finding.vulnerability.epssPercentile }}", "severityRank": "{{ finding.vulnerability.severityRank }}", - "recommendation": "{{ finding.vulnerability.recommendation | split('\n') | join(' ') | trim }}" + "recommendation": "{{ finding.vulnerability.recommendation | trim }}" } }{% if not loop.last %},{% endif %}{% endfor %} ] diff --git a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java index 96079b4304..6ef0066604 100644 --- a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java @@ -18,7 +18,10 @@ */ package org.dependencytrack.resources.v1; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; import static org.dependencytrack.resources.v1.FindingResource.MEDIA_TYPE_SARIF_JSON; +import static org.hamcrest.CoreMatchers.equalTo; import alpine.Config; import alpine.model.About; @@ -595,41 +598,210 @@ public void getAllFindingsGroupedByVulnerabilityWithAclEnabled() { @Test public void getSARIFFindingsByProjectTest() { - Project p1 = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); - Component c1 = createComponent(p1, "Component A", "1.0"); - Component c2 = createComponent(p1, "Component B", "1.0"); - Component c3 = createComponent(p1, "Component C", "1.0"); - Vulnerability v1 = createVulnerability("Vuln-1", Severity.CRITICAL); - Vulnerability v2 = createVulnerability("Vuln-2", Severity.HIGH); - Vulnerability v3 = createVulnerability("Vuln-3", Severity.MEDIUM); - Vulnerability v4 = createVulnerability("Vuln-4", Severity.LOW); + Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + Component c1 = createComponent(project, "Component 1", "1.1.4"); + Component c2 = createComponent(project, "Component 2", "2.78.123"); + c1.setGroup("org.acme"); + c2.setGroup("com.xyz"); + c1.setPurl("pkg:maven/org.acme/component1@1.1.4?type=jar"); + c2.setPurl("pkg:maven/com.xyz/component2@2.78.123?type=jar"); + + Vulnerability v1 = createVulnerability("Vuln-1", Severity.CRITICAL, "Vuln Title 1", "This is a description", null, 80); + Vulnerability v2 = createVulnerability("Vuln-2", Severity.HIGH, "Vuln Title 2", " Yet another description but with surrounding whitespaces ", "", 46); + Vulnerability v3 = createVulnerability("Vuln-3", Severity.LOW, "Vuln Title 3", "A description-with-hyphens-(and parentheses)", " Recommendation with whitespaces ", 23); + + // Note: Same vulnerability added to multiple components to test whether "rules" field doesn't contain duplicates qm.addVulnerability(v1, c1, AnalyzerIdentity.NONE); qm.addVulnerability(v2, c1, AnalyzerIdentity.NONE); + qm.addVulnerability(v3, c1, AnalyzerIdentity.NONE); qm.addVulnerability(v3, c2, AnalyzerIdentity.NONE); - qm.addVulnerability(v4, c3, AnalyzerIdentity.NONE); - Response response = target(V1_FINDING + "/project/" + p1.getUuid().toString()).request() + Response response = target(V1_FINDING + "/project/" + project.getUuid().toString()).request() .header(HttpHeaders.ACCEPT, MEDIA_TYPE_SARIF_JSON) .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertEquals(MEDIA_TYPE_SARIF_JSON, response.getHeaderString(HttpHeaders.CONTENT_TYPE)); - - JsonObject json = parseJsonObject(response); - Assert.assertNotNull(json); - - JsonArray runs = json.getJsonArray("runs"); - Assert.assertNotNull(runs); - JsonObject runsJson = runs.getJsonObject(0); - Assert.assertNotNull(runsJson); - Assert.assertEquals("OWASP Dependency-Track", runsJson.getJsonObject("tool").getJsonObject("driver").getString("name")); - Assert.assertEquals(new About().getVersion(), runsJson.getJsonObject("tool").getJsonObject("driver").getString("version")); - Assert.assertNotNull(runsJson.getJsonObject("tool").getJsonObject("driver").getJsonArray("rules")); - Assert.assertEquals("Vuln-1", runsJson.getJsonObject("tool").getJsonObject("driver").getJsonArray("rules").getJsonObject(0).getString("id")); - Assert.assertNotNull(runsJson.getJsonArray("results")); - Assert.assertEquals("error", runsJson.getJsonArray("results").getJsonObject(0).getString("level")); - Assert.assertEquals("Vuln-1", runsJson.getJsonArray("results").getJsonObject(0).getString("ruleId")); + final String jsonResponse = getPlainTextBody(response); + + assertThatJson(jsonResponse) + .withMatcher("version", equalTo(new About().getVersion())) + .withMatcher("vuln1Id", equalTo(v1.getVulnId())) + .withMatcher("vuln2Id", equalTo(v2.getVulnId())) + .withMatcher("vuln3Id", equalTo(v3.getVulnId())) + .withMatcher("vuln1CweId", equalTo(v1.getCwes().get(0).toString())) + .withMatcher("vuln2CweId", equalTo(v2.getCwes().get(0).toString())) + .withMatcher("vuln3CweId", equalTo(v3.getCwes().get(0).toString())) + .withMatcher("vuln1Desc", equalTo(v1.getDescription().trim())) + .withMatcher("vuln2Desc", equalTo(v2.getDescription().trim())) + .withMatcher("vuln3Desc", equalTo(v3.getDescription().trim())) + .withMatcher("vuln1Level", equalTo(getSARIFLevelFromSeverity(v1.getSeverity()))) + .withMatcher("vuln2Level", equalTo(getSARIFLevelFromSeverity(v2.getSeverity()))) + .withMatcher("vuln3Level", equalTo(getSARIFLevelFromSeverity(v3.getSeverity()))) + .withMatcher("vuln3Recommendation", equalTo(v3.getRecommendation().trim())) + .withMatcher("comp1Purl", equalTo(c1.getPurl().toString())) + .withMatcher("comp2Purl", equalTo(c2.getPurl().toString())) + .withMatcher("comp1Name", equalTo(c1.getName())) + .withMatcher("comp2Name", equalTo(c2.getName())) + .withMatcher("comp1Group", equalTo(c1.getGroup())) + .withMatcher("comp2Group", equalTo(c2.getGroup())) + .withMatcher("comp1Version", equalTo(c1.getVersion())) + .withMatcher("comp2Version", equalTo(c2.getVersion())) + .isEqualTo(json(""" + { + "version": "2.1.0", + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "name": "OWASP Dependency-Track", + "version": "${json-unit.matches:version}", + "informationUri": "https://dependencytrack.org/", + "rules": [ + { + "id": "${json-unit.matches:vuln1Id}", + "name": "ImproperNeutralizationOfScript-relatedHtmlTagsInAWebPage(basicXss)", + "shortDescription": { + "text": "${json-unit.matches:vuln1Desc}" + } + }, + { + "id": "${json-unit.matches:vuln2Id}", + "name": "PathEquivalence:'filename'(trailingSpace)", + "shortDescription": { + "text": "${json-unit.matches:vuln2Desc}" + } + }, + { + "id": "${json-unit.matches:vuln3Id}", + "name": "RelativePathTraversal", + "shortDescription": { + "text": "${json-unit.matches:vuln3Desc}" + } + } + ] + } + }, + "results": [ + { + "ruleId": "${json-unit.matches:vuln1Id}", + "message": { + "text": "${json-unit.matches:vuln1Desc}" + }, + "locations": [ + { + "logicalLocations": [ + { + "fullyQualifiedName": "${json-unit.matches:comp1Purl}" + } + ] + } + ], + "level": "${json-unit.matches:vuln1Level}", + "properties": { + "name": "${json-unit.matches:comp1Name}", + "group": "org.acme", + "version": "${json-unit.matches:comp1Version}", + "source": "INTERNAL", + "cweId": "${json-unit.matches:vuln1CweId}", + "cvssV3BaseScore": "", + "epssScore": "", + "epssPercentile": "", + "severityRank": "0", + "recommendation": "" + } + }, + { + "ruleId": "${json-unit.matches:vuln2Id}", + "message": { + "text": "${json-unit.matches:vuln2Desc}" + }, + "locations": [ + { + "logicalLocations": [ + { + "fullyQualifiedName": "${json-unit.matches:comp1Purl}" + } + ] + } + ], + "level": "${json-unit.matches:vuln2Level}", + "properties": { + "name": "${json-unit.matches:comp1Name}", + "group": "${json-unit.matches:comp1Group}", + "version": "${json-unit.matches:comp1Version}", + "source": "INTERNAL", + "cweId": "${json-unit.matches:vuln2CweId}", + "cvssV3BaseScore": "", + "epssScore": "", + "epssPercentile": "", + "severityRank": "1", + "recommendation": "" + } + }, + { + "ruleId": "${json-unit.matches:vuln3Id}", + "message": { + "text": "${json-unit.matches:vuln3Desc}" + }, + "locations": [ + { + "logicalLocations": [ + { + "fullyQualifiedName": "${json-unit.matches:comp1Purl}" + } + ] + } + ], + "level": "${json-unit.matches:vuln3Level}", + "properties": { + "name": "${json-unit.matches:comp1Name}", + "group": "${json-unit.matches:comp1Group}", + "version": "${json-unit.matches:comp1Version}", + "source": "INTERNAL", + "cweId": "${json-unit.matches:vuln3CweId}", + "cvssV3BaseScore": "", + "epssScore": "", + "epssPercentile": "", + "severityRank": "3", + "recommendation": "${json-unit.matches:vuln3Recommendation}" + } + }, + { + "ruleId": "${json-unit.matches:vuln3Id}", + "message": { + "text": "${json-unit.matches:vuln3Desc}" + }, + "locations": [ + { + "logicalLocations": [ + { + "fullyQualifiedName": "${json-unit.matches:comp2Purl}" + } + ] + } + ], + "level": "${json-unit.matches:vuln3Level}", + "properties": { + "name": "${json-unit.matches:comp2Name}", + "group": "${json-unit.matches:comp2Group}", + "version": "${json-unit.matches:comp2Version}", + "source": "INTERNAL", + "cweId": "${json-unit.matches:vuln3CweId}", + "cvssV3BaseScore": "", + "epssScore": "", + "epssPercentile": "", + "severityRank": "3", + "recommendation": "${json-unit.matches:vuln3Recommendation}" + } + } + ] + } + ] + } + """)); } private Component createComponent(Project project, String name, String version) { @@ -648,4 +820,29 @@ private Vulnerability createVulnerability(String vulnId, Severity severity) { vulnerability.setCwes(List.of(80, 666)); return qm.createVulnerability(vulnerability, false); } + + private Vulnerability createVulnerability(String vulnId, Severity severity, String title, String description, String recommendation, Integer cweId) { + Vulnerability vulnerability = new Vulnerability(); + vulnerability.setVulnId(vulnId); + vulnerability.setSource(Vulnerability.Source.INTERNAL); + vulnerability.setSeverity(severity); + vulnerability.setTitle(title); + vulnerability.setDescription(description); + vulnerability.setRecommendation(recommendation); + vulnerability.setCwes(List.of(cweId)); + return qm.createVulnerability(vulnerability, false); + } + + private static String getSARIFLevelFromSeverity(Severity severity) { + if (Severity.LOW == severity || Severity.INFO == severity) { + return "note"; + } + if (Severity.MEDIUM == severity) { + return "warning"; + } + if (Severity.HIGH == severity || Severity.CRITICAL == severity) { + return "error"; + } + return "none"; + } } From 55300d3b54f9a2d1d2311bb79f220f0572b983a6 Mon Sep 17 00:00:00 2001 From: Aravind Parappil Date: Wed, 20 Mar 2024 21:18:24 -0400 Subject: [PATCH 029/412] Add fullDescription And fullName Since fullName is required by Azure DevOps ingestion rules, added it as concatenation of app name and version. shortDescription has been changed to be vulnerability ID and added fullDescription field as the actual vulnerability description (same as Trivy's SARIF) Signed-off-by: Aravind Parappil --- src/main/resources/templates/findings/sarif.peb | 4 ++++ .../resources/v1/FindingResourceTest.java | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/main/resources/templates/findings/sarif.peb b/src/main/resources/templates/findings/sarif.peb index 8fb65f196a..c2b0d28987 100644 --- a/src/main/resources/templates/findings/sarif.peb +++ b/src/main/resources/templates/findings/sarif.peb @@ -6,6 +6,7 @@ "tool": { "driver": { "name": "OWASP Dependency-Track", + "fullName": "OWASP Dependency-Track - {{ dependencyTrackVersion }}", "version": "{{ dependencyTrackVersion }}", "informationUri": "https://dependencytrack.org/", "rules": [{% for vuln in uniqueVulnerabilities %} @@ -13,6 +14,9 @@ "id": "{{ vuln.vulnId }}", "name": "{{ vuln.cweName }}", "shortDescription": { + "text": "{{ vuln.vulnId }}" + }, + "fullDescription": { "text": "{{ vuln.description | trim }}" } }{% if not loop.last %},{% endif %}{% endfor %} diff --git a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java index 6ef0066604..6b786fb6fa 100644 --- a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java @@ -627,6 +627,7 @@ public void getSARIFFindingsByProjectTest() { assertThatJson(jsonResponse) .withMatcher("version", equalTo(new About().getVersion())) + .withMatcher("fullName", equalTo("OWASP Dependency-Track - " + new About().getVersion())) .withMatcher("vuln1Id", equalTo(v1.getVulnId())) .withMatcher("vuln2Id", equalTo(v2.getVulnId())) .withMatcher("vuln3Id", equalTo(v3.getVulnId())) @@ -657,6 +658,7 @@ public void getSARIFFindingsByProjectTest() { "tool": { "driver": { "name": "OWASP Dependency-Track", + "fullName": "${json-unit.matches:fullName}", "version": "${json-unit.matches:version}", "informationUri": "https://dependencytrack.org/", "rules": [ @@ -664,6 +666,9 @@ public void getSARIFFindingsByProjectTest() { "id": "${json-unit.matches:vuln1Id}", "name": "ImproperNeutralizationOfScript-relatedHtmlTagsInAWebPage(basicXss)", "shortDescription": { + "text": "${json-unit.matches:vuln1Id}" + }, + "fullDescription": { "text": "${json-unit.matches:vuln1Desc}" } }, @@ -671,6 +676,9 @@ public void getSARIFFindingsByProjectTest() { "id": "${json-unit.matches:vuln2Id}", "name": "PathEquivalence:'filename'(trailingSpace)", "shortDescription": { + "text": "${json-unit.matches:vuln2Id}" + }, + "fullDescription": { "text": "${json-unit.matches:vuln2Desc}" } }, @@ -678,6 +686,9 @@ public void getSARIFFindingsByProjectTest() { "id": "${json-unit.matches:vuln3Id}", "name": "RelativePathTraversal", "shortDescription": { + "text": "${json-unit.matches:vuln3Id}" + }, + "fullDescription": { "text": "${json-unit.matches:vuln3Desc}" } } From cbc4bcfa95b1ab2c280e029e71bd5e1fb297b654 Mon Sep 17 00:00:00 2001 From: Massimo Prencipe Date: Thu, 21 Mar 2024 16:10:20 +0200 Subject: [PATCH 030/412] Disable automatic API key generation for teams. Fixes issue #2552. Signed-off-by: Massimo Prencipe --- .../java/org/dependencytrack/resources/v1/TeamResource.java | 2 +- .../java/org/dependencytrack/resources/v1/TeamResourceTest.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java index 2bb2389219..7dc67a54ab 100644 --- a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java @@ -132,7 +132,7 @@ public Response createTeam(Team jsonTeam) { ); try (QueryManager qm = new QueryManager()) { - final Team team = qm.createTeam(jsonTeam.getName(), true); + final Team team = qm.createTeam(jsonTeam.getName(), false); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Team created: " + team.getName()); return Response.status(Response.Status.CREATED).entity(team).build(); } diff --git a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java index 59e73d07b5..0d7cc142cb 100644 --- a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java @@ -134,6 +134,7 @@ public void createTeamTest() { Assert.assertNotNull(json); Assert.assertEquals("My Team", json.getString("name")); Assert.assertTrue(UuidUtil.isValidUUID(json.getString("uuid"))); + Assert.assertTrue(json.getJsonArray("apiKeys").isEmpty()); } @Test From 61e9140d8fafec92022f32619d282bf116c53283 Mon Sep 17 00:00:00 2001 From: Massimo Prencipe Date: Thu, 21 Mar 2024 16:18:27 +0200 Subject: [PATCH 031/412] Fix documentation Signed-off-by: Massimo Prencipe --- docs/_docs/integrations/rest-api.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/_docs/integrations/rest-api.md b/docs/_docs/integrations/rest-api.md index 1e3dc80522..53efd08b76 100644 --- a/docs/_docs/integrations/rest-api.md +++ b/docs/_docs/integrations/rest-api.md @@ -15,7 +15,6 @@ FireFox extensions can be use to quickly use the Swagger UI Console. ![Swagger UI Console](/images/screenshots/swagger-ui-console.png) -Prior to using the REST APIs, an API Key must be generated. By default, creating a team will also create a corresponding -API key. A team may have multiple keys. +Prior to using the REST APIs, an API Key must be generated. By default, creating a team will NOT create a an API key. A team may have multiple keys. ![Teams - API Key](/images/screenshots/teams.png) From f6f5150d57fc363f268b9839b9bbf6b89100db40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 08:11:08 +0000 Subject: [PATCH 032/412] Bump docker/login-action from 3.0.0 to 3.1.0 Bumps [docker/login-action](https://github.com/docker/login-action) from 3.0.0 to 3.1.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/343f7c4344506bcbf9b4de18042ae17996df046d...e92390c5fb421da1463c202d546fed0ec5c39f20) --- updated-dependencies: - dependency-name: docker/login-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 4905722066..fa71ab5b04 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -92,7 +92,7 @@ jobs: install: true - name: Login to Docker.io - uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # tag=v3.0.0 + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # tag=v3.1.0 if: ${{ inputs.publish-container }} with: registry: docker.io From cadf7e3670e119f651e367eb5cb74d61beaa55c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 08:11:14 +0000 Subject: [PATCH 033/412] Bump actions/dependency-review-action from 4.1.3 to 4.2.4 Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.1.3 to 4.2.4. - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/9129d7d40b8c12c1ed0f60400d00c92d437adcce...733dd5d4a5203f238c33806593ec0f5fc5343d8c) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/dependency-review.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index 3a321cedbc..bcae0d0ea2 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -12,4 +12,4 @@ jobs: uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 - name: Dependency Review - uses: actions/dependency-review-action@9129d7d40b8c12c1ed0f60400d00c92d437adcce # tag=v4.1.3 + uses: actions/dependency-review-action@733dd5d4a5203f238c33806593ec0f5fc5343d8c # tag=v4.2.4 From 5e4386dc690d879ce69f492caa7615b2bbda7f1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 08:11:27 +0000 Subject: [PATCH 034/412] Bump github/codeql-action from 3.24.6 to 3.24.9 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.24.6 to 3.24.9. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8a470fddafa5cbb6266ee11b37ef4d8aae19c571...1b1aada464948af03b950897e5eb522f92603cc2) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 4905722066..882a9c7e3a 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -133,6 +133,6 @@ jobs: - name: Upload Trivy Scan Results to GitHub Security Tab if: ${{ inputs.publish-container }} - uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # tag=v3.24.6 + uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # tag=v3.24.9 with: sarif_file: 'trivy-results.sarif' From 6b1b68c365c5b78c8dbc0a026d6cda00fad3eff8 Mon Sep 17 00:00:00 2001 From: Aravind Parappil Date: Mon, 25 Mar 2024 18:44:15 -0400 Subject: [PATCH 035/412] Remove unwanted matchers and replace with string literals For the JSON assertion, removed matchers which can be replaced with string literals directly in the expected JSON string Signed-off-by: Aravind Parappil --- .../resources/v1/FindingResourceTest.java | 105 +++++++----------- 1 file changed, 42 insertions(+), 63 deletions(-) diff --git a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java index 6b786fb6fa..cc71e66647 100644 --- a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java @@ -628,27 +628,6 @@ public void getSARIFFindingsByProjectTest() { assertThatJson(jsonResponse) .withMatcher("version", equalTo(new About().getVersion())) .withMatcher("fullName", equalTo("OWASP Dependency-Track - " + new About().getVersion())) - .withMatcher("vuln1Id", equalTo(v1.getVulnId())) - .withMatcher("vuln2Id", equalTo(v2.getVulnId())) - .withMatcher("vuln3Id", equalTo(v3.getVulnId())) - .withMatcher("vuln1CweId", equalTo(v1.getCwes().get(0).toString())) - .withMatcher("vuln2CweId", equalTo(v2.getCwes().get(0).toString())) - .withMatcher("vuln3CweId", equalTo(v3.getCwes().get(0).toString())) - .withMatcher("vuln1Desc", equalTo(v1.getDescription().trim())) - .withMatcher("vuln2Desc", equalTo(v2.getDescription().trim())) - .withMatcher("vuln3Desc", equalTo(v3.getDescription().trim())) - .withMatcher("vuln1Level", equalTo(getSARIFLevelFromSeverity(v1.getSeverity()))) - .withMatcher("vuln2Level", equalTo(getSARIFLevelFromSeverity(v2.getSeverity()))) - .withMatcher("vuln3Level", equalTo(getSARIFLevelFromSeverity(v3.getSeverity()))) - .withMatcher("vuln3Recommendation", equalTo(v3.getRecommendation().trim())) - .withMatcher("comp1Purl", equalTo(c1.getPurl().toString())) - .withMatcher("comp2Purl", equalTo(c2.getPurl().toString())) - .withMatcher("comp1Name", equalTo(c1.getName())) - .withMatcher("comp2Name", equalTo(c2.getName())) - .withMatcher("comp1Group", equalTo(c1.getGroup())) - .withMatcher("comp2Group", equalTo(c2.getGroup())) - .withMatcher("comp1Version", equalTo(c1.getVersion())) - .withMatcher("comp2Version", equalTo(c2.getVersion())) .isEqualTo(json(""" { "version": "2.1.0", @@ -663,33 +642,33 @@ public void getSARIFFindingsByProjectTest() { "informationUri": "https://dependencytrack.org/", "rules": [ { - "id": "${json-unit.matches:vuln1Id}", + "id": "Vuln-1", "name": "ImproperNeutralizationOfScript-relatedHtmlTagsInAWebPage(basicXss)", "shortDescription": { - "text": "${json-unit.matches:vuln1Id}" + "text": "Vuln-1" }, "fullDescription": { - "text": "${json-unit.matches:vuln1Desc}" + "text": "This is a description" } }, { - "id": "${json-unit.matches:vuln2Id}", + "id": "Vuln-2", "name": "PathEquivalence:'filename'(trailingSpace)", "shortDescription": { - "text": "${json-unit.matches:vuln2Id}" + "text": "Vuln-2" }, "fullDescription": { - "text": "${json-unit.matches:vuln2Desc}" + "text": "Yet another description but with surrounding whitespaces" } }, { - "id": "${json-unit.matches:vuln3Id}", + "id": "Vuln-3", "name": "RelativePathTraversal", "shortDescription": { - "text": "${json-unit.matches:vuln3Id}" + "text": "Vuln-3" }, "fullDescription": { - "text": "${json-unit.matches:vuln3Desc}" + "text": "A description-with-hyphens-(and parentheses)" } } ] @@ -697,26 +676,26 @@ public void getSARIFFindingsByProjectTest() { }, "results": [ { - "ruleId": "${json-unit.matches:vuln1Id}", + "ruleId": "Vuln-1", "message": { - "text": "${json-unit.matches:vuln1Desc}" + "text": "This is a description" }, "locations": [ { "logicalLocations": [ { - "fullyQualifiedName": "${json-unit.matches:comp1Purl}" + "fullyQualifiedName": "pkg:maven/org.acme/component1@1.1.4?type=jar" } ] } ], - "level": "${json-unit.matches:vuln1Level}", + "level": "error", "properties": { - "name": "${json-unit.matches:comp1Name}", + "name": "Component 1", "group": "org.acme", - "version": "${json-unit.matches:comp1Version}", + "version": "1.1.4", "source": "INTERNAL", - "cweId": "${json-unit.matches:vuln1CweId}", + "cweId": "80", "cvssV3BaseScore": "", "epssScore": "", "epssPercentile": "", @@ -725,26 +704,26 @@ public void getSARIFFindingsByProjectTest() { } }, { - "ruleId": "${json-unit.matches:vuln2Id}", + "ruleId": "Vuln-2", "message": { - "text": "${json-unit.matches:vuln2Desc}" + "text": "Yet another description but with surrounding whitespaces" }, "locations": [ { "logicalLocations": [ { - "fullyQualifiedName": "${json-unit.matches:comp1Purl}" + "fullyQualifiedName": "pkg:maven/org.acme/component1@1.1.4?type=jar" } ] } ], - "level": "${json-unit.matches:vuln2Level}", + "level": "error", "properties": { - "name": "${json-unit.matches:comp1Name}", - "group": "${json-unit.matches:comp1Group}", - "version": "${json-unit.matches:comp1Version}", + "name": "Component 1", + "group": "org.acme", + "version": "1.1.4", "source": "INTERNAL", - "cweId": "${json-unit.matches:vuln2CweId}", + "cweId": "46", "cvssV3BaseScore": "", "epssScore": "", "epssPercentile": "", @@ -753,59 +732,59 @@ public void getSARIFFindingsByProjectTest() { } }, { - "ruleId": "${json-unit.matches:vuln3Id}", + "ruleId": "Vuln-3", "message": { - "text": "${json-unit.matches:vuln3Desc}" + "text": "A description-with-hyphens-(and parentheses)" }, "locations": [ { "logicalLocations": [ { - "fullyQualifiedName": "${json-unit.matches:comp1Purl}" + "fullyQualifiedName": "pkg:maven/org.acme/component1@1.1.4?type=jar" } ] } ], - "level": "${json-unit.matches:vuln3Level}", + "level": "note", "properties": { - "name": "${json-unit.matches:comp1Name}", - "group": "${json-unit.matches:comp1Group}", - "version": "${json-unit.matches:comp1Version}", + "name": "Component 1", + "group": "org.acme", + "version": "1.1.4", "source": "INTERNAL", - "cweId": "${json-unit.matches:vuln3CweId}", + "cweId": "23", "cvssV3BaseScore": "", "epssScore": "", "epssPercentile": "", "severityRank": "3", - "recommendation": "${json-unit.matches:vuln3Recommendation}" + "recommendation": "Recommendation with whitespaces" } }, { - "ruleId": "${json-unit.matches:vuln3Id}", + "ruleId": "Vuln-3", "message": { - "text": "${json-unit.matches:vuln3Desc}" + "text": "A description-with-hyphens-(and parentheses)" }, "locations": [ { "logicalLocations": [ { - "fullyQualifiedName": "${json-unit.matches:comp2Purl}" + "fullyQualifiedName": "pkg:maven/com.xyz/component2@2.78.123?type=jar" } ] } ], - "level": "${json-unit.matches:vuln3Level}", + "level": "note", "properties": { - "name": "${json-unit.matches:comp2Name}", - "group": "${json-unit.matches:comp2Group}", - "version": "${json-unit.matches:comp2Version}", + "name": "Component 2", + "group": "com.xyz", + "version": "2.78.123", "source": "INTERNAL", - "cweId": "${json-unit.matches:vuln3CweId}", + "cweId": "23", "cvssV3BaseScore": "", "epssScore": "", "epssPercentile": "", "severityRank": "3", - "recommendation": "${json-unit.matches:vuln3Recommendation}" + "recommendation": "Recommendation with whitespaces" } } ] From 621f1a6684a27ccc6613a16316959d6efbd4ad9a Mon Sep 17 00:00:00 2001 From: Andres Tito Date: Tue, 19 Mar 2024 09:36:16 +0000 Subject: [PATCH 036/412] Introducing Alias feature to VulnDB vulnerabilities In this commit, we have introduced a new feature that computes and synchronizes CVE Aliases for vulnerabilities sourced from VulnDB. By extracting paired CVE information from the nvdAdditionalInfo, we are able to set an Alias. Signed-off-by: Andres Tito --- .../dependencytrack/model/Vulnerability.java | 15 +++++ .../parser/common/resolver/CveResolver.java | 59 +++++++++++++++++++ .../parser/vulndb/ModelConverter.java | 8 +++ .../VulnerabilityQueryManager.java | 1 + .../dependencytrack/tasks/VulnDbSyncTask.java | 20 ++++++- 5 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/dependencytrack/parser/common/resolver/CveResolver.java diff --git a/src/main/java/org/dependencytrack/model/Vulnerability.java b/src/main/java/org/dependencytrack/model/Vulnerability.java index bec30c64e6..692f53e6e3 100644 --- a/src/main/java/org/dependencytrack/model/Vulnerability.java +++ b/src/main/java/org/dependencytrack/model/Vulnerability.java @@ -144,6 +144,14 @@ public static Source resolve(String id) { @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The vulnerability ID may only contain printable characters") private String vulnId; + @Persistent + @Column(name = "ADDITIONAL_ID") + @NotBlank + @Size(min = 1, max = 255) + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The Additional ID may only contain printable characters") + private String additionalId; + @Persistent @Column(name = "SOURCE", allowsNull = "false") @NotBlank @@ -545,6 +553,13 @@ public void addCwe(Cwe cwe) { } } } + //It is common for vulnerabilities to come with an additional ID, e.g VulndbId and CVE-Id + public String getAdditionalVulnId() { + return additionalId; + } + public void setAdditionalVulnId(String additionalId) { + this.additionalId = additionalId; + } public BigDecimal getCvssV2BaseScore() { return cvssV2BaseScore; diff --git a/src/main/java/org/dependencytrack/parser/common/resolver/CveResolver.java b/src/main/java/org/dependencytrack/parser/common/resolver/CveResolver.java new file mode 100644 index 0000000000..1df56ac3ab --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/common/resolver/CveResolver.java @@ -0,0 +1,59 @@ +/* + * 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.parser.common.resolver; + +import java.util.regex.Pattern; +import java.util.regex.Matcher; +import org.apache.commons.lang3.StringUtils; +/** + * Attempts to resolve an internal CVE object from a string + * representation of a CVE. + */ +public class CveResolver { + + private static final CveResolver INSTANCE = new CveResolver(); + + private CveResolver() { + } + + public static CveResolver getInstance() { + return INSTANCE; + } + + /** + * Parses a CVE string returning the CVE ID, or null. + * + * @param cveString the string to parse + * @return a CVE object + */ + public String parseCveString(final String cveString) { + if (StringUtils.isNotBlank(cveString)) { + // Define the regex pattern + String pattern = "^CVE-\\d{4}-\\d+$"; + // Compile the pattern + Pattern regex = Pattern.compile(pattern); + // Match the input against the pattern + Matcher matcher = regex.matcher(cveString); + if (matcher.matches()) { + return cveString; + } + } + return null; + } +} diff --git a/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java b/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java index 70966299b0..dbde604879 100644 --- a/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java @@ -23,6 +23,7 @@ import org.dependencytrack.model.Cwe; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.parser.common.resolver.CweResolver; +import org.dependencytrack.parser.common.resolver.CveResolver; import org.dependencytrack.parser.vulndb.model.Author; import org.dependencytrack.parser.vulndb.model.CvssV2Metric; import org.dependencytrack.parser.vulndb.model.CvssV3Metric; @@ -162,12 +163,19 @@ public static Vulnerability convert(final QueryManager qm, final org.dependencyt if (vulnDbVuln.nvdAdditionalInfo() != null) { final String cweString = vulnDbVuln.nvdAdditionalInfo().cweId(); + final String cve_idString = vulnDbVuln.nvdAdditionalInfo().cveId(); if (cweString != null && cweString.startsWith("CWE-")) { final Cwe cwe = CweResolver.getInstance().lookup(cweString); if (cwe != null) { vuln.addCwe(cwe); } } + if (cve_idString != null && cve_idString.startsWith("CVE-")) { + final String cve_id = CveResolver.getInstance().parseCveString(cve_idString); + if (cve_id != null) { + vuln.setAdditionalVulnId(cve_id); + } + } } return vuln; } diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index 8629b078bb..5c5e702053 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -121,6 +121,7 @@ public Vulnerability updateVulnerability(Vulnerability transientVulnerability, b vulnerability.setOwaspRRTechnicalImpactScore(transientVulnerability.getOwaspRRTechnicalImpactScore()); vulnerability.setOwaspRRVector(transientVulnerability.getOwaspRRVector()); vulnerability.setCwes(transientVulnerability.getCwes()); + vulnerability.setAdditionalVulnId(transientVulnerability.getAdditionalVulnId()); if (transientVulnerability.getVulnerableSoftware() != null) { vulnerability.setVulnerableSoftware(transientVulnerability.getVulnerableSoftware()); } diff --git a/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java b/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java index 0e0486eef3..8ed6c06c89 100644 --- a/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java +++ b/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java @@ -42,6 +42,7 @@ import us.springett.parsers.cpe.CpeParser; import us.springett.parsers.cpe.exceptions.CpeEncodingException; import us.springett.parsers.cpe.exceptions.CpeParsingException; +import org.dependencytrack.model.VulnerabilityAlias; import java.io.File; import java.io.IOException; @@ -112,7 +113,21 @@ public void inform(final Event e) { } } } - + /* + * Compute Alias between VulnDB and CVEs + */ + public List computeAliases(Vulnerability vulnerability, QueryManager qm) { + List vulnerabilityAliasList = new ArrayList<>(); + final String cve_id = vulnerability.getAdditionalVulnId(); + if (cve_id != null) { + final VulnerabilityAlias vulnerabilityAlias = new VulnerabilityAlias(); + vulnerabilityAlias.setVulnDbId(vulnerability.getVulnId()); + vulnerabilityAlias.setCveId(cve_id); + qm.synchronizeVulnerabilityAlias(vulnerabilityAlias); + vulnerabilityAliasList.add(vulnerabilityAlias); + } + return vulnerabilityAliasList; + } /** * Synchronizes the VulnDB vulnerabilities with the internal Dependency-Track database. * @@ -131,6 +146,9 @@ private void updateDatasource(final Results results) { qm.updateAffectedVersionAttributions(synchronizeVulnerability, vsList, Vulnerability.Source.VULNDB); vsList = qm.reconcileVulnerableSoftware(synchronizeVulnerability, vsListOld, vsList, Vulnerability.Source.VULNDB); synchronizeVulnerability.setVulnerableSoftware(vsList); + if (synchronizeVulnerability.getAdditionalVulnId() != null){ + synchronizeVulnerability.setAliases(computeAliases(synchronizeVulnerability, qm)); + } qm.persist(synchronizeVulnerability); } } From 39ce742d28ff82baab4f82e6db1122bc57368be9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Mar 2024 08:18:12 +0000 Subject: [PATCH 037/412] Bump io.github.jeremylong:open-vulnerability-clients from 6.0.0 to 6.0.1 Bumps [io.github.jeremylong:open-vulnerability-clients](https://github.com/jeremylong/vuln-tools) from 6.0.0 to 6.0.1. - [Release notes](https://github.com/jeremylong/vuln-tools/releases) - [Commits](https://github.com/jeremylong/vuln-tools/compare/v6.0.0...v6.0.1) --- updated-dependencies: - dependency-name: io.github.jeremylong:open-vulnerability-clients dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 218cc9f77e..0e045b0ba0 100644 --- a/pom.xml +++ b/pom.xml @@ -98,7 +98,7 @@ 3.2.7 8.11.3 5.15.0 - 6.0.0 + 6.0.1 1.5.0 3.2.2 2.2.0 From f624876d7da429c2b8fcc4a4f774b69f771746a1 Mon Sep 17 00:00:00 2001 From: Andres Tito Date: Tue, 26 Mar 2024 16:06:37 +0000 Subject: [PATCH 038/412] No need to add new column in vulnerability table -Use metric.cveId if nvdAdditionalInfo() is null -There is no need to add a new column in the vulnerability table -Changes were done after a code review. -setAliases in vulndb/ModelConverter.java instead of VulnDbSyncTask. Signed-off-by: Andres Tito --- .../dependencytrack/model/Vulnerability.java | 15 ------ .../parser/common/resolver/CveResolver.java | 33 +++++++----- .../parser/vulndb/ModelConverter.java | 51 ++++++++++++++++--- .../VulnerabilityQueryManager.java | 1 - .../dependencytrack/tasks/VulnDbSyncTask.java | 19 ------- 5 files changed, 64 insertions(+), 55 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/Vulnerability.java b/src/main/java/org/dependencytrack/model/Vulnerability.java index 49064f1153..42380097e2 100644 --- a/src/main/java/org/dependencytrack/model/Vulnerability.java +++ b/src/main/java/org/dependencytrack/model/Vulnerability.java @@ -144,14 +144,6 @@ public static Source resolve(String id) { @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The vulnerability ID may only contain printable characters") private String vulnId; - @Persistent - @Column(name = "ADDITIONAL_ID") - @NotBlank - @Size(min = 1, max = 255) - @JsonDeserialize(using = TrimmedStringDeserializer.class) - @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The Additional ID may only contain printable characters") - private String additionalId; - @Persistent @Column(name = "SOURCE", allowsNull = "false") @NotBlank @@ -553,13 +545,6 @@ public void addCwe(Cwe cwe) { } } } - //It is common for vulnerabilities to come with an additional ID, e.g VulndbId and CVE-Id - public String getAdditionalVulnId() { - return additionalId; - } - public void setAdditionalVulnId(String additionalId) { - this.additionalId = additionalId; - } public BigDecimal getCvssV2BaseScore() { return cvssV2BaseScore; diff --git a/src/main/java/org/dependencytrack/parser/common/resolver/CveResolver.java b/src/main/java/org/dependencytrack/parser/common/resolver/CveResolver.java index 1df56ac3ab..3406c224c7 100644 --- a/src/main/java/org/dependencytrack/parser/common/resolver/CveResolver.java +++ b/src/main/java/org/dependencytrack/parser/common/resolver/CveResolver.java @@ -22,8 +22,7 @@ import java.util.regex.Matcher; import org.apache.commons.lang3.StringUtils; /** - * Attempts to resolve an internal CVE object from a string - * representation of a CVE. + * Attempts to obtain a valid CVE ID from a string */ public class CveResolver { @@ -37,21 +36,27 @@ public static CveResolver getInstance() { } /** - * Parses a CVE string returning the CVE ID, or null. + * Returns a valid CVE ID if found in the input string, or null otherwise + * If the input string is not a valid CVE ID, attempts to add "CVE-" prefix before checking again + * Returns null if the input string is empty, null or do not match. * - * @param cveString the string to parse - * @return a CVE object + * @param cveString the input string, potentially containing a CVE ID + * @return the valid CVE ID */ - public String parseCveString(final String cveString) { + public String getValidCveId(String cveString) { if (StringUtils.isNotBlank(cveString)) { - // Define the regex pattern - String pattern = "^CVE-\\d{4}-\\d+$"; - // Compile the pattern - Pattern regex = Pattern.compile(pattern); - // Match the input against the pattern - Matcher matcher = regex.matcher(cveString); - if (matcher.matches()) { - return cveString; + // Define the regex pattern + String pattern = "^CVE-\\d{4}-\\d+$"; + // Compile the pattern + Pattern regex = Pattern.compile(pattern); + if (!cveString.startsWith("CVE-")){ + // Try adding "CVE-" to the beginning of cveString + cveString = "CVE-" + cveString; + } + // Match the input against the pattern + Matcher matcher = regex.matcher(cveString); + if (matcher.matches()) { + return cveString; } } return null; diff --git a/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java b/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java index 6e4faf0755..3b8ec887e0 100644 --- a/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java @@ -22,6 +22,7 @@ import org.apache.commons.lang3.StringUtils; import org.dependencytrack.model.Cwe; import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.VulnerabilityAlias; import org.dependencytrack.parser.common.resolver.CweResolver; import org.dependencytrack.parser.common.resolver.CveResolver; import org.dependencytrack.parser.vulndb.model.Author; @@ -35,7 +36,9 @@ import java.math.BigDecimal; import java.time.OffsetDateTime; +import java.util.ArrayList; import java.util.Date; +import java.util.List; /** * Utility class that converts various VulnDB to Dependency-Track models. @@ -136,6 +139,7 @@ public static Vulnerability convert(final QueryManager qm, final org.dependencyt } CvssV2 cvssV2; + String cveId = ""; for (final CvssV2Metric metric : vulnDbVuln.cvssV2Metrics()) { cvssV2 = toNormalizedMetric(metric); final Score score = cvssV2.calculateScore(); @@ -144,6 +148,7 @@ public static Vulnerability convert(final QueryManager qm, final org.dependencyt vuln.setCvssV2ImpactSubScore(BigDecimal.valueOf(score.getImpactSubScore())); vuln.setCvssV2ExploitabilitySubScore(BigDecimal.valueOf(score.getExploitabilitySubScore())); if (metric.cveId() != null) { + cveId = metric.cveId(); break; // Always prefer use of the NVD scoring, if available } } @@ -157,6 +162,7 @@ public static Vulnerability convert(final QueryManager qm, final org.dependencyt vuln.setCvssV3ImpactSubScore(BigDecimal.valueOf(score.getImpactSubScore())); vuln.setCvssV3ExploitabilitySubScore(BigDecimal.valueOf(score.getExploitabilitySubScore())); if (metric.cveId() != null) { + cveId = metric.cveId(); break; // Always prefer use of the NVD scoring, if available } } @@ -170,12 +176,10 @@ public static Vulnerability convert(final QueryManager qm, final org.dependencyt vuln.addCwe(cwe); } } - if (cve_idString != null && cve_idString.startsWith("CVE-")) { - final String cve_id = CveResolver.getInstance().parseCveString(cve_idString); - if (cve_id != null) { - vuln.setAdditionalVulnId(cve_id); - } - } + cveId = cve_idString; + } + if (!cveId.isEmpty()) { + setAliasIfValid(vuln, qm, cveId); } return vuln; } @@ -255,4 +259,39 @@ public static CvssV3 toNormalizedMetric(CvssV3Metric metric) { cvss.availability(CvssV3.CIA.valueOf(metric.availabilityImpact())); return cvss; } + /** + * Set corresponding Alias to vulnDbVuln + * If the input `cve_idString` represents a valid CVE ID, this function sets + * the corresponding aliases for the `vuln` object by calling `computeAliases`. + * + * @param vuln the `Vulnerability` object for which to set the aliases + * @param qm the `QueryManager` object used for synchronization + * @param cve_idString the string that may represent a valid CVE ID + */ + private static void setAliasIfValid(Vulnerability vuln,QueryManager qm, String cve_idString) { + final String cve_id = CveResolver.getInstance().getValidCveId(cve_idString); + if (cve_id != null) { + vuln.setAliases(computeAliases(vuln,qm,cve_id)); + } + } + /** + * Computes a list of `VulnerabilityAlias` objects for the given `vulnerability` and valid `cve_id`. + * The aliases are computed by creating a new `VulnerabilityAlias` object with the `vulnDbId` set to the + * `vulnerability`'s `vulnId` and the `cveId` set to the valid `cve_id`. The `VulnerabilityAlias` object + * is then synchronized using the `qm` object and added to a list that is returned as a result. + * + * @param vulnerability the `Vulnerability` object for which to compute the aliases + * @param qm the `QueryManager` object used for synchronization + * @param cve_id the valid CVE ID string + * @return a list of computed `VulnerabilityAlias` objects + */ + private static List computeAliases(Vulnerability vulnerability, QueryManager qm, String cve_id) { + List vulnerabilityAliasList = new ArrayList<>(); + final VulnerabilityAlias vulnerabilityAlias = new VulnerabilityAlias(); + vulnerabilityAlias.setVulnDbId(vulnerability.getVulnId()); + vulnerabilityAlias.setCveId(cve_id); + qm.synchronizeVulnerabilityAlias(vulnerabilityAlias); + vulnerabilityAliasList.add(vulnerabilityAlias); + return vulnerabilityAliasList; + } } diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index 65ade284a9..2576fefb36 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -121,7 +121,6 @@ public Vulnerability updateVulnerability(Vulnerability transientVulnerability, b vulnerability.setOwaspRRTechnicalImpactScore(transientVulnerability.getOwaspRRTechnicalImpactScore()); vulnerability.setOwaspRRVector(transientVulnerability.getOwaspRRVector()); vulnerability.setCwes(transientVulnerability.getCwes()); - vulnerability.setAdditionalVulnId(transientVulnerability.getAdditionalVulnId()); if (transientVulnerability.getVulnerableSoftware() != null) { vulnerability.setVulnerableSoftware(transientVulnerability.getVulnerableSoftware()); } diff --git a/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java b/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java index 1d0238abe6..1fbd5d013c 100644 --- a/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java +++ b/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java @@ -42,7 +42,6 @@ import us.springett.parsers.cpe.CpeParser; import us.springett.parsers.cpe.exceptions.CpeEncodingException; import us.springett.parsers.cpe.exceptions.CpeParsingException; -import org.dependencytrack.model.VulnerabilityAlias; import java.io.File; import java.io.IOException; @@ -113,21 +112,6 @@ public void inform(final Event e) { } } } - /* - * Compute Alias between VulnDB and CVEs - */ - public List computeAliases(Vulnerability vulnerability, QueryManager qm) { - List vulnerabilityAliasList = new ArrayList<>(); - final String cve_id = vulnerability.getAdditionalVulnId(); - if (cve_id != null) { - final VulnerabilityAlias vulnerabilityAlias = new VulnerabilityAlias(); - vulnerabilityAlias.setVulnDbId(vulnerability.getVulnId()); - vulnerabilityAlias.setCveId(cve_id); - qm.synchronizeVulnerabilityAlias(vulnerabilityAlias); - vulnerabilityAliasList.add(vulnerabilityAlias); - } - return vulnerabilityAliasList; - } /** * Synchronizes the VulnDB vulnerabilities with the internal Dependency-Track database. * @@ -146,9 +130,6 @@ private void updateDatasource(final Results results) { qm.updateAffectedVersionAttributions(synchronizeVulnerability, vsList, Vulnerability.Source.VULNDB); vsList = qm.reconcileVulnerableSoftware(synchronizeVulnerability, vsListOld, vsList, Vulnerability.Source.VULNDB); synchronizeVulnerability.setVulnerableSoftware(vsList); - if (synchronizeVulnerability.getAdditionalVulnId() != null){ - synchronizeVulnerability.setAliases(computeAliases(synchronizeVulnerability, qm)); - } qm.persist(synchronizeVulnerability); } } From ee452ae323dc47b4df6d4bd1a7822ac274ddc943 Mon Sep 17 00:00:00 2001 From: Andres Tito Date: Thu, 28 Mar 2024 14:35:03 +0000 Subject: [PATCH 039/412] mapping problem in CVSS3 metrics cveId was receiving the value from source and vice versa Signed-off-by: Andres Tito --- .../dependencytrack/parser/vulndb/VulnDbParser.java | 2 +- .../parser/vulndb/model/CvssV2Metric.java | 13 ++++++++++--- .../parser/vulndb/model/CvssV3Metric.java | 3 ++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/dependencytrack/parser/vulndb/VulnDbParser.java b/src/main/java/org/dependencytrack/parser/vulndb/VulnDbParser.java index cdae929234..2f4d30b306 100644 --- a/src/main/java/org/dependencytrack/parser/vulndb/VulnDbParser.java +++ b/src/main/java/org/dependencytrack/parser/vulndb/VulnDbParser.java @@ -291,8 +291,8 @@ private List parseVulnerabilities(JSONArray rso) { jso.optBigDecimal("score", (BigDecimal) null), StringUtils.trimToNull(jso.optString("privileges_required", (String) null)), StringUtils.trimToNull(jso.optString("user_interaction", (String) null)), - StringUtils.trimToNull(jso.optString("cve_id", (String) null)), StringUtils.trimToNull(jso.optString("source", (String) null)), + StringUtils.trimToNull(jso.optString("cve_id", (String) null)), StringUtils.trimToNull(jso.optString("confidentiality_impact", (String) null)), jso.optBigDecimal("calculated_cvss_base_score", (BigDecimal) null), StringUtils.trimToNull(jso.optString("generated_on", (String) null)), diff --git a/src/main/java/org/dependencytrack/parser/vulndb/model/CvssV2Metric.java b/src/main/java/org/dependencytrack/parser/vulndb/model/CvssV2Metric.java index c37e2a6a35..12d97d761b 100644 --- a/src/main/java/org/dependencytrack/parser/vulndb/model/CvssV2Metric.java +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/CvssV2Metric.java @@ -25,9 +25,16 @@ * This record defines the CvssV2Metric objects returned. * Record created to replace the model class defined here: ... */ -public record CvssV2Metric(int id, String accessComplexity, String cveId, String source, String availabilityImpact, +public record CvssV2Metric(int id, + String accessComplexity, + String cveId, + String source, + String availabilityImpact, String confidentialityImpact, - String authentication, BigDecimal calculatedCvssBaseScore, String generatedOn, - BigDecimal score, String accessVector, + String authentication, + BigDecimal calculatedCvssBaseScore, + String generatedOn, + BigDecimal score, + String accessVector, String integrityImpact) { } diff --git a/src/main/java/org/dependencytrack/parser/vulndb/model/CvssV3Metric.java b/src/main/java/org/dependencytrack/parser/vulndb/model/CvssV3Metric.java index 7a0edd9a85..b92149f061 100644 --- a/src/main/java/org/dependencytrack/parser/vulndb/model/CvssV3Metric.java +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/CvssV3Metric.java @@ -26,7 +26,8 @@ * Record created to replace the model class defined here: ... */ public record CvssV3Metric(int id, - String attackComplexity, String scope, + String attackComplexity, + String scope, String attackVector, String availabilityImpact, BigDecimal score, From 1360c684ec1a7bbf2ae1516ea4bac90eebd9412f Mon Sep 17 00:00:00 2001 From: Andres Tito Date: Thu, 28 Mar 2024 14:45:07 +0000 Subject: [PATCH 040/412] Add tests for ModelConverter and CveResolver Signed-off-by: Andres Tito --- .../common/resolver/CveResolverTest.java | 52 +++ .../parser/vulndb/ModelConverterTest.java | 76 +++++ .../unit/vulndb.jsons/vulnerabilities_0.json | 300 ++++++++++++++++++ 3 files changed, 428 insertions(+) create mode 100644 src/test/java/org/dependencytrack/parser/common/resolver/CveResolverTest.java create mode 100644 src/test/java/org/dependencytrack/parser/vulndb/ModelConverterTest.java create mode 100644 src/test/resources/unit/vulndb.jsons/vulnerabilities_0.json diff --git a/src/test/java/org/dependencytrack/parser/common/resolver/CveResolverTest.java b/src/test/java/org/dependencytrack/parser/common/resolver/CveResolverTest.java new file mode 100644 index 0000000000..28fe042d21 --- /dev/null +++ b/src/test/java/org/dependencytrack/parser/common/resolver/CveResolverTest.java @@ -0,0 +1,52 @@ +/* + * 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.parser.common.resolver; + +import org.junit.Assert; +import org.junit.Test; + +public class CveResolverTest { + + @Test + public void testPositiveResolutionByFullCveId() { + String cve_id = CveResolver.getInstance().getValidCveId("CVE-2020-1234"); + Assert.assertNotNull(cve_id); + Assert.assertEquals("CVE-2020-1234", cve_id); + } + + @Test + public void testPositiveResolutionByPartialCveId() { + String cve_id = CveResolver.getInstance().getValidCveId("2020-1234"); + Assert.assertNotNull(cve_id); + Assert.assertEquals("CVE-2020-1234", cve_id); + } + @Test + public void testNegativeResolutionByInvalidCveId() { + String cve_id = CveResolver.getInstance().getValidCveId("20-1234"); + Assert.assertNull(cve_id); + Assert.assertNotEquals("CVE-2020-1234", cve_id); + } + + @Test + public void testNegativeResolutionByInvalidCweId() { + String cve_id = CveResolver.getInstance().getValidCveId("CVE-12345678"); + Assert.assertNull(cve_id); + Assert.assertNotEquals("CVE-1234-5678", cve_id); + } +} diff --git a/src/test/java/org/dependencytrack/parser/vulndb/ModelConverterTest.java b/src/test/java/org/dependencytrack/parser/vulndb/ModelConverterTest.java new file mode 100644 index 0000000000..d5e879304b --- /dev/null +++ b/src/test/java/org/dependencytrack/parser/vulndb/ModelConverterTest.java @@ -0,0 +1,76 @@ +package org.dependencytrack.parser.vulndb; + +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.VulnerabilityAlias; +import org.dependencytrack.parser.vulndb.model.CvssV3Metric; +import org.dependencytrack.parser.vulndb.model.Results; +import org.dependencytrack.persistence.QueryManager; + +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; + +public class ModelConverterTest { + private List resultList; + + @Before + public void setUp() throws Exception { + String filePath = "src/test/resources/unit/vulndb.jsons/vulnerabilities_0.json"; + File file = new File(filePath); + final VulnDbParser parser = new VulnDbParser(); + try { + final Results results = parser.parse(file, org.dependencytrack.parser.vulndb.model.Vulnerability.class); + resultList = results.getResults(); + } catch (IOException ex) { + ex.printStackTrace(); + fail("Failed to parse file: " + ex.getMessage()); + } + } + @Test + public void testConvert() { + final org.dependencytrack.parser.vulndb.model.Vulnerability vulnDbVuln = (org.dependencytrack.parser.vulndb.model.Vulnerability) resultList.get(0); + Vulnerability vulnerability = ModelConverter.convert(mock(QueryManager.class), vulnDbVuln); + assertNotNull(vulnerability); + assertEquals("1", vulnerability.getVulnId()); + assertEquals("test title", vulnerability.getTitle()); + assertEquals("(AV:N/AC:M/Au:N/C:P/I:N/A:N)", vulnerability.getCvssV2Vector()); + assertEquals("CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N", vulnerability.getCvssV3Vector()); + assertEquals(BigDecimal.valueOf(4.3), vulnerability.getCvssV2BaseScore()); + assertEquals(BigDecimal.valueOf(4.3), vulnerability.getCvssV3BaseScore()); + assertEquals("* [http://example.com](http://example.com)\n", vulnerability.getReferences()); + VulnerabilityAlias alias = vulnerability.getAliases().get(0); + assertEquals("CVE-1234-0000", alias.getCveId()); + } + @Test + public void testConvertWithoutNvdAdditionalInfo() { + final org.dependencytrack.parser.vulndb.model.Vulnerability vulnDbVuln = (org.dependencytrack.parser.vulndb.model.Vulnerability) resultList.get(1); + Vulnerability vulnerability = ModelConverter.convert(mock(QueryManager.class), vulnDbVuln); + assertNotNull(vulnerability); + for (final CvssV3Metric metric : vulnDbVuln.cvssV3Metrics()) { + assertEquals("1234-0000" ,metric.cveId()); + } + VulnerabilityAlias alias = vulnerability.getAliases().get(0); + assertEquals("CVE-1234-0000", alias.getCveId()); + } + @Test + public void testConvertWithoutAnyCve() { + final org.dependencytrack.parser.vulndb.model.Vulnerability vulnDbVuln = (org.dependencytrack.parser.vulndb.model.Vulnerability) resultList.get(2); + Vulnerability vulnerability = ModelConverter.convert(mock(QueryManager.class), vulnDbVuln); + assertNotNull(vulnerability); + for (final CvssV3Metric metric : vulnDbVuln.cvssV3Metrics()) { + assertNull(metric.cveId()); + } + assertNull(vulnerability.getAliases()); + + } +} \ No newline at end of file diff --git a/src/test/resources/unit/vulndb.jsons/vulnerabilities_0.json b/src/test/resources/unit/vulndb.jsons/vulnerabilities_0.json new file mode 100644 index 0000000000..91ebd33f6e --- /dev/null +++ b/src/test/resources/unit/vulndb.jsons/vulnerabilities_0.json @@ -0,0 +1,300 @@ +{ + "current_page": 1, + "total_entries": 1, + "results": [ + { + "vulndb_id": 1, + "title": "test title", + "classifications": [ + { + "id": 1, + "name": "test vulnerability", + "longname": "test vulnerability 1 1", + "description": "test test", + "mediumtext": "some text" + } + ], + "authors": [ + { + "id": 23, + "name": "test author", + "company": "test company" + } + ], + "ext_references": [ + { + "value": "http://example.com", + "type": "Vendor URL" + } + ], + "ext_texts": [ + { + "type": "external test texts", + "value": "external test texts value" + } + ], + "cvss_metrics": [ + { + "id": 1111, + "access_vector": "NETWORK", + "access_complexity": "MEDIUM", + "authentication": "NONE", + "confidentiality_impact": "PARTIAL", + "integrity_impact": "NONE", + "availability_impact": "NONE", + "source": "http://nvd.nist.gov", + "generated_on": "0017-00-00T00:00:37Z", + "cve_id": "1234-0000", + "score": 4.3, + "calculated_cvss_base_score": 4.3 + } + ], + "cvss_version_three_metrics": [ + { + "id": 1111, + "attack_vector": "NETWORK", + "attack_complexity": "LOW", + "privileges_required": "NONE", + "user_interaction": "REQUIRED", + "scope": "UNCHANGED", + "confidentiality_impact": "LOW", + "integrity_impact": "NONE", + "availability_impact": "NONE", + "source": "http://nvd.nist.gov", + "generated_on": "0017-00-00T00:00:37Z", + "cve_id": "1234-0000", + "score": 4.3, + "calculated_cvss_base_score": 4.3 + } + ], + "nvd_additional_information": [ + { + "summary": "test summary", + "cwe_id": "test1", + "cve_id": "CVE-1234-0000" + } + ], + "vendors": [ + { + "vendor": { + "id": 1, + "name": "vendor one test", + "short_name": "test", + "vendor_url": "http://test.com", + "products": [ + { + "id": 45, + "name": "test product name", + "versions": [ + { + "id": 2, + "name": "version 2", + "affected": false, + "cpe": [ + { + "cpe": "test cpe", + "type": "test type" + } + ] + } + ] + } + ] + } + } + ] + }, + { + "vulndb_id": 2, + "title": "test title", + "classifications": [ + { + "id": 1, + "name": "test vulnerability", + "longname": "test vulnerability 1 1", + "description": "test test", + "mediumtext": "some text" + } + ], + "authors": [ + { + "id": 23, + "name": "test author", + "company": "test company" + } + ], + "ext_references": [ + { + "value": "http://example.com", + "type": "Vendor URL" + } + ], + "ext_texts": [ + { + "type": "external test texts", + "value": "external test texts value" + } + ], + "cvss_metrics": [ + { + "id": 1111, + "access_vector": "NETWORK", + "access_complexity": "MEDIUM", + "authentication": "NONE", + "confidentiality_impact": "PARTIAL", + "integrity_impact": "NONE", + "availability_impact": "NONE", + "source": "http://nvd.nist.gov", + "generated_on": "0017-00-00T00:00:37Z", + "cve_id": null, + "score": 4.3, + "calculated_cvss_base_score": 4.3 + } + ], + "cvss_version_three_metrics": [ + { + "id": 1111, + "attack_vector": "NETWORK", + "attack_complexity": "LOW", + "privileges_required": "NONE", + "user_interaction": "REQUIRED", + "scope": "UNCHANGED", + "confidentiality_impact": "LOW", + "integrity_impact": "NONE", + "availability_impact": "NONE", + "source": "http://nvd.nist.gov", + "generated_on": "0017-00-00T00:00:37Z", + "cve_id": "1234-0000", + "score": 4.3, + "calculated_cvss_base_score": 4.3 + } + ], + "nvd_additional_information": [], + "vendors": [ + { + "vendor": { + "id": 1, + "name": "vendor one test", + "short_name": "test", + "vendor_url": "http://test.com", + "products": [ + { + "id": 45, + "name": "test product name", + "versions": [ + { + "id": 2, + "name": "version 2", + "affected": false, + "cpe": [ + { + "cpe": "test cpe", + "type": "test type" + } + ] + } + ] + } + ] + } + } + ] + }, + { + "vulndb_id": 3, + "title": "test title", + "classifications": [ + { + "id": 1, + "name": "test vulnerability", + "longname": "test vulnerability 1 1", + "description": "test test", + "mediumtext": "some text" + } + ], + "authors": [ + { + "id": 23, + "name": "test author", + "company": "test company" + } + ], + "ext_references": [ + { + "value": "http://example.com", + "type": "Vendor URL" + } + ], + "ext_texts": [ + { + "type": "external test texts", + "value": "external test texts value" + } + ], + "cvss_metrics": [ + { + "id": 1111, + "access_vector": "NETWORK", + "access_complexity": "MEDIUM", + "authentication": "NONE", + "confidentiality_impact": "PARTIAL", + "integrity_impact": "NONE", + "availability_impact": "NONE", + "source": "http://nvd.nist.gov", + "generated_on": "0017-00-00T00:00:37Z", + "cve_id": null, + "score": 4.3, + "calculated_cvss_base_score": 4.3 + } + ], + "cvss_version_three_metrics": [ + { + "id": 1111, + "attack_vector": "NETWORK", + "attack_complexity": "LOW", + "privileges_required": "NONE", + "user_interaction": "REQUIRED", + "scope": "UNCHANGED", + "confidentiality_impact": "LOW", + "integrity_impact": "NONE", + "availability_impact": "NONE", + "source": "http://nvd.nist.gov", + "generated_on": "0017-00-00T00:00:37Z", + "cve_id": null, + "score": 4.3, + "calculated_cvss_base_score": 4.3 + } + ], + "nvd_additional_information": [], + "vendors": [ + { + "vendor": { + "id": 1, + "name": "vendor one test", + "short_name": "test", + "vendor_url": "http://test.com", + "products": [ + { + "id": 45, + "name": "test product name", + "versions": [ + { + "id": 2, + "name": "version 2", + "affected": false, + "cpe": [ + { + "cpe": "test cpe", + "type": "test type" + } + ] + } + ] + } + ] + } + } + ] + } + ] + } \ No newline at end of file From 4742b61c777331f7bb059479171cc76d98df0cce Mon Sep 17 00:00:00 2001 From: Andres Tito Date: Thu, 28 Mar 2024 16:22:39 +0000 Subject: [PATCH 041/412] Add Severity to VulnDB Apparently the severity was never added to those vulnerabilities. use setSeverity to fix this issue. Signed-off-by: Andres Tito --- .../parser/vulndb/ModelConverter.java | 9 +++++++++ .../metrics/ComponentMetricsUpdateTask.java | 17 ++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java b/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java index 5c6f3fff96..8682e0b4a1 100644 --- a/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java @@ -28,6 +28,8 @@ import org.dependencytrack.parser.vulndb.model.CvssV3Metric; import org.dependencytrack.parser.vulndb.model.ExternalReference; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.VulnerabilityUtil; + import us.springett.cvss.CvssV2; import us.springett.cvss.CvssV3; import us.springett.cvss.Score; @@ -159,6 +161,13 @@ public static Vulnerability convert(final QueryManager qm, final org.dependencyt break; // Always prefer use of the NVD scoring, if available } } + vuln.setSeverity(VulnerabilityUtil.getSeverity( + vuln.getCvssV2BaseScore(), + vuln.getCvssV3BaseScore(), + vuln.getOwaspRRLikelihoodScore(), + vuln.getOwaspRRTechnicalImpactScore(), + vuln.getOwaspRRBusinessImpactScore() + )); if (vulnDbVuln.nvdAdditionalInfo() != null) { final String cweString = vulnDbVuln.nvdAdditionalInfo().cweId(); diff --git a/src/main/java/org/dependencytrack/tasks/metrics/ComponentMetricsUpdateTask.java b/src/main/java/org/dependencytrack/tasks/metrics/ComponentMetricsUpdateTask.java index 570cb001e0..894bff57ab 100644 --- a/src/main/java/org/dependencytrack/tasks/metrics/ComponentMetricsUpdateTask.java +++ b/src/main/java/org/dependencytrack/tasks/metrics/ComponentMetricsUpdateTask.java @@ -96,13 +96,16 @@ static Counters updateMetrics(final UUID uuid) throws Exception { .forEach(aliasesSeen::add); counters.vulnerabilities++; - - switch (vulnerability.getSeverity()) { - case CRITICAL -> counters.critical++; - case HIGH -> counters.high++; - case MEDIUM -> counters.medium++; - case LOW, INFO -> counters.low++; - case UNASSIGNED -> counters.unassigned++; + try { + switch (vulnerability.getSeverity()) { + case CRITICAL -> counters.critical++; + case HIGH -> counters.high++; + case MEDIUM -> counters.medium++; + case LOW, INFO -> counters.low++; + case UNASSIGNED -> counters.unassigned++; + } + } catch (NullPointerException ex) { + LOGGER.debug("Vulnerability severity is null for" + vulnerability.getSource() + "|" + vulnerability.getVulnId() + "->"+vulnerability.getSeverity()); } } From 801dfdac30ce36ceb16a8379508f186ef0a80f7f Mon Sep 17 00:00:00 2001 From: nscuro Date: Fri, 29 Mar 2024 14:59:30 +0100 Subject: [PATCH 042/412] Validate UUID request parameters Many resources accept UUID parameters, but because they use the type `String`, requests with invalid UUIDs are not properly rejected, causing `HTTP 500` responses being returned. This commit adds a `@ValidUuid` annotation. It further adds a Jersey `ExceptionMapper` to handle validation exceptions properly. Additionally, it add the `format = "uuid"` hint to Swagger annotations, making the API docs less ambiguous as to what format is being expected. Relates to https://github.com/DependencyTrack/frontend/issues/684 Signed-off-by: nscuro --- .../model/validation/ValidUuid.java | 47 ++++++++++ .../resources/v1/AccessControlResource.java | 13 +-- .../resources/v1/AnalysisResource.java | 6 +- .../resources/v1/BadgeResource.java | 9 +- .../resources/v1/BomResource.java | 13 +-- .../resources/v1/ComponentResource.java | 24 ++--- .../resources/v1/DependencyGraphResource.java | 8 +- .../resources/v1/EventResource.java | 5 +- .../resources/v1/FindingResource.java | 11 ++- .../resources/v1/LdapResource.java | 9 +- .../resources/v1/LicenseGroupResource.java | 25 ++--- .../resources/v1/MetricsResource.java | 33 +++---- .../v1/NotificationPublisherResource.java | 5 +- .../v1/NotificationRuleResource.java | 33 +++---- .../resources/v1/OidcResource.java | 21 +++-- .../resources/v1/PermissionResource.java | 9 +- .../resources/v1/PolicyConditionResource.java | 9 +- .../resources/v1/PolicyResource.java | 35 +++---- .../resources/v1/PolicyViolationResource.java | 7 +- .../resources/v1/ProjectPropertyResource.java | 17 ++-- .../resources/v1/ProjectResource.java | 33 +++---- .../resources/v1/RepositoryResource.java | 5 +- .../resources/v1/ServiceResource.java | 15 +-- .../resources/v1/TagResource.java | 5 +- .../resources/v1/TeamResource.java | 9 +- .../resources/v1/VexResource.java | 5 +- .../v1/ViolationAnalysisResource.java | 9 +- .../resources/v1/VulnerabilityResource.java | 39 ++++---- .../ConstraintViolationExceptionMapper.java | 83 +++++++++++++++++ .../exception/JsonMappingExceptionMapper.java | 9 -- ...onstraintViolationExceptionMapperTest.java | 93 +++++++++++++++++++ 31 files changed, 448 insertions(+), 196 deletions(-) create mode 100644 src/main/java/org/dependencytrack/model/validation/ValidUuid.java create mode 100644 src/main/java/org/dependencytrack/resources/v1/exception/ConstraintViolationExceptionMapper.java create mode 100644 src/test/java/org/dependencytrack/resources/v1/exception/ConstraintViolationExceptionMapperTest.java diff --git a/src/main/java/org/dependencytrack/model/validation/ValidUuid.java b/src/main/java/org/dependencytrack/model/validation/ValidUuid.java new file mode 100644 index 0000000000..e15b20181f --- /dev/null +++ b/src/main/java/org/dependencytrack/model/validation/ValidUuid.java @@ -0,0 +1,47 @@ +/* + * 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.model.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import javax.validation.ReportAsSingleViolation; +import javax.validation.constraints.Pattern; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @since 4.11.0 + */ +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Constraint(validatedBy = {}) +@Retention(RUNTIME) +@ReportAsSingleViolation +@Pattern(regexp = "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$") +public @interface ValidUuid { + + String message() default "Invalid UUID"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index 150b2ff6a4..75798be7f0 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -31,6 +31,7 @@ import io.swagger.annotations.Authorization; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Project; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.vo.AclMappingRequest; @@ -73,8 +74,8 @@ public class AccessControlResource extends AlpineResource { @ApiResponse(code = 404, message = "The UUID of the team could not be found"), }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) - public Response retrieveProjects (@ApiParam(value = "The UUID of the team to retrieve mappings for", required = true) - @PathParam("uuid") String uuid, + public Response retrieveProjects (@ApiParam(value = "The UUID of the team to retrieve mappings for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive, @ApiParam(value = "Optionally excludes children projects from being returned", required = false) @@ -141,10 +142,10 @@ public Response addMapping(AclMappingRequest request) { }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response deleteMapping( - @ApiParam(value = "The UUID of the team to delete the mapping for", required = true) - @PathParam("teamUuid") String teamUuid, - @ApiParam(value = "The UUID of the project to delete the mapping for", required = true) - @PathParam("projectUuid") String projectUuid) { + @ApiParam(value = "The UUID of the team to delete the mapping for", format = "uuid", required = true) + @PathParam("teamUuid") @ValidUuid String teamUuid, + @ApiParam(value = "The UUID of the project to delete the mapping for", format = "uuid", required = true) + @PathParam("projectUuid") @ValidUuid String projectUuid) { try (QueryManager qm = new QueryManager()) { final Team team = qm.getObjectByUuid(Team.class, teamUuid); final Project project = qm.getObjectByUuid(Project.class, projectUuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java b/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java index 7882ad705b..19088e2b72 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java @@ -76,11 +76,11 @@ public class AnalysisResource extends AlpineResource { @ApiResponse(code = 404, message = "The project, component, or vulnerability could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) - public Response retrieveAnalysis(@ApiParam(value = "The UUID of the project") + public Response retrieveAnalysis(@ApiParam(value = "The UUID of the project", format = "uuid") @QueryParam("project") String projectUuid, - @ApiParam(value = "The UUID of the component", required = true) + @ApiParam(value = "The UUID of the component", format = "uuid", required = true) @QueryParam("component") String componentUuid, - @ApiParam(value = "The UUID of the vulnerability", required = true) + @ApiParam(value = "The UUID of the vulnerability", format = "uuid", required = true) @QueryParam("vulnerability") String vulnerabilityUuid) { failOnValidationError( new ValidationTask(RegexSequence.Pattern.UUID, projectUuid, "Project is not a valid UUID", false), // this is optional diff --git a/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java b/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java index 6c8024e522..d0f03dfed5 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java @@ -29,6 +29,7 @@ import io.swagger.annotations.ApiResponses; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectMetrics; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.misc.Badger; @@ -72,8 +73,8 @@ private boolean isBadgeSupportEnabled(final QueryManager qm) { }) @AuthenticationNotRequired public Response getProjectVulnerabilitiesBadge( - @ApiParam(value = "The UUID of the project to retrieve metrics for", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the project to retrieve metrics for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { if (isBadgeSupportEnabled(qm)) { final Project project = qm.getObjectByUuid(Project.class, uuid); @@ -138,8 +139,8 @@ public Response getProjectVulnerabilitiesBadge( }) @AuthenticationNotRequired public Response getProjectPolicyViolationsBadge( - @ApiParam(value = "The UUID of the project to retrieve a badge for", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the project to retrieve a badge for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { if (isBadgeSupportEnabled(qm)) { final Project project = qm.getObjectByUuid(Project.class, uuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/src/main/java/org/dependencytrack/resources/v1/BomResource.java index 7469df6311..982a7fed05 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -39,6 +39,7 @@ import org.dependencytrack.event.BomUploadEvent; import org.dependencytrack.model.Component; import org.dependencytrack.model.Project; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.parser.cyclonedx.CycloneDXExporter; import org.dependencytrack.parser.cyclonedx.CycloneDxValidator; import org.dependencytrack.parser.cyclonedx.InvalidBomException; @@ -101,8 +102,8 @@ public class BomResource extends AlpineResource { }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response exportProjectAsCycloneDx ( - @ApiParam(value = "The UUID of the project to export", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the project to export", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "The format to output (defaults to JSON)") @QueryParam("format") String format, @ApiParam(value = "Specifies the CycloneDX variant to export. Value options are 'inventory' and 'withVulnerabilities'. (defaults to 'inventory')") @@ -171,8 +172,8 @@ public Response exportProjectAsCycloneDx ( }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response exportComponentAsCycloneDx ( - @ApiParam(value = "The UUID of the component to export", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the component to export", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "The format to output (defaults to JSON)") @QueryParam("format") String format) { try (QueryManager qm = new QueryManager()) { @@ -398,8 +399,8 @@ determine if any tasks (such as vulnerability analysis) is being performed on th @PermissionRequired(Permissions.Constants.BOM_UPLOAD) @Deprecated(since = "4.11.0") public Response isTokenBeingProcessed ( - @ApiParam(value = "The UUID of the token to query", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the token to query", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { final boolean value = Event.isEventBeingProcessed(UUID.fromString(uuid)); diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java index 86a46d0795..8bd979a0eb 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java @@ -43,6 +43,7 @@ import org.dependencytrack.model.Project; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.util.InternalComponentIdentificationUtil; @@ -89,8 +90,8 @@ public class ComponentResource extends AlpineResource { }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getAllComponents( - @ApiParam(value = "The UUID of the project to retrieve components for", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the project to retrieve components for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "Optionally exclude recent components so only outdated components are returned", required = false) @QueryParam("onlyOutdated") boolean onlyOutdated, @ApiParam(value = "Optionally exclude transitive dependencies so only direct dependencies are returned", required = false) @@ -125,8 +126,8 @@ public Response getAllComponents( }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getComponentByUuid( - @ApiParam(value = "The UUID of the component to retrieve", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the component to retrieve", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "Optionally includes third-party metadata about the component from external repositories", required = false) @QueryParam("includeRepositoryMetaData") boolean includeRepositoryMetaData) { try (QueryManager qm = new QueryManager()) { @@ -177,8 +178,8 @@ public Response getComponentByIdentity(@ApiParam(value = "The group of the compo @QueryParam("cpe") String cpe, @ApiParam(value = "The swidTagId of the component") @QueryParam("swidTagId") String swidTagId, - @ApiParam(value = "The project the component belongs to") - @QueryParam("project") String projectUuid) { + @ApiParam(value = "The project the component belongs to", format = "uuid") + @QueryParam("project") @ValidUuid String projectUuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { Project project = null; if (projectUuid != null) { @@ -249,7 +250,8 @@ public Response getComponentByHash( @ApiResponse(code = 404, message = "The project could not be found") }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) - public Response createComponent(@PathParam("uuid") String uuid, Component jsonComponent) { + public Response createComponent(@ApiParam(value = "The UUID of the project to create a component for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, Component jsonComponent) { final Validator validator = super.getValidator(); failOnValidationError( validator.validateProperty(jsonComponent, "author"), @@ -466,8 +468,8 @@ public Response updateComponent(Component jsonComponent) { }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response deleteComponent( - @ApiParam(value = "The UUID of the component to delete", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the component to delete", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Component component = qm.getObjectByUuid(Component.class, uuid, Component.FetchGroup.ALL.name()); if (component != null) { @@ -516,8 +518,8 @@ public Response identifyInternalComponents() { }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getDependencyGraphForComponent( - @ApiParam(value = "The UUID of the project to get the expanded dependency graph for", required = true) - @PathParam("projectUuid") String projectUuid, + @ApiParam(value = "The UUID of the project to get the expanded dependency graph for", format = "uuid", required = true) + @PathParam("projectUuid") @ValidUuid String projectUuid, @ApiParam(value = "List of UUIDs of the components (separated by |) to get the expanded dependency graph for", required = true) @PathParam("componentUuids") String componentUuids) { try (QueryManager qm = new QueryManager()) { diff --git a/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java b/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java index 56ceaa1326..5c338e1368 100644 --- a/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java @@ -25,6 +25,7 @@ import com.github.packageurl.PackageURL; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; @@ -33,6 +34,7 @@ import org.dependencytrack.model.Project; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.persistence.RepositoryQueryManager; import org.dependencytrack.resources.v1.vo.DependencyGraphResponse; @@ -79,7 +81,8 @@ public class DependencyGraphResource extends AlpineResource { @ApiResponse(code = 404, message = "Any component can be found"), }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response getComponentsAndServicesByProjectUuid(final @PathParam("uuid") String uuid) { + public Response getComponentsAndServicesByProjectUuid(@ApiParam(value = "The UUID of the project", format = "uuid", required = true) + final @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, uuid); @@ -118,7 +121,8 @@ public Response getComponentsAndServicesByProjectUuid(final @PathParam("uuid") S @ApiResponse(code = 404, message = "Any component can be found"), }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response getComponentsAndServicesByComponentUuid(final @PathParam("uuid") String uuid) { + public Response getComponentsAndServicesByComponentUuid(@ApiParam(value = "The UUID of the component", format = "uuid", required = true) + final @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Component component = qm.getObjectByUuid(Component.class, uuid); if (component == null) { diff --git a/src/main/java/org/dependencytrack/resources/v1/EventResource.java b/src/main/java/org/dependencytrack/resources/v1/EventResource.java index e6b8a401e9..6b4b39e387 100644 --- a/src/main/java/org/dependencytrack/resources/v1/EventResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/EventResource.java @@ -26,6 +26,7 @@ import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.resources.v1.vo.IsTokenBeingProcessedResponse; import javax.ws.rs.GET; @@ -68,8 +69,8 @@ public class EventResource extends AlpineResource { @ApiResponse(code = 401, message = "Unauthorized") }) public Response isTokenBeingProcessed ( - @ApiParam(value = "The UUID of the token to query", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the token to query", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { final boolean value = Event.isEventBeingProcessed(UUID.fromString(uuid)); IsTokenBeingProcessedResponse response = new IsTokenBeingProcessedResponse(); response.setProcessing(value); diff --git a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index 66e300d326..0bbb35d0fa 100644 --- a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -49,6 +49,7 @@ import org.dependencytrack.model.GroupedFinding; import org.dependencytrack.model.Project; import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import javax.ws.rs.GET; @@ -95,7 +96,8 @@ public class FindingResource extends AlpineResource { @ApiResponse(code = 404, message = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) - public Response getFindingsByProject(@PathParam("uuid") String uuid, + public Response getFindingsByProject(@ApiParam(value = "The UUID of the project", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "Optionally includes suppressed findings") @QueryParam("suppressed") boolean suppressed, @ApiParam(value = "Optionally limit findings to specific sources of vulnerability intelligence") @@ -145,7 +147,8 @@ public Response getFindingsByProject(@PathParam("uuid") String uuid, @ApiResponse(code = 404, message = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) - public Response exportFindingsByProject(@PathParam("uuid") String uuid) { + public Response exportFindingsByProject(@ApiParam(value = "The UUID of the project", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { @@ -179,8 +182,8 @@ public Response exportFindingsByProject(@PathParam("uuid") String uuid) { }) @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) public Response analyzeProject( - @ApiParam(value = "The UUID of the project to analyze", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the project to analyze", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { diff --git a/src/main/java/org/dependencytrack/resources/v1/LdapResource.java b/src/main/java/org/dependencytrack/resources/v1/LdapResource.java index df07743695..47580e91a0 100644 --- a/src/main/java/org/dependencytrack/resources/v1/LdapResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/LdapResource.java @@ -33,6 +33,7 @@ import io.swagger.annotations.Authorization; import io.swagger.annotations.ResponseHeader; import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.vo.MappedLdapGroupRequest; @@ -129,8 +130,8 @@ public Response retrieveLdapGroups () { @ApiResponse(code = 404, message = "The UUID of the team could not be found"), }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) - public Response retrieveLdapGroups (@ApiParam(value = "The UUID of the team to retrieve mappings for", required = true) - @PathParam("uuid") String uuid) { + public Response retrieveLdapGroups (@ApiParam(value = "The UUID of the team to retrieve mappings for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Team team = qm.getObjectByUuid(Team.class, uuid); if (team != null) { @@ -191,8 +192,8 @@ public Response addMapping(MappedLdapGroupRequest request) { }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response deleteMapping( - @ApiParam(value = "The UUID of the mapping to delete", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the mapping to delete", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final MappedLdapGroup mapping = qm.getObjectByUuid(MappedLdapGroup.class, uuid); if (mapping != null) { diff --git a/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java b/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java index e8cd504b89..01cbbe1439 100644 --- a/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java @@ -32,6 +32,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.License; import org.dependencytrack.model.LicenseGroup; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import javax.validation.Validator; @@ -91,8 +92,8 @@ public Response getLicenseGroups() { }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response getLicenseGroup( - @ApiParam(value = "The UUID of the license group to retrieve", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the license group to retrieve", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final LicenseGroup licenseGroup = qm.getObjectByUuid(LicenseGroup.class, uuid); if (licenseGroup != null) { @@ -179,8 +180,8 @@ public Response updateLicenseGroup(LicenseGroup jsonLicenseGroup) { }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response deleteLicenseGroup( - @ApiParam(value = "The UUID of the license group to delete", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the license group to delete", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final LicenseGroup licenseGroup = qm.getObjectByUuid(LicenseGroup.class, uuid); if (licenseGroup != null) { @@ -208,10 +209,10 @@ public Response deleteLicenseGroup( }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response addLicenseToLicenseGroup( - @ApiParam(value = "A valid license group", required = true) - @PathParam("uuid") String uuid, - @ApiParam(value = "A valid license", required = true) - @PathParam("licenseUuid") String licenseUuid) { + @ApiParam(value = "A valid license group", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, + @ApiParam(value = "A valid license", format = "uuid", required = true) + @PathParam("licenseUuid") @ValidUuid String licenseUuid) { try (QueryManager qm = new QueryManager()) { LicenseGroup licenseGroup = qm.getObjectByUuid(LicenseGroup.class, uuid); if (licenseGroup == null) { @@ -248,10 +249,10 @@ public Response addLicenseToLicenseGroup( }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response removeLicenseFromLicenseGroup( - @ApiParam(value = "A valid license group", required = true) - @PathParam("uuid") String uuid, - @ApiParam(value = "A valid license", required = true) - @PathParam("licenseUuid") String licenseUuid) { + @ApiParam(value = "A valid license group", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, + @ApiParam(value = "A valid license", format = "uuid", required = true) + @PathParam("licenseUuid") @ValidUuid String licenseUuid) { try (QueryManager qm = new QueryManager()) { LicenseGroup licenseGroup = qm.getObjectByUuid(LicenseGroup.class, uuid); if (licenseGroup == null) { diff --git a/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java b/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java index 5553819c9d..5d1322c1d2 100644 --- a/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java @@ -38,6 +38,7 @@ import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.model.VulnerabilityMetrics; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.util.DateUtil; @@ -184,8 +185,8 @@ public Response RefreshPortfolioMetrics() { }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProjectCurrentMetrics( - @ApiParam(value = "The UUID of the project to retrieve metrics for", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the project to retrieve metrics for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { @@ -219,8 +220,8 @@ public Response getProjectCurrentMetrics( }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProjectMetricsSince( - @ApiParam(value = "The UUID of the project to retrieve metrics for", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the project to retrieve metrics for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "The start date to retrieve metrics for", required = true) @PathParam("date") String date) { @@ -244,8 +245,8 @@ public Response getProjectMetricsSince( }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProjectMetricsXDays( - @ApiParam(value = "The UUID of the project to retrieve metrics for", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the project to retrieve metrics for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "The number of days back to retrieve metrics for", required = true) @PathParam("days") int days) { @@ -267,8 +268,8 @@ public Response getProjectMetricsXDays( }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response RefreshProjectMetrics( - @ApiParam(value = "The UUID of the project to refresh metrics on", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the project to refresh metrics on", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { @@ -299,8 +300,8 @@ public Response RefreshProjectMetrics( }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getComponentCurrentMetrics( - @ApiParam(value = "The UUID of the component to retrieve metrics for", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the component to retrieve metrics for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Component component = qm.getObjectByUuid(Component.class, uuid); if (component != null) { @@ -334,8 +335,8 @@ public Response getComponentCurrentMetrics( }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getComponentMetricsSince( - @ApiParam(value = "The UUID of the component to retrieve metrics for", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the component to retrieve metrics for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "The start date to retrieve metrics for", required = true) @PathParam("date") String date) { @@ -362,8 +363,8 @@ public Response getComponentMetricsSince( }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getComponentMetricsXDays( - @ApiParam(value = "The UUID of the component to retrieve metrics for", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the component to retrieve metrics for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "The number of days back to retrieve metrics for", required = true) @PathParam("days") int days) { @@ -385,8 +386,8 @@ public Response getComponentMetricsXDays( }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response RefreshComponentMetrics( - @ApiParam(value = "The UUID of the component to refresh metrics on", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the component to refresh metrics on", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Component component = qm.getObjectByUuid(Component.class, uuid); if (component != null) { diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java index 6128b4c2f3..514e125579 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java @@ -34,6 +34,7 @@ import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.NotificationPublisher; import org.dependencytrack.model.NotificationRule; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; @@ -223,8 +224,8 @@ public Response updateNotificationPublisher(NotificationPublisher jsonNotificati @ApiResponse(code = 404, message = "The UUID of the notification publisher could not be found") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) - public Response deleteNotificationPublisher(@ApiParam(value = "The UUID of the notification publisher to delete", required = true) - @PathParam("notificationPublisherUuid") String notificationPublisherUuid) { + public Response deleteNotificationPublisher(@ApiParam(value = "The UUID of the notification publisher to delete", format = "uuid", required = true) + @PathParam("notificationPublisherUuid") @ValidUuid String notificationPublisherUuid) { try (QueryManager qm = new QueryManager()) { final NotificationPublisher notificationPublisher = qm.getObjectByUuid(NotificationPublisher.class, notificationPublisherUuid); if (notificationPublisher != null) { diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java index 1a37a5e909..adfd65a1b0 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java @@ -35,6 +35,7 @@ import org.dependencytrack.model.NotificationPublisher; import org.dependencytrack.model.NotificationRule; import org.dependencytrack.model.Project; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.notification.publisher.SendMailPublisher; import org.dependencytrack.persistence.QueryManager; @@ -196,10 +197,10 @@ public Response deleteNotificationRule(NotificationRule jsonRule) { }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response addProjectToRule( - @ApiParam(value = "The UUID of the rule to add a project to", required = true) - @PathParam("ruleUuid") String ruleUuid, - @ApiParam(value = "The UUID of the project to add to the rule", required = true) - @PathParam("projectUuid") String projectUuid) { + @ApiParam(value = "The UUID of the rule to add a project to", format = "uuid", required = true) + @PathParam("ruleUuid") @ValidUuid String ruleUuid, + @ApiParam(value = "The UUID of the project to add to the rule", format = "uuid", required = true) + @PathParam("projectUuid") @ValidUuid String projectUuid) { try (QueryManager qm = new QueryManager()) { final NotificationRule rule = qm.getObjectByUuid(NotificationRule.class, ruleUuid); if (rule == null) { @@ -238,10 +239,10 @@ public Response addProjectToRule( }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response removeProjectFromRule( - @ApiParam(value = "The UUID of the rule to remove the project from", required = true) - @PathParam("ruleUuid") String ruleUuid, - @ApiParam(value = "The UUID of the project to remove from the rule", required = true) - @PathParam("projectUuid") String projectUuid) { + @ApiParam(value = "The UUID of the rule to remove the project from", format = "uuid", required = true) + @PathParam("ruleUuid") @ValidUuid String ruleUuid, + @ApiParam(value = "The UUID of the project to remove from the rule", format = "uuid", required = true) + @PathParam("projectUuid") @ValidUuid String projectUuid) { try (QueryManager qm = new QueryManager()) { final NotificationRule rule = qm.getObjectByUuid(NotificationRule.class, ruleUuid); if (rule == null) { @@ -280,10 +281,10 @@ public Response removeProjectFromRule( }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response addTeamToRule( - @ApiParam(value = "The UUID of the rule to add a team to", required = true) - @PathParam("ruleUuid") String ruleUuid, - @ApiParam(value = "The UUID of the team to add to the rule", required = true) - @PathParam("teamUuid") String teamUuid) { + @ApiParam(value = "The UUID of the rule to add a team to", format = "uuid", required = true) + @PathParam("ruleUuid") @ValidUuid String ruleUuid, + @ApiParam(value = "The UUID of the team to add to the rule", format = "uuid", required = true) + @PathParam("teamUuid") @ValidUuid String teamUuid) { try (QueryManager qm = new QueryManager()) { final NotificationRule rule = qm.getObjectByUuid(NotificationRule.class, ruleUuid); if (rule == null) { @@ -322,10 +323,10 @@ public Response addTeamToRule( }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response removeTeamFromRule( - @ApiParam(value = "The UUID of the rule to remove the project from", required = true) - @PathParam("ruleUuid") String ruleUuid, - @ApiParam(value = "The UUID of the project to remove from the rule", required = true) - @PathParam("teamUuid") String teamUuid) { + @ApiParam(value = "The UUID of the rule to remove the project from", format = "uuid", required = true) + @PathParam("ruleUuid") @ValidUuid String ruleUuid, + @ApiParam(value = "The UUID of the project to remove from the rule", format = "uuid", required = true) + @PathParam("teamUuid") @ValidUuid String teamUuid) { try (QueryManager qm = new QueryManager()) { final NotificationRule rule = qm.getObjectByUuid(NotificationRule.class, ruleUuid); if (rule == null) { diff --git a/src/main/java/org/dependencytrack/resources/v1/OidcResource.java b/src/main/java/org/dependencytrack/resources/v1/OidcResource.java index 663ac234fa..9a6f9170f9 100644 --- a/src/main/java/org/dependencytrack/resources/v1/OidcResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/OidcResource.java @@ -33,6 +33,7 @@ import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.vo.MappedOidcGroupRequest; import org.owasp.security.logging.SecurityMarkers; @@ -171,8 +172,8 @@ public Response updateGroup(final OidcGroup jsonGroup) { @ApiResponse(code = 404, message = "The group could not be found") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) - public Response deleteGroup(@ApiParam(value = "The UUID of the group to delete", required = true) - @PathParam("uuid") final String uuid) { + public Response deleteGroup(@ApiParam(value = "The UUID of the group to delete", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid final String uuid) { try (QueryManager qm = new QueryManager()) { final OidcGroup group = qm.getObjectByUuid(OidcGroup.class, uuid); if (group != null) { @@ -200,8 +201,8 @@ public Response deleteGroup(@ApiParam(value = "The UUID of the group to delete", @ApiResponse(code = 404, message = "The UUID of the mapping could not be found"), }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) - public Response retrieveTeamsMappedToGroup(@ApiParam(value = "The UUID of the mapping to retrieve the team for", required = true) - @PathParam("uuid") final String uuid) { + public Response retrieveTeamsMappedToGroup(@ApiParam(value = "The UUID of the mapping to retrieve the team for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid final String uuid) { try (final QueryManager qm = new QueryManager()) { final OidcGroup oidcGroup = qm.getObjectByUuid(OidcGroup.class, uuid); if (oidcGroup != null) { @@ -271,8 +272,8 @@ public Response addMapping(final MappedOidcGroupRequest request) { @ApiResponse(code = 404, message = "The UUID of the mapping could not be found"), }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) - public Response deleteMappingByUuid(@ApiParam(value = "The UUID of the mapping to delete", required = true) - @PathParam("uuid") final String uuid) { + public Response deleteMappingByUuid(@ApiParam(value = "The UUID of the mapping to delete", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid final String uuid) { try (QueryManager qm = new QueryManager()) { final MappedOidcGroup mapping = qm.getObjectByUuid(MappedOidcGroup.class, uuid); if (mapping != null) { @@ -298,10 +299,10 @@ public Response deleteMappingByUuid(@ApiParam(value = "The UUID of the mapping t @ApiResponse(code = 404, message = "The UUID of the mapping could not be found"), }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) - public Response deleteMapping(@ApiParam(value = "The UUID of the group to delete a mapping for", required = true) - @PathParam("groupUuid") final String groupUuid, - @ApiParam(value = "The UUID of the team to delete a mapping for", required = true) - @PathParam("teamUuid") final String teamUuid) { + public Response deleteMapping(@ApiParam(value = "The UUID of the group to delete a mapping for", format = "uuid", required = true) + @PathParam("groupUuid") @ValidUuid final String groupUuid, + @ApiParam(value = "The UUID of the team to delete a mapping for", format = "uuid", required = true) + @PathParam("teamUuid") @ValidUuid final String teamUuid) { try (QueryManager qm = new QueryManager()) { final Team team = qm.getObjectByUuid(Team.class, teamUuid); if (team == null) { diff --git a/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java b/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java index 715580f4b7..02e2cc168f 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java @@ -31,6 +31,7 @@ import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.owasp.security.logging.SecurityMarkers; @@ -174,8 +175,8 @@ public Response removePermissionFromUser( }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response addPermissionToTeam( - @ApiParam(value = "A valid team uuid", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "A valid team uuid", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "A valid permission", required = true) @PathParam("permission") String permissionName) { try (QueryManager qm = new QueryManager()) { @@ -215,8 +216,8 @@ public Response addPermissionToTeam( }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response removePermissionFromTeam( - @ApiParam(value = "A valid team uuid", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "A valid team uuid", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "A valid permission", required = true) @PathParam("permission") String permissionName) { try (QueryManager qm = new QueryManager()) { diff --git a/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java b/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java index cfe22ad81c..326dbc7f5a 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java @@ -31,6 +31,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Policy; import org.dependencytrack.model.PolicyCondition; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import javax.validation.Validator; @@ -70,8 +71,8 @@ public class PolicyConditionResource extends AlpineResource { }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response createPolicyCondition( - @ApiParam(value = "The UUID of the policy", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the policy", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, PolicyCondition jsonPolicyCondition) { final Validator validator = super.getValidator(); failOnValidationError( @@ -134,8 +135,8 @@ public Response updatePolicyCondition(PolicyCondition jsonPolicyCondition) { }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response deletePolicyCondition( - @ApiParam(value = "The UUID of the policy condition to delete", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the policy condition to delete", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final PolicyCondition pc = qm.getObjectByUuid(PolicyCondition.class, uuid); if (pc != null) { diff --git a/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java b/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java index f1da82c080..cb1b18e76b 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java @@ -33,6 +33,7 @@ import org.dependencytrack.model.Policy; import org.dependencytrack.model.Project; import org.dependencytrack.model.Tag; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import javax.validation.Validator; @@ -92,8 +93,8 @@ public Response getPolicies() { }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response getPolicy( - @ApiParam(value = "The UUID of the policy to retrieve", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the policy to retrieve", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Policy policy = qm.getObjectByUuid(Policy.class, uuid); if (policy != null) { @@ -193,8 +194,8 @@ public Response updatePolicy(Policy jsonPolicy) { }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response deletePolicy( - @ApiParam(value = "The UUID of the policy to delete", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the policy to delete", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Policy policy = qm.getObjectByUuid(Policy.class, uuid); if (policy != null) { @@ -222,10 +223,10 @@ public Response deletePolicy( }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response addProjectToPolicy( - @ApiParam(value = "The UUID of the policy to add a project to", required = true) - @PathParam("policyUuid") String policyUuid, - @ApiParam(value = "The UUID of the project to add to the rule", required = true) - @PathParam("projectUuid") String projectUuid) { + @ApiParam(value = "The UUID of the policy to add a project to", format = "uuid", required = true) + @PathParam("policyUuid") @ValidUuid String policyUuid, + @ApiParam(value = "The UUID of the project to add to the rule", format = "uuid", required = true) + @PathParam("projectUuid") @ValidUuid String projectUuid) { try (QueryManager qm = new QueryManager()) { final Policy policy = qm.getObjectByUuid(Policy.class, policyUuid); if (policy == null) { @@ -261,10 +262,10 @@ public Response addProjectToPolicy( }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response removeProjectFromPolicy( - @ApiParam(value = "The UUID of the policy to remove the project from", required = true) - @PathParam("policyUuid") String policyUuid, - @ApiParam(value = "The UUID of the project to remove from the policy", required = true) - @PathParam("projectUuid") String projectUuid) { + @ApiParam(value = "The UUID of the policy to remove the project from", format = "uuid", required = true) + @PathParam("policyUuid") @ValidUuid String policyUuid, + @ApiParam(value = "The UUID of the project to remove from the policy", format = "uuid", required = true) + @PathParam("projectUuid") @ValidUuid String projectUuid) { try (QueryManager qm = new QueryManager()) { final Policy policy = qm.getObjectByUuid(Policy.class, policyUuid); if (policy == null) { @@ -300,10 +301,10 @@ public Response removeProjectFromPolicy( }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response addTagToPolicy( - @ApiParam(value = "The UUID of the policy to add a project to", required = true) - @PathParam("policyUuid") String policyUuid, + @ApiParam(value = "The UUID of the policy to add a project to", format = "uuid", required = true) + @PathParam("policyUuid") @ValidUuid String policyUuid, @ApiParam(value = "The name of the tag to add to the rule", required = true) - @PathParam("tagName") String tagName) { + @PathParam("tagName")String tagName) { try (QueryManager qm = new QueryManager()) { final Policy policy = qm.getObjectByUuid(Policy.class, policyUuid); if (policy == null) { @@ -340,8 +341,8 @@ public Response addTagToPolicy( }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response removeTagFromPolicy( - @ApiParam(value = "The UUID of the policy to remove the tag from", required = true) - @PathParam("policyUuid") String policyUuid, + @ApiParam(value = "The UUID of the policy to remove the tag from", format = "uuid", required = true) + @PathParam("policyUuid") @ValidUuid String policyUuid, @ApiParam(value = "The name of the tag to remove from the policy", required = true) @PathParam("tagName") String tagName) { try (QueryManager qm = new QueryManager()) { diff --git a/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java b/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java index f2e4ed444c..0b19af88aa 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java @@ -32,6 +32,7 @@ import org.dependencytrack.model.Component; import org.dependencytrack.model.PolicyViolation; import org.dependencytrack.model.Project; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import javax.jdo.FetchPlan; @@ -94,7 +95,8 @@ public Response getViolations(@ApiParam(value = "Optionally includes suppressed @ApiResponse(code = 404, message = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_POLICY_VIOLATION) - public Response getViolationsByProject(@PathParam("uuid") String uuid, + public Response getViolationsByProject(@ApiParam(value = "The UUID of the project", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "Optionally includes suppressed violations") @QueryParam("suppressed") boolean suppressed) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { @@ -130,7 +132,8 @@ public Response getViolationsByProject(@PathParam("uuid") String uuid, @ApiResponse(code = 404, message = "The component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_POLICY_VIOLATION) - public Response getViolationsByComponent(@PathParam("uuid") String uuid, + public Response getViolationsByComponent(@ApiParam(value = "The UUID of the component", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "Optionally includes suppressed violations") @QueryParam("suppressed") boolean suppressed) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java index a1eee5c182..e322bae29e 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java @@ -29,6 +29,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectProperty; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import javax.validation.Validator; @@ -69,8 +70,8 @@ public class ProjectPropertyResource extends AbstractConfigPropertyResource { }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response getProperties( - @ApiParam(value = "The UUID of the project to retrieve properties for", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the project to retrieve properties for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { @@ -113,8 +114,8 @@ public Response getProperties( }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response createProperty( - @ApiParam(value = "The UUID of the project to create a property for", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the project to create a property for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, ProjectProperty json) { final Validator validator = super.getValidator(); failOnValidationError( @@ -169,8 +170,8 @@ public Response createProperty( }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response updateProperty( - @ApiParam(value = "The UUID of the project to create a property for", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the project to create a property for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, ProjectProperty json) { final Validator validator = super.getValidator(); failOnValidationError( @@ -212,8 +213,8 @@ public Response updateProperty( }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response deleteProperty( - @ApiParam(value = "The UUID of the project to delete a property from", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the project to delete a property from", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, ProjectProperty json) { final Validator validator = super.getValidator(); failOnValidationError( diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index a1248cfac3..305fb6619d 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -38,6 +38,7 @@ import org.dependencytrack.model.Classifier; import org.dependencytrack.model.Project; import org.dependencytrack.model.Tag; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.vo.CloneProjectRequest; @@ -93,8 +94,8 @@ public Response getProjects(@ApiParam(value = "The optional name of the project @QueryParam("excludeInactive") boolean excludeInactive, @ApiParam(value = "Optionally excludes children projects from being returned", required = false) @QueryParam("onlyRoot") boolean onlyRoot, - @ApiParam(value = "The UUID of the team which projects shall be excluded", required = false) - @QueryParam("notAssignedToTeamWithUuid") String notAssignedToTeamWithUuid) { + @ApiParam(value = "The UUID of the team which projects shall be excluded", format = "uuid", required = false) + @QueryParam("notAssignedToTeamWithUuid") @ValidUuid String notAssignedToTeamWithUuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { Team notAssignedToTeam = null; if (StringUtils.isNotEmpty(notAssignedToTeamWithUuid)) { @@ -124,8 +125,8 @@ public Response getProjects(@ApiParam(value = "The optional name of the project }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProject( - @ApiParam(value = "The UUID of the project to retrieve", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the project to retrieve", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getProject(uuid); if (project != null) { @@ -383,8 +384,8 @@ public Response updateProject(Project jsonProject) { }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response patchProject( - @ApiParam(value = "The UUID of the project to modify", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the project to modify", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, Project jsonProject) { final Validator validator = getValidator(); failOnValidationError( @@ -510,8 +511,8 @@ private boolean setIfDifferent(final Project source, final Project target, f }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response deleteProject( - @ApiParam(value = "The UUID of the project to delete", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the project to delete", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, uuid, Project.FetchGroup.ALL.name()); if (project != null) { @@ -585,8 +586,8 @@ public Response cloneProject(CloneProjectRequest jsonRequest) { @ApiResponse(code = 404, message = "The UUID of the project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response getChildrenProjects(@ApiParam(value = "The UUID of the project to get the children from", required = true) - @PathParam("uuid") String uuid, + public Response getChildrenProjects(@ApiParam(value = "The UUID of the project to get the children from", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { @@ -623,8 +624,8 @@ public Response getChildrenProjects(@ApiParam(value = "The UUID of the project t public Response getChildrenProjectsByClassifier( @ApiParam(value = "The classifier to query on", required = true) @PathParam("classifier") String classifierString, - @ApiParam(value = "The UUID of the project to get the children from", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the project to get the children from", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { @@ -662,8 +663,8 @@ public Response getChildrenProjectsByClassifier( public Response getChildrenProjectsByTag( @ApiParam(value = "The tag to query on", required = true) @PathParam("tag") String tagString, - @ApiParam(value = "The UUID of the project to get the children from", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the project to get the children from", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { @@ -699,8 +700,8 @@ public Response getChildrenProjectsByTag( }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProjectsWithoutDescendantsOf( - @ApiParam(value = "The UUID of the project which descendants will be excluded", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the project which descendants will be excluded", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "The optional name of the project to query on", required = false) @QueryParam("name") String name, @ApiParam(value = "Optionally excludes inactive projects from being returned", required = false) diff --git a/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java b/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java index 2572c50d71..ccb66b41c9 100644 --- a/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java @@ -36,6 +36,7 @@ import org.dependencytrack.model.Repository; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import javax.validation.Validator; @@ -240,8 +241,8 @@ public Response updateRepository(Repository jsonRepository) { }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response deleteRepository( - @ApiParam(value = "The UUID of the repository to delete", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the repository to delete", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Repository repository = qm.getObjectByUuid(Repository.class, uuid); if (repository != null) { diff --git a/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java b/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java index 442ae7c567..50bd58f35a 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java @@ -32,6 +32,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Project; import org.dependencytrack.model.ServiceComponent; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import javax.validation.Validator; @@ -72,7 +73,8 @@ public class ServiceResource extends AlpineResource { @ApiResponse(code = 404, message = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response getAllServices(@PathParam("uuid") String uuid) { + public Response getAllServices(@ApiParam(value = "The UUID of the project", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { @@ -103,8 +105,8 @@ public Response getAllServices(@PathParam("uuid") String uuid) { }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getServiceByUuid( - @ApiParam(value = "The UUID of the service to retrieve", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the service to retrieve", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final ServiceComponent service = qm.getObjectByUuid(ServiceComponent.class, uuid); if (service != null) { @@ -137,7 +139,8 @@ public Response getServiceByUuid( @ApiResponse(code = 404, message = "The project could not be found") }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) - public Response createService(@PathParam("uuid") String uuid, ServiceComponent jsonService) { + public Response createService(@ApiParam(value = "The UUID of the project", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, ServiceComponent jsonService) { final Validator validator = super.getValidator(); failOnValidationError( validator.validateProperty(jsonService, "name"), @@ -241,8 +244,8 @@ public Response updateService(ServiceComponent jsonService) { }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response deleteService( - @ApiParam(value = "The UUID of the service to delete", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the service to delete", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final ServiceComponent service = qm.getObjectByUuid(ServiceComponent.class, uuid, ServiceComponent.FetchGroup.ALL.name()); if (service != null) { diff --git a/src/main/java/org/dependencytrack/resources/v1/TagResource.java b/src/main/java/org/dependencytrack/resources/v1/TagResource.java index 31bd2d6175..38a6f56152 100644 --- a/src/main/java/org/dependencytrack/resources/v1/TagResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/TagResource.java @@ -30,6 +30,7 @@ import io.swagger.annotations.ResponseHeader; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Tag; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import javax.ws.rs.GET; @@ -57,8 +58,8 @@ public class TagResource extends AlpineResource { @ApiResponse(code = 401, message = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response getTags(@ApiParam(value = "The UUID of the policy", required = true) - @PathParam("policyUuid") String policyUuid){ + public Response getTags(@ApiParam(value = "The UUID of the policy", format = "uuid", required = true) + @PathParam("policyUuid") @ValidUuid String policyUuid){ try (QueryManager qm = new QueryManager(getAlpineRequest())) { final PaginatedResult result = qm.getTags(policyUuid); return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); diff --git a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java index a02e327c58..9b22b3e328 100644 --- a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java @@ -32,6 +32,7 @@ import io.swagger.annotations.Authorization; import io.swagger.annotations.ResponseHeader; import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.vo.TeamSelfResponse; import org.owasp.security.logging.SecurityMarkers; @@ -98,8 +99,8 @@ public Response getTeams() { }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response getTeam( - @ApiParam(value = "The UUID of the team to retrieve", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the team to retrieve", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Team team = qm.getObjectByUuid(Team.class, uuid); if (team != null) { @@ -212,8 +213,8 @@ public Response deleteTeam(Team jsonTeam) { }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response generateApiKey( - @ApiParam(value = "The UUID of the team to generate a key for", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the team to generate a key for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Team team = qm.getObjectByUuid(Team.class, uuid); if (team != null) { diff --git a/src/main/java/org/dependencytrack/resources/v1/VexResource.java b/src/main/java/org/dependencytrack/resources/v1/VexResource.java index 38a89b2f36..60e1b718eb 100644 --- a/src/main/java/org/dependencytrack/resources/v1/VexResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/VexResource.java @@ -36,6 +36,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.VexUploadEvent; import org.dependencytrack.model.Project; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.parser.cyclonedx.CycloneDXExporter; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.problems.InvalidBomProblemDetails; @@ -88,8 +89,8 @@ public class VexResource extends AlpineResource { }) @PermissionRequired(Permissions.Constants.VULNERABILITY_ANALYSIS) public Response exportProjectAsCycloneDx ( - @ApiParam(value = "The UUID of the project to export", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the project to export", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "Force the resulting VEX to be downloaded as a file (defaults to 'false')") @QueryParam("download") boolean download) { try (QueryManager qm = new QueryManager()) { diff --git a/src/main/java/org/dependencytrack/resources/v1/ViolationAnalysisResource.java b/src/main/java/org/dependencytrack/resources/v1/ViolationAnalysisResource.java index 517eb8b730..28732cb172 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ViolationAnalysisResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ViolationAnalysisResource.java @@ -38,6 +38,7 @@ import org.dependencytrack.model.PolicyViolation; import org.dependencytrack.model.ViolationAnalysis; import org.dependencytrack.model.ViolationAnalysisState; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.vo.ViolationAnalysisRequest; import org.dependencytrack.util.NotificationUtil; @@ -74,10 +75,10 @@ public class ViolationAnalysisResource extends AlpineResource { @ApiResponse(code = 404, message = "The component or policy violation could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_POLICY_VIOLATION) - public Response retrieveAnalysis(@ApiParam(value = "The UUID of the component", required = true) - @QueryParam("component") String componentUuid, - @ApiParam(value = "The UUID of the policy violation", required = true) - @QueryParam("policyViolation") String violationUuid) { + public Response retrieveAnalysis(@ApiParam(value = "The UUID of the component", format = "uuid", required = true) + @QueryParam("component") @ValidUuid String componentUuid, + @ApiParam(value = "The UUID of the policy violation", format = "uuid", required = true) + @QueryParam("policyViolation") @ValidUuid String violationUuid) { failOnValidationError( new ValidationTask(RegexSequence.Pattern.UUID, componentUuid, "Component is not a valid UUID"), new ValidationTask(RegexSequence.Pattern.UUID, violationUuid, "Policy violation is not a valid UUID") diff --git a/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java b/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java index a58d6e470e..e3731ac849 100644 --- a/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java @@ -35,6 +35,7 @@ import org.dependencytrack.model.Project; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerableSoftware; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.parser.common.resolver.CweResolver; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.vo.AffectedComponent; @@ -88,7 +89,8 @@ public class VulnerabilityResource extends AlpineResource { @ApiResponse(code = 404, message = "The component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response getVulnerabilitiesByComponent(@PathParam("uuid") String uuid, + public Response getVulnerabilitiesByComponent(@ApiParam(value = "The UUID of the component to retrieve vulnerabilities for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "Optionally includes suppressed vulnerabilities") @QueryParam("suppressed") boolean suppressed) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { @@ -122,7 +124,8 @@ public Response getVulnerabilitiesByComponent(@PathParam("uuid") String uuid, @ApiResponse(code = 404, message = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response getVulnerabilitiesByProject(@PathParam("uuid") String uuid, + public Response getVulnerabilitiesByProject(@ApiParam(value = "The UUID of the project to retrieve vulnerabilities for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, @ApiParam(value = "Optionally includes suppressed vulnerabilities") @QueryParam("suppressed") boolean suppressed) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { @@ -153,8 +156,8 @@ public Response getVulnerabilitiesByProject(@PathParam("uuid") String uuid, @ApiResponse(code = 404, message = "The vulnerability could not be found") }) @PermissionRequired(Permissions.Constants.VULNERABILITY_MANAGEMENT) - public Response getVulnerabilityByUuid(@ApiParam(value = "The UUID of the vulnerability", required = true) - @PathParam("uuid") String uuid) { + public Response getVulnerabilityByUuid(@ApiParam(value = "The UUID of the vulnerability", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, uuid); if (vulnerability != null) { @@ -419,8 +422,8 @@ public Response updateVulnerability(Vulnerability jsonVuln) { }) @PermissionRequired(Permissions.Constants.VULNERABILITY_MANAGEMENT) public Response deleteVulnerability( - @ApiParam(value = "The UUID of the vulnerability to delete", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the vulnerability to delete", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, uuid); if (vulnerability != null) { @@ -516,8 +519,8 @@ public Response assignVulnerability(@ApiParam(value = "The vulnerability source" @PathParam("source") String source, @ApiParam(value = "The vulnId", required = true) @PathParam("vulnId") String vulnId, - @ApiParam(value = "The UUID of the component", required = true) - @PathParam("component") String componentUuid) { + @ApiParam(value = "The UUID of the component", format = "uuid", required = true) + @PathParam("component") @ValidUuid String componentUuid) { try (QueryManager qm = new QueryManager()) { Vulnerability vulnerability = qm.getVulnerabilityByVulnId(source, vulnId); if (vulnerability == null) { @@ -551,10 +554,10 @@ public Response assignVulnerability(@ApiParam(value = "The vulnerability source" @ApiResponse(code = 404, message = "The vulnerability or component could not be found") }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) - public Response assignVulnerability(@ApiParam(value = "The UUID of the vulnerability", required = true) - @PathParam("uuid") String uuid, - @ApiParam(value = "The UUID of the component", required = true) - @PathParam("component") String componentUuid) { + public Response assignVulnerability(@ApiParam(value = "The UUID of the vulnerability", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, + @ApiParam(value = "The UUID of the component", format = "uuid", required = true) + @PathParam("component") @ValidUuid String componentUuid) { try (QueryManager qm = new QueryManager()) { Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, uuid); if (vulnerability == null) { @@ -592,8 +595,8 @@ public Response unassignVulnerability(@ApiParam(value = "The vulnerability sourc @PathParam("source") String source, @ApiParam(value = "The vulnId", required = true) @PathParam("vulnId") String vulnId, - @ApiParam(value = "The UUID of the component", required = true) - @PathParam("component") String componentUuid) { + @ApiParam(value = "The UUID of the component", format = "uuid", required = true) + @PathParam("component") @ValidUuid String componentUuid) { try (QueryManager qm = new QueryManager()) { Vulnerability vulnerability = qm.getVulnerabilityByVulnId(source, vulnId); if (vulnerability == null) { @@ -627,10 +630,10 @@ public Response unassignVulnerability(@ApiParam(value = "The vulnerability sourc @ApiResponse(code = 404, message = "The vulnerability or component could not be found") }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) - public Response unassignVulnerability(@ApiParam(value = "The UUID of the vulnerability", required = true) - @PathParam("uuid") String uuid, - @ApiParam(value = "The UUID of the component", required = true) - @PathParam("component") String componentUuid) { + public Response unassignVulnerability(@ApiParam(value = "The UUID of the vulnerability", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, + @ApiParam(value = "The UUID of the component", format = "uuid", required = true) + @PathParam("component") @ValidUuid String componentUuid) { try (QueryManager qm = new QueryManager()) { Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, uuid); if (vulnerability == null) { diff --git a/src/main/java/org/dependencytrack/resources/v1/exception/ConstraintViolationExceptionMapper.java b/src/main/java/org/dependencytrack/resources/v1/exception/ConstraintViolationExceptionMapper.java new file mode 100644 index 0000000000..de24ab4aa3 --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/exception/ConstraintViolationExceptionMapper.java @@ -0,0 +1,83 @@ +/* + * 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.resources.v1.exception; + +import alpine.server.resources.AlpineResource; +import org.apache.commons.lang3.StringUtils; +import org.glassfish.jersey.server.validation.ValidationError; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * An {@link ExceptionMapper} for {@link ConstraintViolationException}s. + *

+ * This mapper allows for Jersey resource parameter validation to be used, + * while staying consistent with the response format returned by Alpine's + * {@link AlpineResource#failOnValidationError(Set[])}, which is used for + * validation entire objects. + * + * @since 4.11.0 + */ +@Provider +@SuppressWarnings("JavadocReference") +public class ConstraintViolationExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(final ConstraintViolationException exception) { + final List errors = mapToValidationErrors(exception.getConstraintViolations()); + + return Response + .status(Response.Status.BAD_REQUEST) + .entity(errors) + .build(); + } + + /** + * Copied from {@link AlpineResource#contOnValidationError(Set[])}. + * + * @param violations A {@link Collection} or one or more {@link ConstraintViolation}s + * @return A {@link List} of zero or more {@link ValidationError}s + * @see Source + */ + private static List mapToValidationErrors(final Collection> violations) { + final List errors = new ArrayList<>(violations.size()); + + for (final ConstraintViolation violation : violations) { + if (violation.getPropertyPath().iterator().next().getName() != null) { + final String path = violation.getPropertyPath() != null ? violation.getPropertyPath().toString() : null; + final String message = violation.getMessage() != null ? StringUtils.removeStart(violation.getMessage(), path + ".") : null; + final String messageTemplate = violation.getMessageTemplate(); + final String invalidValue = violation.getInvalidValue() != null ? violation.getInvalidValue().toString() : null; + final ValidationError error = new ValidationError(message, messageTemplate, path, invalidValue); + errors.add(error); + } + } + + return errors; + } + +} diff --git a/src/main/java/org/dependencytrack/resources/v1/exception/JsonMappingExceptionMapper.java b/src/main/java/org/dependencytrack/resources/v1/exception/JsonMappingExceptionMapper.java index cf073541fd..c681f89abb 100644 --- a/src/main/java/org/dependencytrack/resources/v1/exception/JsonMappingExceptionMapper.java +++ b/src/main/java/org/dependencytrack/resources/v1/exception/JsonMappingExceptionMapper.java @@ -25,9 +25,6 @@ import org.dependencytrack.resources.v1.vo.VexSubmitRequest; import javax.annotation.Priority; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.container.ResourceInfo; -import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; @@ -40,12 +37,6 @@ @Priority(1) public class JsonMappingExceptionMapper implements ExceptionMapper { - @Context - private HttpServletRequest request; - - @Context - private ResourceInfo resourceInfo; - @Override public Response toResponse(final JsonMappingException exception) { final var problemDetails = new ProblemDetails(); diff --git a/src/test/java/org/dependencytrack/resources/v1/exception/ConstraintViolationExceptionMapperTest.java b/src/test/java/org/dependencytrack/resources/v1/exception/ConstraintViolationExceptionMapperTest.java new file mode 100644 index 0000000000..6af6b3330b --- /dev/null +++ b/src/test/java/org/dependencytrack/resources/v1/exception/ConstraintViolationExceptionMapperTest.java @@ -0,0 +1,93 @@ +/* + * 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.resources.v1.exception; + +import net.javacrumbs.jsonunit.core.Option; +import org.dependencytrack.ResourceTest; +import org.dependencytrack.model.validation.ValidUuid; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.ServletDeploymentContext; +import org.junit.Test; + +import javax.validation.constraints.Pattern; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +public class ConstraintViolationExceptionMapperTest extends ResourceTest { + + @Override + protected DeploymentContext configureDeployment() { + return ServletDeploymentContext.forServlet(new ServletContainer( + new ResourceConfig(TestResource.class) + .register(ConstraintViolationExceptionMapper.class))) + .build(); + } + + @Test + public void test() { + final Response response = target("/not-a-uuid") + .queryParam("foo", "666") + .request() + .get(); + assertThat(response.getStatus()).isEqualTo(400); + assertThatJson(getPlainTextBody(response)) + .withOptions(Option.IGNORING_ARRAY_ORDER) + .isEqualTo(""" + [ + { + "message": "Invalid UUID", + "messageTemplate": "Invalid UUID", + "path": "get.arg0", + "invalidValue": "not-a-uuid" + }, + { + "message": "must match \\"^[a-z]+$\\"", + "messageTemplate": "{javax.validation.constraints.Pattern.message}", + "path": "get.arg2", + "invalidValue": "666" + } + ] + """); + } + + @Path("/") + public static class TestResource { + + @GET + @Path("/{uuid}") + @Produces(MediaType.APPLICATION_JSON) + public Response get(@PathParam("uuid") @ValidUuid final String uuid, + @QueryParam("optionalUuid") @ValidUuid final String optionalUuid, + @QueryParam("foo") @Pattern(regexp = "^[a-z]+$") final String foo) { + return Response.noContent().build(); + } + + } + +} \ No newline at end of file From 3dd3645398f9eeb72da2d63592db559d09fc564f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 08:08:58 +0000 Subject: [PATCH 043/412] Bump aquasecurity/trivy-action from 0.18.0 to 0.19.0 Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.18.0 to 0.19.0. - [Release notes](https://github.com/aquasecurity/trivy-action/releases) - [Commits](https://github.com/aquasecurity/trivy-action/compare/062f2592684a31eb3aa050cc61e7ca1451cecd3d...d710430a6722f083d3b36b8339ff66b32f22ee55) --- updated-dependencies: - dependency-name: aquasecurity/trivy-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index af43e748a2..58d541b2b9 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -123,7 +123,7 @@ jobs: - name: Run Trivy Vulnerability Scanner if: ${{ inputs.publish-container }} - uses: aquasecurity/trivy-action@062f2592684a31eb3aa050cc61e7ca1451cecd3d # tag=0.18.0 + uses: aquasecurity/trivy-action@d710430a6722f083d3b36b8339ff66b32f22ee55 # tag=0.19.0 with: image-ref: docker.io/dependencytrack/${{ matrix.distribution }}:${{ inputs.app-version }} format: 'sarif' From 64a2e2fd623832ac6216e6d39eb96f1ef807c204 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 08:09:02 +0000 Subject: [PATCH 044/412] Bump actions/dependency-review-action from 4.2.4 to 4.2.5 Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.2.4 to 4.2.5. - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/733dd5d4a5203f238c33806593ec0f5fc5343d8c...5bbc3ba658137598168acb2ab73b21c432dd411b) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/dependency-review.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index bcae0d0ea2..8499414f06 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -12,4 +12,4 @@ jobs: uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 - name: Dependency Review - uses: actions/dependency-review-action@733dd5d4a5203f238c33806593ec0f5fc5343d8c # tag=v4.2.4 + uses: actions/dependency-review-action@5bbc3ba658137598168acb2ab73b21c432dd411b # tag=v4.2.5 From b76e6d39a8611c982fc8ae6a52b58e605548e3f5 Mon Sep 17 00:00:00 2001 From: Andres Tito Date: Tue, 2 Apr 2024 08:38:39 +0000 Subject: [PATCH 045/412] Remove Line Break Signed-off-by: Andres Tito --- .../java/org/dependencytrack/parser/vulndb/ModelConverter.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java b/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java index 8682e0b4a1..18c98a9c1b 100644 --- a/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java @@ -29,7 +29,6 @@ import org.dependencytrack.parser.vulndb.model.ExternalReference; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.util.VulnerabilityUtil; - import us.springett.cvss.CvssV2; import us.springett.cvss.CvssV3; import us.springett.cvss.Score; From b908eb69c185f3b825605ee8f7c6915f6451dcc3 Mon Sep 17 00:00:00 2001 From: Greg Back Date: Tue, 2 Apr 2024 16:06:00 -0400 Subject: [PATCH 046/412] Normalize capitalization of PyPI Signed-off-by: Greg Back --- README.md | 2 +- docs/_docs/datasources/repositories.md | 2 +- docs/_posts/2018-12-22-v3.4.0.md | 2 +- docs/index.md | 2 +- docs/odt-odc-comparison.html | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9a76912cf9..86fa530031 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ CI/CD environments. * NPM (Javascript) * CPAN (Perl) * NuGet (.NET) - * Pypi (Python) + * PyPI (Python) * More coming soon. * Identifies APIs and external service components including: * Service provider diff --git a/docs/_docs/datasources/repositories.md b/docs/_docs/datasources/repositories.md index 448d292869..f94c3d607c 100644 --- a/docs/_docs/datasources/repositories.md +++ b/docs/_docs/datasources/repositories.md @@ -30,7 +30,7 @@ Dependency-Track supports the following default repositories: | | Google Android | 5 | | npm | NPM | 1 | | nuget | NuGet | 1 | -| pypi | PyPi | 1 | +| pypi | PyPI | 1 | | cpan | CPAN | 1 | diff --git a/docs/_posts/2018-12-22-v3.4.0.md b/docs/_posts/2018-12-22-v3.4.0.md index 10dcd02d1e..cfc3666705 100644 --- a/docs/_posts/2018-12-22-v3.4.0.md +++ b/docs/_posts/2018-12-22-v3.4.0.md @@ -10,7 +10,7 @@ type: major * Added support for external integrations including: * Fortify Software Security Center * Kenna Security -* Added repository (and outdated version detection) support for NuGet and Pypi +* Added repository (and outdated version detection) support for NuGet and PyPI * Updated SPDX license list to v3.3 * Added support for identifying FSF Libre licenses * Updated Java version in Docker container diff --git a/docs/index.md b/docs/index.md index dedff56a3e..5a571d120c 100755 --- a/docs/index.md +++ b/docs/index.md @@ -57,7 +57,7 @@ CI/CD environments. * Maven (Java) * NPM (Javascript) * NuGet (.NET) - * Pypi (Python) + * PyPI (Python) * More coming soon. * Identifies APIs and external service components including: * Service provider diff --git a/docs/odt-odc-comparison.html b/docs/odt-odc-comparison.html index 8bc29ad880..f5edfc063a 100644 --- a/docs/odt-odc-comparison.html +++ b/docs/odt-odc-comparison.html @@ -72,7 +72,7 @@

  • Maven (Java)
  • NPM (JavaScript)
  • NuGet (.NET)
  • -
  • PyPi (Python)
  • +
  • PyPI (Python)
  • None From 65f13b69c22c32d18e012a9ad53011b001ebef18 Mon Sep 17 00:00:00 2001 From: Andres Tito Date: Wed, 3 Apr 2024 13:05:16 +0000 Subject: [PATCH 047/412] Changes and suggestions by nscuro Signed-off-by: Andres Tito --- .../parser/common/resolver/CveResolver.java | 64 ------------------- .../parser/vulndb/ModelConverter.java | 40 ++++++------ .../dependencytrack/tasks/VulnDbSyncTask.java | 1 + .../util/VulnerabilityUtil.java | 30 +++++++++ .../common/resolver/CveResolverTest.java | 52 --------------- .../util/VulnerabilityUtilTest.java | 29 +++++++++ 6 files changed, 80 insertions(+), 136 deletions(-) delete mode 100644 src/main/java/org/dependencytrack/parser/common/resolver/CveResolver.java delete mode 100644 src/test/java/org/dependencytrack/parser/common/resolver/CveResolverTest.java diff --git a/src/main/java/org/dependencytrack/parser/common/resolver/CveResolver.java b/src/main/java/org/dependencytrack/parser/common/resolver/CveResolver.java deleted file mode 100644 index 3406c224c7..0000000000 --- a/src/main/java/org/dependencytrack/parser/common/resolver/CveResolver.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.parser.common.resolver; - -import java.util.regex.Pattern; -import java.util.regex.Matcher; -import org.apache.commons.lang3.StringUtils; -/** - * Attempts to obtain a valid CVE ID from a string - */ -public class CveResolver { - - private static final CveResolver INSTANCE = new CveResolver(); - - private CveResolver() { - } - - public static CveResolver getInstance() { - return INSTANCE; - } - - /** - * Returns a valid CVE ID if found in the input string, or null otherwise - * If the input string is not a valid CVE ID, attempts to add "CVE-" prefix before checking again - * Returns null if the input string is empty, null or do not match. - * - * @param cveString the input string, potentially containing a CVE ID - * @return the valid CVE ID - */ - public String getValidCveId(String cveString) { - if (StringUtils.isNotBlank(cveString)) { - // Define the regex pattern - String pattern = "^CVE-\\d{4}-\\d+$"; - // Compile the pattern - Pattern regex = Pattern.compile(pattern); - if (!cveString.startsWith("CVE-")){ - // Try adding "CVE-" to the beginning of cveString - cveString = "CVE-" + cveString; - } - // Match the input against the pattern - Matcher matcher = regex.matcher(cveString); - if (matcher.matches()) { - return cveString; - } - } - return null; - } -} diff --git a/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java b/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java index 3b8ec887e0..b01f9c3362 100644 --- a/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java @@ -24,12 +24,13 @@ import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerabilityAlias; import org.dependencytrack.parser.common.resolver.CweResolver; -import org.dependencytrack.parser.common.resolver.CveResolver; import org.dependencytrack.parser.vulndb.model.Author; import org.dependencytrack.parser.vulndb.model.CvssV2Metric; import org.dependencytrack.parser.vulndb.model.CvssV3Metric; import org.dependencytrack.parser.vulndb.model.ExternalReference; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.VulnerabilityUtil; + import us.springett.cvss.CvssV2; import us.springett.cvss.CvssV3; import us.springett.cvss.Score; @@ -169,14 +170,14 @@ public static Vulnerability convert(final QueryManager qm, final org.dependencyt if (vulnDbVuln.nvdAdditionalInfo() != null) { final String cweString = vulnDbVuln.nvdAdditionalInfo().cweId(); - final String cve_idString = vulnDbVuln.nvdAdditionalInfo().cveId(); + final String cveString = vulnDbVuln.nvdAdditionalInfo().cveId(); if (cweString != null && cweString.startsWith("CWE-")) { final Cwe cwe = CweResolver.getInstance().lookup(cweString); if (cwe != null) { vuln.addCwe(cwe); } } - cveId = cve_idString; + cveId = cveString; } if (!cveId.isEmpty()) { setAliasIfValid(vuln, qm, cveId); @@ -261,37 +262,36 @@ public static CvssV3 toNormalizedMetric(CvssV3Metric metric) { } /** * Set corresponding Alias to vulnDbVuln - * If the input `cve_idString` represents a valid CVE ID, this function sets + * If the input `cveString` represents a valid CVE ID, this function sets * the corresponding aliases for the `vuln` object by calling `computeAliases`. * * @param vuln the `Vulnerability` object for which to set the aliases * @param qm the `QueryManager` object used for synchronization - * @param cve_idString the string that may represent a valid CVE ID + * @param cveString the string that may represent a valid CVE ID */ - private static void setAliasIfValid(Vulnerability vuln,QueryManager qm, String cve_idString) { - final String cve_id = CveResolver.getInstance().getValidCveId(cve_idString); - if (cve_id != null) { - vuln.setAliases(computeAliases(vuln,qm,cve_id)); + private static void setAliasIfValid(Vulnerability vuln,QueryManager qm, String cveString) { + final String cveId = VulnerabilityUtil.getValidCveId(cveString); + final List aliases = new ArrayList<>(); + if (cveId != null) { + aliases.add(computeAlias(vuln,qm,cveId)); + vuln.setAliases(aliases); } } /** - * Computes a list of `VulnerabilityAlias` objects for the given `vulnerability` and valid `cve_id`. - * The aliases are computed by creating a new `VulnerabilityAlias` object with the `vulnDbId` set to the - * `vulnerability`'s `vulnId` and the `cveId` set to the valid `cve_id`. The `VulnerabilityAlias` object - * is then synchronized using the `qm` object and added to a list that is returned as a result. + * Computes a list of `VulnerabilityAlias` objects for the given `vulnerability` and valid `cveId`. + * The aliases are computed by creating a new `VulnerabilityAlias` object with the `vulnDbId` + * and the `cveId`. The `VulnerabilityAlias` object is then synchronized using the `qm` object. * * @param vulnerability the `Vulnerability` object for which to compute the aliases * @param qm the `QueryManager` object used for synchronization - * @param cve_id the valid CVE ID string - * @return a list of computed `VulnerabilityAlias` objects + * @param cveId the valid CVE ID string + * @return a `VulnerabilityAlias` object */ - private static List computeAliases(Vulnerability vulnerability, QueryManager qm, String cve_id) { - List vulnerabilityAliasList = new ArrayList<>(); + private static VulnerabilityAlias computeAlias(Vulnerability vulnerability, QueryManager qm, String cveId) { final VulnerabilityAlias vulnerabilityAlias = new VulnerabilityAlias(); vulnerabilityAlias.setVulnDbId(vulnerability.getVulnId()); - vulnerabilityAlias.setCveId(cve_id); + vulnerabilityAlias.setCveId(cveId); qm.synchronizeVulnerabilityAlias(vulnerabilityAlias); - vulnerabilityAliasList.add(vulnerabilityAlias); - return vulnerabilityAliasList; + return vulnerabilityAlias; } } diff --git a/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java b/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java index 1fbd5d013c..d4a8414a87 100644 --- a/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java +++ b/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java @@ -112,6 +112,7 @@ public void inform(final Event e) { } } } + /** * Synchronizes the VulnDB vulnerabilities with the internal Dependency-Track database. * diff --git a/src/main/java/org/dependencytrack/util/VulnerabilityUtil.java b/src/main/java/org/dependencytrack/util/VulnerabilityUtil.java index 86e78ed623..cd8bdd6201 100644 --- a/src/main/java/org/dependencytrack/util/VulnerabilityUtil.java +++ b/src/main/java/org/dependencytrack/util/VulnerabilityUtil.java @@ -18,6 +18,7 @@ */ package org.dependencytrack.util; +import org.apache.commons.lang3.StringUtils; import org.dependencytrack.model.Severity; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerabilityAlias; @@ -29,6 +30,8 @@ import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public final class VulnerabilityUtil { @@ -60,6 +63,10 @@ public final class VulnerabilityUtil { Severity.UNASSIGNED, Severity.UNASSIGNED ) ); + // Define the CVE regex pattern + private static final String pattern = "^CVE-\\d{4}-\\d+$"; + // Compile the pattern + private static final Pattern regex = Pattern.compile(pattern); private VulnerabilityUtil() { } @@ -237,4 +244,27 @@ public static Set> getUniqueAliases(fina return uniqueAliases; } + /** + * Returns a valid CVE ID if found in the input string, or null otherwise + * If the input string is not a valid CVE ID, attempts to add "CVE-" prefix before checking again + * Returns null if the input string is empty, null or do not match. + * + * @param cveString the input string, potentially containing a CVE ID + * @return the valid CVE ID + */ + public static String getValidCveId(String cveString) { + if (StringUtils.isNotBlank(cveString)) { + if (!cveString.startsWith("CVE-")){ + // Try adding "CVE-" to the beginning of cveString + cveString = "CVE-" + cveString; + } + // Match the input against the pattern + Matcher matcher = regex.matcher(cveString); + if (matcher.matches()) { + return cveString; + } + } + return null; + } + } diff --git a/src/test/java/org/dependencytrack/parser/common/resolver/CveResolverTest.java b/src/test/java/org/dependencytrack/parser/common/resolver/CveResolverTest.java deleted file mode 100644 index 28fe042d21..0000000000 --- a/src/test/java/org/dependencytrack/parser/common/resolver/CveResolverTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.parser.common.resolver; - -import org.junit.Assert; -import org.junit.Test; - -public class CveResolverTest { - - @Test - public void testPositiveResolutionByFullCveId() { - String cve_id = CveResolver.getInstance().getValidCveId("CVE-2020-1234"); - Assert.assertNotNull(cve_id); - Assert.assertEquals("CVE-2020-1234", cve_id); - } - - @Test - public void testPositiveResolutionByPartialCveId() { - String cve_id = CveResolver.getInstance().getValidCveId("2020-1234"); - Assert.assertNotNull(cve_id); - Assert.assertEquals("CVE-2020-1234", cve_id); - } - @Test - public void testNegativeResolutionByInvalidCveId() { - String cve_id = CveResolver.getInstance().getValidCveId("20-1234"); - Assert.assertNull(cve_id); - Assert.assertNotEquals("CVE-2020-1234", cve_id); - } - - @Test - public void testNegativeResolutionByInvalidCweId() { - String cve_id = CveResolver.getInstance().getValidCveId("CVE-12345678"); - Assert.assertNull(cve_id); - Assert.assertNotEquals("CVE-1234-5678", cve_id); - } -} diff --git a/src/test/java/org/dependencytrack/util/VulnerabilityUtilTest.java b/src/test/java/org/dependencytrack/util/VulnerabilityUtilTest.java index d73e3099e3..e97c412f21 100644 --- a/src/test/java/org/dependencytrack/util/VulnerabilityUtilTest.java +++ b/src/test/java/org/dependencytrack/util/VulnerabilityUtilTest.java @@ -5,6 +5,7 @@ import org.dependencytrack.model.Severity; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerabilityAlias; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -85,6 +86,34 @@ public void testGetSeverity(BigDecimal cvssV2BaseScore, BigDecimal cvssV3BaseSco assertThat(VulnerabilityUtil.getSeverity(cvssV2BaseScore, cvssV3BaseScore, owaspRRLikelihoodScore, owaspRRTechnicalImpactScore, owaspRRBusinessImpactScore)).isEqualTo(severity); } + @Test + public void testPositiveResolutionByFullCveId() { + String cveId = VulnerabilityUtil.getValidCveId("CVE-2020-1234"); + Assert.assertNotNull(cveId); + Assert.assertEquals("CVE-2020-1234", cveId); + } + + @Test + public void testPositiveResolutionByPartialCveId() { + String cveId = VulnerabilityUtil.getValidCveId("2020-1234"); + Assert.assertNotNull(cveId); + Assert.assertEquals("CVE-2020-1234", cveId); + } + + @Test + public void testNegativeResolutionByInvalidString() { + String cveId = VulnerabilityUtil.getValidCveId("20-1234"); + Assert.assertNull(cveId); + Assert.assertNotEquals("CVE-2020-1234", cveId); + } + + @Test + public void testNegativeResolutionByInvalidCveId() { + String cveId = VulnerabilityUtil.getValidCveId("CVE-12345678"); + Assert.assertNull(cveId); + Assert.assertNotEquals("CVE-1234-5678", cveId); + } + private VulnerabilityAlias createAlias(final Consumer customizer) { final var alias = new VulnerabilityAlias(); customizer.accept(alias); From 0c7a548f51444efa2bb7ffbf12a6781f6e02d28f Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 7 Apr 2024 19:37:44 +0200 Subject: [PATCH 048/412] Advertise official Helm chart in docs Signed-off-by: nscuro --- README.md | 15 +-------- docs/_docs/getting-started/configuration.md | 2 +- docs/_docs/getting-started/data-directory.md | 2 +- .../_docs/getting-started/database-support.md | 2 +- docs/_docs/getting-started/deploy-exewar.md | 2 +- .../getting-started/deploy-kubernetes.md | 33 +++++++++++++++++++ docs/_docs/getting-started/initial-startup.md | 2 +- docs/_docs/getting-started/internal-ca.md | 2 +- .../getting-started/ldap-configuration.md | 2 +- docs/_docs/getting-started/monitoring.md | 2 +- .../openidconnect-configuration.md | 2 +- docs/_docs/getting-started/recurring-tasks.md | 2 +- 12 files changed, 44 insertions(+), 24 deletions(-) create mode 100644 docs/_docs/getting-started/deploy-kubernetes.md diff --git a/README.md b/README.md index 86fa530031..ad76dc11c6 100644 --- a/README.md +++ b/README.md @@ -171,21 +171,8 @@ The Traditional variant combines the API Server and the Frontend user interface container. This variant is not supported, deprecated, and will be discontinued in a future release. ## Deploying on Kubernetes with Helm -You can install on Kubernetes using the [community-maintained chart](https://github.com/evryfs/helm-charts/tree/master/charts/dependency-track) like this: -Helm v3: -```shell -helm repo add evryfs-oss https://evryfs.github.io/helm-charts/ -helm install dependency-track evryfs-oss/dependency-track --namespace dependency-track --create-namespace -``` - -Helm v2: -```shell -helm repo add evryfs-oss https://evryfs.github.io/helm-charts/ -helm install evryfs-oss/dependency-track --name dependency-track --namespace dependency-track --create-namespace -``` - -by default, it will install PostgreSQL and use persistent volume claims for the data-directory used for vulnerability feeds. +Refer to https://github.com/DependencyTrack/helm-charts. ## Contributing diff --git a/docs/_docs/getting-started/configuration.md b/docs/_docs/getting-started/configuration.md index ddf923462a..2df1f8f6d0 100644 --- a/docs/_docs/getting-started/configuration.md +++ b/docs/_docs/getting-started/configuration.md @@ -2,7 +2,7 @@ title: Configuration category: Getting Started chapter: 1 -order: 5 +order: 6 --- ### API server diff --git a/docs/_docs/getting-started/data-directory.md b/docs/_docs/getting-started/data-directory.md index 9bc2343a57..3dcaed534b 100644 --- a/docs/_docs/getting-started/data-directory.md +++ b/docs/_docs/getting-started/data-directory.md @@ -2,7 +2,7 @@ title: Data Directory category: Getting Started chapter: 1 -order: 7 +order: 8 --- Dependency-Track uses `~/.dependency-track` on UNIX/Linux systems and `.dependency-track` in the current users diff --git a/docs/_docs/getting-started/database-support.md b/docs/_docs/getting-started/database-support.md index 0be82cbbcd..0b61053404 100644 --- a/docs/_docs/getting-started/database-support.md +++ b/docs/_docs/getting-started/database-support.md @@ -2,7 +2,7 @@ title: Database Support category: Getting Started chapter: 1 -order: 6 +order: 7 --- Dependency-Track includes an embedded H2 database enabled by default. The intended purpose of this diff --git a/docs/_docs/getting-started/deploy-exewar.md b/docs/_docs/getting-started/deploy-exewar.md index e6685072f0..4d856fef19 100755 --- a/docs/_docs/getting-started/deploy-exewar.md +++ b/docs/_docs/getting-started/deploy-exewar.md @@ -2,7 +2,7 @@ title: Deploying the Executable WAR category: Getting Started chapter: 1 -order: 2 +order: 3 --- An executable WAR is a traditional Java Web Archive (WAR) that is packaged in a way where it can executed from diff --git a/docs/_docs/getting-started/deploy-kubernetes.md b/docs/_docs/getting-started/deploy-kubernetes.md new file mode 100644 index 0000000000..9777243b9a --- /dev/null +++ b/docs/_docs/getting-started/deploy-kubernetes.md @@ -0,0 +1,33 @@ +--- +title: Deploying on Kubernetes +category: Getting Started +chapter: 1 +order: 2 +--- + +Kubernetes deployment is supported via [Helm]. Charts are maintained in the [helm-charts] repository. + +To add the Helm repository: + +```shell +helm repo add dependency-track https://dependencytrack.github.io/helm-charts +``` + +The following will deploy Dependency-Track as [release](https://helm.sh/docs/intro/cheatsheet/) `dtrack` +into the `dtrack` namespace, creating the namespace if it doesn't exist already: + +```shell +helm install dtrack dependency-track/dependency-track \ + --namespace dtrack --create-namespace +``` + +For more details, such as available configuration options, please refer to [the chart's documentation]. + +Note that the chart does not include an external database such as PostgreSQL, +and instead defaults to an embedded H2 database. H2 is not intended for production +usage, please refer to the [database support] page for further information. + +[the chart's documentation]: https://github.com/DependencyTrack/helm-charts/tree/main/charts/dependency-track +[database support]: {{ site.baseurl }}{% link _docs/getting-started/database-support.md %} +[Helm]: https://helm.sh/ +[helm-charts]: https://github.com/DependencyTrack/helm-charts \ No newline at end of file diff --git a/docs/_docs/getting-started/initial-startup.md b/docs/_docs/getting-started/initial-startup.md index b87f975d87..692b90b8eb 100644 --- a/docs/_docs/getting-started/initial-startup.md +++ b/docs/_docs/getting-started/initial-startup.md @@ -2,7 +2,7 @@ title: Initial Startup category: Getting Started chapter: 1 -order: 4 +order: 5 --- Upon starting Dependency-Track for the first time, multiple tasks occur including: diff --git a/docs/_docs/getting-started/internal-ca.md b/docs/_docs/getting-started/internal-ca.md index 698a0e682a..008a7435fe 100644 --- a/docs/_docs/getting-started/internal-ca.md +++ b/docs/_docs/getting-started/internal-ca.md @@ -2,7 +2,7 @@ title: Internal Certificate Authorities category: Getting Started chapter: 1 -order: 11 +order: 12 --- Many organizations use their own [certificate authority](https://en.wikipedia.org/wiki/Certificate_authority) (CA) to diff --git a/docs/_docs/getting-started/ldap-configuration.md b/docs/_docs/getting-started/ldap-configuration.md index 1c50ed2ddf..860644f8db 100644 --- a/docs/_docs/getting-started/ldap-configuration.md +++ b/docs/_docs/getting-started/ldap-configuration.md @@ -2,7 +2,7 @@ title: LDAP Configuration category: Getting Started chapter: 1 -order: 9 +order: 10 --- Dependency-Track has been tested with multiple LDAP servers. The following are diff --git a/docs/_docs/getting-started/monitoring.md b/docs/_docs/getting-started/monitoring.md index fdea44c1d8..7e41fa6cfa 100644 --- a/docs/_docs/getting-started/monitoring.md +++ b/docs/_docs/getting-started/monitoring.md @@ -2,7 +2,7 @@ title: Monitoring category: Getting Started chapter: 1 -order: 12 +order: 13 --- diff --git a/docs/_docs/getting-started/openidconnect-configuration.md b/docs/_docs/getting-started/openidconnect-configuration.md index 7bebbb0d6f..2f11148fa3 100644 --- a/docs/_docs/getting-started/openidconnect-configuration.md +++ b/docs/_docs/getting-started/openidconnect-configuration.md @@ -2,7 +2,7 @@ title: OpenID Connect Configuration category: Getting Started chapter: 1 -order: 10 +order: 11 --- > OpenID Connect is supported in Dependency-Track 4.0.0 and above diff --git a/docs/_docs/getting-started/recurring-tasks.md b/docs/_docs/getting-started/recurring-tasks.md index 7dd7c619c0..50bdfb3e4c 100644 --- a/docs/_docs/getting-started/recurring-tasks.md +++ b/docs/_docs/getting-started/recurring-tasks.md @@ -2,7 +2,7 @@ title: Recurring Tasks category: Getting Started chapter: 1 -order: 8 +order: 9 --- Dependency-Track heavily relies on asynchronous recurring tasks to perform various forms of analyses, calculations, From 6246b3d88fd390f121ec1ece2e10b5751d3c0a33 Mon Sep 17 00:00:00 2001 From: Magnus Viernickel Date: Mon, 8 Apr 2024 08:50:01 +0200 Subject: [PATCH 049/412] [fix] use Caffeine for caching instead of singleton for caching of nixpkgs json Signed-off-by: Magnus Viernickel --- .../repositories/HackageMetaAnalyzer.java | 1 - .../tasks/repositories/IMetaAnalyzer.java | 2 +- .../repositories/NixpkgsMetaAnalyzer.java | 101 ++++++++---------- .../repositories/NixpkgsMetaAnalyzerTest.java | 2 +- 4 files changed, 48 insertions(+), 58 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java index b6b74cce45..a988d8901d 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java @@ -48,7 +48,6 @@ public RepositoryType supportedRepositoryType() { * {@inheritDoc} */ public boolean isApplicable(Component component) { - // FUTUREWORK(mangoiv): add hackage to https://github.com/package-url/packageurl-java/blob/master/src/main/java/com/github/packageurl/PackageURL.java final var purl = component.getPurl(); return purl != null && "hackage".equals(purl.getType()); } diff --git a/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java index b165c5d4bb..9214a5984f 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java @@ -141,7 +141,7 @@ static IMetaAnalyzer build(Component component) { return analyzer; } } else if ("nixpkgs".equals(component.getPurl().getType())) { - IMetaAnalyzer analyzer = NixpkgsMetaAnalyzer.getNixpkgsMetaAnalyzer(); + IMetaAnalyzer analyzer = new NixpkgsMetaAnalyzer(); if (analyzer.isApplicable(component)) { return analyzer; } diff --git a/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java index 0825937bbb..3f2b7df932 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java @@ -19,6 +19,8 @@ package org.dependencytrack.tasks.repositories; import alpine.common.logging.Logger; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; @@ -36,51 +38,64 @@ import java.io.InputStreamReader; import java.net.URISyntaxException; import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; public class NixpkgsMetaAnalyzer extends AbstractMetaAnalyzer { private static final Logger LOGGER = Logger.getLogger(NixpkgsMetaAnalyzer.class); private static final String DEFAULT_CHANNEL_URL = "https://channels.nixos.org/nixpkgs-unstable/packages.json.br"; - private static NixpkgsMetaAnalyzer nixpkgsMetaAnalyzer = new NixpkgsMetaAnalyzer(); - // this doesn't really make sense wrt the "AbstractMetaAnalyzer" - // because this statically known url will just redirect us to - // the actual URL - private final HashMap latestVersion; + private static final Cache> CACHE = Caffeine.newBuilder() + .expireAfterWrite(60, TimeUnit.MINUTES) + .maximumSize(1) + .build(); - private NixpkgsMetaAnalyzer() { + NixpkgsMetaAnalyzer() { this.baseUrl = DEFAULT_CHANNEL_URL; - HashMap newLatestVersion = new HashMap<>(); + } - try (final CloseableHttpClient client = HttpClients.createDefault()) { - try (final CloseableHttpResponse packagesResponse = processHttpRequest5(client)) { - if (packagesResponse != null && packagesResponse.getCode() == HttpStatus.SC_OK) { - var reader = new BufferedReader(new InputStreamReader(packagesResponse.getEntity().getContent())); - var packages = new JSONObject(new JSONTokener(reader)).getJSONObject("packages").toMap().values(); - packages.forEach(pkg -> { - // FUTUREWORK(mangoiv): there are potentially packages with the same pname - if (pkg instanceof HashMap jsonPkg) { - final var pname = jsonPkg.get("pname"); - final var version = jsonPkg.get("version"); - newLatestVersion.putIfAbsent((String) pname, (String) version); - } - }); + /** + * {@inheritDoc} + */ + public MetaModel analyze(Component component) { + Map latestVersions = CACHE.get("nixpkgs", cacheKey -> { + final var versions = new HashMap(); + try (final CloseableHttpClient client = HttpClients.createDefault()) { + try (final CloseableHttpResponse packagesResponse = processHttpRequest5(client)) { + if (packagesResponse != null && packagesResponse.getCode() == HttpStatus.SC_OK) { + var reader = new BufferedReader(new InputStreamReader(packagesResponse.getEntity().getContent())); + var packages = new JSONObject(new JSONTokener(reader)).getJSONObject("packages").toMap().values(); + packages.forEach(pkg -> { + // FUTUREWORK(mangoiv): there are potentially packages with the same pname + if (pkg instanceof HashMap jsonPkg) { + final var pname = jsonPkg.get("pname"); + final var version = jsonPkg.get("version"); + versions.putIfAbsent((String) pname, (String) version); + } + }); + } } + } catch (IOException ex) { + LOGGER.debug(ex.toString()); + handleRequestException(LOGGER, ex); + } catch (Exception ex) { + LOGGER.debug(ex.toString()); + throw new MetaAnalyzerException(ex); + } + return versions; + }); + final var meta = new MetaModel(component); + final var purl = component.getPurl(); + if (purl != null) { + final var newerVersion = latestVersions.get(purl.getName()); + if (newerVersion != null) { + meta.setLatestVersion(newerVersion); } - } catch (IOException ex) { - LOGGER.debug(ex.toString()); - handleRequestException(LOGGER, ex); - } catch (Exception ex) { - LOGGER.debug(ex.toString()); - throw new MetaAnalyzerException(ex); } - this.latestVersion = newLatestVersion; - LOGGER.info("finished updating the nixpkgs meta analyzer"); + return meta; } - public static NixpkgsMetaAnalyzer getNixpkgsMetaAnalyzer() { - return nixpkgsMetaAnalyzer; - } private CloseableHttpResponse processHttpRequest5(CloseableHttpClient client) throws IOException { try { @@ -96,14 +111,6 @@ private CloseableHttpResponse processHttpRequest5(CloseableHttpClient client) th } } - /** - * updates the NixpkgsMetaAnalyzer asynchronously by fetching a new version - * of the standard channel - */ - public void updateNixpkgsMetaAnalyzer() { - new Thread(() -> nixpkgsMetaAnalyzer = new NixpkgsMetaAnalyzer()).start(); - } - /** * {@inheritDoc} */ @@ -115,23 +122,7 @@ public RepositoryType supportedRepositoryType() { * {@inheritDoc} */ public boolean isApplicable(Component component) { - // FUTUREWORK(mangoiv): add nixpkgs to https://github.com/package-url/packageurl-java/blob/master/src/main/java/com/github/packageurl/PackageURL.java final var purl = component.getPurl(); return purl != null && "nixpkgs".equals(purl.getType()); } - - /** - * {@inheritDoc} - */ - public MetaModel analyze(Component component) { - final var meta = new MetaModel(component); - final var purl = component.getPurl(); - if (purl != null) { - final var newerVersion = latestVersion.get(purl.getName()); - if (newerVersion != null) { - meta.setLatestVersion(newerVersion); - } - } - return meta; - } } diff --git a/src/test/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzerTest.java b/src/test/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzerTest.java index a33bde76a5..5d8fa076db 100644 --- a/src/test/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzerTest.java +++ b/src/test/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzerTest.java @@ -13,7 +13,7 @@ public void testAnalyzer() throws Exception { final var component2 = new Component(); component1.setPurl(new PackageURL("pkg:nixpkgs/SDL_sound@1.0.3")); component2.setPurl(new PackageURL("pkg:nixpkgs/amarok@2.9.71")); - final var analyzer = NixpkgsMetaAnalyzer.getNixpkgsMetaAnalyzer(); + final var analyzer = new NixpkgsMetaAnalyzer(); Assert.assertTrue(analyzer.isApplicable(component1)); Assert.assertTrue(analyzer.isApplicable(component2)); Assert.assertEquals(RepositoryType.NIXPKGS, analyzer.supportedRepositoryType()); From 14f1547f14fe38f7eb10f747002017c7214f4ca4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 08:28:55 +0000 Subject: [PATCH 050/412] Bump docker/setup-buildx-action from 3.2.0 to 3.3.0 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.2.0 to 3.3.0. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/2b51285047da1547ffb1b2203d8be4c0af6b1f20...d70bba72b1f3fd22344832f00baa16ece964efeb) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 58d541b2b9..62295cf80e 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -86,7 +86,7 @@ jobs: uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # tag=v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20 # tag=v3.2.0 + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # tag=v3.3.0 id: buildx with: install: true From 80edfdeedf98bfb0868876cb17c662d95b84bfed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 08:29:00 +0000 Subject: [PATCH 051/412] Bump github/codeql-action from 3.24.9 to 3.24.10 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.24.9 to 3.24.10. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/1b1aada464948af03b950897e5eb522f92603cc2...4355270be187e1b672a7a1c7c7bae5afdc1ab94a) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 58d541b2b9..06fb1ae59f 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -133,6 +133,6 @@ jobs: - name: Upload Trivy Scan Results to GitHub Security Tab if: ${{ inputs.publish-container }} - uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # tag=v3.24.9 + uses: github/codeql-action/upload-sarif@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # tag=v3.24.10 with: sarif_file: 'trivy-results.sarif' From f15fcf10f36c1d75b79a6640cdb53094a2314e75 Mon Sep 17 00:00:00 2001 From: Andres Tito Date: Tue, 9 Apr 2024 08:09:12 +0000 Subject: [PATCH 052/412] Explicit null check for severities Signed-off-by: Andres Tito --- .../tasks/metrics/ComponentMetricsUpdateTask.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/metrics/ComponentMetricsUpdateTask.java b/src/main/java/org/dependencytrack/tasks/metrics/ComponentMetricsUpdateTask.java index 894bff57ab..a4eafad6da 100644 --- a/src/main/java/org/dependencytrack/tasks/metrics/ComponentMetricsUpdateTask.java +++ b/src/main/java/org/dependencytrack/tasks/metrics/ComponentMetricsUpdateTask.java @@ -96,7 +96,11 @@ static Counters updateMetrics(final UUID uuid) throws Exception { .forEach(aliasesSeen::add); counters.vulnerabilities++; - try { + + if (vulnerability.getSeverity() == null) { + LOGGER.warn("Vulnerability severity is " + vulnerability.getSeverity()+ " null for " + vulnerability.getSource() + "|" + vulnerability.getVulnId()); + } + else { switch (vulnerability.getSeverity()) { case CRITICAL -> counters.critical++; case HIGH -> counters.high++; @@ -104,8 +108,6 @@ static Counters updateMetrics(final UUID uuid) throws Exception { case LOW, INFO -> counters.low++; case UNASSIGNED -> counters.unassigned++; } - } catch (NullPointerException ex) { - LOGGER.debug("Vulnerability severity is null for" + vulnerability.getSource() + "|" + vulnerability.getVulnId() + "->"+vulnerability.getSeverity()); } } From 2e7124209bf03a1fe5c3b4c4bfc2df3617b7d98d Mon Sep 17 00:00:00 2001 From: Magnus Viernickel Date: Tue, 9 Apr 2024 13:29:03 +0200 Subject: [PATCH 053/412] [chore] update license headers to license to OWAS foundation Signed-off-by: Magnus Viernickel --- .../dependencytrack/tasks/repositories/HackageMetaAnalyzer.java | 2 +- .../dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java index a988d8901d..a65c21b8b3 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/HackageMetaAnalyzer.java @@ -14,7 +14,7 @@ * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) Steve Springett. All Rights Reserved. + * Copyright (c) OWASP Foundation. All Rights Reserved. */ package org.dependencytrack.tasks.repositories; diff --git a/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java index 3f2b7df932..4f0185f035 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/NixpkgsMetaAnalyzer.java @@ -14,7 +14,7 @@ * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) Steve Springett. All Rights Reserved. + * Copyright (c) OWASP Foundation. All Rights Reserved. */ package org.dependencytrack.tasks.repositories; From 8626ecb63a9a1a2e15bf5fef6b8f5fd3c8a6bcaf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:35:08 +0000 Subject: [PATCH 054/412] Bump debian from `d10f054` to `2c96e00` in /src/main/docker Bumps debian from `d10f054` to `2c96e00`. --- updated-dependencies: - dependency-name: debian dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- src/main/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docker/Dockerfile b/src/main/docker/Dockerfile index 208895f76b..69ee7b7c19 100644 --- a/src/main/docker/Dockerfile +++ b/src/main/docker/Dockerfile @@ -1,6 +1,6 @@ FROM eclipse-temurin:21.0.2_13-jre-jammy@sha256:d9f7b8326b9d396d070432982a998015a04ffb8885b145e97777a5ae324a8df1 AS jre-build -FROM debian:stable-slim@sha256:d10f0545d14bad5f4d230301f7e4fd904384f2dd16fda16d708f936c2fa1db3e +FROM debian:stable-slim@sha256:2c96e0011b1b2855d8e482e6d4ced6d292ea50750e9e535d09510167ae4858f9 # Arguments that can be passed at build time # Directory names must end with / to avoid errors when ADDing and COPYing From e0da8c51beeca5d627120756e38e392e7fac45af Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 14 Apr 2024 19:59:45 +0200 Subject: [PATCH 055/412] Update changelog for v4.11 with recent changes Signed-off-by: nscuro --- docs/_posts/2024-xx-xx-v4.11.0.md | 64 +++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index 7c91914d7d..bd45f4ae99 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -17,7 +17,18 @@ schema. While this allowed BOMs to be processed that did not strictly adhere to when uploaded files were accepted, but then failed to be ingested during asynchronous processing. Starting with this release, uploaded files will be rejected if they fail schema validation. Note that this may reveal issues in BOM generators that currently produce invalid CycloneDX documents. Validation may be turned off by setting the -environment variable `BOM_VALIDATION_ENABLED` to `false`. +environment variable `BOM_VALIDATION_ENABLED` to `false`. + * *This feature was demoed in our April community meeting! Watch it [here](https://www.youtube.com/watch?v=3iIeajRJK8o&t=450s)* +* **Global Vulnerability Audit View**. This new interface allows users to discover and filter vulnerabilities that affect +their portfolio, across all projects. When portfolio access control is enabled, this view is limited to projects a user +has explicit access to. It is possible to inspect individual findings, or aggregates grouped by vulnerability, +making it possible to spot the most prevalent vulnerabilities. + * *This feature was demoed in our April community meeting! Watch it [here](https://www.youtube.com/watch?v=3iIeajRJK8o&t=572s).* +* **Trivy Analyzer Integration**. It is now possible to leverage [Trivy] in [server mode] for vulnerability analysis. + * *This feature was demoed in our April community meeting! Watch it [here](https://www.youtube.com/watch?v=3iIeajRJK8o&t=725s).* +* **Official Helm Chart**. The Dependency-Track project now offers an official Helm chart for Kubernetes deployments. +Community input and contributions are explicitly requested. The chart repository can be found at +[https://github.com/DependencyTrack/helm-charts](https://github.com/DependencyTrack/helm-charts) **Features:** @@ -47,7 +58,12 @@ environment variable `BOM_VALIDATION_ENABLED` to `false`. * Add auto-generated changelog to GitHub releases - [apiserver/#3502] * Bump SPDX license list to v3.23 - [apiserver/#3508] * Validate uploaded BOMs against CycloneDX schema prior to processing them - [apiserver/#3522] +* Add support for Hackage repositories - [apiserver/#3549] +* Add support for Nix repositories - [apiserver/#3549] * Add *required permissions* to OpenAPI descriptions of endpoints - [apiserver/#3557] +* Add support for exporting findings in SARIF format - [apiserver/#3561] +* Ingest vulnerability alias information from VulnDB - [apiserver/#3588] +* Properly validate UUID request parameters to prevent internal server errors - [apiserver/#3590] * Show component count in projects list - [frontend/#683] * Add current *fail*, *warn*, and *info* values to bottom of policy violation metrics - [frontend/#707] * Remove unused policy violation widget - [frontend/#710] @@ -61,6 +77,13 @@ environment variable `BOM_VALIDATION_ENABLED` to `false`. * Add ESLint and prettier - [frontend/#752] * Display *created* and *last used* timestamps for API keys - [frontend/#768] * Display API key comments and make them editable - [frontend/#768] +* Add *internal* column to component search view - [frontend/#775] +* Add *classification* badge to component details to highlight *internal* components - [frontend/#776] +* Add *group* to component breadcrumb - [frontend/#777] +* Add *deprecated* column to license list - [frontend/#792] +* Use *concise* endpoint to populate license list - [frontend/#793] +* Display *comment* field of external references - [frontend/#803] +* Add support for localization based on browser language - [frontend/#805] **Fixes:** @@ -81,6 +104,8 @@ environment variable `BOM_VALIDATION_ENABLED` to `false`. * Fix unclear error response when base64 encoded `bom` and `vex` values exceed character limit - [apiserver/#3558] * Fix unhandled `NotFoundException`s causing a `HTTP 500` response - [apiserver/#3559] * Fix inability to store PURLs longer than 255 characters - [apiserver/#3560] +* Disable automatic API key generation for newly created teams - [apiserver/#3574] +* Fix severity not being set for vulnerabilities from VulnDB - [apiserver/#3595] * Fix `VUE_APP_SERVER_URL` being ignored - [frontend/#682] * Fix visibility of "Vulnerabilities" and "Policy Violations" columns not being toggle-able individually - [frontend/#686] * Fix finding search routes - [frontend/#689] @@ -92,6 +117,9 @@ environment variable `BOM_VALIDATION_ENABLED` to `false`. * Fix redundant requests to `/api/v1/component` when loading project page - [frontend/#726] * Fix column visibility preferences triggering redundant requests - [frontend/#727] * Fix `@` being appended when rendering CPEs in "Affected Components" view - [frontend/#748] +* Fix aliases not being displayed in vulnerabilities list - [frontend/#766] +* Fix link to *portfolio access control* view - [frontend/#774] +* Fix *Download BOM* button requiring higher privileges than necessary - [frontend/#812] **Upgrade Notes:** @@ -127,9 +155,9 @@ For a complete list of changes, refer to the respective GitHub milestones: We thank all organizations and individuals who contributed to this release, from logging issues to taking part in discussions on GitHub & Slack to testing of fixes. Special thanks to everyone who contributed code to implement enhancements and fix defects: -[@a5a351e7], [@acdha], [@AnthonyMastrean], [@LaVibeX], [@VithikaS], [@baburkin], [@fnxpt], [@kepten], [@leec94], -[@lukas-braune], [@malice00], [@mehab], [@mge-mm], [@mikkeschiren], [@mykter], [@rbt-mm], -[@rkg-mm], [@sahibamittal], [@sebD], [@setchy] +[@AnthonyMastrean], [@LaVibeX], [@MangoIV], [@Robbilie], [@VithikaS], [@a5a351e7], [@acdha], [@aravindparappil46], +[@baburkin], [@fnxpt], [@kepten], [@leec94], [@lukas-braune], [@malice00], [@mehab], [@mge-mm] +[@mikkeschiren], [@mykter], [@rbt-mm], [@rkg-mm], [@sahibamittal], [@sebD], [@setchy] ###### dependency-track-apiserver.jar @@ -187,10 +215,16 @@ Special thanks to everyone who contributed code to implement enhancements and fi [apiserver/#3512]: https://github.com/DependencyTrack/dependency-track/pull/3512 [apiserver/#3513]: https://github.com/DependencyTrack/dependency-track/pull/3513 [apiserver/#3522]: https://github.com/DependencyTrack/dependency-track/pull/3522 +[apiserver/#3549]: https://github.com/DependencyTrack/dependency-track/pull/3549 [apiserver/#3557]: https://github.com/DependencyTrack/dependency-track/pull/3557 [apiserver/#3558]: https://github.com/DependencyTrack/dependency-track/pull/3558 [apiserver/#3559]: https://github.com/DependencyTrack/dependency-track/pull/3559 [apiserver/#3560]: https://github.com/DependencyTrack/dependency-track/pull/3560 +[apiserver/#3561]: https://github.com/DependencyTrack/dependency-track/pull/3561 +[apiserver/#3574]: https://github.com/DependencyTrack/dependency-track/pull/3574 +[apiserver/#3588]: https://github.com/DependencyTrack/dependency-track/pull/3588 +[apiserver/#3590]: https://github.com/DependencyTrack/dependency-track/pull/3590 +[apiserver/#3595]: https://github.com/DependencyTrack/dependency-track/pull/3595 [frontend/#682]: https://github.com/DependencyTrack/frontend/pull/682 [frontend/#683]: https://github.com/DependencyTrack/frontend/pull/683 @@ -214,13 +248,26 @@ Special thanks to everyone who contributed code to implement enhancements and fi [frontend/#744]: https://github.com/DependencyTrack/frontend/pull/744 [frontend/#748]: https://github.com/DependencyTrack/frontend/pull/748 [frontend/#752]: https://github.com/DependencyTrack/frontend/pull/752 +[frontend/#766]: https://github.com/DependencyTrack/frontend/pull/766 [frontend/#768]: https://github.com/DependencyTrack/frontend/pull/768 +[frontend/#774]: https://github.com/DependencyTrack/frontend/pull/774 +[frontend/#775]: https://github.com/DependencyTrack/frontend/pull/775 +[frontend/#776]: https://github.com/DependencyTrack/frontend/pull/776 +[frontend/#777]: https://github.com/DependencyTrack/frontend/pull/777 +[frontend/#792]: https://github.com/DependencyTrack/frontend/pull/792 +[frontend/#793]: https://github.com/DependencyTrack/frontend/pull/793 +[frontend/#803]: https://github.com/DependencyTrack/frontend/pull/803 +[frontend/#805]: https://github.com/DependencyTrack/frontend/pull/805 +[frontend/#812]: https://github.com/DependencyTrack/frontend/pull/812 -[@a5a351e7]: https://github.com/a5a351e7 -[@acdha]: https://github.com/acdha [@AnthonyMastrean]: https://github.com/AnthonyMastrean [@LaVibeX]: https://github.com/LaVibeX +[@MangoIV]: https://github.com/MangoIV +[@Robbilie]: https://github.com/Robbilie [@VithikaS]: https://github.com/VithikaS +[@a5a351e7]: https://github.com/a5a351e7 +[@acdha]: https://github.com/acdha +[@aravindparappil46]: https://github.com/aravindparappil46 [@baburkin]: https://github.com/baburkin [@fnxpt]: https://github.com/fnxpt [@kepten]: https://github.com/kepten @@ -230,6 +277,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [@mehab]: https://github.com/mehab [@mge-mm]: https://github.com/mge-mm [@mikkeschiren]: https://github.com/mikkeschiren +[@mprencipe]: https://github.com/mprencipe [@mykter]: https://github.com/mykter [@rbt-mm]: https://github.com/rbt-mm [@rkg-mm]: https://github.com/rkg-mm @@ -238,6 +286,8 @@ Special thanks to everyone who contributed code to implement enhancements and fi [@setchy]: https://github.com/setchy [Mapped Diagnostic Context]: https://logback.qos.ch/manual/mdc.html +[Trivy]: https://trivy.dev/ [component identity]: https://docs.dependencytrack.org/analysis-types/component-identity/ [customized their logging configuration]: https://docs.dependencytrack.org/getting-started/monitoring/#custom-logging-configuration -[logback.xml]: https://github.com/DependencyTrack/dependency-track/blob/master/src/main/docker/logback.xml \ No newline at end of file +[logback.xml]: https://github.com/DependencyTrack/dependency-track/blob/master/src/main/docker/logback.xml +[server mode]: https://aquasecurity.github.io/trivy/v0.50/docs/references/modes/client-server/ \ No newline at end of file From 1fbd88ff462827d324217d45b959ca36664922ca Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 26 Feb 2024 22:51:01 +0100 Subject: [PATCH 056/412] Add component property model and resource Supersedes #2717 Co-authored-by: Robert Kesterson Signed-off-by: nscuro --- .../org/dependencytrack/model/Component.java | 13 + .../model/ComponentProperty.java | 149 +++++++ .../persistence/ComponentQueryManager.java | 57 +++ .../persistence/QueryManager.java | 17 + .../v1/ComponentPropertyResource.java | 235 +++++++++++ src/main/resources/META-INF/persistence.xml | 1 + .../model/ComponentPropertyTest.java | 77 ++++ .../v1/ComponentPropertyResourceTest.java | 364 ++++++++++++++++++ 8 files changed, 913 insertions(+) create mode 100644 src/main/java/org/dependencytrack/model/ComponentProperty.java create mode 100644 src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java create mode 100644 src/test/java/org/dependencytrack/model/ComponentPropertyTest.java create mode 100644 src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java diff --git a/src/main/java/org/dependencytrack/model/Component.java b/src/main/java/org/dependencytrack/model/Component.java index 29924065f4..7396382f53 100644 --- a/src/main/java/org/dependencytrack/model/Component.java +++ b/src/main/java/org/dependencytrack/model/Component.java @@ -74,6 +74,7 @@ @Persistent(name = "externalReferences"), @Persistent(name = "parent"), @Persistent(name = "children"), + @Persistent(name = "properties"), @Persistent(name = "vulnerabilities"), }), @FetchGroup(name = "INTERNAL_IDENTIFICATION", members = { @@ -330,6 +331,10 @@ public enum FetchGroup { @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "id ASC")) private Collection children; + @Persistent(mappedBy = "component", defaultFetchGroup = "false") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "groupName ASC, propertyName ASC")) + private List properties; + @Persistent(table = "COMPONENTS_VULNERABILITIES") @Join(column = "COMPONENT_ID") @Element(column = "VULNERABILITY_ID") @@ -712,6 +717,14 @@ public void setChildren(Collection children) { this.children = children; } + public List getProperties() { + return properties; + } + + public void setProperties(final List properties) { + this.properties = properties; + } + public List getVulnerabilities() { return vulnerabilities; } diff --git a/src/main/java/org/dependencytrack/model/ComponentProperty.java b/src/main/java/org/dependencytrack/model/ComponentProperty.java new file mode 100644 index 0000000000..ccac4048dc --- /dev/null +++ b/src/main/java/org/dependencytrack/model/ComponentProperty.java @@ -0,0 +1,149 @@ +/* + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.model; + +import alpine.model.IConfigProperty; +import alpine.server.json.TrimmedStringDeserializer; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import javax.jdo.annotations.Column; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; +import javax.jdo.annotations.Unique; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.io.Serializable; + +/** + * @since 4.11.0 + */ +@PersistenceCapable(table = "COMPONENT_PROPERTY") +@Unique(name = "COMPONENT_PROPERTY_KEYS_IDX", members = {"component", "groupName", "propertyName"}) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ComponentProperty implements IConfigProperty, Serializable { + + private static final long serialVersionUID = -7510889645969713080L; + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + @Persistent + @Column(name = "COMPONENT_ID", allowsNull = "false") + private Component component; + + @Persistent + @Column(name = "GROUPNAME", allowsNull = "false") + @NotBlank + @Size(min = 1, max = 255) + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = "\\P{Cc}+", message = "The groupName must not contain control characters") + private String groupName; + + @Persistent + @Column(name = "PROPERTYNAME", allowsNull = "false") + @NotBlank + @Size(min = 1, max = 255) + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = "\\P{Cc}+", message = "The propertyName must not contain control characters") + private String propertyName; + + @Persistent + @Column(name = "PROPERTYVALUE", length = 1024) + @Size(min = 0, max = 1024) + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = "\\P{Cc}+", message = "The propertyValue must not contain control characters") + private String propertyValue; + + @Persistent + @Column(name = "PROPERTYTYPE", jdbcType = "VARCHAR", allowsNull = "false") + @NotNull + private PropertyType propertyType; + + @Persistent + @Column(name = "DESCRIPTION") + @Size(max = 255) + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = "\\P{Cc}+", message = "The description must not contain control characters") + private String description; + + public long getId() { + return id; + } + + public void setId(final long id) { + this.id = id; + } + + public Component getComponent() { + return component; + } + + public void setComponent(final Component component) { + this.component = component; + } + + public String getGroupName() { + return groupName; + } + + public void setGroupName(final String groupName) { + this.groupName = groupName; + } + + public String getPropertyName() { + return propertyName; + } + + public void setPropertyName(final String propertyName) { + this.propertyName = propertyName; + } + + public String getPropertyValue() { + return propertyValue; + } + + public void setPropertyValue(final String propertyValue) { + this.propertyValue = propertyValue; + } + + public PropertyType getPropertyType() { + return propertyType; + } + + public void setPropertyType(final PropertyType propertyType) { + this.propertyType = propertyType; + } + + public String getDescription() { + return description; + } + + public void setDescription(final String description) { + this.description = description; + } + +} \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java index dec72efdd9..e4e215614d 100644 --- a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java @@ -21,6 +21,7 @@ import alpine.common.logging.Logger; import alpine.event.framework.Event; import alpine.model.ApiKey; +import alpine.model.IConfigProperty; import alpine.model.Team; import alpine.model.UserPrincipal; import alpine.persistence.PaginatedResult; @@ -31,6 +32,7 @@ import org.dependencytrack.event.IndexEvent; import org.dependencytrack.model.Component; import org.dependencytrack.model.ComponentIdentity; +import org.dependencytrack.model.ComponentProperty; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Project; import org.dependencytrack.model.RepositoryMetaComponent; @@ -830,4 +832,59 @@ private void getDirectDependenciesForPathDependencies(Map dep } dependencyGraph.putAll(addToDependencyGraph); } + + /** + * Returns a ComponentProperty with the specified groupName and propertyName. + * + * @param component the component the property belongs to + * @param groupName the group name of the config property + * @param propertyName the name of the property + * @return a ComponentProperty object + */ + @Override + public ComponentProperty getComponentProperty(final Component component, final String groupName, final String propertyName) { + final Query query = this.pm.newQuery(ComponentProperty.class, "component == :component && groupName == :groupName && propertyName == :propertyName"); + query.setRange(0, 1); + return singleResult(query.execute(component, groupName, propertyName)); + } + + /** + * Returns a List of ProjectProperty's for the specified project. + * + * @param component the project the property belongs to + * @return a List ProjectProperty objects + */ + @Override + @SuppressWarnings("unchecked") + public List getComponentProperties(final Component component) { + final Query query = this.pm.newQuery(ComponentProperty.class, "component == :component"); + query.setOrdering("groupName asc, propertyName asc"); + return (List) query.execute(component); + } + + /** + * Creates a key/value pair (ComponentProperty) for the specified Project. + * + * @param component the Component to create the property for + * @param groupName the group name of the property + * @param propertyName the name of the property + * @param propertyValue the value of the property + * @param propertyType the type of property + * @param description a description of the property + * @return the created ComponentProperty object + */ + @Override + public ComponentProperty createComponentProperty(final Component component, final String groupName, final String propertyName, + final String propertyValue, final IConfigProperty.PropertyType propertyType, + final String description) { + final ComponentProperty property = new ComponentProperty(); + property.setComponent(component); + property.setGroupName(groupName); + property.setPropertyName(propertyName); + property.setPropertyValue(propertyValue); + property.setPropertyType(propertyType); + property.setDescription(description); + return persist(property); + } + } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 08680dbe6b..358438c2c0 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -22,6 +22,7 @@ import alpine.event.framework.Event; import alpine.model.ApiKey; import alpine.model.ConfigProperty; +import alpine.model.IConfigProperty; import alpine.model.Team; import alpine.model.UserPrincipal; import alpine.notification.NotificationLevel; @@ -44,6 +45,7 @@ import org.dependencytrack.model.Component; import org.dependencytrack.model.ComponentAnalysisCache; import org.dependencytrack.model.ComponentIdentity; +import org.dependencytrack.model.ComponentProperty; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.DependencyMetrics; import org.dependencytrack.model.Finding; @@ -557,6 +559,21 @@ public Map getDependencyGraphForComponents(Project project, L return getComponentQueryManager().getDependencyGraphForComponents(project, components); } + public List getComponentProperties(final Component component) { + return getComponentQueryManager().getComponentProperties(component); + } + + public ComponentProperty getComponentProperty(final Component component, final String groupName, final String propertyName) { + return getComponentQueryManager().getComponentProperty(component, groupName, propertyName); + } + + public ComponentProperty createComponentProperty(final Component component, final String groupName, final String propertyName, + final String propertyValue, final IConfigProperty.PropertyType propertyType, + final String description) { + return getComponentQueryManager() + .createComponentProperty(component, groupName, propertyName, propertyValue, propertyType, description); + } + public PaginatedResult getLicenses() { return getLicenseQueryManager().getLicenses(); } diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java new file mode 100644 index 0000000000..3a5eaac9e3 --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java @@ -0,0 +1,235 @@ +/* + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.resources.v1; + +import alpine.server.auth.PermissionRequired; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import org.apache.commons.lang3.StringUtils; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.ComponentProperty; +import org.dependencytrack.persistence.QueryManager; + +import javax.validation.Validator; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; + +/** + * @since 4.11.0 + */ +@Path("/v1/component/{uuid}/property") +@Api(value = "componentProperty", authorizations = @Authorization(value = "X-Api-Key")) +public class ComponentPropertyResource extends AbstractConfigPropertyResource { + + @GET + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Returns a list of all ComponentProperties for the specified component", + response = ComponentProperty.class, + responseContainer = "List" + ) + @ApiResponses(value = { + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), + @ApiResponse(code = 404, message = "The project could not be found") + }) + @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) + public Response getProperties( + @ApiParam(value = "The UUID of the component to retrieve properties for", required = true) + @PathParam("uuid") String uuid) { + try (QueryManager qm = new QueryManager(getAlpineRequest())) { + final Component component = qm.getObjectByUuid(Component.class, uuid); + if (component != null) { + if (qm.hasAccess(super.getPrincipal(), component.getProject())) { + final List properties = qm.getComponentProperties(component); + // Detaches the objects and closes the persistence manager so that if/when encrypted string + // values are replaced by the placeholder, they are not erroneously persisted to the database. + qm.getPersistenceManager().detachCopyAll(properties); + qm.close(); + for (final ComponentProperty property : properties) { + // Replace the value of encrypted strings with the pre-defined placeholder + if (ComponentProperty.PropertyType.ENCRYPTEDSTRING == property.getPropertyType()) { + property.setPropertyValue(ENCRYPTED_PLACEHOLDER); + } + } + return Response.ok(properties).build(); + } else { + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + } + } else { + return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); + } + } + } + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Creates a new component property", + response = ComponentProperty.class, + code = 201 + ) + @ApiResponses(value = { + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 403, message = "Access to the specified component is forbidden"), + @ApiResponse(code = 404, message = "The component could not be found"), + @ApiResponse(code = 409, message = "A property with the specified component/group/name combination already exists") + }) + @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) + public Response createProperty( + @ApiParam(value = "The UUID of the component to create a property for", required = true) + @PathParam("uuid") String uuid, + ComponentProperty json) { + final Validator validator = super.getValidator(); + failOnValidationError( + validator.validateProperty(json, "groupName"), + validator.validateProperty(json, "propertyName"), + validator.validateProperty(json, "propertyValue") + ); + try (QueryManager qm = new QueryManager()) { + final Component component = qm.getObjectByUuid(Component.class, uuid); + if (component != null) { + if (qm.hasAccess(super.getPrincipal(), component.getProject())) { + final ComponentProperty existing = qm.getComponentProperty(component, + StringUtils.trimToNull(json.getGroupName()), StringUtils.trimToNull(json.getPropertyName())); + if (existing == null) { + final ComponentProperty property = qm.createComponentProperty(component, + StringUtils.trimToNull(json.getGroupName()), + StringUtils.trimToNull(json.getPropertyName()), + null, // Set value to null - this will be taken care of by updatePropertyValue below + json.getPropertyType(), + StringUtils.trimToNull(json.getDescription())); + updatePropertyValue(qm, json, property); + qm.getPersistenceManager().detachCopy(component); + qm.close(); + if (ComponentProperty.PropertyType.ENCRYPTEDSTRING == property.getPropertyType()) { + property.setPropertyValue(ENCRYPTED_PLACEHOLDER); + } + return Response.status(Response.Status.CREATED).entity(property).build(); + } else { + return Response.status(Response.Status.CONFLICT).entity("A property with the specified component/group/name combination already exists.").build(); + } + } else { + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); + } + } else { + return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); + } + } + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Updates a project property", + response = ComponentProperty.class + ) + @ApiResponses(value = { + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), + @ApiResponse(code = 404, message = "The component could not be found"), + }) + @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) + public Response updateProperty( + @ApiParam(value = "The UUID of the component to create a property for", required = true) + @PathParam("uuid") String uuid, + ComponentProperty json) { + final Validator validator = super.getValidator(); + failOnValidationError( + validator.validateProperty(json, "groupName"), + validator.validateProperty(json, "propertyName"), + validator.validateProperty(json, "propertyValue") + ); + try (QueryManager qm = new QueryManager()) { + final Component component = qm.getObjectByUuid(Component.class, uuid); + if (component != null) { + if (qm.hasAccess(super.getPrincipal(), component.getProject())) { + final ComponentProperty property = qm.getComponentProperty(component, json.getGroupName(), json.getPropertyName()); + if (property != null) { + return updatePropertyValue(qm, json, property); + } else { + return Response.status(Response.Status.NOT_FOUND).entity("A property with the specified component/group/name combination could not be found.").build(); + } + } else { + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); + } + } else { + return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); + } + } + } + + @DELETE + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Deletes a config property", + response = ComponentProperty.class + ) + @ApiResponses(value = { + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 403, message = "Access to the specified component is forbidden"), + @ApiResponse(code = 404, message = "The component or component property could not be found"), + }) + @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) + public Response deleteProperty( + @ApiParam(value = "The UUID of the component to delete a property from", required = true) + @PathParam("uuid") String uuid, + ComponentProperty json) { + final Validator validator = super.getValidator(); + failOnValidationError( + validator.validateProperty(json, "groupName"), + validator.validateProperty(json, "propertyName") + ); + try (QueryManager qm = new QueryManager()) { + final Component component = qm.getObjectByUuid(Component.class, uuid); + if (component != null) { + if (qm.hasAccess(super.getPrincipal(), component.getProject())) { + final ComponentProperty property = qm.getComponentProperty(component, json.getGroupName(), json.getPropertyName()); + if (property != null) { + qm.delete(property); + return Response.status(Response.Status.NO_CONTENT).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).entity("The component property could not be found.").build(); + } + } else { + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); + } + } else { + return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml index 1a2763d086..48f4e91804 100644 --- a/src/main/resources/META-INF/persistence.xml +++ b/src/main/resources/META-INF/persistence.xml @@ -27,6 +27,7 @@ org.dependencytrack.model.Bom org.dependencytrack.model.Component org.dependencytrack.model.ComponentAnalysisCache + org.dependencytrack.model.ComponentProperty org.dependencytrack.model.DependencyMetrics org.dependencytrack.model.FindingAttribution org.dependencytrack.model.License diff --git a/src/test/java/org/dependencytrack/model/ComponentPropertyTest.java b/src/test/java/org/dependencytrack/model/ComponentPropertyTest.java new file mode 100644 index 0000000000..f261d56484 --- /dev/null +++ b/src/test/java/org/dependencytrack/model/ComponentPropertyTest.java @@ -0,0 +1,77 @@ +/* + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.model; + +import alpine.model.IConfigProperty; +import org.junit.Assert; +import org.junit.Test; + +public class ComponentPropertyTest { + + @Test + public void testId() { + ComponentProperty property = new ComponentProperty(); + property.setId(111L); + Assert.assertEquals(111L, property.getId()); + } + + @Test + public void testProject() { + Component component = new Component(); + ComponentProperty property = new ComponentProperty(); + property.setComponent(component); + Assert.assertEquals(component, property.getComponent()); + } + + @Test + public void testGroupName() { + ComponentProperty property = new ComponentProperty(); + property.setGroupName("Group Name"); + Assert.assertEquals("Group Name", property.getGroupName()); + } + + @Test + public void testPropertyName() { + ComponentProperty property = new ComponentProperty(); + property.setPropertyName("Property Name"); + Assert.assertEquals("Property Name", property.getPropertyName()); + } + + @Test + public void testPropertyValue() { + ComponentProperty property = new ComponentProperty(); + property.setPropertyValue("Property Value"); + Assert.assertEquals("Property Value", property.getPropertyValue()); + } + + @Test + public void testPropertyType() { + ComponentProperty property = new ComponentProperty(); + property.setPropertyType(IConfigProperty.PropertyType.STRING); + Assert.assertEquals(IConfigProperty.PropertyType.STRING, property.getPropertyType()); + } + + @Test + public void testDescription() { + ComponentProperty property = new ComponentProperty(); + property.setDescription("Property Description"); + Assert.assertEquals("Property Description", property.getDescription()); + } + +} \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java new file mode 100644 index 0000000000..1983ea7de6 --- /dev/null +++ b/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java @@ -0,0 +1,364 @@ +/* + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.resources.v1; + +import alpine.model.IConfigProperty.PropertyType; +import alpine.server.filters.ApiFilter; +import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.ResourceTest; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.ComponentProperty; +import org.dependencytrack.model.Project; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.ServletDeploymentContext; +import org.junit.Test; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.UUID; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +public class ComponentPropertyResourceTest extends ResourceTest { + + @Override + protected DeploymentContext configureDeployment() { + return ServletDeploymentContext.forServlet(new ServletContainer( + new ResourceConfig(ComponentPropertyResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class))) + .build(); + } + + @Test + public void getPropertiesTest() { + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final var propertyA = new ComponentProperty(); + propertyA.setComponent(component); + propertyA.setGroupName("foo-a"); + propertyA.setPropertyName("bar-a"); + propertyA.setPropertyValue("baz-a"); + propertyA.setPropertyType(PropertyType.STRING); + propertyA.setDescription("qux-a"); + qm.persist(propertyA); + + final var propertyB = new ComponentProperty(); + propertyB.setComponent(component); + propertyB.setGroupName("foo-b"); + propertyB.setPropertyName("bar-b"); + propertyB.setPropertyValue("baz-b"); + propertyB.setPropertyType(PropertyType.ENCRYPTEDSTRING); + propertyB.setDescription("qux-b"); + qm.persist(propertyB); + + final Response response = target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() + .header(X_API_KEY, apiKey) + .get(); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); + assertThatJson(getPlainTextBody(response)).isEqualTo(""" + [ + { + "groupName": "foo-a", + "propertyName": "bar-a", + "propertyValue": "baz-a", + "propertyType": "STRING", + "description": "qux-a" + }, + { + "groupName": "foo-b", + "propertyName": "bar-b", + "propertyValue": "HiddenDecryptedPropertyPlaceholder", + "propertyType": "ENCRYPTEDSTRING", + "description": "qux-b" + } + ] + """); + } + + @Test + public void getPropertiesInvalidTest() { + final Response response = target("%s/%s/property".formatted(V1_COMPONENT, UUID.randomUUID())).request() + .header(X_API_KEY, apiKey) + .get(Response.class); + + assertThat(response.getStatus()).isEqualTo(404); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); + assertThat(getPlainTextBody(response)).isEqualTo("The component could not be found."); + } + + @Test + public void createPropertyTest() { + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Response response = target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "groupName": "foo", + "propertyName": "bar", + "propertyValue": "baz", + "propertyType": "STRING", + "description": "qux" + } + """, MediaType.APPLICATION_JSON)); + + assertThat(response.getStatus()).isEqualTo(201); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); + assertThatJson(getPlainTextBody(response)).isEqualTo(""" + { + "groupName": "foo", + "propertyName": "bar", + "propertyValue": "baz", + "propertyType": "STRING", + "description": "qux" + } + """); + } + + @Test + public void createPropertyEncryptedTest() { + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Response response = target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "groupName": "foo", + "propertyName": "bar", + "propertyValue": "baz", + "propertyType": "ENCRYPTEDSTRING", + "description": "qux" + } + """, MediaType.APPLICATION_JSON)); + + assertThat(response.getStatus()).isEqualTo(201); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); + assertThatJson(getPlainTextBody(response)).isEqualTo(""" + { + "groupName": "foo", + "propertyName": "bar", + "propertyValue": "HiddenDecryptedPropertyPlaceholder", + "propertyType": "ENCRYPTEDSTRING", + "description": "qux" + } + """); + } + + @Test + public void createPropertyDuplicateTest() { + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final var property = new ComponentProperty(); + property.setComponent(component); + property.setGroupName("foo"); + property.setPropertyName("bar"); + property.setPropertyValue("baz"); + property.setPropertyType(PropertyType.STRING); + qm.persist(property); + + final Response response = target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "groupName": "foo", + "propertyName": "bar", + "propertyValue": "baz", + "propertyType": "STRING", + "description": "qux" + } + """, MediaType.APPLICATION_JSON)); + + assertThat(response.getStatus()).isEqualTo(409); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); + assertThat(getPlainTextBody(response)).isEqualTo(""" + A property with the specified component/group/name combination already exists."""); + } + + @Test + public void createPropertyInvalidTest() { + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Response response = target("%s/%s/property".formatted(V1_COMPONENT, UUID.randomUUID())).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(""" + { + "groupName": "foo", + "propertyName": "bar", + "propertyValue": "baz", + "propertyType": "STRING", + "description": "qux" + } + """, MediaType.APPLICATION_JSON)); + + assertThat(response.getStatus()).isEqualTo(404); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); + assertThat(getPlainTextBody(response)).isEqualTo("The component could not be found."); + } + + @Test + public void updatePropertyTest() { + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final var property = new ComponentProperty(); + property.setComponent(component); + property.setGroupName("foo"); + property.setPropertyName("bar"); + property.setPropertyValue("baz"); + property.setPropertyType(PropertyType.STRING); + qm.persist(property); + + final Response response = target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() + .header(X_API_KEY, apiKey) + .post(Entity.entity(""" + { + "groupName": "foo", + "propertyName": "bar", + "propertyValue": "qux" + } + """, MediaType.APPLICATION_JSON)); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); + assertThatJson(getPlainTextBody(response)).isEqualTo(""" + { + "groupName": "foo", + "propertyName": "bar", + "propertyValue": "qux", + "propertyType": "STRING" + } + """); + } + + @Test + public void updatePropertyInvalidTest() { + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final var property = new ComponentProperty(); + property.setComponent(component); + property.setGroupName("foo"); + property.setPropertyName("bar"); + property.setPropertyValue("baz"); + property.setPropertyType(PropertyType.STRING); + qm.persist(property); + + final Response response = target("%s/%s/property".formatted(V1_COMPONENT, UUID.randomUUID())).request() + .header(X_API_KEY, apiKey) + .post(Entity.entity(""" + { + "groupName": "foo", + "propertyName": "bar", + "propertyValue": "qux" + } + """, MediaType.APPLICATION_JSON)); + + assertThat(response.getStatus()).isEqualTo(404); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); + assertThat(getPlainTextBody(response)).isEqualTo("The component could not be found."); + } + + @Test + public void deletePropertyTest() { + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final var property = new ComponentProperty(); + property.setComponent(component); + property.setGroupName("foo"); + property.setPropertyName("bar"); + property.setPropertyValue("baz"); + property.setPropertyType(PropertyType.STRING); + qm.persist(property); + + final Response response = target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() + .header(X_API_KEY, apiKey) + .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) + .method("DELETE", Entity.entity(""" + { + "groupName": "foo", + "propertyName": "bar" + } + """, MediaType.APPLICATION_JSON)); + + assertThat(response.getStatus()).isEqualTo(204); + assertThat(getPlainTextBody(response)).isEmpty(); + } +} \ No newline at end of file From 0cd433241a82a94b946d363f470d8ffe857a9416 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 26 Feb 2024 23:36:49 +0100 Subject: [PATCH 057/412] Ingest component properties from BOM Signed-off-by: nscuro --- .../parser/cyclonedx/util/ModelConverter.java | 45 +++++++++++++++++++ .../tasks/BomUploadProcessingTaskV2.java | 38 ++++++++++++++++ .../tasks/BomUploadProcessingTaskTest.java | 19 +++++++- src/test/resources/unit/bom-1.xml | 5 +++ 4 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java index a413a30e00..20469f1a58 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -19,6 +19,7 @@ package org.dependencytrack.parser.cyclonedx.util; import alpine.common.logging.Logger; +import alpine.model.IConfigProperty.PropertyType; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; import org.apache.commons.collections4.CollectionUtils; @@ -38,6 +39,7 @@ import org.dependencytrack.model.Classifier; import org.dependencytrack.model.Component; import org.dependencytrack.model.ComponentIdentity; +import org.dependencytrack.model.ComponentProperty; import org.dependencytrack.model.Cwe; import org.dependencytrack.model.DataClassification; import org.dependencytrack.model.ExternalReference; @@ -168,6 +170,7 @@ public static Component convertComponent(final org.cyclonedx.model.Component cdx component.setCopyright(trimToNull(cdxComponent.getCopyright())); component.setCpe(trimToNull(cdxComponent.getCpe())); component.setExternalReferences(convertExternalReferences(cdxComponent.getExternalReferences())); + component.setProperties(convertToComponentProperties(cdxComponent.getProperties())); if (cdxComponent.getPurl() != null) { try { @@ -255,6 +258,48 @@ public static Component convertComponent(final org.cyclonedx.model.Component cdx return component; } + private static List convertToComponentProperties(final List cdxProperties) { + if (cdxProperties == null || cdxProperties.isEmpty()) { + return Collections.emptyList(); + } + + return cdxProperties.stream() + .map(ModelConverter::convertToComponentProperty) + .filter(Objects::nonNull) + .toList(); + } + + private static ComponentProperty convertToComponentProperty(final org.cyclonedx.model.Property cdxProperty) { + if (cdxProperty == null) { + return null; + } + + final var property = new ComponentProperty(); + property.setPropertyValue(trimToNull(cdxProperty.getValue())); + property.setPropertyType(PropertyType.STRING); + + final String cdxPropertyName = trimToNull(cdxProperty.getName()); + if (cdxPropertyName == null) { + // TODO: What to do here? + // * Generate groupName and propertyName? + // * Log a warning and ignore? + return null; + } + + // Treat property names according to the CycloneDX namespace syntax: + // https://cyclonedx.github.io/cyclonedx-property-taxonomy/ + final int lastSeparatorIndex = cdxPropertyName.lastIndexOf(':'); + if (lastSeparatorIndex < 0) { + property.setGroupName("internal"); + property.setPropertyName(cdxPropertyName); + } else { + property.setGroupName(cdxPropertyName.substring(0, lastSeparatorIndex)); + property.setPropertyName(cdxPropertyName.substring(lastSeparatorIndex + 1)); + } + + return property; + } + public static List convertServices(final List cdxServices) { if (cdxServices == null || cdxServices.isEmpty()) { return Collections.emptyList(); diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java index 09bab0f182..c27a01fe2b 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java @@ -41,6 +41,7 @@ import org.dependencytrack.model.Bom; import org.dependencytrack.model.Component; import org.dependencytrack.model.ComponentIdentity; +import org.dependencytrack.model.ComponentProperty; import org.dependencytrack.model.DependencyMetrics; import org.dependencytrack.model.FindingAttribution; import org.dependencytrack.model.License; @@ -74,6 +75,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -436,12 +438,15 @@ private Map processComponents(final QueryManager q applyIfChanged(persistentComponent, component, Component::getResolvedLicense, persistentComponent::setResolvedLicense); applyIfChanged(persistentComponent, component, Component::getLicense, persistentComponent::setLicense); applyIfChanged(persistentComponent, component, Component::getLicenseUrl, persistentComponent::setLicenseUrl); + applyIfChanged(persistentComponent, component, Component::getLicenseExpression, persistentComponent::setLicenseExpression); applyIfChanged(persistentComponent, component, Component::isInternal, persistentComponent::setInternal); applyIfChanged(persistentComponent, component, Component::getExternalReferences, persistentComponent::setExternalReferences); idsOfComponentsToDelete.remove(persistentComponent.getId()); } + processComponentProperties(qm, persistentComponent, component.getProperties()); + // Update component identities in our Identity->BOMRef map, // as after persisting the components, their identities now include UUIDs. final var newIdentity = new ComponentIdentity(persistentComponent); @@ -463,6 +468,39 @@ private Map processComponents(final QueryManager q return persistentComponents; } + private void processComponentProperties(final QueryManager qm, final Component component, final List properties) { + if (properties == null || properties.isEmpty()) { + // TODO: Should we delete pre-existing properties that no longer exist in the BOM? + return; + } + + if (component.getProperties() == null || component.getProperties().isEmpty()) { + for (final ComponentProperty property : properties) { + property.setComponent(component); + qm.getPersistenceManager().makePersistent(property); + } + + return; + } + + for (final ComponentProperty property : component.getProperties()) { + final Optional optionalPersistentProperty = component.getProperties().stream() + .filter(persistentProperty -> Objects.equals(persistentProperty.getGroupName(), property.getGroupName())) + .filter(persistentProperty -> Objects.equals(persistentProperty.getPropertyName(), property.getPropertyName())) + .findFirst(); + if (optionalPersistentProperty.isEmpty()) { + property.setComponent(component); + qm.getPersistenceManager().makePersistent(property); + continue; + } + + final ComponentProperty persistentProperty = optionalPersistentProperty.get(); + applyIfChanged(persistentProperty, property, ComponentProperty::getPropertyValue, persistentProperty::setPropertyValue); + applyIfChanged(persistentProperty, property, ComponentProperty::getPropertyType, persistentProperty::setPropertyType); + applyIfChanged(persistentProperty, property, ComponentProperty::getDescription, persistentProperty::setDescription); + } + } + private Map processServices(final QueryManager qm, final Project project, final List services, diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index 2b3ed2cf83..bf7e5b0192 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -20,6 +20,7 @@ import alpine.event.framework.Event; import alpine.event.framework.EventService; +import alpine.model.IConfigProperty.PropertyType; import alpine.notification.Notification; import alpine.notification.NotificationLevel; import alpine.notification.NotificationService; @@ -177,7 +178,7 @@ public void informTest() throws Exception { final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-1.xml")); bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); - assertConditionWithTimeout(() -> NOTIFICATIONS.size() >= 6, Duration.ofSeconds(5)); + awaitBomProcessedNotification(); qm.getPersistenceManager().refresh(project); assertThat(project.getClassifier()).isEqualTo(Classifier.APPLICATION); @@ -237,6 +238,22 @@ public void informTest() throws Exception { assertThat(component.getCpe()).isEqualTo("cpe:/a:example:xmlutil:1.0.0"); assertThat(component.getPurl().canonicalize()).isEqualTo("pkg:maven/com.example/xmlutil@1.0.0?packaging=jar"); assertThat(component.getLicenseUrl()).isEqualTo("https://www.apache.org/licenses/LICENSE-2.0.txt"); + assertThat(component.getProperties()).satisfiesExactly( + property -> { + assertThat(property.getGroupName()).isEqualTo("foo"); + assertThat(property.getPropertyName()).isEqualTo("bar"); + assertThat(property.getPropertyValue()).isEqualTo("baz"); + assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); + assertThat(property.getDescription()).isNull(); + }, + property -> { + assertThat(property.getGroupName()).isEqualTo("internal"); + assertThat(property.getPropertyName()).isEqualTo("foo"); + assertThat(property.getPropertyValue()).isEqualTo("bar"); + assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); + assertThat(property.getDescription()).isNull(); + } + ); assertThat(qm.getAllVulnerabilities(component)).hasSize(2); assertThat(NOTIFICATIONS).satisfiesExactly( diff --git a/src/test/resources/unit/bom-1.xml b/src/test/resources/unit/bom-1.xml index f647f67487..f64a234bc2 100644 --- a/src/test/resources/unit/bom-1.xml +++ b/src/test/resources/unit/bom-1.xml @@ -86,6 +86,11 @@ cpe:/a:example:xmlutil:1.0.0 pkg:maven/com.example/xmlutil@1.0.0?packaging=jar false + + foo + bar + baz + From 5164bb88701774838edcc3662589782d279060e9 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 26 Feb 2024 23:43:37 +0100 Subject: [PATCH 058/412] Update Trivy IT to use OS component and properties Relates to #3369 Signed-off-by: nscuro --- .../TrivyAnalysisTaskIntegrationTest.java | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java b/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java index 90f2662e97..e2261d8a15 100644 --- a/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java +++ b/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java @@ -18,6 +18,7 @@ */ package org.dependencytrack.tasks.scanners; +import alpine.model.IConfigProperty; import alpine.security.crypto.DataEncryption; import org.dependencytrack.PersistenceCapableTest; import org.dependencytrack.event.TrivyAnalysisEvent; @@ -177,19 +178,30 @@ public void testWithUnrecognizedPackageName() { final var project = new Project(); project.setName("acme-app"); qm.persist(project); - - final var componentA = new Component(); - componentA.setProject(project); - componentA.setName("libc6"); - componentA.setVersion("2.35-0ubuntu3.4"); - componentA.setClassifier(Classifier.LIBRARY); - componentA.setPurl("pkg:deb/ubuntu/libc6@2.35-0ubuntu3.4?arch=amd64&distro=ubuntu-22.04"); - qm.persist(componentA); - - final var analysisEvent = new TrivyAnalysisEvent(List.of(componentA)); + + final var osComponent = new Component(); + osComponent.setProject(project); + osComponent.setName("ubuntu"); + osComponent.setVersion("22.04"); + osComponent.setClassifier(Classifier.OPERATING_SYSTEM); + qm.persist(osComponent); + + final var component = new Component(); + component.setProject(project); + component.setName("libc6"); + component.setVersion("2.35-0ubuntu3.4"); + component.setClassifier(Classifier.LIBRARY); + component.setPurl("pkg:deb/ubuntu/libc6@2.35-0ubuntu3.4?arch=amd64&distro=ubuntu-22.04"); + qm.persist(component); + + qm.createComponentProperty(component, "aquasecurity:trivy", "SrcName", "glibc", IConfigProperty.PropertyType.STRING, null); + qm.createComponentProperty(component, "aquasecurity:trivy", "SrcVersion", "2.35", IConfigProperty.PropertyType.STRING, null); + qm.createComponentProperty(component, "aquasecurity:trivy", "SrcRelease", "0ubuntu3.4", IConfigProperty.PropertyType.STRING, null); + + final var analysisEvent = new TrivyAnalysisEvent(List.of(osComponent, component)); new TrivyAnalysisTask().inform(analysisEvent); - assertThat(qm.getAllVulnerabilities(componentA)).isEmpty(); + assertThat(qm.getAllVulnerabilities(component)).isEmpty(); } } From 9bc453c107b56df2e9e12373290917a7d83c4c25 Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 27 Feb 2024 09:42:56 +0100 Subject: [PATCH 059/412] Skip property assertions for legacy BOM processing task Signed-off-by: nscuro --- .../tasks/BomUploadProcessingTaskTest.java | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index bf7e5b0192..0989d8a616 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -96,17 +96,20 @@ public void inform(final Notification notification) { private static final ConcurrentLinkedQueue EVENTS = new ConcurrentLinkedQueue<>(); private static final ConcurrentLinkedQueue NOTIFICATIONS = new ConcurrentLinkedQueue<>(); - @Parameterized.Parameters + @Parameterized.Parameters(name = "{index}: {0}") public static Collection data() { return Arrays.asList(new Object[][]{ - {(Supplier) BomUploadProcessingTask::new}, - {(Supplier) BomUploadProcessingTaskV2::new} + {BomUploadProcessingTask.class.getSimpleName(), (Supplier) BomUploadProcessingTask::new}, + {BomUploadProcessingTaskV2.class.getSimpleName(), (Supplier) BomUploadProcessingTaskV2::new} }); } + private final String bomUploadProcessingTaskName; private final Supplier bomUploadProcessingTaskSupplier; - public BomUploadProcessingTaskTest(final Supplier bomUploadProcessingTaskSupplier) { + public BomUploadProcessingTaskTest(final String bomUploadProcessingTaskName, + final Supplier bomUploadProcessingTaskSupplier) { + this.bomUploadProcessingTaskName = bomUploadProcessingTaskName; this.bomUploadProcessingTaskSupplier = bomUploadProcessingTaskSupplier; } @@ -238,22 +241,26 @@ public void informTest() throws Exception { assertThat(component.getCpe()).isEqualTo("cpe:/a:example:xmlutil:1.0.0"); assertThat(component.getPurl().canonicalize()).isEqualTo("pkg:maven/com.example/xmlutil@1.0.0?packaging=jar"); assertThat(component.getLicenseUrl()).isEqualTo("https://www.apache.org/licenses/LICENSE-2.0.txt"); - assertThat(component.getProperties()).satisfiesExactly( - property -> { - assertThat(property.getGroupName()).isEqualTo("foo"); - assertThat(property.getPropertyName()).isEqualTo("bar"); - assertThat(property.getPropertyValue()).isEqualTo("baz"); - assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); - assertThat(property.getDescription()).isNull(); - }, - property -> { - assertThat(property.getGroupName()).isEqualTo("internal"); - assertThat(property.getPropertyName()).isEqualTo("foo"); - assertThat(property.getPropertyValue()).isEqualTo("bar"); - assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); - assertThat(property.getDescription()).isNull(); - } - ); + + // TODO: Implement for legacy version of the task. + if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTaskV2) { + assertThat(component.getProperties()).satisfiesExactly( + property -> { + assertThat(property.getGroupName()).isEqualTo("foo"); + assertThat(property.getPropertyName()).isEqualTo("bar"); + assertThat(property.getPropertyValue()).isEqualTo("baz"); + assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); + assertThat(property.getDescription()).isNull(); + }, + property -> { + assertThat(property.getGroupName()).isEqualTo("internal"); + assertThat(property.getPropertyName()).isEqualTo("foo"); + assertThat(property.getPropertyValue()).isEqualTo("bar"); + assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); + assertThat(property.getDescription()).isNull(); + } + ); + } assertThat(qm.getAllVulnerabilities(component)).hasSize(2); assertThat(NOTIFICATIONS).satisfiesExactly( @@ -263,12 +270,12 @@ public void informTest() throws Exception { n -> { assertThat(n.getGroup()).isEqualTo(NotificationGroup.NEW_VULNERABILITY.name()); NewVulnerabilityIdentified nvi = (NewVulnerabilityIdentified) n.getSubject(); - assertThat(nvi.getVulnerabilityAnalysisLevel().equals(VulnerabilityAnalysisLevel.BOM_UPLOAD_ANALYSIS)); + assertThat(nvi.getVulnerabilityAnalysisLevel()).isEqualTo(VulnerabilityAnalysisLevel.BOM_UPLOAD_ANALYSIS); }, n -> { assertThat(n.getGroup()).isEqualTo(NotificationGroup.NEW_VULNERABILITY.name()); NewVulnerabilityIdentified nvi = (NewVulnerabilityIdentified) n.getSubject(); - assertThat(nvi.getVulnerabilityAnalysisLevel().toString().equals(VulnerabilityAnalysisLevel.BOM_UPLOAD_ANALYSIS)); + assertThat(nvi.getVulnerabilityAnalysisLevel()).isEqualTo(VulnerabilityAnalysisLevel.BOM_UPLOAD_ANALYSIS); }, n -> assertThat(n.getGroup()).isEqualTo(NotificationGroup.NEW_VULNERABLE_DEPENDENCY.name()) ); From 57cf065f2d16c5b46bfae420e6a529abbeb59ba6 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sat, 16 Mar 2024 14:02:46 +0100 Subject: [PATCH 060/412] Include project and component properties in CycloneDX exports Signed-off-by: nscuro --- .../parser/cyclonedx/util/ModelConverter.java | 21 ++++++++++- .../resources/v1/BomResourceTest.java | 37 ++++++++++++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java index 20469f1a58..4931162af5 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -19,6 +19,7 @@ package org.dependencytrack.parser.cyclonedx.util; import alpine.common.logging.Logger; +import alpine.model.IConfigProperty; import alpine.model.IConfigProperty.PropertyType; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; @@ -705,6 +706,7 @@ public static org.cyclonedx.model.Component convert(final QueryManager qm, final cycloneComponent.setCpe(StringUtils.trimToNull(component.getCpe())); cycloneComponent.setAuthor(StringUtils.trimToNull(component.getAuthor())); cycloneComponent.setSupplier(convert(component.getSupplier())); + cycloneComponent.setProperties(convert(component.getProperties())); if (component.getSwidTagId() != null) { final Swid swid = new Swid(); @@ -766,7 +768,7 @@ public static org.cyclonedx.model.Component convert(final QueryManager qm, final } - if (component.getExternalReferences() != null && component.getExternalReferences().size() > 0) { + if (component.getExternalReferences() != null && !component.getExternalReferences().isEmpty()) { List references = new ArrayList<>(); for (ExternalReference ref: component.getExternalReferences()) { org.cyclonedx.model.ExternalReference cdxRef = new org.cyclonedx.model.ExternalReference(); @@ -799,6 +801,22 @@ public static org.cyclonedx.model.Component convert(final QueryManager qm, final return cycloneComponent; } + private static List convert(final Collection dtProperties) { + if (dtProperties == null || dtProperties.isEmpty()) { + return Collections.emptyList(); + } + + final List cdxProperties = new ArrayList<>(); + for (final T dtProperty : dtProperties) { + final var cdxProperty = new org.cyclonedx.model.Property(); + cdxProperty.setName("%s:%s".formatted(dtProperty.getGroupName(), dtProperty.getPropertyName())); + cdxProperty.setValue(dtProperty.getPropertyValue()); + cdxProperties.add(cdxProperty); + } + + return cdxProperties; + } + public static org.cyclonedx.model.Metadata createMetadata(final Project project) { final org.cyclonedx.model.Metadata metadata = new org.cyclonedx.model.Metadata(); final org.cyclonedx.model.Tool tool = new org.cyclonedx.model.Tool(); @@ -849,6 +867,7 @@ public static org.cyclonedx.model.Metadata createMetadata(final Project project) cycloneComponent.setExternalReferences(references); } cycloneComponent.setSupplier(convert(project.getSupplier())); + cycloneComponent.setProperties(convert(project.getProperties())); metadata.setComponent(cycloneComponent); if (project.getMetadata() != null) { diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index 80bb4cc26d..3d4e66518b 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -19,6 +19,7 @@ package org.dependencytrack.resources.v1; import alpine.common.util.UuidUtil; +import alpine.model.IConfigProperty; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import com.fasterxml.jackson.core.StreamReadConstraints; @@ -29,10 +30,12 @@ import org.dependencytrack.model.AnalysisState; import org.dependencytrack.model.Classifier; import org.dependencytrack.model.Component; +import org.dependencytrack.model.ComponentProperty; import org.dependencytrack.model.OrganizationalContact; import org.dependencytrack.model.OrganizationalEntity; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectMetadata; +import org.dependencytrack.model.ProjectProperty; import org.dependencytrack.model.Severity; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.parser.cyclonedx.CycloneDxValidator; @@ -115,6 +118,7 @@ public void exportProjectAsCycloneDxInventoryTest() { projectManufacturer.setName("projectManufacturer"); final var projectSupplier = new OrganizationalEntity(); projectSupplier.setName("projectSupplier"); + var project = new Project(); project.setName("acme-app"); project.setClassifier(Classifier.APPLICATION); @@ -122,6 +126,14 @@ public void exportProjectAsCycloneDxInventoryTest() { project.setSupplier(projectSupplier); project = qm.createProject(project, null, false); + final var projectProperty = new ProjectProperty(); + projectProperty.setProject(project); + projectProperty.setGroupName("foo"); + projectProperty.setPropertyName("bar"); + projectProperty.setPropertyValue("baz"); + projectProperty.setPropertyType(IConfigProperty.PropertyType.STRING); + qm.persist(projectProperty); + final var bomSupplier = new OrganizationalEntity(); bomSupplier.setName("bomSupplier"); final var bomAuthor = new OrganizationalContact(); @@ -134,6 +146,7 @@ public void exportProjectAsCycloneDxInventoryTest() { final var componentSupplier = new OrganizationalEntity(); componentSupplier.setName("componentSupplier"); + var componentWithoutVuln = new Component(); componentWithoutVuln.setProject(project); componentWithoutVuln.setName("acme-lib-a"); @@ -142,6 +155,14 @@ public void exportProjectAsCycloneDxInventoryTest() { componentWithoutVuln.setDirectDependencies("[]"); componentWithoutVuln = qm.createComponent(componentWithoutVuln, false); + final var componentProperty = new ComponentProperty(); + componentProperty.setComponent(componentWithoutVuln); + componentProperty.setGroupName("foo"); + componentProperty.setPropertyName("bar"); + componentProperty.setPropertyValue("baz"); + componentProperty.setPropertyType(IConfigProperty.PropertyType.STRING); + qm.persist(componentProperty); + var componentWithVuln = new Component(); componentWithVuln.setProject(project); componentWithVuln.setName("acme-lib-b"); @@ -214,7 +235,13 @@ public void exportProjectAsCycloneDxInventoryTest() { "name": "projectSupplier" }, "name": "acme-app", - "version": "SNAPSHOT" + "version": "SNAPSHOT", + "properties": [ + { + "name": "foo:bar", + "value": "baz" + } + ] }, "manufacture": { "name": "projectManufacturer" @@ -238,7 +265,13 @@ public void exportProjectAsCycloneDxInventoryTest() { "name": "componentSupplier" }, "name": "acme-lib-a", - "version": "1.0.0" + "version": "1.0.0", + "properties": [ + { + "name": "foo:bar", + "value": "baz" + } + ] }, { "type": "library", From d812964fe8cc32b3e750f0ac675aa023b4e2e010 Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 26 Mar 2024 20:26:55 +0100 Subject: [PATCH 061/412] Apply copyright change Signed-off-by: nscuro --- .../java/org/dependencytrack/model/ComponentProperty.java | 4 +--- .../resources/v1/ComponentPropertyResource.java | 2 +- .../resources/v1/ComponentPropertyResourceTest.java | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/ComponentProperty.java b/src/main/java/org/dependencytrack/model/ComponentProperty.java index ccac4048dc..e56cc41489 100644 --- a/src/main/java/org/dependencytrack/model/ComponentProperty.java +++ b/src/main/java/org/dependencytrack/model/ComponentProperty.java @@ -14,7 +14,7 @@ * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) Steve Springett. All Rights Reserved. + * Copyright (c) OWASP Foundation. All Rights Reserved. */ package org.dependencytrack.model; @@ -29,7 +29,6 @@ import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; -import javax.jdo.annotations.Unique; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; @@ -40,7 +39,6 @@ * @since 4.11.0 */ @PersistenceCapable(table = "COMPONENT_PROPERTY") -@Unique(name = "COMPONENT_PROPERTY_KEYS_IDX", members = {"component", "groupName", "propertyName"}) @JsonInclude(JsonInclude.Include.NON_NULL) public class ComponentProperty implements IConfigProperty, Serializable { diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java index 3a5eaac9e3..d45ef63da5 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java @@ -14,7 +14,7 @@ * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) Steve Springett. All Rights Reserved. + * Copyright (c) OWASP Foundation. All Rights Reserved. */ package org.dependencytrack.resources.v1; diff --git a/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java index 1983ea7de6..f680d492c4 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java @@ -14,7 +14,7 @@ * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) Steve Springett. All Rights Reserved. + * Copyright (c) OWASP Foundation. All Rights Reserved. */ package org.dependencytrack.resources.v1; From d489c2c2a8cd6bd6ef94efaa5595dd38892e50e8 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 1 Apr 2024 17:18:01 +0200 Subject: [PATCH 062/412] Add UUID validation; Add Swagger docs for required permissions Signed-off-by: nscuro --- .../v1/ComponentPropertyResource.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java index d45ef63da5..ce33d372ab 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java @@ -29,6 +29,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Component; import org.dependencytrack.model.ComponentProperty; +import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import javax.validation.Validator; @@ -56,17 +57,18 @@ public class ComponentPropertyResource extends AbstractConfigPropertyResource { @ApiOperation( value = "Returns a list of all ComponentProperties for the specified component", response = ComponentProperty.class, - responseContainer = "List" + responseContainer = "List", + notes = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), @ApiResponse(code = 404, message = "The project could not be found") }) - @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) + @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProperties( - @ApiParam(value = "The UUID of the component to retrieve properties for", required = true) - @PathParam("uuid") String uuid) { + @ApiParam(value = "The UUID of the component to retrieve properties for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Component component = qm.getObjectByUuid(Component.class, uuid); if (component != null) { @@ -98,7 +100,8 @@ public Response getProperties( @ApiOperation( value = "Creates a new component property", response = ComponentProperty.class, - code = 201 + code = 201, + notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -108,8 +111,8 @@ public Response getProperties( }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response createProperty( - @ApiParam(value = "The UUID of the component to create a property for", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the component to create a property for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, ComponentProperty json) { final Validator validator = super.getValidator(); failOnValidationError( @@ -154,7 +157,8 @@ public Response createProperty( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Updates a project property", - response = ComponentProperty.class + response = ComponentProperty.class, + notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -163,8 +167,8 @@ public Response createProperty( }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response updateProperty( - @ApiParam(value = "The UUID of the component to create a property for", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the component to create a property for", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, ComponentProperty json) { final Validator validator = super.getValidator(); failOnValidationError( @@ -196,7 +200,8 @@ public Response updateProperty( @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Deletes a config property", - response = ComponentProperty.class + response = ComponentProperty.class, + notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " ) @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @@ -205,8 +210,8 @@ public Response updateProperty( }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response deleteProperty( - @ApiParam(value = "The UUID of the component to delete a property from", required = true) - @PathParam("uuid") String uuid, + @ApiParam(value = "The UUID of the component to delete a property from", format = "uuid", required = true) + @PathParam("uuid") @ValidUuid String uuid, ComponentProperty json) { final Validator validator = super.getValidator(); failOnValidationError( From 59e8e2244e7ba0fa5523b4ed59ffa5b1f24a18fb Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 8 Apr 2024 23:49:20 +0200 Subject: [PATCH 063/412] Make `groupName` optional for component properties This is primarily to handle CycloneDX properties which do not use namespaces. Signed-off-by: nscuro --- .../model/ComponentProperty.java | 20 +++- .../parser/cyclonedx/util/ModelConverter.java | 26 ++++-- .../tasks/BomUploadProcessingTaskV2.java | 12 ++- .../tasks/BomUploadProcessingTaskTest.java | 93 ++++++++++++++++++- 4 files changed, 134 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/ComponentProperty.java b/src/main/java/org/dependencytrack/model/ComponentProperty.java index e56cc41489..0337c5d568 100644 --- a/src/main/java/org/dependencytrack/model/ComponentProperty.java +++ b/src/main/java/org/dependencytrack/model/ComponentProperty.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.common.base.MoreObjects; import javax.jdo.annotations.Column; import javax.jdo.annotations.IdGeneratorStrategy; @@ -54,8 +55,7 @@ public class ComponentProperty implements IConfigProperty, Serializable { private Component component; @Persistent - @Column(name = "GROUPNAME", allowsNull = "false") - @NotBlank + @Column(name = "GROUPNAME") @Size(min = 1, max = 255) @JsonDeserialize(using = TrimmedStringDeserializer.class) @Pattern(regexp = "\\P{Cc}+", message = "The groupName must not contain control characters") @@ -71,7 +71,7 @@ public class ComponentProperty implements IConfigProperty, Serializable { @Persistent @Column(name = "PROPERTYVALUE", length = 1024) - @Size(min = 0, max = 1024) + @Size(max = 1024) @JsonDeserialize(using = TrimmedStringDeserializer.class) @Pattern(regexp = "\\P{Cc}+", message = "The propertyValue must not contain control characters") private String propertyValue; @@ -144,4 +144,18 @@ public void setDescription(final String description) { this.description = description; } + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("component", component) + .add("groupName", groupName) + .add("propertyName", propertyName) + .add("propertyValue", propertyValue) + .add("propertyType", propertyType) + .add("description", description) + .omitNullValues() + .toString(); + } + } \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java index 4931162af5..1028ca846b 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -281,21 +281,17 @@ private static ComponentProperty convertToComponentProperty(final org.cyclonedx. final String cdxPropertyName = trimToNull(cdxProperty.getName()); if (cdxPropertyName == null) { - // TODO: What to do here? - // * Generate groupName and propertyName? - // * Log a warning and ignore? return null; } // Treat property names according to the CycloneDX namespace syntax: - // https://cyclonedx.github.io/cyclonedx-property-taxonomy/ - final int lastSeparatorIndex = cdxPropertyName.lastIndexOf(':'); - if (lastSeparatorIndex < 0) { - property.setGroupName("internal"); + // https://cyclonedx.github.io/cyclonedx-property-taxonomy/ + final int firstSeparatorIndex = cdxPropertyName.indexOf(':'); + if (firstSeparatorIndex < 0) { property.setPropertyName(cdxPropertyName); } else { - property.setGroupName(cdxPropertyName.substring(0, lastSeparatorIndex)); - property.setPropertyName(cdxPropertyName.substring(lastSeparatorIndex + 1)); + property.setGroupName(cdxPropertyName.substring(0, firstSeparatorIndex)); + property.setPropertyName(cdxPropertyName.substring(firstSeparatorIndex + 1)); } return property; @@ -808,8 +804,18 @@ private static List co final List cdxProperties = new ArrayList<>(); for (final T dtProperty : dtProperties) { + if (dtProperty.getPropertyType() == PropertyType.ENCRYPTEDSTRING) { + // We treat encrypted properties as internal. + // They shall not be leaked when exporting. + continue; + } + final var cdxProperty = new org.cyclonedx.model.Property(); - cdxProperty.setName("%s:%s".formatted(dtProperty.getGroupName(), dtProperty.getPropertyName())); + if (dtProperty.getGroupName() == null) { + cdxProperty.setName(dtProperty.getPropertyName()); + } else { + cdxProperty.setName("%s:%s".formatted(dtProperty.getGroupName(), dtProperty.getPropertyName())); + } cdxProperty.setValue(dtProperty.getPropertyValue()); cdxProperties.add(cdxProperty); } diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java index c27a01fe2b..0ec410a125 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java @@ -470,7 +470,15 @@ private Map processComponents(final QueryManager q private void processComponentProperties(final QueryManager qm, final Component component, final List properties) { if (properties == null || properties.isEmpty()) { - // TODO: Should we delete pre-existing properties that no longer exist in the BOM? + // TODO: We currently remove all existing properties that are no longer included in the BOM. + // This is to stay consistent with the BOM being the source of truth. However, this may feel + // counter-intuitive to some users, who might expect their manual changes to persist. + // If we want to support that, we need a way to track which properties were added and / or + // modified manually. + if (component.getProperties() != null) { + qm.getPersistenceManager().deletePersistentAll(component.getProperties()); + } + return; } @@ -483,7 +491,7 @@ private void processComponentProperties(final QueryManager qm, final Component c return; } - for (final ComponentProperty property : component.getProperties()) { + for (final ComponentProperty property : properties) { final Optional optionalPersistentProperty = component.getProperties().stream() .filter(persistentProperty -> Objects.equals(persistentProperty.getGroupName(), property.getGroupName())) .filter(persistentProperty -> Objects.equals(persistentProperty.getPropertyName(), property.getPropertyName())) diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index 0989d8a616..72e46ec2cd 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -35,6 +35,7 @@ import org.dependencytrack.model.Bom; import org.dependencytrack.model.Classifier; import org.dependencytrack.model.Component; +import org.dependencytrack.model.ComponentProperty; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.License; import org.dependencytrack.model.Project; @@ -244,7 +245,7 @@ public void informTest() throws Exception { // TODO: Implement for legacy version of the task. if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTaskV2) { - assertThat(component.getProperties()).satisfiesExactly( + assertThat(component.getProperties()).satisfiesExactlyInAnyOrder( property -> { assertThat(property.getGroupName()).isEqualTo("foo"); assertThat(property.getPropertyName()).isEqualTo("bar"); @@ -253,7 +254,7 @@ public void informTest() throws Exception { assertThat(property.getDescription()).isNull(); }, property -> { - assertThat(property.getGroupName()).isEqualTo("internal"); + assertThat(property.getGroupName()).isNull(); assertThat(property.getPropertyName()).isEqualTo("foo"); assertThat(property.getPropertyValue()).isEqualTo("bar"); assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); @@ -917,6 +918,94 @@ public void informWithBomContainingServiceTest() throws Exception { assertThat(qm.getAllServiceComponents(project)).isNotEmpty(); } + @Test + public void informWithExistingComponentPropertiesAndBomWithoutComponentProperties() { + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + component.setClassifier(Classifier.LIBRARY); + qm.persist(component); + + final var componentProperty = new ComponentProperty(); + componentProperty.setComponent(component); + componentProperty.setPropertyName("foo"); + componentProperty.setPropertyValue("bar"); + componentProperty.setPropertyType(PropertyType.STRING); + qm.persist(componentProperty); + + final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), """ + { + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "components": [ + { + "type": "library", + "name": "acme-lib" + } + ] + } + """.getBytes()); + bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + awaitBomProcessedNotification(); + + qm.getPersistenceManager().refresh(component); + assertThat(component.getProperties()).isEmpty(); + } + + @Test + public void informWithExistingComponentPropertiesAndBomWithComponentProperties() { + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + component.setClassifier(Classifier.LIBRARY); + qm.persist(component); + + final var componentProperty = new ComponentProperty(); + componentProperty.setComponent(component); + componentProperty.setPropertyName("foo"); + componentProperty.setPropertyValue("bar"); + componentProperty.setPropertyType(PropertyType.STRING); + qm.persist(componentProperty); + + final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), """ + { + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "components": [ + { + "type": "library", + "name": "acme-lib", + "properties": [ + { + "name": "foo", + "value": "baz" + } + ] + } + ] + } + """.getBytes()); + bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + awaitBomProcessedNotification(); + + qm.getPersistenceManager().refresh(componentProperty); + assertThat(componentProperty.getGroupName()).isNull(); + assertThat(componentProperty.getPropertyName()).isEqualTo("foo"); + assertThat(componentProperty.getPropertyValue()).isEqualTo("baz"); + } + @Test // https://github.com/DependencyTrack/dependency-track/issues/1905 public void informIssue1905Test() throws Exception { // Known to now work with old task implementation. From 70b02e96a3e5a79086c307c128a98c8f824e162e Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 14 Apr 2024 18:13:25 +0200 Subject: [PATCH 064/412] Handle duplicate component properties This extends the identity of a `ComponentProperty` to also include its value. As a consequence, encrypted values will not be supported. In order to support duplicate `groupName` / `propertyValue` pairs, the `ComponentProperty` class now has a separate `uuid` field in order to still be able to address individual properties via REST API (e.g. for deletion operations). It is no longer possible to update a `ComponentProperty` via REST API. Uniqueness of properties is now enforced across `groupName`, `propertyName`, *and* `propertyValue`. Signed-off-by: nscuro --- docs/_posts/2024-xx-xx-v4.11.0.md | 2 + .../org/dependencytrack/model/Component.java | 2 +- .../model/ComponentProperty.java | 33 ++++ .../model/validation/EnumValue.java | 43 ++++++ .../model/validation/EnumValueValidator.java | 47 ++++++ .../persistence/ComponentQueryManager.java | 67 ++++---- .../persistence/QueryManager.java | 8 +- .../v1/ComponentPropertyResource.java | 81 ++-------- .../tasks/BomUploadProcessingTaskV2.java | 39 +++-- .../v1/ComponentPropertyResourceTest.java | 145 +++++++----------- .../tasks/BomUploadProcessingTaskTest.java | 26 +++- 11 files changed, 275 insertions(+), 218 deletions(-) create mode 100644 src/main/java/org/dependencytrack/model/validation/EnumValue.java create mode 100644 src/main/java/org/dependencytrack/model/validation/EnumValueValidator.java diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index bd45f4ae99..a2d9deb450 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -55,6 +55,7 @@ Community input and contributions are explicitly requested. The chart repository * Add attribution notice to NVD documentation - [apiserver/#3490] * Bump CWE dictionary to v4.13 - [apiserver/#3491] * Align retry configuration and behavior across analyzers - [apiserver/#3494] +* Add support for component properties - [apiserver/#3499] * Add auto-generated changelog to GitHub releases - [apiserver/#3502] * Bump SPDX license list to v3.23 - [apiserver/#3508] * Validate uploaded BOMs against CycloneDX schema prior to processing them - [apiserver/#3522] @@ -209,6 +210,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [apiserver/#3490]: https://github.com/DependencyTrack/dependency-track/pull/3490 [apiserver/#3491]: https://github.com/DependencyTrack/dependency-track/pull/3491 [apiserver/#3494]: https://github.com/DependencyTrack/dependency-track/pull/3494 +[apiserver/#3499]: https://github.com/DependencyTrack/dependency-track/pull/3499 [apiserver/#3502]: https://github.com/DependencyTrack/dependency-track/pull/3502 [apiserver/#3508]: https://github.com/DependencyTrack/dependency-track/pull/3508 [apiserver/#3511]: https://github.com/DependencyTrack/dependency-track/pull/3511 diff --git a/src/main/java/org/dependencytrack/model/Component.java b/src/main/java/org/dependencytrack/model/Component.java index 7396382f53..cd4f92327a 100644 --- a/src/main/java/org/dependencytrack/model/Component.java +++ b/src/main/java/org/dependencytrack/model/Component.java @@ -332,7 +332,7 @@ public enum FetchGroup { private Collection children; @Persistent(mappedBy = "component", defaultFetchGroup = "false") - @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "groupName ASC, propertyName ASC")) + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "groupName ASC, propertyName ASC, id ASC")) private List properties; @Persistent(table = "COMPONENTS_VULNERABILITIES") diff --git a/src/main/java/org/dependencytrack/model/ComponentProperty.java b/src/main/java/org/dependencytrack/model/ComponentProperty.java index 0337c5d568..15ed1a123e 100644 --- a/src/main/java/org/dependencytrack/model/ComponentProperty.java +++ b/src/main/java/org/dependencytrack/model/ComponentProperty.java @@ -24,17 +24,20 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.common.base.MoreObjects; +import org.dependencytrack.model.validation.EnumValue; import javax.jdo.annotations.Column; import javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; +import javax.jdo.annotations.Unique; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; import java.io.Serializable; +import java.util.UUID; /** * @since 4.11.0 @@ -45,6 +48,14 @@ public class ComponentProperty implements IConfigProperty, Serializable { private static final long serialVersionUID = -7510889645969713080L; + public record Identity(String group, String name, String value) { + + public Identity(final ComponentProperty property) { + this(property.getGroupName(), property.getPropertyName(), property.getPropertyValue()); + } + + } + @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) @JsonIgnore @@ -79,6 +90,13 @@ public class ComponentProperty implements IConfigProperty, Serializable { @Persistent @Column(name = "PROPERTYTYPE", jdbcType = "VARCHAR", allowsNull = "false") @NotNull + // NB: Encrypted values are disallowed because it complicates identity management. + // Because duplicate groupName/propertyName combinations are allowed, the value + // is critical to determine property uniqueness. We'd need to decrypt encrypted + // values prior to uniqueness checks. We'd also open the door for attackers to + // guess the encrypted value. As of now, there is no known use-case for encrypted + // properties on the component level. + @EnumValue(disallowed = "ENCRYPTEDSTRING", message = "Encrypted component property values are not supported") private PropertyType propertyType; @Persistent @@ -88,6 +106,12 @@ public class ComponentProperty implements IConfigProperty, Serializable { @Pattern(regexp = "\\P{Cc}+", message = "The description must not contain control characters") private String description; + @Persistent(customValueStrategy = "uuid") + @Unique(name = "COMPONENT_PROPERTY_UUID_IDX") + @Column(name = "UUID", jdbcType = "VARCHAR", length = 36, allowsNull = "false") + @NotNull + private UUID uuid; + public long getId() { return id; } @@ -144,6 +168,14 @@ public void setDescription(final String description) { this.description = description; } + public UUID getUuid() { + return uuid; + } + + public void setUuid(final UUID uuid) { + this.uuid = uuid; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) @@ -154,6 +186,7 @@ public String toString() { .add("propertyValue", propertyValue) .add("propertyType", propertyType) .add("description", description) + .add("uuid", uuid) .omitNullValues() .toString(); } diff --git a/src/main/java/org/dependencytrack/model/validation/EnumValue.java b/src/main/java/org/dependencytrack/model/validation/EnumValue.java new file mode 100644 index 0000000000..5e3a37a454 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/validation/EnumValue.java @@ -0,0 +1,43 @@ +/* + * 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.model.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @since 4.11.0 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Constraint(validatedBy = EnumValueValidator.class) +public @interface EnumValue { + + String[] disallowed() default {}; + String message() default ""; + Class[] groups() default {}; + Class[] payload() default {}; + +} diff --git a/src/main/java/org/dependencytrack/model/validation/EnumValueValidator.java b/src/main/java/org/dependencytrack/model/validation/EnumValueValidator.java new file mode 100644 index 0000000000..25e4d466a3 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/validation/EnumValueValidator.java @@ -0,0 +1,47 @@ +/* + * 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.model.validation; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.Arrays; +import java.util.Set; + +/** + * @since 4.11.0 + */ +public class EnumValueValidator implements ConstraintValidator> { + + private Set disallowedValues; + + @Override + public void initialize(final EnumValue constraintAnnotation) { + disallowedValues = Set.copyOf(Arrays.asList(constraintAnnotation.disallowed())); + } + + @Override + public boolean isValid(final Enum value, final ConstraintValidatorContext context) { + if (value == null) { + return true; + } + + return !disallowedValues.contains(value.name()); + } + +} diff --git a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java index e4e215614d..e37b2f62b1 100644 --- a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java @@ -833,49 +833,35 @@ private void getDirectDependenciesForPathDependencies(Map dep dependencyGraph.putAll(addToDependencyGraph); } - /** - * Returns a ComponentProperty with the specified groupName and propertyName. - * - * @param component the component the property belongs to - * @param groupName the group name of the config property - * @param propertyName the name of the property - * @return a ComponentProperty object - */ @Override - public ComponentProperty getComponentProperty(final Component component, final String groupName, final String propertyName) { - final Query query = this.pm.newQuery(ComponentProperty.class, "component == :component && groupName == :groupName && propertyName == :propertyName"); - query.setRange(0, 1); - return singleResult(query.execute(component, groupName, propertyName)); + public List getComponentProperties(final Component component, final String groupName, final String propertyName) { + final Query query = pm.newQuery(ComponentProperty.class); + query.setFilter("component == :component && groupName == :groupName && propertyName == :propertyName"); + query.setParameters(component, groupName, propertyName); + try { + return List.copyOf(query.executeList()); + } finally { + query.closeAll(); + } } - /** - * Returns a List of ProjectProperty's for the specified project. - * - * @param component the project the property belongs to - * @return a List ProjectProperty objects - */ @Override - @SuppressWarnings("unchecked") public List getComponentProperties(final Component component) { - final Query query = this.pm.newQuery(ComponentProperty.class, "component == :component"); - query.setOrdering("groupName asc, propertyName asc"); - return (List) query.execute(component); + final Query query = pm.newQuery(ComponentProperty.class, "component == :component"); + query.setOrdering("groupName ASC, propertyName ASC, id ASC"); + try { + return List.copyOf(query.executeList()); + } finally { + query.closeAll(); + } } - /** - * Creates a key/value pair (ComponentProperty) for the specified Project. - * - * @param component the Component to create the property for - * @param groupName the group name of the property - * @param propertyName the name of the property - * @param propertyValue the value of the property - * @param propertyType the type of property - * @param description a description of the property - * @return the created ComponentProperty object - */ @Override - public ComponentProperty createComponentProperty(final Component component, final String groupName, final String propertyName, - final String propertyValue, final IConfigProperty.PropertyType propertyType, + public ComponentProperty createComponentProperty(final Component component, + final String groupName, + final String propertyName, + final String propertyValue, + final IConfigProperty.PropertyType propertyType, final String description) { final ComponentProperty property = new ComponentProperty(); property.setComponent(component); @@ -887,4 +873,15 @@ public ComponentProperty createComponentProperty(final Component component, fina return persist(property); } + @Override + public long deleteComponentPropertyByUuid(final Component component, final UUID uuid) { + final Query query = pm.newQuery(ComponentProperty.class); + query.setFilter("component == :component && uuid == :uuid"); + try { + return query.deletePersistentAll(component, uuid); + } finally { + query.closeAll(); + } + } + } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 358438c2c0..7fc1516a4b 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -563,8 +563,8 @@ public List getComponentProperties(final Component component) return getComponentQueryManager().getComponentProperties(component); } - public ComponentProperty getComponentProperty(final Component component, final String groupName, final String propertyName) { - return getComponentQueryManager().getComponentProperty(component, groupName, propertyName); + public List getComponentProperties(final Component component, final String groupName, final String propertyName) { + return getComponentQueryManager().getComponentProperties(component, groupName, propertyName); } public ComponentProperty createComponentProperty(final Component component, final String groupName, final String propertyName, @@ -574,6 +574,10 @@ public ComponentProperty createComponentProperty(final Component component, fina .createComponentProperty(component, groupName, propertyName, propertyValue, propertyType, description); } + public long deleteComponentPropertyByUuid(final Component component, final UUID uuid) { + return getComponentQueryManager().deleteComponentPropertyByUuid(component, uuid); + } + public PaginatedResult getLicenses() { return getLicenseQueryManager().getLicenses(); } diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java index ce33d372ab..9a90a8c27c 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java @@ -36,7 +36,6 @@ import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; -import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; @@ -44,6 +43,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.List; +import java.util.UUID; /** * @since 4.11.0 @@ -118,15 +118,20 @@ public Response createProperty( failOnValidationError( validator.validateProperty(json, "groupName"), validator.validateProperty(json, "propertyName"), - validator.validateProperty(json, "propertyValue") + validator.validateProperty(json, "propertyValue"), + validator.validateProperty(json, "propertyType") ); try (QueryManager qm = new QueryManager()) { final Component component = qm.getObjectByUuid(Component.class, uuid); if (component != null) { if (qm.hasAccess(super.getPrincipal(), component.getProject())) { - final ComponentProperty existing = qm.getComponentProperty(component, + final List existingProperties = qm.getComponentProperties(component, StringUtils.trimToNull(json.getGroupName()), StringUtils.trimToNull(json.getPropertyName())); - if (existing == null) { + final var jsonPropertyIdentity = new ComponentProperty.Identity(json); + final boolean isDuplicate = existingProperties.stream() + .map(ComponentProperty.Identity::new) + .anyMatch(jsonPropertyIdentity::equals); + if (existingProperties.isEmpty() || !isDuplicate) { final ComponentProperty property = qm.createComponentProperty(component, StringUtils.trimToNull(json.getGroupName()), StringUtils.trimToNull(json.getPropertyName()), @@ -134,57 +139,9 @@ public Response createProperty( json.getPropertyType(), StringUtils.trimToNull(json.getDescription())); updatePropertyValue(qm, json, property); - qm.getPersistenceManager().detachCopy(component); - qm.close(); - if (ComponentProperty.PropertyType.ENCRYPTEDSTRING == property.getPropertyType()) { - property.setPropertyValue(ENCRYPTED_PLACEHOLDER); - } return Response.status(Response.Status.CREATED).entity(property).build(); } else { - return Response.status(Response.Status.CONFLICT).entity("A property with the specified component/group/name combination already exists.").build(); - } - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); - } - } else { - return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); - } - } - } - - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Updates a project property", - response = ComponentProperty.class, - notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " - ) - @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The component could not be found"), - }) - @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) - public Response updateProperty( - @ApiParam(value = "The UUID of the component to create a property for", format = "uuid", required = true) - @PathParam("uuid") @ValidUuid String uuid, - ComponentProperty json) { - final Validator validator = super.getValidator(); - failOnValidationError( - validator.validateProperty(json, "groupName"), - validator.validateProperty(json, "propertyName"), - validator.validateProperty(json, "propertyValue") - ); - try (QueryManager qm = new QueryManager()) { - final Component component = qm.getObjectByUuid(Component.class, uuid); - if (component != null) { - if (qm.hasAccess(super.getPrincipal(), component.getProject())) { - final ComponentProperty property = qm.getComponentProperty(component, json.getGroupName(), json.getPropertyName()); - if (property != null) { - return updatePropertyValue(qm, json, property); - } else { - return Response.status(Response.Status.NOT_FOUND).entity("A property with the specified component/group/name combination could not be found.").build(); + return Response.status(Response.Status.CONFLICT).entity("A property with the specified component/group/name/value combination already exists.").build(); } } else { return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); @@ -196,6 +153,7 @@ public Response updateProperty( } @DELETE + @Path("/{propertyUuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @ApiOperation( @@ -211,20 +169,15 @@ public Response updateProperty( @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response deleteProperty( @ApiParam(value = "The UUID of the component to delete a property from", format = "uuid", required = true) - @PathParam("uuid") @ValidUuid String uuid, - ComponentProperty json) { - final Validator validator = super.getValidator(); - failOnValidationError( - validator.validateProperty(json, "groupName"), - validator.validateProperty(json, "propertyName") - ); + @PathParam("uuid") @ValidUuid final String componentUuid, + @ApiParam(value = "The UUID of the component property to delete", format = "uuid", required = true) + @PathParam("propertyUuid") @ValidUuid final String propertyUuid) { try (QueryManager qm = new QueryManager()) { - final Component component = qm.getObjectByUuid(Component.class, uuid); + final Component component = qm.getObjectByUuid(Component.class, componentUuid); if (component != null) { if (qm.hasAccess(super.getPrincipal(), component.getProject())) { - final ComponentProperty property = qm.getComponentProperty(component, json.getGroupName(), json.getPropertyName()); - if (property != null) { - qm.delete(property); + final long propertiesDeleted = qm.deleteComponentPropertyByUuid(component, UUID.fromString(propertyUuid)); + if (propertiesDeleted > 0) { return Response.status(Response.Status.NO_CONTENT).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The component property could not be found.").build(); diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java index 0ec410a125..e4027341c0 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java @@ -75,11 +75,12 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.trim; @@ -491,21 +492,27 @@ private void processComponentProperties(final QueryManager qm, final Component c return; } - for (final ComponentProperty property : properties) { - final Optional optionalPersistentProperty = component.getProperties().stream() - .filter(persistentProperty -> Objects.equals(persistentProperty.getGroupName(), property.getGroupName())) - .filter(persistentProperty -> Objects.equals(persistentProperty.getPropertyName(), property.getPropertyName())) - .findFirst(); - if (optionalPersistentProperty.isEmpty()) { - property.setComponent(component); - qm.getPersistenceManager().makePersistent(property); - continue; - } + // Group properties by group, name, and value. Because CycloneDX supports duplicate + // property names, uniqueness can only be determined by also considering the value. + final var existingPropertiesByIdentity = component.getProperties().stream() + .collect(Collectors.toMap(ComponentProperty.Identity::new, Function.identity())); + final var incomingPropertiesByIdentity = properties.stream() + .collect(Collectors.toMap(ComponentProperty.Identity::new, Function.identity())); - final ComponentProperty persistentProperty = optionalPersistentProperty.get(); - applyIfChanged(persistentProperty, property, ComponentProperty::getPropertyValue, persistentProperty::setPropertyValue); - applyIfChanged(persistentProperty, property, ComponentProperty::getPropertyType, persistentProperty::setPropertyType); - applyIfChanged(persistentProperty, property, ComponentProperty::getDescription, persistentProperty::setDescription); + final var propertyIdentities = new HashSet(); + propertyIdentities.addAll(existingPropertiesByIdentity.keySet()); + propertyIdentities.addAll(incomingPropertiesByIdentity.keySet()); + + for (final ComponentProperty.Identity identity : propertyIdentities) { + final ComponentProperty existingProperty = existingPropertiesByIdentity.get(identity); + final ComponentProperty incomingProperty = incomingPropertiesByIdentity.get(identity); + + if (existingProperty == null) { + incomingProperty.setComponent(component); + qm.getPersistenceManager().makePersistent(incomingProperty); + } else if (incomingProperty == null) { + qm.getPersistenceManager().deletePersistent(existingProperty); + } } } @@ -719,7 +726,7 @@ private static void resolveAndApplyLicense(final QueryManager qm, if (isNotBlank(licenseCandidate.getName())) { final License resolvedLicense = licenseCache.computeIfAbsent(licenseCandidate.getName(), - licenseName -> resolveLicense(qm, licenseName)); + licenseName -> resolveLicense(qm, licenseName)); if (resolvedLicense != License.UNRESOLVED) { component.setResolvedLicense(resolvedLicense); component.setLicenseUrl(trimToNull(licenseCandidate.getUrl())); diff --git a/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java index f680d492c4..9756832a42 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java @@ -25,7 +25,6 @@ import org.dependencytrack.model.Component; import org.dependencytrack.model.ComponentProperty; import org.dependencytrack.model.Project; -import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.servlet.ServletContainer; import org.glassfish.jersey.test.DeploymentContext; @@ -39,6 +38,7 @@ import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; public class ComponentPropertyResourceTest extends ResourceTest { @@ -76,7 +76,7 @@ public void getPropertiesTest() { propertyB.setGroupName("foo-b"); propertyB.setPropertyName("bar-b"); propertyB.setPropertyValue("baz-b"); - propertyB.setPropertyType(PropertyType.ENCRYPTEDSTRING); + propertyB.setPropertyType(PropertyType.STRING); propertyB.setDescription("qux-b"); qm.persist(propertyB); @@ -86,24 +86,29 @@ public void getPropertiesTest() { assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); - assertThatJson(getPlainTextBody(response)).isEqualTo(""" - [ - { - "groupName": "foo-a", - "propertyName": "bar-a", - "propertyValue": "baz-a", - "propertyType": "STRING", - "description": "qux-a" - }, - { - "groupName": "foo-b", - "propertyName": "bar-b", - "propertyValue": "HiddenDecryptedPropertyPlaceholder", - "propertyType": "ENCRYPTEDSTRING", - "description": "qux-b" - } - ] - """); + assertThatJson(getPlainTextBody(response)) + .withMatcher("property-a-uuid", equalTo(propertyA.getUuid().toString())) + .withMatcher("property-b-uuid", equalTo(propertyB.getUuid().toString())) + .isEqualTo(""" + [ + { + "groupName": "foo-a", + "propertyName": "bar-a", + "propertyValue": "baz-a", + "propertyType": "STRING", + "description": "qux-a", + "uuid": "${json-unit.matches:property-a-uuid}" + }, + { + "groupName": "foo-b", + "propertyName": "bar-b", + "propertyValue": "baz-b", + "propertyType": "STRING", + "description": "qux-b", + "uuid": "${json-unit.matches:property-b-uuid}" + } + ] + """); } @Test @@ -148,13 +153,14 @@ public void createPropertyTest() { "propertyName": "bar", "propertyValue": "baz", "propertyType": "STRING", - "description": "qux" + "description": "qux", + "uuid": "${json-unit.any-string}" } """); } @Test - public void createPropertyEncryptedTest() { + public void createPropertyWithoutGroupTest() { final var project = new Project(); project.setName("acme-app"); qm.persist(project); @@ -168,10 +174,9 @@ public void createPropertyEncryptedTest() { .header(X_API_KEY, apiKey) .put(Entity.entity(""" { - "groupName": "foo", "propertyName": "bar", "propertyValue": "baz", - "propertyType": "ENCRYPTEDSTRING", + "propertyType": "STRING", "description": "qux" } """, MediaType.APPLICATION_JSON)); @@ -180,11 +185,11 @@ public void createPropertyEncryptedTest() { assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); assertThatJson(getPlainTextBody(response)).isEqualTo(""" { - "groupName": "foo", "propertyName": "bar", - "propertyValue": "HiddenDecryptedPropertyPlaceholder", - "propertyType": "ENCRYPTEDSTRING", - "description": "qux" + "propertyValue": "baz", + "propertyType": "STRING", + "description": "qux", + "uuid": "${json-unit.any-string}" } """); } @@ -223,11 +228,11 @@ public void createPropertyDuplicateTest() { assertThat(response.getStatus()).isEqualTo(409); assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); assertThat(getPlainTextBody(response)).isEqualTo(""" - A property with the specified component/group/name combination already exists."""); + A property with the specified component/group/name/value combination already exists."""); } @Test - public void createPropertyInvalidTest() { + public void createPropertyDisallowedPropertyTypeTest() { final var project = new Project(); project.setName("acme-app"); qm.persist(project); @@ -237,66 +242,34 @@ public void createPropertyInvalidTest() { component.setName("acme-lib"); qm.persist(component); - final Response response = target("%s/%s/property".formatted(V1_COMPONENT, UUID.randomUUID())).request() + final Response response = target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() .header(X_API_KEY, apiKey) .put(Entity.entity(""" { "groupName": "foo", "propertyName": "bar", "propertyValue": "baz", - "propertyType": "STRING", + "propertyType": "ENCRYPTEDSTRING", "description": "qux" } """, MediaType.APPLICATION_JSON)); - assertThat(response.getStatus()).isEqualTo(404); - assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); - assertThat(getPlainTextBody(response)).isEqualTo("The component could not be found."); - } - - @Test - public void updatePropertyTest() { - final var project = new Project(); - project.setName("acme-app"); - qm.persist(project); - - final var component = new Component(); - component.setProject(project); - component.setName("acme-lib"); - qm.persist(component); - - final var property = new ComponentProperty(); - property.setComponent(component); - property.setGroupName("foo"); - property.setPropertyName("bar"); - property.setPropertyValue("baz"); - property.setPropertyType(PropertyType.STRING); - qm.persist(property); - - final Response response = target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() - .header(X_API_KEY, apiKey) - .post(Entity.entity(""" - { - "groupName": "foo", - "propertyName": "bar", - "propertyValue": "qux" - } - """, MediaType.APPLICATION_JSON)); - - assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getStatus()).isEqualTo(400); assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); assertThatJson(getPlainTextBody(response)).isEqualTo(""" - { - "groupName": "foo", - "propertyName": "bar", - "propertyValue": "qux", - "propertyType": "STRING" - } + [ + { + "message": "Encrypted component property values are not supported", + "messageTemplate": "Encrypted component property values are not supported", + "path": "propertyType", + "invalidValue":"ENCRYPTEDSTRING" + } + ] """); } @Test - public void updatePropertyInvalidTest() { + public void createPropertyComponentNotFoundTest() { final var project = new Project(); project.setName("acme-app"); qm.persist(project); @@ -306,21 +279,15 @@ public void updatePropertyInvalidTest() { component.setName("acme-lib"); qm.persist(component); - final var property = new ComponentProperty(); - property.setComponent(component); - property.setGroupName("foo"); - property.setPropertyName("bar"); - property.setPropertyValue("baz"); - property.setPropertyType(PropertyType.STRING); - qm.persist(property); - final Response response = target("%s/%s/property".formatted(V1_COMPONENT, UUID.randomUUID())).request() .header(X_API_KEY, apiKey) - .post(Entity.entity(""" + .put(Entity.entity(""" { "groupName": "foo", "propertyName": "bar", - "propertyValue": "qux" + "propertyValue": "baz", + "propertyType": "STRING", + "description": "qux" } """, MediaType.APPLICATION_JSON)); @@ -348,15 +315,9 @@ public void deletePropertyTest() { property.setPropertyType(PropertyType.STRING); qm.persist(property); - final Response response = target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() + final Response response = target("%s/%s/property/%s".formatted(V1_COMPONENT, component.getUuid(), property.getUuid())).request() .header(X_API_KEY, apiKey) - .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) - .method("DELETE", Entity.entity(""" - { - "groupName": "foo", - "propertyName": "bar" - } - """, MediaType.APPLICATION_JSON)); + .delete(); assertThat(response.getStatus()).isEqualTo(204); assertThat(getPlainTextBody(response)).isEmpty(); diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index 72e46ec2cd..9ab1796245 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -105,12 +105,10 @@ public static Collection data() { }); } - private final String bomUploadProcessingTaskName; private final Supplier bomUploadProcessingTaskSupplier; - public BomUploadProcessingTaskTest(final String bomUploadProcessingTaskName, + public BomUploadProcessingTaskTest(final String ignoredBomUploadProcessingTaskName, final Supplier bomUploadProcessingTaskSupplier) { - this.bomUploadProcessingTaskName = bomUploadProcessingTaskName; this.bomUploadProcessingTaskSupplier = bomUploadProcessingTaskSupplier; } @@ -243,7 +241,6 @@ public void informTest() throws Exception { assertThat(component.getPurl().canonicalize()).isEqualTo("pkg:maven/com.example/xmlutil@1.0.0?packaging=jar"); assertThat(component.getLicenseUrl()).isEqualTo("https://www.apache.org/licenses/LICENSE-2.0.txt"); - // TODO: Implement for legacy version of the task. if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTaskV2) { assertThat(component.getProperties()).satisfiesExactlyInAnyOrder( property -> { @@ -920,6 +917,11 @@ public void informWithBomContainingServiceTest() throws Exception { @Test public void informWithExistingComponentPropertiesAndBomWithoutComponentProperties() { + // Known to now work with old task implementation. + if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTask) { + return; + } + final var project = new Project(); project.setName("acme-app"); qm.persist(project); @@ -960,6 +962,11 @@ public void informWithExistingComponentPropertiesAndBomWithoutComponentPropertie @Test public void informWithExistingComponentPropertiesAndBomWithComponentProperties() { + // Known to now work with old task implementation. + if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTask) { + return; + } + final var project = new Project(); project.setName("acme-app"); qm.persist(project); @@ -1000,10 +1007,13 @@ public void informWithExistingComponentPropertiesAndBomWithComponentProperties() bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); awaitBomProcessedNotification(); - qm.getPersistenceManager().refresh(componentProperty); - assertThat(componentProperty.getGroupName()).isNull(); - assertThat(componentProperty.getPropertyName()).isEqualTo("foo"); - assertThat(componentProperty.getPropertyValue()).isEqualTo("baz"); + qm.getPersistenceManager().refresh(component); + assertThat(component.getProperties()).satisfiesExactly(property -> { + assertThat(property.getGroupName()).isNull(); + assertThat(property.getPropertyName()).isEqualTo("foo"); + assertThat(property.getPropertyValue()).isEqualTo("baz"); + assertThat(property.getUuid()).isNotEqualTo(componentProperty.getUuid()); + }); } @Test // https://github.com/DependencyTrack/dependency-track/issues/1905 From e711933271a7e4969ee1b2626161eefd0dedb84a Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 14 Apr 2024 18:53:43 +0200 Subject: [PATCH 065/412] De-duplicate component properties during model conversion Also don't include project properties in BOM exports (yet). Signed-off-by: nscuro --- .../parser/cyclonedx/util/ModelConverter.java | 11 ++++++++++- .../tasks/BomUploadProcessingTaskV2.java | 5 +++++ .../dependencytrack/resources/v1/BomResourceTest.java | 8 +------- .../tasks/BomUploadProcessingTaskTest.java | 7 +++++++ src/test/resources/unit/bom-1.xml | 2 ++ 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java index 1028ca846b..b32cf8fe4d 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -73,6 +73,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -264,9 +265,11 @@ private static List convertToComponentProperties(final List(); return cdxProperties.stream() .map(ModelConverter::convertToComponentProperty) .filter(Objects::nonNull) + .filter(property -> identitiesSeen.add(new ComponentProperty.Identity(property))) .toList(); } @@ -873,7 +876,13 @@ public static org.cyclonedx.model.Metadata createMetadata(final Project project) cycloneComponent.setExternalReferences(references); } cycloneComponent.setSupplier(convert(project.getSupplier())); - cycloneComponent.setProperties(convert(project.getProperties())); + + // NB: Project properties are currently used to configure integrations + // such as Defect Dojo. They can also contain encrypted values that most + // definitely are not safe to share. Before we can include project properties + // in BOM exports, we need a filtering mechanism. + // cycloneComponent.setProperties(convert(project.getProperties())); + metadata.setComponent(cycloneComponent); if (project.getMetadata() != null) { diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java index e4027341c0..e08b3b444f 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java @@ -470,6 +470,11 @@ private Map processComponents(final QueryManager q } private void processComponentProperties(final QueryManager qm, final Component component, final List properties) { + if (component.isNew()) { + // If the component is new, its properties are already in the desired state. + return; + } + if (properties == null || properties.isEmpty()) { // TODO: We currently remove all existing properties that are no longer included in the BOM. // This is to stay consistent with the BOM being the source of truth. However, this may feel diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index 3d4e66518b..9f9001e31d 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -235,13 +235,7 @@ public void exportProjectAsCycloneDxInventoryTest() { "name": "projectSupplier" }, "name": "acme-app", - "version": "SNAPSHOT", - "properties": [ - { - "name": "foo:bar", - "value": "baz" - } - ] + "version": "SNAPSHOT" }, "manufacture": { "name": "projectManufacturer" diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index 9ab1796245..db20129164 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -256,6 +256,13 @@ public void informTest() throws Exception { assertThat(property.getPropertyValue()).isEqualTo("bar"); assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); assertThat(property.getDescription()).isNull(); + }, + property -> { + assertThat(property.getGroupName()).isEqualTo("foo"); + assertThat(property.getPropertyName()).isEqualTo("bar"); + assertThat(property.getPropertyValue()).isEqualTo("qux"); + assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); + assertThat(property.getDescription()).isNull(); } ); } diff --git a/src/test/resources/unit/bom-1.xml b/src/test/resources/unit/bom-1.xml index f64a234bc2..adde0b1861 100644 --- a/src/test/resources/unit/bom-1.xml +++ b/src/test/resources/unit/bom-1.xml @@ -90,6 +90,8 @@ foo bar baz + qux + qux
    From 0fabefcd1693cfdf12f070caca922dc70ff94355 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 14 Apr 2024 19:02:41 +0200 Subject: [PATCH 066/412] Update property names in Trivy integration test Signed-off-by: nscuro --- .../tasks/scanners/TrivyAnalysisTaskIntegrationTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java b/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java index e2261d8a15..38790d7c45 100644 --- a/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java +++ b/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java @@ -194,9 +194,9 @@ public void testWithUnrecognizedPackageName() { component.setPurl("pkg:deb/ubuntu/libc6@2.35-0ubuntu3.4?arch=amd64&distro=ubuntu-22.04"); qm.persist(component); - qm.createComponentProperty(component, "aquasecurity:trivy", "SrcName", "glibc", IConfigProperty.PropertyType.STRING, null); - qm.createComponentProperty(component, "aquasecurity:trivy", "SrcVersion", "2.35", IConfigProperty.PropertyType.STRING, null); - qm.createComponentProperty(component, "aquasecurity:trivy", "SrcRelease", "0ubuntu3.4", IConfigProperty.PropertyType.STRING, null); + qm.createComponentProperty(component, "aquasecurity", "trivy:SrcName", "glibc", IConfigProperty.PropertyType.STRING, null); + qm.createComponentProperty(component, "aquasecurity", "trivy:SrcVersion", "2.35", IConfigProperty.PropertyType.STRING, null); + qm.createComponentProperty(component, "aquasecurity", "trivy:SrcRelease", "0ubuntu3.4", IConfigProperty.PropertyType.STRING, null); final var analysisEvent = new TrivyAnalysisEvent(List.of(osComponent, component)); new TrivyAnalysisTask().inform(analysisEvent); From 650e6c2ac51df8c2c9aece64ed778f332c03868d Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 14 Apr 2024 19:21:24 +0200 Subject: [PATCH 067/412] Fix `getComponentProperties` query Signed-off-by: nscuro --- .../dependencytrack/persistence/ComponentQueryManager.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java index e37b2f62b1..ae0a432b8d 100644 --- a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java @@ -847,7 +847,9 @@ public List getComponentProperties(final Component component, @Override public List getComponentProperties(final Component component) { - final Query query = pm.newQuery(ComponentProperty.class, "component == :component"); + final Query query = pm.newQuery(ComponentProperty.class); + query.setFilter("component == :component"); + query.setParameters(component); query.setOrdering("groupName ASC, propertyName ASC, id ASC"); try { return List.copyOf(query.executeList()); From f39fc0cc00efc8f5e01da1eee8d52d07e4ba3fb3 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 14 Apr 2024 19:22:00 +0200 Subject: [PATCH 068/412] Ignore `component` field when serializing `ComponentProperty` Signed-off-by: nscuro --- src/main/java/org/dependencytrack/model/ComponentProperty.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/dependencytrack/model/ComponentProperty.java b/src/main/java/org/dependencytrack/model/ComponentProperty.java index 15ed1a123e..170e0f4289 100644 --- a/src/main/java/org/dependencytrack/model/ComponentProperty.java +++ b/src/main/java/org/dependencytrack/model/ComponentProperty.java @@ -63,6 +63,7 @@ public Identity(final ComponentProperty property) { @Persistent @Column(name = "COMPONENT_ID", allowsNull = "false") + @JsonIgnore private Component component; @Persistent From bb30ecf17167da7dbc87c984e5044f0407790036 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 14 Apr 2024 20:01:31 +0200 Subject: [PATCH 069/412] Attempt at avoid race conditions with test subscribers Signed-off-by: nscuro --- .../dependencytrack/tasks/BomUploadProcessingTaskTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index db20129164..52d04fe942 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -134,13 +134,13 @@ public void setUp() { @After public void tearDown() { - EVENTS.clear(); - NOTIFICATIONS.clear(); - EventService.getInstance().unsubscribe(EventSubscriber.class); EventService.getInstance().unsubscribe(VulnerabilityAnalysisTask.class); EventService.getInstance().unsubscribe(NewVulnerableDependencyAnalysisTask.class); NotificationService.getInstance().unsubscribe(new Subscription(NotificationSubscriber.class)); + + EVENTS.clear(); + NOTIFICATIONS.clear(); } @Test From 009cc7423e5a757c91344af9b0747fac8d1ad0b9 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 14 Apr 2024 21:27:56 +0200 Subject: [PATCH 070/412] Try to fix test flakiness by waiting for event processing completion Signed-off-by: nscuro --- .../tasks/BomUploadProcessingTaskTest.java | 77 +++++++++++-------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index 52d04fe942..c2045db008 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -180,7 +180,7 @@ public void informTest() throws Exception { final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-1.xml")); bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); - awaitBomProcessedNotification(); + awaitBomProcessedNotification(bomUploadEvent); qm.getPersistenceManager().refresh(project); assertThat(project.getClassifier()).isEqualTo(Classifier.APPLICATION); @@ -298,7 +298,7 @@ public void informWithEmptyBomTest() throws Exception { final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-empty.json")); bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); - awaitBomProcessedNotification(); + awaitBomProcessedNotification(bomUploadEvent); qm.getPersistenceManager().refresh(project); assertThat(project.getClassifier()).isNull(); @@ -379,7 +379,7 @@ public void informWithComponentsUnderMetadataBomTest() throws Exception { final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-metadata-components.json")); bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); - awaitBomProcessedNotification(); + awaitBomProcessedNotification(bomUploadEvent); final List boms = qm.getAllBoms(project); assertThat(boms).hasSize(1); @@ -447,8 +447,9 @@ public void informWithExistingDuplicateComponentsTest() { } """.getBytes(StandardCharsets.UTF_8); - bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes)); - awaitBomProcessedNotification(); + final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes); + bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + awaitBomProcessedNotification(bomUploadEvent); qm.getPersistenceManager().evictAll(); assertThat(qm.getAllComponents(project)).satisfiesExactly(c -> { @@ -500,7 +501,7 @@ public void informWithBloatedBomTest() throws Exception { final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-bloated.json")); bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); - awaitBomProcessedNotification(); + awaitBomProcessedNotification(bomUploadEvent); final List boms = qm.getAllBoms(project); assertThat(boms).hasSize(1); @@ -560,7 +561,7 @@ public void informWithCustomLicenseResolutionTest() throws Exception { final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-custom-license.json")); bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); - awaitBomProcessedNotification(); + awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).satisfiesExactly( component -> { @@ -611,8 +612,9 @@ public void informWithBomContainingLicenseExpressionTest() { } """.getBytes(StandardCharsets.UTF_8); - bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes)); - awaitBomProcessedNotification(); + final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes); + bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { assertThat(component.getLicense()).isNull(); @@ -655,8 +657,9 @@ public void informWithBomContainingLicenseExpressionWithSingleIdTest() { } """.getBytes(StandardCharsets.UTF_8); - bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes)); - awaitBomProcessedNotification(); + final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes); + bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { assertThat(component.getResolvedLicense()).isNotNull(); @@ -695,8 +698,9 @@ public void informWithBomContainingInvalidLicenseExpressionTest() { } """.getBytes(StandardCharsets.UTF_8); - bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes)); - awaitBomProcessedNotification(); + final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes); + bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { assertThat(component.getLicense()).isNull(); @@ -738,8 +742,9 @@ public void informIssue3433Test() { } """.getBytes(StandardCharsets.UTF_8); - bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes)); - awaitBomProcessedNotification(); + final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes); + bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { assertThat(component.getResolvedLicense()).isNotNull(); @@ -788,8 +793,9 @@ public void informUpdateExistingLicenseTest() { } """.getBytes(StandardCharsets.UTF_8); - bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), existingBomBytes)); - awaitBomProcessedNotification(); + final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), existingBomBytes); + bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { assertThat(component.getResolvedLicense()).isNotNull(); @@ -823,7 +829,7 @@ public void informUpdateExistingLicenseTest() { """.getBytes(StandardCharsets.UTF_8); bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), updatedBomBytes)); - awaitBomProcessedNotification(); + awaitBomProcessedNotification(bomUploadEvent); qm.getPersistenceManager().evictAll(); assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { @@ -869,8 +875,9 @@ public void informDeleteExistingLicenseTest() { } """.getBytes(StandardCharsets.UTF_8); - bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), existingBomBytes)); - awaitBomProcessedNotification(); + final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), existingBomBytes); + bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { assertThat(component.getResolvedLicense()).isNotNull(); @@ -898,7 +905,7 @@ public void informDeleteExistingLicenseTest() { """.getBytes(StandardCharsets.UTF_8); bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), updatedBomBytes)); - awaitBomProcessedNotification(); + awaitBomProcessedNotification(bomUploadEvent); qm.getPersistenceManager().evictAll(); assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { @@ -916,7 +923,7 @@ public void informWithBomContainingServiceTest() throws Exception { final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-service.json")); bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); - awaitBomProcessedNotification(); + awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).isNotEmpty(); assertThat(qm.getAllServiceComponents(project)).isNotEmpty(); @@ -961,7 +968,7 @@ public void informWithExistingComponentPropertiesAndBomWithoutComponentPropertie } """.getBytes()); bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); - awaitBomProcessedNotification(); + awaitBomProcessedNotification(bomUploadEvent); qm.getPersistenceManager().refresh(component); assertThat(component.getProperties()).isEmpty(); @@ -1012,7 +1019,7 @@ public void informWithExistingComponentPropertiesAndBomWithComponentProperties() } """.getBytes()); bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); - awaitBomProcessedNotification(); + awaitBomProcessedNotification(bomUploadEvent); qm.getPersistenceManager().refresh(component); assertThat(component.getProperties()).satisfiesExactly(property -> { @@ -1038,7 +1045,7 @@ public void informIssue1905Test() throws Exception { bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); // Make sure processing did not fail. - awaitBomProcessedNotification(); + awaitBomProcessedNotification(bomUploadEvent); NOTIFICATIONS.clear(); // Ensure all expected components are present. @@ -1079,7 +1086,7 @@ public void informIssue2519Test() throws Exception { bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); // Make sure processing did not fail. - awaitBomProcessedNotification(); + awaitBomProcessedNotification(bomUploadEvent); NOTIFICATIONS.clear(); // Ensure the expected amount of components is present. @@ -1127,14 +1134,16 @@ public void informIssue3309Test() { } """.getBytes(); - bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes)); - awaitBomProcessedNotification(); + var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes); + bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + awaitBomProcessedNotification(bomUploadEvent); assertProjectAuthors.run(); NOTIFICATIONS.clear(); - bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes)); - awaitBomProcessedNotification(); + bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes); + bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + awaitBomProcessedNotification(bomUploadEvent); assertProjectAuthors.run(); } @@ -1155,7 +1164,7 @@ public void informIssue3371Test() throws Exception { bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); // Make sure processing did not fail. - awaitBomProcessedNotification(); + awaitBomProcessedNotification(bomUploadEvent); NOTIFICATIONS.clear(); // Ensure the expected amount of components is present. @@ -1174,7 +1183,7 @@ public void informIssue3371Test() throws Exception { } } - private void awaitBomProcessedNotification() { + private void awaitBomProcessedNotification(final BomUploadEvent bomUploadEvent) { try { await("BOM Processed Notification") .atMost(Duration.ofSeconds(3)) @@ -1191,6 +1200,10 @@ private void awaitBomProcessedNotification() { final var subject = (BomProcessingFailed) optionalNotification.get().getSubject(); fail("Expected BOM processing to succeed, but it failed due to: %s", subject.getCause()); } + + await("Event Processing Completion") + .atMost(Duration.ofSeconds(3)) + .untilAsserted(() -> assertThat(Event.isEventBeingProcessed(bomUploadEvent.getChainIdentifier())).isFalse()); } } From 523b5f092dcf65eb746435ad8d1464d94b0dcf46 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 14 Apr 2024 22:09:11 +0200 Subject: [PATCH 071/412] Add `@rkesters` to release credits For his work on the component property feature in #2717. Signed-off-by: nscuro --- docs/_posts/2024-xx-xx-v4.11.0.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index a2d9deb450..47b81f2877 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -158,7 +158,7 @@ We thank all organizations and individuals who contributed to this release, from Special thanks to everyone who contributed code to implement enhancements and fix defects: [@AnthonyMastrean], [@LaVibeX], [@MangoIV], [@Robbilie], [@VithikaS], [@a5a351e7], [@acdha], [@aravindparappil46], [@baburkin], [@fnxpt], [@kepten], [@leec94], [@lukas-braune], [@malice00], [@mehab], [@mge-mm] -[@mikkeschiren], [@mykter], [@rbt-mm], [@rkg-mm], [@sahibamittal], [@sebD], [@setchy] +[@mikkeschiren], [@mykter], [@rbt-mm], [@rkesters], [@rkg-mm], [@sahibamittal], [@sebD], [@setchy] ###### dependency-track-apiserver.jar @@ -282,6 +282,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [@mprencipe]: https://github.com/mprencipe [@mykter]: https://github.com/mykter [@rbt-mm]: https://github.com/rbt-mm +[@rkesters]: https://github.com/rkesters [@rkg-mm]: https://github.com/rkg-mm [@sahibamittal]: https://github.com/sahibamittal [@sebD]: https://github.com/sebD From 11020d15ed01fe0394f734f28a75944ce91673f2 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 14 Apr 2024 23:04:28 +0200 Subject: [PATCH 072/412] Implement component property ingest for legacy BOM processing task Signed-off-by: nscuro --- .../parser/cyclonedx/util/ModelConverter.java | 7 +++ .../persistence/ComponentQueryManager.java | 56 ++++++++++++++++++ .../persistence/QueryManager.java | 4 ++ .../tasks/BomUploadProcessingTaskV2.java | 59 +------------------ .../tasks/BomUploadProcessingTaskTest.java | 58 ++++++++---------- 5 files changed, 91 insertions(+), 93 deletions(-) diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java index b32cf8fe4d..003d85a18f 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -604,6 +604,13 @@ else if (StringUtils.isNotBlank(cycloneLicense.getName())) component.setExternalReferences(null); } + final List properties = convertToComponentProperties(cycloneDxComponent.getProperties()); + if (component.getId() == 0) { + component.setProperties(properties); + } else { + qm.synchronizeComponentProperties(component, properties); + } + if (cycloneDxComponent.getComponents() != null && !cycloneDxComponent.getComponents().isEmpty()) { final Collection components = new ArrayList<>(); for (int i = 0; i < cycloneDxComponent.getComponents().size(); i++) { diff --git a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java index ae0a432b8d..83d956797a 100644 --- a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java @@ -52,6 +52,11 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.dependencytrack.util.PersistenceUtil.assertNonPersistent; +import static org.dependencytrack.util.PersistenceUtil.assertPersistent; final class ComponentQueryManager extends QueryManager implements IQueryManager { @@ -886,4 +891,55 @@ public long deleteComponentPropertyByUuid(final Component component, final UUID } } + public void synchronizeComponentProperties(final Component component, final List properties) { + assertPersistent(component, "component must be persistent"); + + if (properties == null || properties.isEmpty()) { + // TODO: We currently remove all existing properties that are no longer included in the BOM. + // This is to stay consistent with the BOM being the source of truth. However, this may feel + // counter-intuitive to some users, who might expect their manual changes to persist. + // If we want to support that, we need a way to track which properties were added and / or + // modified manually. + if (component.getProperties() != null) { + pm.deletePersistentAll(component.getProperties()); + } + + return; + } + + properties.forEach(property -> assertNonPersistent(property, "property must not be persistent")); + + if (component.getProperties() == null || component.getProperties().isEmpty()) { + for (final ComponentProperty property : properties) { + property.setComponent(component); + pm.makePersistent(property); + } + + return; + } + + // Group properties by group, name, and value. Because CycloneDX supports duplicate + // property names, uniqueness can only be determined by also considering the value. + final var existingPropertiesByIdentity = component.getProperties().stream() + .collect(Collectors.toMap(ComponentProperty.Identity::new, Function.identity())); + final var incomingPropertiesByIdentity = properties.stream() + .collect(Collectors.toMap(ComponentProperty.Identity::new, Function.identity())); + + final var propertyIdentities = new HashSet(); + propertyIdentities.addAll(existingPropertiesByIdentity.keySet()); + propertyIdentities.addAll(incomingPropertiesByIdentity.keySet()); + + for (final ComponentProperty.Identity identity : propertyIdentities) { + final ComponentProperty existingProperty = existingPropertiesByIdentity.get(identity); + final ComponentProperty incomingProperty = incomingPropertiesByIdentity.get(identity); + + if (existingProperty == null) { + incomingProperty.setComponent(component); + pm.makePersistent(incomingProperty); + } else if (incomingProperty == null) { + pm.deletePersistent(existingProperty); + } + } + } + } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 7fc1516a4b..e8b2f7f813 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -578,6 +578,10 @@ public long deleteComponentPropertyByUuid(final Component component, final UUID return getComponentQueryManager().deleteComponentPropertyByUuid(component, uuid); } + public void synchronizeComponentProperties(final Component component, final List properties) { + getComponentQueryManager().synchronizeComponentProperties(component, properties); + } + public PaginatedResult getLicenses() { return getLicenseQueryManager().getLicenses(); } diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java index e08b3b444f..9c0fee24d3 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java @@ -41,7 +41,6 @@ import org.dependencytrack.model.Bom; import org.dependencytrack.model.Component; import org.dependencytrack.model.ComponentIdentity; -import org.dependencytrack.model.ComponentProperty; import org.dependencytrack.model.DependencyMetrics; import org.dependencytrack.model.FindingAttribution; import org.dependencytrack.model.License; @@ -78,9 +77,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; -import java.util.function.Function; import java.util.function.Predicate; -import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.trim; @@ -442,12 +439,10 @@ private Map processComponents(final QueryManager q applyIfChanged(persistentComponent, component, Component::getLicenseExpression, persistentComponent::setLicenseExpression); applyIfChanged(persistentComponent, component, Component::isInternal, persistentComponent::setInternal); applyIfChanged(persistentComponent, component, Component::getExternalReferences, persistentComponent::setExternalReferences); - + qm.synchronizeComponentProperties(persistentComponent, component.getProperties()); idsOfComponentsToDelete.remove(persistentComponent.getId()); } - processComponentProperties(qm, persistentComponent, component.getProperties()); - // Update component identities in our Identity->BOMRef map, // as after persisting the components, their identities now include UUIDs. final var newIdentity = new ComponentIdentity(persistentComponent); @@ -469,58 +464,6 @@ private Map processComponents(final QueryManager q return persistentComponents; } - private void processComponentProperties(final QueryManager qm, final Component component, final List properties) { - if (component.isNew()) { - // If the component is new, its properties are already in the desired state. - return; - } - - if (properties == null || properties.isEmpty()) { - // TODO: We currently remove all existing properties that are no longer included in the BOM. - // This is to stay consistent with the BOM being the source of truth. However, this may feel - // counter-intuitive to some users, who might expect their manual changes to persist. - // If we want to support that, we need a way to track which properties were added and / or - // modified manually. - if (component.getProperties() != null) { - qm.getPersistenceManager().deletePersistentAll(component.getProperties()); - } - - return; - } - - if (component.getProperties() == null || component.getProperties().isEmpty()) { - for (final ComponentProperty property : properties) { - property.setComponent(component); - qm.getPersistenceManager().makePersistent(property); - } - - return; - } - - // Group properties by group, name, and value. Because CycloneDX supports duplicate - // property names, uniqueness can only be determined by also considering the value. - final var existingPropertiesByIdentity = component.getProperties().stream() - .collect(Collectors.toMap(ComponentProperty.Identity::new, Function.identity())); - final var incomingPropertiesByIdentity = properties.stream() - .collect(Collectors.toMap(ComponentProperty.Identity::new, Function.identity())); - - final var propertyIdentities = new HashSet(); - propertyIdentities.addAll(existingPropertiesByIdentity.keySet()); - propertyIdentities.addAll(incomingPropertiesByIdentity.keySet()); - - for (final ComponentProperty.Identity identity : propertyIdentities) { - final ComponentProperty existingProperty = existingPropertiesByIdentity.get(identity); - final ComponentProperty incomingProperty = incomingPropertiesByIdentity.get(identity); - - if (existingProperty == null) { - incomingProperty.setComponent(component); - qm.getPersistenceManager().makePersistent(incomingProperty); - } else if (incomingProperty == null) { - qm.getPersistenceManager().deletePersistent(existingProperty); - } - } - } - private Map processServices(final QueryManager qm, final Project project, final List services, diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index c2045db008..ddf003722e 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -241,31 +241,29 @@ public void informTest() throws Exception { assertThat(component.getPurl().canonicalize()).isEqualTo("pkg:maven/com.example/xmlutil@1.0.0?packaging=jar"); assertThat(component.getLicenseUrl()).isEqualTo("https://www.apache.org/licenses/LICENSE-2.0.txt"); - if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTaskV2) { - assertThat(component.getProperties()).satisfiesExactlyInAnyOrder( - property -> { - assertThat(property.getGroupName()).isEqualTo("foo"); - assertThat(property.getPropertyName()).isEqualTo("bar"); - assertThat(property.getPropertyValue()).isEqualTo("baz"); - assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); - assertThat(property.getDescription()).isNull(); - }, - property -> { - assertThat(property.getGroupName()).isNull(); - assertThat(property.getPropertyName()).isEqualTo("foo"); - assertThat(property.getPropertyValue()).isEqualTo("bar"); - assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); - assertThat(property.getDescription()).isNull(); - }, - property -> { - assertThat(property.getGroupName()).isEqualTo("foo"); - assertThat(property.getPropertyName()).isEqualTo("bar"); - assertThat(property.getPropertyValue()).isEqualTo("qux"); - assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); - assertThat(property.getDescription()).isNull(); - } - ); - } + assertThat(component.getProperties()).satisfiesExactlyInAnyOrder( + property -> { + assertThat(property.getGroupName()).isEqualTo("foo"); + assertThat(property.getPropertyName()).isEqualTo("bar"); + assertThat(property.getPropertyValue()).isEqualTo("baz"); + assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); + assertThat(property.getDescription()).isNull(); + }, + property -> { + assertThat(property.getGroupName()).isNull(); + assertThat(property.getPropertyName()).isEqualTo("foo"); + assertThat(property.getPropertyValue()).isEqualTo("bar"); + assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); + assertThat(property.getDescription()).isNull(); + }, + property -> { + assertThat(property.getGroupName()).isEqualTo("foo"); + assertThat(property.getPropertyName()).isEqualTo("bar"); + assertThat(property.getPropertyValue()).isEqualTo("qux"); + assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); + assertThat(property.getDescription()).isNull(); + } + ); assertThat(qm.getAllVulnerabilities(component)).hasSize(2); assertThat(NOTIFICATIONS).satisfiesExactly( @@ -931,11 +929,6 @@ public void informWithBomContainingServiceTest() throws Exception { @Test public void informWithExistingComponentPropertiesAndBomWithoutComponentProperties() { - // Known to now work with old task implementation. - if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTask) { - return; - } - final var project = new Project(); project.setName("acme-app"); qm.persist(project); @@ -976,11 +969,6 @@ public void informWithExistingComponentPropertiesAndBomWithoutComponentPropertie @Test public void informWithExistingComponentPropertiesAndBomWithComponentProperties() { - // Known to now work with old task implementation. - if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTask) { - return; - } - final var project = new Project(); project.setName("acme-app"); qm.persist(project); From ba990a0ff3c18b16452121fdff161185b7b20a14 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 08:13:41 +0000 Subject: [PATCH 073/412] Bump org.slf4j:log4j-over-slf4j from 2.0.12 to 2.0.13 Bumps org.slf4j:log4j-over-slf4j from 2.0.12 to 2.0.13. --- updated-dependencies: - dependency-name: org.slf4j:log4j-over-slf4j dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5b71ad6bac..35028e17e8 100644 --- a/pom.xml +++ b/pom.xml @@ -108,7 +108,7 @@ 2.1.1 4.5.14 5.3.1 - 2.0.12 + 2.0.13 1.318 12.6.1.jre11 From 84ea121380c54fa650673b1246aed240ef9d44ff Mon Sep 17 00:00:00 2001 From: Marlon Pina Tojal Date: Mon, 15 Apr 2024 14:53:26 +0200 Subject: [PATCH 074/412] support for proprieties on trivy scans Signed-off-by: Marlon Pina Tojal --- .../parser/trivy/model/Package.java | 11 ++++++++--- .../tasks/scanners/TrivyAnalysisTask.java | 18 +++++++++++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/dependencytrack/parser/trivy/model/Package.java b/src/main/java/org/dependencytrack/parser/trivy/model/Package.java index f1d3132084..3b3961ea05 100644 --- a/src/main/java/org/dependencytrack/parser/trivy/model/Package.java +++ b/src/main/java/org/dependencytrack/parser/trivy/model/Package.java @@ -32,21 +32,26 @@ public class Package { private String srcVersion; @SerializedName("src_epoch") private Integer srcEpoch; + @SerializedName("src_release") + private String srcRelease; private String[] licenses; private OS layer; - public Package(String name, String version, String arch, Integer epoch) { + public Package(String name, String version, String arch, Integer epoch, String srcName, String srcVersion, String srcRelease) { this.name = name; this.version = version; this.arch = arch; this.epoch = epoch; - this.srcName = name; - this.srcVersion = version; + this.srcName = (srcName == null) ? name : srcName; + this.srcVersion = (srcVersion == null) ? version : srcVersion; this.srcEpoch = epoch; + this.srcRelease = srcRelease; this.licenses = new String[] {}; this.layer = new OS(); } + + } \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java index b28a2f9395..709cffcba4 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java @@ -46,6 +46,7 @@ import org.dependencytrack.event.TrivyAnalysisEvent; import org.dependencytrack.model.Classifier; import org.dependencytrack.model.Component; +import org.dependencytrack.model.ComponentProperty; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerabilityAnalysisLevel; @@ -225,6 +226,10 @@ public void analyze(final List components) { LOGGER.debug("add library %s".formatted(component.toString())); app.addLibrary(new Library(name, component.getVersion())); } else { + String srcName = null; + String srcVersion = null; + String srcRelease = null; + String pkgType = component.getPurl().getType(); String arch = null; Integer epoch = null; @@ -246,6 +251,17 @@ public void analyze(final List components) { } } + for (final ComponentProperty property : component.getProperties()) { + + if (property.getPropertyName().equals("trivy:SrcName")) { + srcName = property.getPropertyValue(); + } else if (property.getPropertyName().equals("trivy:SrcVersion")) { + srcVersion = property.getPropertyValue(); + } else if (property.getPropertyName().equals("trivy:SrcRelease")) { + srcRelease = property.getPropertyValue(); + } + } + final PackageInfo pkg = pkgs.computeIfAbsent(pkgType, ignored -> new PackageInfo()); versionKey += component.getVersion(); @@ -254,7 +270,7 @@ public void analyze(final List components) { LOGGER.debug("Add key %s to map".formatted(key)); map.put(key, component); LOGGER.debug("add package %s".formatted(component.toString())); - pkg.addPackage(new Package(component.getName(), component.getVersion(), arch != null ? arch : "x86_64", epoch)); + pkg.addPackage(new Package(component.getName(), component.getVersion(), arch != null ? arch : "x86_64", epoch, srcName, srcVersion, srcRelease)); } } From 0a8ba40ec8dace5ef4027218ffd592acb9d5571b Mon Sep 17 00:00:00 2001 From: Marlon Pina Tojal Date: Mon, 15 Apr 2024 18:38:11 +0200 Subject: [PATCH 075/412] support for experimental configurations Signed-off-by: Marlon Pina Tojal --- docs/_posts/2024-xx-xx-v4.11.0.md | 6 ++-- .../org/dependencytrack/common/ConfigKey.java | 4 +-- .../event/EventSubsystemInitializer.java | 35 +++++++++++++------ .../model/ConfigPropertyConstants.java | 4 ++- .../resources/v1/BomResource.java | 11 +++--- .../resources/v1/ConfigPropertyResource.java | 23 ++++++++++++ 6 files changed, 61 insertions(+), 22 deletions(-) diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index 47b81f2877..47ac06cfe1 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -10,14 +10,12 @@ overhauled to be more reliable and efficient. Further, BOM processing is now an occurring midway do not cause a partial state to be left behind. De-duplication of components and services is more predictable, and log messages emitted during processing contain additional context, making them easier to correlate. Because the new implementation can have a big impact on how Dependency-Track behaves regarding BOM uploads, -it is disabled by default for this release. It may be enabled by setting the environment variable `BOM_PROCESSING_TASK_V2_ENABLED` -to `true`. Users are highly encouraged to do so. +it is disabled by default for this release. It may be enabled in the experimental panel in administration. * **BOM Validation**. Historically, Dependency-Track did not validate uploaded BOMs and VEXs against the CycloneDX schema. While this allowed BOMs to be processed that did not strictly adhere to the schema, it could lead to confusion when uploaded files were accepted, but then failed to be ingested during asynchronous processing. Starting with this release, uploaded files will be rejected if they fail schema validation. Note that this may reveal issues in BOM -generators that currently produce invalid CycloneDX documents. Validation may be turned off by setting the -environment variable `BOM_VALIDATION_ENABLED` to `false`. +generators that currently produce invalid CycloneDX documents. Validation may be turned off by disabling it in the experimental panel in administration. * *This feature was demoed in our April community meeting! Watch it [here](https://www.youtube.com/watch?v=3iIeajRJK8o&t=450s)* * **Global Vulnerability Audit View**. This new interface allows users to discover and filter vulnerabilities that affect their portfolio, across all projects. When portfolio access control is enabled, this view is limited to projects a user diff --git a/src/main/java/org/dependencytrack/common/ConfigKey.java b/src/main/java/org/dependencytrack/common/ConfigKey.java index 6e543bffed..a4e8b4ed08 100644 --- a/src/main/java/org/dependencytrack/common/ConfigKey.java +++ b/src/main/java/org/dependencytrack/common/ConfigKey.java @@ -39,9 +39,7 @@ public enum ConfigKey implements Config.Key { REPO_META_ANALYZER_CACHE_STAMPEDE_BLOCKER_ENABLED("repo.meta.analyzer.cacheStampedeBlocker.enabled", true), REPO_META_ANALYZER_CACHE_STAMPEDE_BLOCKER_LOCK_BUCKETS("repo.meta.analyzer.cacheStampedeBlocker.lock.buckets", 1000), REPO_META_ANALYZER_CACHE_STAMPEDE_BLOCKER_MAX_ATTEMPTS("repo.meta.analyzer.cacheStampedeBlocker.max.attempts", 10), - SYSTEM_REQUIREMENT_CHECK_ENABLED("system.requirement.check.enabled", true), - BOM_PROCESSING_TASK_V2_ENABLED("bom.processing.task.v2.enabled", false), - BOM_VALIDATION_ENABLED("bom.validation.enabled", true); + SYSTEM_REQUIREMENT_CHECK_ENABLED("system.requirement.check.enabled", true); private final String propertyName; private final Object defaultValue; diff --git a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java index f53471ef58..741a053174 100644 --- a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java +++ b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java @@ -18,14 +18,14 @@ */ package org.dependencytrack.event; -import alpine.Config; import alpine.common.logging.Logger; import alpine.event.LdapSyncEvent; import alpine.event.framework.EventService; import alpine.event.framework.SingleThreadedEventService; import alpine.server.tasks.LdapSyncTask; import org.dependencytrack.RequirementsVerifier; -import org.dependencytrack.common.ConfigKey; +import org.dependencytrack.model.ConfigPropertyConstants; +import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.tasks.BomUploadProcessingTask; import org.dependencytrack.tasks.BomUploadProcessingTaskV2; import org.dependencytrack.tasks.CallbackTask; @@ -87,11 +87,20 @@ public void contextInitialized(final ServletContextEvent event) { if (RequirementsVerifier.failedValidation()) { return; } - if (Config.getInstance().getPropertyAsBoolean(ConfigKey.BOM_PROCESSING_TASK_V2_ENABLED)) { - EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTaskV2.class); - } else { - EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTask.class); + + //EXPERIMENTAL: FUTURE RELEASES SHOULD JUST REMOVE THE FOLLOWING BLOCK AND ENABLE THE COMMENT CODE + try (QueryManager qm = new QueryManager()) { + if (qm.isEnabled(ConfigPropertyConstants.BOM_PROCESSING_TASK_V2_ENABLED)) { + LOGGER.info("V2"); + EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTaskV2.class); + } else { + LOGGER.info("V1"); + EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTask.class); + } } + // EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTaskV2.class); + //EXPERIMENTAL + EVENT_SERVICE.subscribe(VexUploadEvent.class, VexUploadProcessingTask.class); EVENT_SERVICE.subscribe(LdapSyncEvent.class, LdapSyncTask.class); EVENT_SERVICE.subscribe(InternalAnalysisEvent.class, InternalAnalysisTask.class); @@ -135,11 +144,17 @@ public void contextDestroyed(final ServletContextEvent event) { LOGGER.info("Shutting down asynchronous event subsystem"); TaskScheduler.getInstance().shutdown(); - if (Config.getInstance().getPropertyAsBoolean(ConfigKey.BOM_PROCESSING_TASK_V2_ENABLED)) { - EVENT_SERVICE.unsubscribe(BomUploadProcessingTaskV2.class); - } else { - EVENT_SERVICE.unsubscribe(BomUploadProcessingTask.class); + //EXPERIMENTAL: FUTURE RELEASES SHOULD JUST REMOVE THE FOLLOWING BLOCK AND ENABLE THE COMMENT CODE + try (QueryManager qm = new QueryManager()) { + if (qm.isEnabled(ConfigPropertyConstants.BOM_PROCESSING_TASK_V2_ENABLED)) { + EVENT_SERVICE.unsubscribe(BomUploadProcessingTaskV2.class); + } else { + EVENT_SERVICE.unsubscribe(BomUploadProcessingTask.class); + } } + // EVENT_SERVICE.unsubscribe(BomUploadProcessingTaskV2.class); + //EXPERIMENTAL + EVENT_SERVICE.unsubscribe(VexUploadProcessingTask.class); EVENT_SERVICE.unsubscribe(LdapSyncTask.class); EVENT_SERVICE.unsubscribe(InternalAnalysisTask.class); diff --git a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index 5d6cc55848..c418f26d01 100644 --- a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -108,7 +108,9 @@ public enum ConfigPropertyConstants { TASK_SCHEDULER_COMPONENT_ANALYSIS_CACHE_CLEAR_CADENCE("task-scheduler", "component.analysis.cache.clear.cadence", "24", PropertyType.INTEGER, "Cleanup cadence (in hours) for component analysis cache"), SEARCH_INDEXES_CONSISTENCY_CHECK_ENABLED("search-indexes", "consistency.check.enabled", "true", PropertyType.BOOLEAN, "Flag to enable lucene indexes periodic consistency check"), SEARCH_INDEXES_CONSISTENCY_CHECK_CADENCE("search-indexes", "consistency.check.cadence", "4320", PropertyType.INTEGER, "Lucene indexes consistency check cadence (in minutes)"), - SEARCH_INDEXES_CONSISTENCY_CHECK_DELTA_THRESHOLD("search-indexes", "consistency.check.delta.threshold", "20", PropertyType.INTEGER, "Threshold used to trigger an index rebuild when comparing database table and corresponding lucene index (in percentage). It must be an integer between 1 and 100"); + SEARCH_INDEXES_CONSISTENCY_CHECK_DELTA_THRESHOLD("search-indexes", "consistency.check.delta.threshold", "20", PropertyType.INTEGER, "Threshold used to trigger an index rebuild when comparing database table and corresponding lucene index (in percentage). It must be an integer between 1 and 100"), + BOM_PROCESSING_TASK_V2_ENABLED("experimental", "bom.processing.task.v2.enabled", "false", PropertyType.BOOLEAN, "Flag to enable BOM UPLOAD V2"), + BOM_VALIDATION_ENABLED("experimental", "bom.validation.enabled", "true", PropertyType.BOOLEAN, "Flag to control bom validation"); private String groupName; private String propertyName; diff --git a/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/src/main/java/org/dependencytrack/resources/v1/BomResource.java index 982a7fed05..c5c88f094a 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -18,7 +18,6 @@ */ package org.dependencytrack.resources.v1; -import alpine.Config; import alpine.common.logging.Logger; import alpine.event.framework.Event; import alpine.server.auth.PermissionRequired; @@ -35,9 +34,9 @@ import org.cyclonedx.CycloneDxMediaType; import org.cyclonedx.exception.GeneratorException; import org.dependencytrack.auth.Permissions; -import org.dependencytrack.common.ConfigKey; import org.dependencytrack.event.BomUploadEvent; import org.dependencytrack.model.Component; +import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Project; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.parser.cyclonedx.CycloneDXExporter; @@ -468,9 +467,13 @@ private Response process(QueryManager qm, Project project, List list) { for (ConfigProperty item : list) { final ConfigProperty property = qm.getConfigProperty(item.getGroupName(), item.getPropertyName()); returnList.add(updatePropertyValue(qm, item, property).getEntity()); + + //EXPERIMENTAL: FUTURE RELEASES SHOULD REMOVE THIS BLOCK + if (item.getGroupName().equals("experimental") && + item.getPropertyName().equals("bom.processing.task.v2.enabled")) { + final EventService EVENT_SERVICE = EventService.getInstance(); + final Logger LOGGER = Logger.getLogger(ConfigPropertyResource.class); + + if (Boolean.parseBoolean(item.getPropertyValue())) { + LOGGER.info("Set V2"); + EVENT_SERVICE.unsubscribe(BomUploadProcessingTask.class); + EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTaskV2.class); + } else { + LOGGER.info("Set V1"); + EVENT_SERVICE.unsubscribe(BomUploadProcessingTaskV2.class); + EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTask.class); + } + } + //EXPERIMENTAL } } return Response.ok(returnList).build(); From 6a9eadafb18ad4fc3a3b50fe5c0ae4085c9cfd31 Mon Sep 17 00:00:00 2001 From: Marlon Pina Tojal Date: Tue, 16 Apr 2024 16:13:44 +0200 Subject: [PATCH 076/412] fix tests Signed-off-by: Marlon Pina Tojal --- .../TrivyAnalysisTaskIntegrationTest.java | 96 ++++++++++++++++++- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java b/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java index 38790d7c45..51f876b5d3 100644 --- a/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java +++ b/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java @@ -128,7 +128,7 @@ public void test() { /** * This test documents the case where Trivy is unable to correlate a package with vulnerabilities - * in its database, unless additional properties are provided. When including libc6 in an SBOM, + * when additional properties are not provided. When including libc6 in an SBOM, * Trivy adds metadata to the component, which among other things includes alternative package names. *

    * Here's an excerpt of the properties included: @@ -174,11 +174,85 @@ public void test() { * @see Support component properties with Trivy */ @Test - public void testWithUnrecognizedPackageName() { + public void testWithPackageWithoutTrivyProperties() { final var project = new Project(); project.setName("acme-app"); qm.persist(project); - + + final var osComponent = new Component(); + osComponent.setProject(project); + osComponent.setName("ubuntu"); + osComponent.setVersion("22.04"); + osComponent.setClassifier(Classifier.OPERATING_SYSTEM); + qm.persist(osComponent); + + final var component = new Component(); + component.setProject(project); + component.setName("libc6"); + component.setVersion("2.35-0ubuntu3.4"); + component.setClassifier(Classifier.LIBRARY); + component.setPurl("pkg:deb/ubuntu/libc6@2.35-0ubuntu3.4?arch=amd64&distro=ubuntu-22.04"); + qm.persist(component); + + final var analysisEvent = new TrivyAnalysisEvent(List.of(osComponent, component)); + new TrivyAnalysisTask().inform(analysisEvent); + + assertThat(qm.getAllVulnerabilities(component)).isEmpty(); + } + + /** + * This test documents the case where Trivy is able to correlate a package with vulnerabilities + * when additional properties provided. When including libc6 in an SBOM, + * Trivy adds metadata to the component, which among other things includes alternative package names. + *

    + * Here's an excerpt of the properties included: + *

    +     * "properties": [
    +     *   {
    +     *     "name": "aquasecurity:trivy:LayerDiffID",
    +     *     "value": "sha256:256d88da41857db513b95b50ba9a9b28491b58c954e25477d5dad8abb465430b"
    +     *   },
    +     *   {
    +     *     "name": "aquasecurity:trivy:LayerDigest",
    +     *     "value": "sha256:43f89b94cd7df92a2f7e565b8fb1b7f502eff2cd225508cbd7ea2d36a9a3a601"
    +     *   },
    +     *   {
    +     *     "name": "aquasecurity:trivy:PkgID",
    +     *     "value": "libc6@2.35-0ubuntu3.4"
    +     *   },
    +     *   {
    +     *     "name": "aquasecurity:trivy:PkgType",
    +     *     "value": "ubuntu"
    +     *   },
    +     *   {
    +     *     "name": "aquasecurity:trivy:SrcName",
    +     *     "value": "glibc"
    +     *   },
    +     *   {
    +     *     "name": "aquasecurity:trivy:SrcRelease",
    +     *     "value": "0ubuntu3.4"
    +     *   },
    +     *   {
    +     *     "name": "aquasecurity:trivy:SrcVersion",
    +     *     "value": "2.35"
    +     *   }
    +     * ]
    +     * 
    + *

    + * To reproduce, run: + *

    +     * docker run -it --rm aquasec/trivy image --format cyclonedx registry.hub.knime.com/knime/knime-full:r-5.1.2-433
    +     * 
    + * + * @see Add support for CycloneDX component properties + * @see Support component properties with Trivy + */ + @Test + public void testWithPackageWithTrivyProperties() { + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + final var osComponent = new Component(); osComponent.setProject(project); osComponent.setName("ubuntu"); @@ -201,7 +275,21 @@ public void testWithUnrecognizedPackageName() { final var analysisEvent = new TrivyAnalysisEvent(List.of(osComponent, component)); new TrivyAnalysisTask().inform(analysisEvent); - assertThat(qm.getAllVulnerabilities(component)).isEmpty(); + assertThat(qm.getAllVulnerabilities(component)).anySatisfy(vuln -> { + assertThat(vuln.getVulnId()).isEqualTo("CVE-2016-20013"); + assertThat(vuln.getSource()).isEqualTo(Vulnerability.Source.NVD.name()); + + // NB: Can't assert specific values here, as we're testing against + // a moving target. These values may change over time. We do proper + // assertions in TrivyAnalyzerTaskTest. + assertThat(vuln.getTitle()).isBlank(); + assertThat(vuln.getDescription()).isNotBlank(); + assertThat(vuln.getCreated()).isNotNull(); + assertThat(vuln.getPublished()).isNotNull(); + assertThat(vuln.getUpdated()).isNotNull(); + assertThat(vuln.getSeverity()).isNotNull(); + assertThat(vuln.getReferences()).isNotBlank(); + }); } } From 8cd2748089cff09b4c5a6bc340907223b4f89a79 Mon Sep 17 00:00:00 2001 From: Marlon Pina Tojal Date: Tue, 16 Apr 2024 17:37:44 +0200 Subject: [PATCH 077/412] remove unnecessary logs Signed-off-by: Marlon Pina Tojal --- .../org/dependencytrack/event/EventSubsystemInitializer.java | 2 -- .../dependencytrack/resources/v1/ConfigPropertyResource.java | 4 ---- 2 files changed, 6 deletions(-) diff --git a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java index 741a053174..9b61ec13c9 100644 --- a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java +++ b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java @@ -91,10 +91,8 @@ public void contextInitialized(final ServletContextEvent event) { //EXPERIMENTAL: FUTURE RELEASES SHOULD JUST REMOVE THE FOLLOWING BLOCK AND ENABLE THE COMMENT CODE try (QueryManager qm = new QueryManager()) { if (qm.isEnabled(ConfigPropertyConstants.BOM_PROCESSING_TASK_V2_ENABLED)) { - LOGGER.info("V2"); EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTaskV2.class); } else { - LOGGER.info("V1"); EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTask.class); } } diff --git a/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java index 7bf07d5e14..f68d560887 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java @@ -42,7 +42,6 @@ import javax.ws.rs.core.Response; import java.util.ArrayList; import java.util.List; -import alpine.common.logging.Logger; /** * JAX-RS resources for processing ConfigProperties @@ -143,14 +142,11 @@ public Response updateConfigProperty(List list) { if (item.getGroupName().equals("experimental") && item.getPropertyName().equals("bom.processing.task.v2.enabled")) { final EventService EVENT_SERVICE = EventService.getInstance(); - final Logger LOGGER = Logger.getLogger(ConfigPropertyResource.class); if (Boolean.parseBoolean(item.getPropertyValue())) { - LOGGER.info("Set V2"); EVENT_SERVICE.unsubscribe(BomUploadProcessingTask.class); EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTaskV2.class); } else { - LOGGER.info("Set V1"); EVENT_SERVICE.unsubscribe(BomUploadProcessingTaskV2.class); EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTask.class); } From a5543f0c07dad12e7044b339445a779c0adce648 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 10 Mar 2024 17:35:50 +0100 Subject: [PATCH 078/412] Improve Lucene observability Collect basic metrics: * Total number of index operations (`add`, `update`, `delete`, `commit`), grouped by index * Number of index documents in RAM * Number of bytes used by the index * Total number of documents in the index Also, integrate Lucene's `InfoStream` with Dependency-Track's logging system. Lucene output will now be included when configuring `LOGGING_LEVEL=DEBUG`, or when the respective logger is explicitly configured in `logback.xml`. Relates to #3429 Signed-off-by: nscuro --- docs/_posts/2024-xx-xx-v4.11.0.md | 2 + .../search/ComponentIndexer.java | 69 ++--- .../dependencytrack/search/IndexManager.java | 241 ++++++++++++++---- .../search/IndexManagerFactory.java | 2 + .../search/LicenseIndexer.java | 63 ++--- .../search/LoggingInfoStream.java | 85 ++++++ .../dependencytrack/search/ObjectIndexer.java | 7 + .../search/ProjectIndexer.java | 92 +++---- .../search/ServiceComponentIndexer.java | 69 ++--- .../search/VulnerabilityIndexer.java | 65 ++--- .../search/VulnerableSoftwareIndexer.java | 71 ++---- .../org/dependencytrack/tasks/IndexTask.java | 10 +- 12 files changed, 456 insertions(+), 320 deletions(-) create mode 100644 src/main/java/org/dependencytrack/search/LoggingInfoStream.java diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index 47b81f2877..af06ee6fc8 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -59,6 +59,7 @@ Community input and contributions are explicitly requested. The chart repository * Add auto-generated changelog to GitHub releases - [apiserver/#3502] * Bump SPDX license list to v3.23 - [apiserver/#3508] * Validate uploaded BOMs against CycloneDX schema prior to processing them - [apiserver/#3522] +* Improve observability of Lucene search indexes - [apiserver/#3535] * Add support for Hackage repositories - [apiserver/#3549] * Add support for Nix repositories - [apiserver/#3549] * Add *required permissions* to OpenAPI descriptions of endpoints - [apiserver/#3557] @@ -217,6 +218,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [apiserver/#3512]: https://github.com/DependencyTrack/dependency-track/pull/3512 [apiserver/#3513]: https://github.com/DependencyTrack/dependency-track/pull/3513 [apiserver/#3522]: https://github.com/DependencyTrack/dependency-track/pull/3522 +[apiserver/#3535]: https://github.com/DependencyTrack/dependency-track/pull/3535 [apiserver/#3549]: https://github.com/DependencyTrack/dependency-track/pull/3549 [apiserver/#3557]: https://github.com/DependencyTrack/dependency-track/pull/3557 [apiserver/#3558]: https://github.com/DependencyTrack/dependency-track/pull/3558 diff --git a/src/main/java/org/dependencytrack/search/ComponentIndexer.java b/src/main/java/org/dependencytrack/search/ComponentIndexer.java index d10e42048d..39c9471d7e 100644 --- a/src/main/java/org/dependencytrack/search/ComponentIndexer.java +++ b/src/main/java/org/dependencytrack/search/ComponentIndexer.java @@ -19,21 +19,14 @@ package org.dependencytrack.search; import alpine.common.logging.Logger; -import alpine.notification.Notification; -import alpine.notification.NotificationLevel; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; -import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.Term; import org.dependencytrack.model.Component; -import org.dependencytrack.notification.NotificationConstants; -import org.dependencytrack.notification.NotificationGroup; -import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.search.document.ComponentDocument; import javax.jdo.Query; -import java.io.IOException; import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; @@ -50,7 +43,7 @@ public final class ComponentIndexer extends IndexManager implements ObjectIndexe private static final Logger LOGGER = Logger.getLogger(ComponentIndexer.class); private static final ComponentIndexer INSTANCE = new ComponentIndexer(); - protected static ComponentIndexer getInstance() { + static ComponentIndexer getInstance() { return INSTANCE; } @@ -72,28 +65,15 @@ public String[] getSearchFields() { * @param component A persisted Component object. */ public void add(final ComponentDocument component) { - final Document doc = new Document(); - addField(doc, IndexConstants.COMPONENT_UUID, component.uuid().toString(), Field.Store.YES, false); - addField(doc, IndexConstants.COMPONENT_NAME, component.name(), Field.Store.YES, true); - addField(doc, IndexConstants.COMPONENT_GROUP, component.group(), Field.Store.YES, true); - addField(doc, IndexConstants.COMPONENT_VERSION, component.version(), Field.Store.YES, false); - addField(doc, IndexConstants.COMPONENT_SHA1, component.sha1(), Field.Store.YES, true); - addField(doc, IndexConstants.COMPONENT_DESCRIPTION, component.description(), Field.Store.YES, true); + final Document doc = convertToDocument(component); + addDocument(doc); + } - try { - getIndexWriter().addDocument(doc); - } catch (CorruptIndexException e) { - handleCorruptIndexException(e); - } catch (IOException e) { - LOGGER.error("An error occurred while adding component to index", e); - Notification.dispatch(new Notification() - .scope(NotificationScope.SYSTEM) - .group(NotificationGroup.INDEXING_SERVICE) - .title(NotificationConstants.Title.COMPONENT_INDEXER) - .content("An error occurred while adding component to index. Check log for details. " + e.getMessage()) - .level(NotificationLevel.ERROR) - ); - } + @Override + public void update(ComponentDocument component) { + final Term term = convertToTerm(component); + final Document doc = convertToDocument(component); + updateDocument(term, doc); } /** @@ -102,20 +82,8 @@ public void add(final ComponentDocument component) { * @param component A persisted Component object. */ public void remove(final ComponentDocument component) { - try { - getIndexWriter().deleteDocuments(new Term(IndexConstants.COMPONENT_UUID, component.uuid().toString())); - } catch (CorruptIndexException e) { - handleCorruptIndexException(e); - } catch (IOException e) { - LOGGER.error("An error occurred while removing a component from the index", e); - Notification.dispatch(new Notification() - .scope(NotificationScope.SYSTEM) - .group(NotificationGroup.INDEXING_SERVICE) - .title(NotificationConstants.Title.COMPONENT_INDEXER) - .content("An error occurred while removing a component from the index. Check log for details. " + e.getMessage()) - .level(NotificationLevel.ERROR) - ); - } + final Term term = convertToTerm(component); + deleteDocuments(term); } /** @@ -163,4 +131,19 @@ private static List fetchNext(final QueryManager qm, final Lo } } + private Document convertToDocument(final ComponentDocument component) { + final var doc = new Document(); + addField(doc, IndexConstants.COMPONENT_UUID, component.uuid().toString(), Field.Store.YES, false); + addField(doc, IndexConstants.COMPONENT_NAME, component.name(), Field.Store.YES, true); + addField(doc, IndexConstants.COMPONENT_GROUP, component.group(), Field.Store.YES, true); + addField(doc, IndexConstants.COMPONENT_VERSION, component.version(), Field.Store.YES, false); + addField(doc, IndexConstants.COMPONENT_SHA1, component.sha1(), Field.Store.YES, true); + addField(doc, IndexConstants.COMPONENT_DESCRIPTION, component.description(), Field.Store.YES, true); + return doc; + } + + private static Term convertToTerm(final ComponentDocument component) { + return new Term(IndexConstants.COMPONENT_UUID, component.uuid().toString()); + } + } diff --git a/src/main/java/org/dependencytrack/search/IndexManager.java b/src/main/java/org/dependencytrack/search/IndexManager.java index 5a9177be58..78911643f0 100644 --- a/src/main/java/org/dependencytrack/search/IndexManager.java +++ b/src/main/java/org/dependencytrack/search/IndexManager.java @@ -20,10 +20,16 @@ import alpine.Config; import alpine.common.logging.Logger; +import alpine.common.metrics.Metrics; import alpine.event.framework.Event; import alpine.model.ConfigProperty; import alpine.notification.Notification; import alpine.notification.NotificationLevel; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.binder.BaseUnits; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.io.FileDeleteStrategy; import org.apache.commons.io.output.NullPrintStream; @@ -81,12 +87,23 @@ public abstract class IndexManager implements AutoCloseable { private static final Logger LOGGER = Logger.getLogger(IndexManager.class); + + private final Logger logger; + private final IndexType indexType; + private final Tag indexTag; + private final Counter addOperationCounter; + private final Counter updateOperationCounter; + private final Counter deleteOperationCounter; + private final Counter commitOperationCounter; private IndexWriter iwriter; private DirectoryReader searchReader; - private final IndexType indexType; + private Gauge docsRamTotalGauge; + private Gauge ramBytesUsedGauge; + private Gauge numDocsGauge; /** * This methods should be overwritten. + * * @return an array of all fields that can be searched on * @since 3.0.0 */ @@ -96,6 +113,7 @@ public String[] getSearchFields() { /** * Defines the type of supported indexes. + * * @since 3.0.0 */ public enum IndexType { @@ -126,7 +144,7 @@ public static Optional getIndexType(String type) { } } - public static UUID getUuid(Class clazz) { + public static UUID getUuid(Class clazz) { return Arrays.stream(values()) .filter(type -> clazz == type.getClazz()) .map(type -> type.uuid) @@ -138,15 +156,40 @@ public static UUID getUuid(Class clazz) { /** * Constructs a new IndexManager. All classes that extend this class should call * super(indexType) in their constructor. + * * @param indexType the type of index to use * @since 3.0.0 */ protected IndexManager(final IndexType indexType) { + this.logger = Logger.getLogger(getClass()); this.indexType = indexType; + + this.indexTag = Tag.of("index", indexType.name()); + this.addOperationCounter = Counter.builder("search_index_operations") + .description("Total number of index operations") + .tags(Tags.of(indexTag, Tag.of("operation", "add"))) + .baseUnit(BaseUnits.OPERATIONS) + .register(Metrics.getRegistry()); + this.updateOperationCounter = Counter.builder("search_index_operations") + .description("Total number of index operations") + .tags(Tags.of(indexTag, Tag.of("operation", "update"))) + .baseUnit(BaseUnits.OPERATIONS) + .register(Metrics.getRegistry()); + this.deleteOperationCounter = Counter.builder("search_index_operations") + .description("Total number of index operations") + .tags(Tags.of(indexTag, Tag.of("operation", "delete"))) + .baseUnit(BaseUnits.OPERATIONS) + .register(Metrics.getRegistry()); + this.commitOperationCounter = Counter.builder("search_index_operations") + .description("Total number of index operations") + .tags(Tags.of(indexTag, Tag.of("operation", "commit"))) + .baseUnit(BaseUnits.OPERATIONS) + .register(Metrics.getRegistry()); } /** * Returns the index type. + * * @return the index type * @since 3.0.0 */ @@ -156,6 +199,7 @@ public IndexType getIndexType() { /** * Retrieves the index directory based on the type of index used. + * * @return a Directory * @throws IOException when the directory cannot be accessed * @since 3.0.0 @@ -164,7 +208,7 @@ private synchronized Directory getDirectory() throws IOException { final File indexDir = getIndexDirectory(indexType); if (!indexDir.exists()) { if (!indexDir.mkdirs()) { - LOGGER.error("Unable to create index directory: " + indexDir.getCanonicalPath()); + logger.error("Unable to create index directory: " + indexDir.getCanonicalPath()); Notification.dispatch(new Notification() .scope(NotificationScope.SYSTEM) .group(NotificationGroup.FILE_SYSTEM) @@ -179,18 +223,42 @@ private synchronized Directory getDirectory() throws IOException { /** * Opens the index. + * * @throws IOException when the index cannot be opened * @since 3.0.0 */ protected void openIndex() throws IOException { - final Analyzer analyzer = new StandardAnalyzer(); - final IndexWriterConfig config = new IndexWriterConfig(analyzer); + final var analyzer = new StandardAnalyzer(); + + final var config = new IndexWriterConfig(analyzer); config.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); + config.setInfoStream(new LoggingInfoStream(getClass())); + config.setCommitOnClose(true); + iwriter = new IndexWriter(getDirectory(), config); + + docsRamTotalGauge = Gauge.builder("search_index_docs_ram_total", iwriter, IndexWriter::numRamDocs) + .description("Number of documents currently buffered in RAM") + .tags(Tags.of(indexTag)) + .baseUnit(BaseUnits.OBJECTS) + .register(Metrics.getRegistry()); + ramBytesUsedGauge = Gauge.builder("search_index_ram_used", iwriter, IndexWriter::ramBytesUsed) + .description("Memory usage of the index in bytes") + .tags(Tags.of(indexTag)) + .baseUnit(BaseUnits.BYTES) + .register(Metrics.getRegistry()); + numDocsGauge = Gauge.builder("search_index_docs_total", iwriter, w -> w.getDocStats().numDocs) + .description(""" + Number of docs in this index, including docs not yet flushed (still in the RAM buffer), \ + and including deletions""") + .tags(Tags.of(indexTag)) + .baseUnit(BaseUnits.OBJECTS) + .register(Metrics.getRegistry()); } /** * Returns an IndexWriter, by opening the index if necessary. + * * @return an IndexWriter * @throws IOException when the index cannot be opened * @since 3.0.0 @@ -227,6 +295,7 @@ protected synchronized IndexSearcher getIndexSearcher() throws IOException { /** * Returns a QueryParser. + * * @return a QueryParser * @since 3.0.0 */ @@ -238,34 +307,90 @@ protected QueryParser getQueryParser() { return qparser; } + public void addDocument(final Document document) { + try { + getIndexWriter().addDocument(document); + addOperationCounter.increment(); + } catch (CorruptIndexException e) { + handleCorruptIndexException(e); + } catch (IOException e) { + logger.error("An error occurred while adding document to index", e); + Notification.dispatch(new Notification() + .scope(NotificationScope.SYSTEM) + .group(NotificationGroup.INDEXING_SERVICE) + .title(NotificationConstants.Title.COMPONENT_INDEXER) + .content("An error occurred while adding document to index: %s; Check log for details.".formatted(e.getMessage())) + .level(NotificationLevel.ERROR) + ); + } + } + + public void updateDocument(final Term term, final Document document) { + try { + getIndexWriter().updateDocument(term, document); + updateOperationCounter.increment(); + } catch (CorruptIndexException e) { + handleCorruptIndexException(e); + } catch (IOException e) { + logger.error("An error occurred while updating document in index", e); + Notification.dispatch(new Notification() + .scope(NotificationScope.SYSTEM) + .group(NotificationGroup.INDEXING_SERVICE) + .title(getNotificationTitle()) + .content("An error occurred while updating document in index: %s; Check log for details.".formatted(e.getMessage())) + .level(NotificationLevel.ERROR) + ); + } + } + + public void deleteDocuments(final Term term) { + try { + getIndexWriter().deleteDocuments(term); + deleteOperationCounter.increment(); + } catch (CorruptIndexException e) { + handleCorruptIndexException(e); + } catch (IOException e) { + logger.error("An error occurred while deleting documents from index with term %s".formatted(term), e); + Notification.dispatch(new Notification() + .scope(NotificationScope.SYSTEM) + .group(NotificationGroup.INDEXING_SERVICE) + .title(getNotificationTitle()) + .content("An error occurred while removing a component from index: %s; Check log for details.".formatted(e.getMessage())) + .level(NotificationLevel.ERROR) + ); + } + } + /** * Commits changes to the index and closes the IndexWriter. + * * @since 3.0.0 */ public void commit() { try { getIndexWriter().commit(); + commitOperationCounter.increment(); } catch (CorruptIndexException e) { handleCorruptIndexException(e); } catch (IOException e) { - LOGGER.error("Error committing index", e); + logger.error("Error committing index", e); Notification.dispatch(new Notification() .scope(NotificationScope.SYSTEM) .group(NotificationGroup.INDEXING_SERVICE) - .title(NotificationConstants.Title.CORE_INDEXING_SERVICES) - .content("Error committing index. Check log for details. " + e.getMessage()) + .title(getNotificationTitle()) + .content("Error committing index: %s; Check log for details.".formatted(e.getMessage())) .level(NotificationLevel.ERROR) ); } } protected void handleCorruptIndexException(CorruptIndexException e) { - LOGGER.error("Corrupted Lucene index detected", e); + LOGGER.error("Corrupted index detected", e); Notification.dispatch(new Notification() .scope(NotificationScope.SYSTEM) .group(NotificationGroup.INDEXING_SERVICE) - .title(NotificationConstants.Title.CORE_INDEXING_SERVICES + "(" + indexType.name().toLowerCase() + ")") - .content("Corrupted Lucene index detected. Check log for details. " + e.getMessage()) + .title(getNotificationTitle()) + .content("Corrupted index detected: %s; Check log for details.".formatted(e.getMessage())) .level(NotificationLevel.ERROR) ); LOGGER.info("Trying to rebuild the corrupted index " + indexType.name()); @@ -274,6 +399,7 @@ protected void handleCorruptIndexException(CorruptIndexException e) { /** * Closes the IndexWriter. + * * @since 3.0.0 */ public void close() { @@ -286,14 +412,25 @@ public void close() { // do nothing... } } + + if (docsRamTotalGauge != null) { + docsRamTotalGauge.close(); + } + if (ramBytesUsedGauge != null) { + ramBytesUsedGauge.close(); + } + if (numDocsGauge != null) { + numDocsGauge.close(); + } } /** * Adds a Field to a Document. - * @param doc the Lucene Document to add a field to - * @param name the name of the field - * @param value the value of the field - * @param store storage options + * + * @param doc the Lucene Document to add a field to + * @param name the name of the field + * @param value the value of the field + * @param store storage options * @param tokenize specifies if the field should be tokenized or not * @since 3.0.0 */ @@ -310,25 +447,11 @@ protected void addField(final Document doc, final String name, String value, fin doc.add(field); } - /** - * Updates a Field in a Document. - * @param doc the Lucene Document to update the field in - * @param name the name of the field - * @param value the value of the field - * @since 3.0.0 - */ - protected void updateField(final Document doc, final String name, String value) { - if (StringUtils.isBlank(value)) { - value = ""; - } - final Field field = (Field) doc.getField(name); - field.setStringValue(value); - } - /** * Retrieves a specific Lucene Document for the specified Object, or null if not found. + * * @param fieldName the name of the field - * @param uuid the UUID to retrieve a Document for + * @param uuid the UUID to retrieve a Document for * @return a Lucene Document * @since 3.0.0 */ @@ -362,6 +485,7 @@ protected Document getDocument(final String fieldName, final String uuid) { /** * Returns the directory where this index is located. + * * @return a File object * @since 3.4.0 */ @@ -373,6 +497,7 @@ private static File getIndexDirectory(final IndexType indexType) { /** * Deletes the index directory. This method should be both overwritten and called via overwriting method. + * * @since 3.4.0 */ public void reindex() { @@ -391,6 +516,7 @@ public void reindex() { /** * Deletes the index directory. + * * @since 3.4.0 */ public static void delete(final IndexType indexType) { @@ -411,8 +537,8 @@ public static void delete(final IndexType indexType) { public static void ensureIndexesExists() { Arrays.stream(IndexManager.IndexType.values()).forEach(indexType -> { if (!isIndexHealthy(indexType)) { - LOGGER.info("(Re)Building index "+indexType.name().toLowerCase()); - LOGGER.debug("Dispatching event to reindex "+indexType.name().toLowerCase()); + LOGGER.info("(Re)Building index " + indexType.name().toLowerCase()); + LOGGER.debug("Dispatching event to reindex " + indexType.name().toLowerCase()); Event.dispatch(new IndexEvent(IndexEvent.Action.REINDEX, indexType.getClazz())); } }); @@ -422,11 +548,11 @@ public static void ensureIndexesExists() { * Check that the index exists and is not corrupted */ private static boolean isIndexHealthy(final IndexType indexType) { - LOGGER.info("Checking the health of index "+indexType.name()); + LOGGER.info("Checking the health of index " + indexType.name()); File indexDirectoryFile = getIndexDirectory(indexType); - LOGGER.debug("Checking FS directory "+indexDirectoryFile.toPath()); - if(!indexDirectoryFile.exists()) { - LOGGER.warn("The index "+indexType.name()+" does not exist"); + LOGGER.debug("Checking FS directory " + indexDirectoryFile.toPath()); + if (!indexDirectoryFile.exists()) { + LOGGER.warn("The index " + indexType.name() + " does not exist"); return false; } LOGGER.debug("Checking lucene index health"); @@ -441,14 +567,14 @@ private static boolean isIndexHealthy(final IndexType indexType) { luceneIndexDirectory = FSDirectory.open(indexDirectoryFile.toPath()); checkIndex = new CheckIndex(luceneIndexDirectory); checkIndex.setFailFast(true); - if(LOGGER.isDebugEnabled()) { + if (LOGGER.isDebugEnabled()) { checkIndex.setInfoStream(System.out); } else { checkIndex.setInfoStream(new NullPrintStream()); } CheckIndex.Status status = checkIndex.checkIndex(); - if(status.clean) { - LOGGER.info("The index "+indexType.name()+" is healthy"); + if (status.clean) { + LOGGER.info("The index " + indexType.name() + " is healthy"); } else { LOGGER.error("The index " + indexType.name().toLowerCase() + " seems to be corrupted"); } @@ -481,18 +607,18 @@ public static void checkIndexesConsistency() { SEARCH_INDEXES_CONSISTENCY_CHECK_DELTA_THRESHOLD.getGroupName(), SEARCH_INDEXES_CONSISTENCY_CHECK_DELTA_THRESHOLD.getPropertyName()); double deltaThreshold = Double.parseDouble(deltaThresholdProperty.getPropertyValue()); double databaseEntityCount = qm.getCount(indexType.getClazz()); - LOGGER.info("Database entity count for type "+indexType.name()+" : "+databaseEntityCount); + LOGGER.info("Database entity count for type " + indexType.name() + " : " + databaseEntityCount); IndexManager indexManager = IndexManagerFactory.getIndexManager(indexType.getClazz()); indexManager.ensureDirectoryReaderOpen(); double indexDocumentCount = indexManager.searchReader.numDocs(); - LOGGER.info("Index document count for type "+indexType.name()+" : "+indexDocumentCount); - double max = Math.max(Math.max(databaseEntityCount, indexDocumentCount),1); - double delta = 100 * (Math.abs(databaseEntityCount-indexDocumentCount) / max); + LOGGER.info("Index document count for type " + indexType.name() + " : " + indexDocumentCount); + double max = Math.max(Math.max(databaseEntityCount, indexDocumentCount), 1); + double delta = 100 * (Math.abs(databaseEntityCount - indexDocumentCount) / max); delta = Math.max(Math.round(delta), 1); - LOGGER.info("Delta ratio for type "+indexType.name()+" : "+delta+"%"); - if(delta > deltaThreshold) { - LOGGER.info("Delta ratio is above the threshold of "+deltaThresholdProperty.getPropertyValue()+"%"); - LOGGER.debug("Dispatching event to reindex "+indexType.name().toLowerCase()); + LOGGER.info("Delta ratio for type " + indexType.name() + " : " + delta + "%"); + if (delta > deltaThreshold) { + LOGGER.info("Delta ratio is above the threshold of " + deltaThresholdProperty.getPropertyValue() + "%"); + LOGGER.debug("Dispatching event to reindex " + indexType.name().toLowerCase()); Event.dispatch(new IndexEvent(IndexEvent.Action.REINDEX, indexType.getClazz())); } } catch (IOException e) { @@ -507,4 +633,23 @@ public static void checkIndexesConsistency() { } }); } + + private String getNotificationTitle() { + if (this instanceof ComponentIndexer) { + return NotificationConstants.Title.COMPONENT_INDEXER; + } else if (this instanceof LicenseIndexer) { + return NotificationConstants.Title.LICENSE_INDEXER; + } else if (this instanceof ProjectIndexer) { + return NotificationConstants.Title.PROJECT_INDEXER; + } else if (this instanceof ServiceComponentIndexer) { + return NotificationConstants.Title.SERVICECOMPONENT_INDEXER; + } else if (this instanceof VulnerabilityIndexer) { + return NotificationConstants.Title.VULNERABILITY_INDEXER; + } else if (this instanceof VulnerableSoftwareIndexer) { + return NotificationConstants.Title.VULNERABLESOFTWARE_INDEXER; + } + + throw new IllegalStateException("Unrecognized indexer class %s".formatted(getClass())); + } + } diff --git a/src/main/java/org/dependencytrack/search/IndexManagerFactory.java b/src/main/java/org/dependencytrack/search/IndexManagerFactory.java index cc7d803bb9..f380edd7cd 100644 --- a/src/main/java/org/dependencytrack/search/IndexManagerFactory.java +++ b/src/main/java/org/dependencytrack/search/IndexManagerFactory.java @@ -51,6 +51,8 @@ public static ObjectIndexer getIndexManager(final Inde @Override public void add(final DummyDocument object) { } @Override + public void update(final DummyDocument object) { } + @Override public void remove(final DummyDocument object) { } @Override public void commit() { } diff --git a/src/main/java/org/dependencytrack/search/LicenseIndexer.java b/src/main/java/org/dependencytrack/search/LicenseIndexer.java index 6844107876..9e5fc908de 100644 --- a/src/main/java/org/dependencytrack/search/LicenseIndexer.java +++ b/src/main/java/org/dependencytrack/search/LicenseIndexer.java @@ -19,21 +19,14 @@ package org.dependencytrack.search; import alpine.common.logging.Logger; -import alpine.notification.Notification; -import alpine.notification.NotificationLevel; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; -import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.Term; import org.dependencytrack.model.License; -import org.dependencytrack.notification.NotificationConstants; -import org.dependencytrack.notification.NotificationGroup; -import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.search.document.LicenseDocument; import javax.jdo.Query; -import java.io.IOException; import java.time.Duration; import java.util.List; @@ -48,7 +41,7 @@ public final class LicenseIndexer extends IndexManager implements ObjectIndexer< private static final Logger LOGGER = Logger.getLogger(LicenseIndexer.class); private static final LicenseIndexer INSTANCE = new LicenseIndexer(); - protected static LicenseIndexer getInstance() { + static LicenseIndexer getInstance() { return INSTANCE; } @@ -70,25 +63,15 @@ public String[] getSearchFields() { * @param license A persisted License object. */ public void add(final LicenseDocument license) { - final Document doc = new Document(); - addField(doc, IndexConstants.LICENSE_UUID, license.uuid().toString(), Field.Store.YES, false); - addField(doc, IndexConstants.LICENSE_LICENSEID, license.licenseId(), Field.Store.YES, true); - addField(doc, IndexConstants.LICENSE_NAME, license.name(), Field.Store.YES, true); + final Document doc = convertToDocument(license); + addDocument(doc); + } - try { - getIndexWriter().addDocument(doc); - } catch (CorruptIndexException e) { - handleCorruptIndexException(e); - } catch (IOException e) { - LOGGER.error("An error occurred while adding a license to the index", e); - Notification.dispatch(new Notification() - .scope(NotificationScope.SYSTEM) - .group(NotificationGroup.INDEXING_SERVICE) - .title(NotificationConstants.Title.LICENSE_INDEXER) - .content("An error occurred while adding a license to the index. Check log for details. " + e.getMessage()) - .level(NotificationLevel.ERROR) - ); - } + @Override + public void update(final LicenseDocument license) { + final Term term = convertToTerm(license); + final Document doc = convertToDocument(license); + updateDocument(term, doc); } /** @@ -97,20 +80,8 @@ public void add(final LicenseDocument license) { * @param license A persisted License object. */ public void remove(final LicenseDocument license) { - try { - getIndexWriter().deleteDocuments(new Term(IndexConstants.LICENSE_UUID, license.uuid().toString())); - } catch (CorruptIndexException e) { - handleCorruptIndexException(e); - } catch (IOException e) { - LOGGER.error("An error occurred while removing a license from the index", e); - Notification.dispatch(new Notification() - .scope(NotificationScope.SYSTEM) - .group(NotificationGroup.INDEXING_SERVICE) - .title(NotificationConstants.Title.LICENSE_INDEXER) - .content("An error occurred while removing a license from the index. Check log for details. " + e.getMessage()) - .level(NotificationLevel.ERROR) - ); - } + final Term term = convertToTerm(license); + deleteDocuments(term); } /** @@ -153,4 +124,16 @@ private static List fetchNext(final QueryManager qm, final Long } } + private Document convertToDocument(final LicenseDocument license) { + final var doc = new Document(); + addField(doc, IndexConstants.LICENSE_UUID, license.uuid().toString(), Field.Store.YES, false); + addField(doc, IndexConstants.LICENSE_LICENSEID, license.licenseId(), Field.Store.YES, true); + addField(doc, IndexConstants.LICENSE_NAME, license.name(), Field.Store.YES, true); + return doc; + } + + private static Term convertToTerm(final LicenseDocument license) { + return new Term(IndexConstants.LICENSE_UUID, license.uuid().toString()); + } + } diff --git a/src/main/java/org/dependencytrack/search/LoggingInfoStream.java b/src/main/java/org/dependencytrack/search/LoggingInfoStream.java new file mode 100644 index 0000000000..e1c5645d95 --- /dev/null +++ b/src/main/java/org/dependencytrack/search/LoggingInfoStream.java @@ -0,0 +1,85 @@ +/* + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.search; + +import org.apache.lucene.util.InfoStream; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * Lucene {@link InfoStream} implementation for Dependency-Track's SLF4J-based logging mechanism. + *

    + * Logging of Lucene messages can be enabled on a per-index basis: + * + *

    {@code
    + *     
    + *         
    + *     
    + * }
    + *

    + * It is also possible to enable logging selectively for specific Lucene components: + * + *

    {@code
    + *     
    + *         
    + *     
    + * }
    + *

    + * A few known Lucene components and their abbreviations are: + *

      + *
    • {@link org.apache.lucene.index.DocumentsWriterFlushControl} (DWFC)
    • + *
    • {@link org.apache.lucene.index.DocumentsWriter} (DW)
    • + *
    • {@link org.apache.lucene.index.IndexFileDeleter} (IFD)
    • + *
    • {@link org.apache.lucene.index.IndexWriter} (IW)
    • + *
    • {@link org.apache.lucene.index.MergePolicy} (MP)
    • + *
    • {@link org.apache.lucene.index.MergeScheduler} (MS)
    • + *
    + * + * @since 4.11.0 + */ +@SuppressWarnings("JavadocReference") +class LoggingInfoStream extends InfoStream { + + private final Class parentLoggerClass; + + LoggingInfoStream(final Class parentLoggerClass) { + this.parentLoggerClass = parentLoggerClass; + } + + @Override + public void message(final String component, final String message) { + getLogger(component).debug(message); + } + + @Override + public boolean isEnabled(final String component) { + return getLogger(component).isDebugEnabled(); + } + + @Override + public void close() throws IOException { + } + + private org.slf4j.Logger getLogger(final String component) { + final String loggerName = "%s.lucene.%s".formatted(parentLoggerClass.getName(), component); + return LoggerFactory.getLogger(loggerName); + } + +} diff --git a/src/main/java/org/dependencytrack/search/ObjectIndexer.java b/src/main/java/org/dependencytrack/search/ObjectIndexer.java index dfe2d183e7..4dbd215cc0 100644 --- a/src/main/java/org/dependencytrack/search/ObjectIndexer.java +++ b/src/main/java/org/dependencytrack/search/ObjectIndexer.java @@ -41,6 +41,13 @@ public interface ObjectIndexer { */ void add(T object); + /** + * Update object in index. + * @param object the object to update + * @since 4.11.0 + */ + void update(T object); + /** * Remove object from index. * @param object the object to remove diff --git a/src/main/java/org/dependencytrack/search/ProjectIndexer.java b/src/main/java/org/dependencytrack/search/ProjectIndexer.java index 430937aabd..9c4ebed901 100644 --- a/src/main/java/org/dependencytrack/search/ProjectIndexer.java +++ b/src/main/java/org/dependencytrack/search/ProjectIndexer.java @@ -19,21 +19,14 @@ package org.dependencytrack.search; import alpine.common.logging.Logger; -import alpine.notification.Notification; -import alpine.notification.NotificationLevel; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; -import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.Term; import org.dependencytrack.model.Project; -import org.dependencytrack.notification.NotificationConstants; -import org.dependencytrack.notification.NotificationGroup; -import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.search.document.ProjectDocument; import javax.jdo.Query; -import java.io.IOException; import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; @@ -50,7 +43,7 @@ public final class ProjectIndexer extends IndexManager implements ObjectIndexer< private static final Logger LOGGER = Logger.getLogger(ProjectIndexer.class); private static final ProjectIndexer INSTANCE = new ProjectIndexer(); - protected static ProjectIndexer getInstance() { + static ProjectIndexer getInstance() { return INSTANCE; } @@ -72,39 +65,15 @@ public String[] getSearchFields() { * @param project A persisted Project object. */ public void add(final ProjectDocument project) { - final Document doc = new Document(); - addField(doc, IndexConstants.PROJECT_UUID, project.uuid().toString(), Field.Store.YES, false); - addField(doc, IndexConstants.PROJECT_NAME, project.name(), Field.Store.YES, true); - addField(doc, IndexConstants.PROJECT_VERSION, project.version(), Field.Store.YES, false); - addField(doc, IndexConstants.PROJECT_DESCRIPTION, project.description(), Field.Store.YES, true); - - /* - // There's going to potentially be confidential information in the project properties. Do not index. - - final StringBuilder sb = new StringBuilder(); - if (project.getProperties() != null) { - for (ProjectProperty property : project.getProperties()) { - sb.append(property.getPropertyValue()).append(" "); - } - } - - addField(doc, IndexConstants.PROJECT_PROPERTIES, sb.toString().trim(), Field.Store.YES, true); - */ + final Document doc = convertToDocument(project); + addDocument(doc); + } - try { - getIndexWriter().addDocument(doc); - } catch (CorruptIndexException e) { - handleCorruptIndexException(e); - } catch (IOException e) { - LOGGER.error("An error occurred while adding a project to the index", e); - Notification.dispatch(new Notification() - .scope(NotificationScope.SYSTEM) - .group(NotificationGroup.INDEXING_SERVICE) - .title(NotificationConstants.Title.PROJECT_INDEXER) - .content("An error occurred while adding a project to the index. Check log for details. " + e.getMessage()) - .level(NotificationLevel.ERROR) - ); - } + @Override + public void update(final ProjectDocument project) { + final Term term = convertToTerm(project); + final Document doc = convertToDocument(project); + updateDocument(term, doc); } /** @@ -113,20 +82,8 @@ public void add(final ProjectDocument project) { * @param project A persisted Project object. */ public void remove(final ProjectDocument project) { - try { - getIndexWriter().deleteDocuments(new Term(IndexConstants.PROJECT_UUID, project.uuid().toString())); - } catch (CorruptIndexException e) { - handleCorruptIndexException(e); - } catch (IOException e) { - LOGGER.error("An error occurred while removing a project from the index", e); - Notification.dispatch(new Notification() - .scope(NotificationScope.SYSTEM) - .group(NotificationGroup.INDEXING_SERVICE) - .title(NotificationConstants.Title.PROJECT_INDEXER) - .content("An error occurred while removing a project from the index. Check log for details. " + e.getMessage()) - .level(NotificationLevel.ERROR) - ); - } + final Term term = convertToTerm(project); + deleteDocuments(term); } /** @@ -174,4 +131,31 @@ private static List fetchNext(final QueryManager qm, final Long } } + private Document convertToDocument(final ProjectDocument project) { + final var doc = new Document(); + addField(doc, IndexConstants.PROJECT_UUID, project.uuid().toString(), Field.Store.YES, false); + addField(doc, IndexConstants.PROJECT_NAME, project.name(), Field.Store.YES, true); + addField(doc, IndexConstants.PROJECT_VERSION, project.version(), Field.Store.YES, false); + addField(doc, IndexConstants.PROJECT_DESCRIPTION, project.description(), Field.Store.YES, true); + + /* + // There's going to potentially be confidential information in the project properties. Do not index. + + final StringBuilder sb = new StringBuilder(); + if (project.getProperties() != null) { + for (ProjectProperty property : project.getProperties()) { + sb.append(property.getPropertyValue()).append(" "); + } + } + + addField(doc, IndexConstants.PROJECT_PROPERTIES, sb.toString().trim(), Field.Store.YES, true); + */ + + return doc; + } + + private static Term convertToTerm(final ProjectDocument project) { + return new Term(IndexConstants.PROJECT_UUID, project.uuid().toString()); + } + } diff --git a/src/main/java/org/dependencytrack/search/ServiceComponentIndexer.java b/src/main/java/org/dependencytrack/search/ServiceComponentIndexer.java index d909a256f5..c8f3beb7cb 100644 --- a/src/main/java/org/dependencytrack/search/ServiceComponentIndexer.java +++ b/src/main/java/org/dependencytrack/search/ServiceComponentIndexer.java @@ -19,21 +19,14 @@ package org.dependencytrack.search; import alpine.common.logging.Logger; -import alpine.notification.Notification; -import alpine.notification.NotificationLevel; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; -import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.Term; import org.dependencytrack.model.ServiceComponent; -import org.dependencytrack.notification.NotificationConstants; -import org.dependencytrack.notification.NotificationGroup; -import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.search.document.ServiceComponentDocument; import javax.jdo.Query; -import java.io.IOException; import java.time.Duration; import java.util.List; @@ -48,7 +41,7 @@ public final class ServiceComponentIndexer extends IndexManager implements Objec private static final Logger LOGGER = Logger.getLogger(ServiceComponentIndexer.class); private static final ServiceComponentIndexer INSTANCE = new ServiceComponentIndexer(); - protected static ServiceComponentIndexer getInstance() { + static ServiceComponentIndexer getInstance() { return INSTANCE; } @@ -70,28 +63,15 @@ public String[] getSearchFields() { * @param service A persisted ServiceComponent object. */ public void add(final ServiceComponentDocument service) { - final Document doc = new Document(); - addField(doc, IndexConstants.SERVICECOMPONENT_UUID, service.uuid().toString(), Field.Store.YES, false); - addField(doc, IndexConstants.SERVICECOMPONENT_NAME, service.name(), Field.Store.YES, true); - addField(doc, IndexConstants.SERVICECOMPONENT_GROUP, service.group(), Field.Store.YES, true); - addField(doc, IndexConstants.SERVICECOMPONENT_VERSION, service.version(), Field.Store.YES, false); - // TODO: addField(doc, IndexConstants.SERVICECOMPONENT_URL, service.getUrl(), Field.Store.YES, true); - addField(doc, IndexConstants.SERVICECOMPONENT_DESCRIPTION, service.description(), Field.Store.YES, true); + final Document doc = convertToDocument(service); + addDocument(doc); + } - try { - getIndexWriter().addDocument(doc); - } catch (CorruptIndexException e) { - handleCorruptIndexException(e); - } catch (IOException e) { - LOGGER.error("An error occurred while adding service to index", e); - Notification.dispatch(new Notification() - .scope(NotificationScope.SYSTEM) - .group(NotificationGroup.INDEXING_SERVICE) - .title(NotificationConstants.Title.SERVICECOMPONENT_INDEXER) - .content("An error occurred while adding service to index. Check log for details. " + e.getMessage()) - .level(NotificationLevel.ERROR) - ); - } + @Override + public void update(final ServiceComponentDocument service) { + final Term term = convertToTerm(service); + final Document doc = convertToDocument(service); + updateDocument(term, doc); } /** @@ -100,20 +80,8 @@ public void add(final ServiceComponentDocument service) { * @param service A persisted ServiceComponent object. */ public void remove(final ServiceComponentDocument service) { - try { - getIndexWriter().deleteDocuments(new Term(IndexConstants.SERVICECOMPONENT_UUID, service.uuid().toString())); - } catch (CorruptIndexException e) { - handleCorruptIndexException(e); - } catch (IOException e) { - LOGGER.error("An error occurred while removing a service from the index", e); - Notification.dispatch(new Notification() - .scope(NotificationScope.SYSTEM) - .group(NotificationGroup.INDEXING_SERVICE) - .title(NotificationConstants.Title.SERVICECOMPONENT_INDEXER) - .content("An error occurred while removing a service from the index. Check log for details. " + e.getMessage()) - .level(NotificationLevel.ERROR) - ); - } + final Term term = convertToTerm(service); + deleteDocuments(term); } /** @@ -156,4 +124,19 @@ private static List fetchNext(final QueryManager qm, f } } + private Document convertToDocument(final ServiceComponentDocument service) { + final var doc = new Document(); + addField(doc, IndexConstants.SERVICECOMPONENT_UUID, service.uuid().toString(), Field.Store.YES, false); + addField(doc, IndexConstants.SERVICECOMPONENT_NAME, service.name(), Field.Store.YES, true); + addField(doc, IndexConstants.SERVICECOMPONENT_GROUP, service.group(), Field.Store.YES, true); + addField(doc, IndexConstants.SERVICECOMPONENT_VERSION, service.version(), Field.Store.YES, false); + // TODO: addField(doc, IndexConstants.SERVICECOMPONENT_URL, service.getUrl(), Field.Store.YES, true); + addField(doc, IndexConstants.SERVICECOMPONENT_DESCRIPTION, service.description(), Field.Store.YES, true); + return doc; + } + + private static Term convertToTerm(final ServiceComponentDocument service) { + return new Term(IndexConstants.SERVICECOMPONENT_UUID, service.uuid().toString()); + } + } diff --git a/src/main/java/org/dependencytrack/search/VulnerabilityIndexer.java b/src/main/java/org/dependencytrack/search/VulnerabilityIndexer.java index bfb5b0cb54..4664945c6e 100644 --- a/src/main/java/org/dependencytrack/search/VulnerabilityIndexer.java +++ b/src/main/java/org/dependencytrack/search/VulnerabilityIndexer.java @@ -19,21 +19,14 @@ package org.dependencytrack.search; import alpine.common.logging.Logger; -import alpine.notification.Notification; -import alpine.notification.NotificationLevel; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; -import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.Term; import org.dependencytrack.model.Vulnerability; -import org.dependencytrack.notification.NotificationConstants; -import org.dependencytrack.notification.NotificationGroup; -import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.search.document.VulnerabilityDocument; import javax.jdo.Query; -import java.io.IOException; import java.time.Duration; import java.util.List; @@ -48,7 +41,7 @@ public final class VulnerabilityIndexer extends IndexManager implements ObjectIn private static final Logger LOGGER = Logger.getLogger(VulnerabilityIndexer.class); private static final VulnerabilityIndexer INSTANCE = new VulnerabilityIndexer(); - protected static VulnerabilityIndexer getInstance() { + static VulnerabilityIndexer getInstance() { return INSTANCE; } @@ -70,26 +63,15 @@ public String[] getSearchFields() { * @param vulnerability A persisted Vulnerability object. */ public void add(final VulnerabilityDocument vulnerability) { - final Document doc = new Document(); - addField(doc, IndexConstants.VULNERABILITY_UUID, vulnerability.uuid().toString(), Field.Store.YES, false); - addField(doc, IndexConstants.VULNERABILITY_VULNID, vulnerability.vulnId(), Field.Store.YES, true); - addField(doc, IndexConstants.VULNERABILITY_DESCRIPTION, vulnerability.description(), Field.Store.YES, true); - addField(doc, IndexConstants.VULNERABILITY_SOURCE, vulnerability.source(), Field.Store.YES, false); + final Document doc = convertToDocument(vulnerability); + addDocument(doc); + } - try { - getIndexWriter().addDocument(doc); - } catch (CorruptIndexException e) { - handleCorruptIndexException(e); - } catch (IOException e) { - LOGGER.error("An error occurred while adding a vulnerability to the index", e); - Notification.dispatch(new Notification() - .scope(NotificationScope.SYSTEM) - .group(NotificationGroup.INDEXING_SERVICE) - .title(NotificationConstants.Title.VULNERABILITY_INDEXER) - .content("An error occurred while adding a vulnerability to the index. Check log for details. " + e.getMessage()) - .level(NotificationLevel.ERROR) - ); - } + @Override + public void update(final VulnerabilityDocument vuln) { + final Term term = convertToTerm(vuln); + final Document doc = convertToDocument(vuln); + updateDocument(term, doc); } /** @@ -98,20 +80,8 @@ public void add(final VulnerabilityDocument vulnerability) { * @param vulnerability A persisted Vulnerability object. */ public void remove(final VulnerabilityDocument vulnerability) { - try { - getIndexWriter().deleteDocuments(new Term(IndexConstants.VULNERABILITY_UUID, vulnerability.uuid().toString())); - } catch (CorruptIndexException e) { - handleCorruptIndexException(e); - } catch (IOException e) { - LOGGER.error("An error occurred while removing a vulnerability from the index", e); - Notification.dispatch(new Notification() - .scope(NotificationScope.SYSTEM) - .group(NotificationGroup.INDEXING_SERVICE) - .title(NotificationConstants.Title.VULNERABILITY_INDEXER) - .content("An error occurred while removing a vulnerability from the index. Check log for details. " + e.getMessage()) - .level(NotificationLevel.ERROR) - ); - } + final Term term = convertToTerm(vulnerability); + deleteDocuments(term); } /** @@ -154,4 +124,17 @@ private static List fetchNext(final QueryManager qm, fina } } + private Document convertToDocument(final VulnerabilityDocument vuln) { + final var doc = new Document(); + addField(doc, IndexConstants.VULNERABILITY_UUID, vuln.uuid().toString(), Field.Store.YES, false); + addField(doc, IndexConstants.VULNERABILITY_VULNID, vuln.vulnId(), Field.Store.YES, true); + addField(doc, IndexConstants.VULNERABILITY_DESCRIPTION, vuln.description(), Field.Store.YES, true); + addField(doc, IndexConstants.VULNERABILITY_SOURCE, vuln.source(), Field.Store.YES, false); + return doc; + } + + private static Term convertToTerm(final VulnerabilityDocument vuln) { + return new Term(IndexConstants.VULNERABILITY_UUID, vuln.uuid().toString()); + } + } diff --git a/src/main/java/org/dependencytrack/search/VulnerableSoftwareIndexer.java b/src/main/java/org/dependencytrack/search/VulnerableSoftwareIndexer.java index b7485ae432..2f13b518a5 100644 --- a/src/main/java/org/dependencytrack/search/VulnerableSoftwareIndexer.java +++ b/src/main/java/org/dependencytrack/search/VulnerableSoftwareIndexer.java @@ -19,21 +19,14 @@ package org.dependencytrack.search; import alpine.common.logging.Logger; -import alpine.notification.Notification; -import alpine.notification.NotificationLevel; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; -import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.Term; import org.dependencytrack.model.VulnerableSoftware; -import org.dependencytrack.notification.NotificationConstants; -import org.dependencytrack.notification.NotificationGroup; -import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.search.document.VulnerableSoftwareDocument; import javax.jdo.Query; -import java.io.IOException; import java.time.Duration; import java.util.List; @@ -48,7 +41,7 @@ public final class VulnerableSoftwareIndexer extends IndexManager implements Obj private static final Logger LOGGER = Logger.getLogger(VulnerableSoftwareIndexer.class); private static final VulnerableSoftwareIndexer INSTANCE = new VulnerableSoftwareIndexer(); - protected static VulnerableSoftwareIndexer getInstance() { + static VulnerableSoftwareIndexer getInstance() { return INSTANCE; } @@ -70,29 +63,15 @@ public String[] getSearchFields() { * @param vs A persisted VulnerableSoftware object. */ public void add(final VulnerableSoftwareDocument vs) { - final Document doc = new Document(); - addField(doc, IndexConstants.VULNERABLESOFTWARE_UUID, vs.uuid().toString(), Field.Store.YES, false); - addField(doc, IndexConstants.VULNERABLESOFTWARE_CPE_22, vs.cpe22(), Field.Store.YES, false); - addField(doc, IndexConstants.VULNERABLESOFTWARE_CPE_23, vs.cpe23(), Field.Store.YES, false); - addField(doc, IndexConstants.VULNERABLESOFTWARE_VENDOR, vs.vendor(), Field.Store.YES, true); - addField(doc, IndexConstants.VULNERABLESOFTWARE_PRODUCT, vs.product(), Field.Store.YES, true); - addField(doc, IndexConstants.VULNERABLESOFTWARE_VERSION, vs.version(), Field.Store.YES, true); - //todo: index the affected version range fields as well + final Document doc = convertToDocument(vs); + addDocument(doc); + } - try { - getIndexWriter().addDocument(doc); - } catch (CorruptIndexException e) { - handleCorruptIndexException(e); - } catch (IOException e) { - LOGGER.error("An error occurred while adding a VulnerableSoftware to the index", e); - Notification.dispatch(new Notification() - .scope(NotificationScope.SYSTEM) - .group(NotificationGroup.INDEXING_SERVICE) - .title(NotificationConstants.Title.VULNERABLESOFTWARE_INDEXER) - .content("An error occurred while adding a VulnerableSoftware to the index. Check log for details. " + e.getMessage()) - .level(NotificationLevel.ERROR) - ); - } + @Override + public void update(final VulnerableSoftwareDocument vs) { + final Term term = convertToTerm(vs); + final Document doc = convertToDocument(vs); + updateDocument(term, doc); } /** @@ -101,20 +80,8 @@ public void add(final VulnerableSoftwareDocument vs) { * @param vs A persisted VulnerableSoftware object. */ public void remove(final VulnerableSoftwareDocument vs) { - try { - getIndexWriter().deleteDocuments(new Term(IndexConstants.VULNERABLESOFTWARE_UUID, vs.uuid().toString())); - } catch (CorruptIndexException e) { - handleCorruptIndexException(e); - } catch (IOException e) { - LOGGER.error("An error occurred while removing a VulnerableSoftware from the index", e); - Notification.dispatch(new Notification() - .scope(NotificationScope.SYSTEM) - .group(NotificationGroup.INDEXING_SERVICE) - .title(NotificationConstants.Title.VULNERABLESOFTWARE_INDEXER) - .content("An error occurred while removing a VulnerableSoftware from the index. Check log for details. " + e.getMessage()) - .level(NotificationLevel.ERROR) - ); - } + final Term term = convertToTerm(vs); + deleteDocuments(term); } /** @@ -156,4 +123,20 @@ private static List fetchNext(final QueryManager qm, } } + private Document convertToDocument(final VulnerableSoftwareDocument vs) { + final var doc = new Document(); + addField(doc, IndexConstants.VULNERABLESOFTWARE_UUID, vs.uuid().toString(), Field.Store.YES, false); + addField(doc, IndexConstants.VULNERABLESOFTWARE_CPE_22, vs.cpe22(), Field.Store.YES, false); + addField(doc, IndexConstants.VULNERABLESOFTWARE_CPE_23, vs.cpe23(), Field.Store.YES, false); + addField(doc, IndexConstants.VULNERABLESOFTWARE_VENDOR, vs.vendor(), Field.Store.YES, true); + addField(doc, IndexConstants.VULNERABLESOFTWARE_PRODUCT, vs.product(), Field.Store.YES, true); + addField(doc, IndexConstants.VULNERABLESOFTWARE_VERSION, vs.version(), Field.Store.YES, true); + //todo: index the affected version range fields as well + return doc; + } + + private static Term convertToTerm(final VulnerableSoftwareDocument vs) { + return new Term(IndexConstants.VULNERABLESOFTWARE_UUID, vs.uuid().toString()); + } + } diff --git a/src/main/java/org/dependencytrack/tasks/IndexTask.java b/src/main/java/org/dependencytrack/tasks/IndexTask.java index 6c4234eec7..1d7198fad4 100644 --- a/src/main/java/org/dependencytrack/tasks/IndexTask.java +++ b/src/main/java/org/dependencytrack/tasks/IndexTask.java @@ -18,7 +18,6 @@ */ package org.dependencytrack.tasks; -import alpine.common.logging.Logger; import alpine.common.metrics.Metrics; import alpine.event.framework.Event; import alpine.event.framework.Subscriber; @@ -36,8 +35,6 @@ */ public class IndexTask implements Subscriber { - private static final Logger LOGGER = Logger.getLogger(IndexTask.class); - /** * {@inheritDoc} */ @@ -53,12 +50,11 @@ public void inform(final Event e) { final ObjectIndexer indexManager = IndexManagerFactory.getIndexManager(event); if (IndexEvent.Action.CREATE == event.getAction()) { - indexManager.add((event).getDocument()); + indexManager.add(event.getDocument()); } else if (IndexEvent.Action.UPDATE == event.getAction()) { - indexManager.remove((event).getDocument()); - indexManager.add((event).getDocument()); + indexManager.update(event.getDocument()); } else if (IndexEvent.Action.DELETE == event.getAction()) { - indexManager.remove((event).getDocument()); + indexManager.remove(event.getDocument()); } else if (IndexEvent.Action.COMMIT == event.getAction()) { indexManager.commit(); } else if (IndexEvent.Action.REINDEX == event.getAction()) { From 15d8e6b7d814f9ef43dd3d9c12ea0610ccf54fb1 Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 16 Apr 2024 21:52:16 +0200 Subject: [PATCH 079/412] Fix license header; Add metrics documentation Signed-off-by: nscuro --- docs/_docs/getting-started/monitoring.md | 17 +++++++++++++++++ .../search/LoggingInfoStream.java | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/_docs/getting-started/monitoring.md b/docs/_docs/getting-started/monitoring.md index 7e41fa6cfa..2b729a852c 100644 --- a/docs/_docs/getting-started/monitoring.md +++ b/docs/_docs/getting-started/monitoring.md @@ -310,6 +310,23 @@ doing keeping up with the work it's being exposed to. For example, a constantly value combined with a high number of `executor_queued_tasks` may indicate that the configured `alpine.worker.pool.size` is too small for the workload at hand. +##### Search Indexes + +```yaml +# HELP search_index_ram_used_bytes Memory usage of the index in bytes +# TYPE search_index_ram_used_bytes gauge +search_index_ram_used_bytes{index="",} 0.0 +# HELP search_index_docs_ram_total_objects Number of documents currently buffered in RAM +# TYPE search_index_docs_ram_total_objects gauge +search_index_docs_ram_total_objects{index="",} 0.0 +# HELP search_index_docs_total_objects Number of docs in this index, including docs not yet flushed (still in the RAM buffer), and including deletions +# TYPE search_index_docs_total_objects gauge +search_index_docs_total_objects{index="",} 0.0 +# HELP search_index_operations_total Total number of index operations +# TYPE search_index_operations_total counter +search_index_operations_total{index="",operation="",} 0.0 +``` + ##### Retries Dependency-Track will occasionally retry requests to external services. Metrics about this behavior are diff --git a/src/main/java/org/dependencytrack/search/LoggingInfoStream.java b/src/main/java/org/dependencytrack/search/LoggingInfoStream.java index e1c5645d95..6cca4fd3a9 100644 --- a/src/main/java/org/dependencytrack/search/LoggingInfoStream.java +++ b/src/main/java/org/dependencytrack/search/LoggingInfoStream.java @@ -14,7 +14,7 @@ * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) Steve Springett. All Rights Reserved. + * Copyright (c) OWASP Foundation. All Rights Reserved. */ package org.dependencytrack.search; From 58859401c2d7844aabe54aa5831924db4f044139 Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 16 Apr 2024 21:59:44 +0200 Subject: [PATCH 080/412] Remove unnecessary closing of counters Their `close` method is a no-op. Signed-off-by: nscuro --- .../java/org/dependencytrack/search/IndexManager.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/main/java/org/dependencytrack/search/IndexManager.java b/src/main/java/org/dependencytrack/search/IndexManager.java index 78911643f0..b5a1f94c06 100644 --- a/src/main/java/org/dependencytrack/search/IndexManager.java +++ b/src/main/java/org/dependencytrack/search/IndexManager.java @@ -412,16 +412,6 @@ public void close() { // do nothing... } } - - if (docsRamTotalGauge != null) { - docsRamTotalGauge.close(); - } - if (ramBytesUsedGauge != null) { - ramBytesUsedGauge.close(); - } - if (numDocsGauge != null) { - numDocsGauge.close(); - } } /** From 0630b5c5de8d17110389baa510caec4741379d5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Apr 2024 08:18:11 +0000 Subject: [PATCH 081/412] Bump com.google.cloud.sql:cloud-sql-connector-jdbc-sqlserver Bumps com.google.cloud.sql:cloud-sql-connector-jdbc-sqlserver from 1.17.1 to 1.18.0. --- updated-dependencies: - dependency-name: com.google.cloud.sql:cloud-sql-connector-jdbc-sqlserver dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 35028e17e8..83739b9055 100644 --- a/pom.xml +++ b/pom.xml @@ -85,7 +85,7 @@ ${project.parent.version} 4.2.0 10.12.5 - 1.17.1 + 1.18.0 1.16.0 1.16.0 2.1.0 From d09d3038f495733e6d294ba978dc79d95ce046f5 Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 17 Apr 2024 15:29:11 +0200 Subject: [PATCH 082/412] Include pagination parameters in OpenAPI spec Relates to #3608 Signed-off-by: nscuro --- docs/_posts/2024-xx-xx-v4.11.0.md | 2 + .../resources/v1/AccessControlResource.java | 4 ++ .../resources/v1/ComponentResource.java | 6 ++ .../resources/v1/CweResource.java | 2 + .../resources/v1/LicenseGroupResource.java | 2 + .../resources/v1/LicenseResource.java | 2 + .../v1/NotificationRuleResource.java | 3 +- .../resources/v1/PolicyResource.java | 2 + .../resources/v1/PolicyViolationResource.java | 4 ++ .../resources/v1/ProjectResource.java | 8 +++ .../resources/v1/RepositoryResource.java | 4 +- .../resources/v1/ServiceResource.java | 2 + .../resources/v1/TagResource.java | 2 +- .../resources/v1/VulnerabilityResource.java | 4 ++ .../resources/v1/openapi/PaginatedApi.java | 67 +++++++++++++++++++ 15 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/dependencytrack/resources/v1/openapi/PaginatedApi.java diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index af06ee6fc8..6b8ee1bcb9 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -66,6 +66,7 @@ Community input and contributions are explicitly requested. The chart repository * Add support for exporting findings in SARIF format - [apiserver/#3561] * Ingest vulnerability alias information from VulnDB - [apiserver/#3588] * Properly validate UUID request parameters to prevent internal server errors - [apiserver/#3590] +* Document pagination query parameters in OpenAPI specification - [apiserver/#3625] * Show component count in projects list - [frontend/#683] * Add current *fail*, *warn*, and *info* values to bottom of policy violation metrics - [frontend/#707] * Remove unused policy violation widget - [frontend/#710] @@ -229,6 +230,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [apiserver/#3588]: https://github.com/DependencyTrack/dependency-track/pull/3588 [apiserver/#3590]: https://github.com/DependencyTrack/dependency-track/pull/3590 [apiserver/#3595]: https://github.com/DependencyTrack/dependency-track/pull/3595 +[apiserver/#3625]: https://github.com/DependencyTrack/dependency-track/pull/3625 [frontend/#682]: https://github.com/DependencyTrack/frontend/pull/682 [frontend/#683]: https://github.com/DependencyTrack/frontend/pull/683 diff --git a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index 75798be7f0..7c81e19339 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -29,10 +29,12 @@ import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; +import io.swagger.annotations.ResponseHeader; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Project; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; import org.dependencytrack.resources.v1.vo.AclMappingRequest; import javax.validation.Validator; @@ -67,8 +69,10 @@ public class AccessControlResource extends AlpineResource { value = "Returns the projects assigned to the specified team", response = String.class, responseContainer = "List", + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects"), notes = "

    Requires permission ACCESS_MANAGEMENT

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 404, message = "The UUID of the team could not be found"), diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java index 8bd979a0eb..23e90d1252 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java @@ -45,6 +45,7 @@ import org.dependencytrack.model.RepositoryType; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; import org.dependencytrack.util.InternalComponentIdentificationUtil; import javax.validation.Validator; @@ -83,6 +84,7 @@ public class ComponentResource extends AlpineResource { responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of components"), notes = "

    Requires permission VIEW_PORTFOLIO

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), @@ -160,8 +162,10 @@ public Response getComponentByUuid( value = "Returns a list of components that have the specified component identity. This resource accepts coordinates (group, name, version) or purl, cpe, or swidTagId", responseContainer = "List", response = Component.class, + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of components"), notes = "

    Requires permission VIEW_PORTFOLIO

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) @@ -219,8 +223,10 @@ public Response getComponentByIdentity(@ApiParam(value = "The group of the compo value = "Returns a list of components that have the specified hash value", responseContainer = "List", response = Component.class, + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of components"), notes = "

    Requires permission VIEW_PORTFOLIO

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) diff --git a/src/main/java/org/dependencytrack/resources/v1/CweResource.java b/src/main/java/org/dependencytrack/resources/v1/CweResource.java index 79a79e5978..0b46568e8e 100644 --- a/src/main/java/org/dependencytrack/resources/v1/CweResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/CweResource.java @@ -29,6 +29,7 @@ import io.swagger.annotations.ResponseHeader; import org.dependencytrack.model.Cwe; import org.dependencytrack.parser.common.resolver.CweResolver; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -55,6 +56,7 @@ public class CweResource extends AlpineResource { responseContainer = "List", responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of CWEs") ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) diff --git a/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java b/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java index 01cbbe1439..563fce13b7 100644 --- a/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java @@ -34,6 +34,7 @@ import org.dependencytrack.model.LicenseGroup; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; import javax.validation.Validator; import javax.ws.rs.Consumes; @@ -67,6 +68,7 @@ public class LicenseGroupResource extends AlpineResource { responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of license groups"), notes = "

    Requires permission POLICY_MANAGEMENT

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) diff --git a/src/main/java/org/dependencytrack/resources/v1/LicenseResource.java b/src/main/java/org/dependencytrack/resources/v1/LicenseResource.java index d8653e66fe..9a730c0a00 100644 --- a/src/main/java/org/dependencytrack/resources/v1/LicenseResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/LicenseResource.java @@ -32,6 +32,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.License; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; import javax.validation.Validator; import javax.ws.rs.DELETE; @@ -64,6 +65,7 @@ public class LicenseResource extends AlpineResource { responseContainer = "List", responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of licenses") ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java index adfd65a1b0..edbaa2f12d 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java @@ -39,6 +39,7 @@ import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.notification.publisher.SendMailPublisher; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; import javax.validation.Validator; import javax.ws.rs.Consumes; @@ -73,8 +74,8 @@ public class NotificationRuleResource extends AlpineResource { responseContainer = "List", responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of notification rules"), notes = "

    Requires permission SYSTEM_CONFIGURATION

    " - ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) diff --git a/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java b/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java index cb1b18e76b..c382720831 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java @@ -35,6 +35,7 @@ import org.dependencytrack.model.Tag; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; import javax.validation.Validator; import javax.ws.rs.Consumes; @@ -68,6 +69,7 @@ public class PolicyResource extends AlpineResource { responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policies"), notes = "

    Requires permission POLICY_MANAGEMENT

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) diff --git a/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java b/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java index 0b19af88aa..0f2c89b85d 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java @@ -34,6 +34,7 @@ import org.dependencytrack.model.Project; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; import javax.jdo.FetchPlan; import javax.jdo.PersistenceManager; @@ -65,6 +66,7 @@ public class PolicyViolationResource extends AlpineResource { responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policy violations"), notes = "

    Requires permission VIEW_POLICY_VIOLATION

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) @@ -89,6 +91,7 @@ public Response getViolations(@ApiParam(value = "Optionally includes suppressed responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policy violations"), notes = "

    Requires permission VIEW_POLICY_VIOLATION

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), @@ -126,6 +129,7 @@ public Response getViolationsByProject(@ApiParam(value = "The UUID of the projec responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policy violations"), notes = "

    Requires permission VIEW_POLICY_VIOLATION

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 403, message = "Access to the specified component is forbidden"), diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index 305fb6619d..64d9693c14 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -40,6 +40,7 @@ import org.dependencytrack.model.Tag; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; import org.dependencytrack.resources.v1.vo.CloneProjectRequest; import javax.jdo.FetchGroup; @@ -84,6 +85,7 @@ public class ProjectResource extends AlpineResource { responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects"), notes = "

    Requires permission VIEW_PORTFOLIO

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) @@ -185,6 +187,7 @@ public Response getProject( responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects with the tag"), notes = "

    Requires permission VIEW_PORTFOLIO

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) @@ -213,6 +216,7 @@ public Response getProjectsByTag( responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects of the specified classifier"), notes = "

    Requires permission VIEW_PORTFOLIO

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) @@ -580,6 +584,7 @@ public Response cloneProject(CloneProjectRequest jsonRequest) { responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects"), notes = "

    Requires permission VIEW_PORTFOLIO

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), @@ -615,6 +620,7 @@ public Response getChildrenProjects(@ApiParam(value = "The UUID of the project t responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects"), notes = "

    Requires permission VIEW_PORTFOLIO

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), @@ -654,6 +660,7 @@ public Response getChildrenProjectsByClassifier( responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects"), notes = "

    Requires permission VIEW_PORTFOLIO

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), @@ -693,6 +700,7 @@ public Response getChildrenProjectsByTag( responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects"), notes = "

    Requires permission VIEW_PORTFOLIO

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), diff --git a/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java b/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java index ccb66b41c9..f717b04dd9 100644 --- a/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java @@ -38,6 +38,7 @@ import org.dependencytrack.model.RepositoryType; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; import javax.validation.Validator; import javax.ws.rs.Consumes; @@ -73,6 +74,7 @@ public class RepositoryResource extends AlpineResource { responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of repositories"), notes = "

    Requires permission SYSTEM_CONFIGURATION

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) @@ -93,8 +95,8 @@ public Response getRepositories() { responseContainer = "List", responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of repositories"), notes = "

    Requires permission SYSTEM_CONFIGURATION

    " - ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) diff --git a/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java b/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java index 50bd58f35a..4c15445028 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java @@ -34,6 +34,7 @@ import org.dependencytrack.model.ServiceComponent; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; import javax.validation.Validator; import javax.ws.rs.Consumes; @@ -67,6 +68,7 @@ public class ServiceResource extends AlpineResource { responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of services"), notes = "

    Requires permission VIEW_PORTFOLIO

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), diff --git a/src/main/java/org/dependencytrack/resources/v1/TagResource.java b/src/main/java/org/dependencytrack/resources/v1/TagResource.java index 38a6f56152..03ddb3d257 100644 --- a/src/main/java/org/dependencytrack/resources/v1/TagResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/TagResource.java @@ -48,7 +48,7 @@ public class TagResource extends AlpineResource { @Path("/{policyUuid}") @Produces(MediaType.APPLICATION_JSON) @ApiOperation( - value = "Returns a list of all tags", + value = "Returns a list of all tags associated with a given policy", response = Tag.class, responseContainer = "List", responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of tags"), diff --git a/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java b/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java index e3731ac849..091c77e41e 100644 --- a/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/VulnerabilityResource.java @@ -38,6 +38,7 @@ import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.parser.common.resolver.CweResolver; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; import org.dependencytrack.resources.v1.vo.AffectedComponent; import org.dependencytrack.resources.v1.vo.AffectedProject; import org.dependencytrack.tasks.scanners.AnalyzerIdentity; @@ -83,6 +84,7 @@ public class VulnerabilityResource extends AlpineResource { responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of vulnerabilities"), notes = "

    Requires permission VIEW_PORTFOLIO

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"), @ApiResponse(code = 403, message = "Access to the specified component is forbidden"), @@ -132,6 +134,7 @@ public Response getVulnerabilitiesByProject(@ApiParam(value = "The UUID of the p final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { if (qm.hasAccess(super.getPrincipal(), project)) { + // TODO: This should honor pagination but it doesn't. final List vulnerabilities = qm.getVulnerabilities(project, suppressed); return Response.ok(vulnerabilities).header(TOTAL_COUNT_HEADER, vulnerabilities.size()).build(); } else { @@ -246,6 +249,7 @@ public Response getAffectedProject(@PathParam("source") String source, responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of vulnerabilities"), notes = "

    Requires permission VIEW_PORTFOLIO

    " ) + @PaginatedApi @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized") }) diff --git a/src/main/java/org/dependencytrack/resources/v1/openapi/PaginatedApi.java b/src/main/java/org/dependencytrack/resources/v1/openapi/PaginatedApi.java new file mode 100644 index 0000000000..1dbf7249e0 --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/openapi/PaginatedApi.java @@ -0,0 +1,67 @@ +/* + * 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.resources.v1.openapi; + +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @since 4.11.0 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@ApiImplicitParams({ + @ApiImplicitParam( + name = "pageNumber", + dataType = "int", + paramType = "query", + defaultValue = "1", + value = "The page to return. To be used in conjunction with pageSize." + ), + @ApiImplicitParam( + name = "pageSize", + dataType = "int", + paramType = "query", + defaultValue = "100", + value = "Number of elements to return per page. To be used in conjunction with pageNumber." + ), + @ApiImplicitParam( + name = "offset", + dataType = "int", + paramType = "query", + defaultValue = "0", + value = "Offset to start returning elements from. To be used in conjunction with limit." + ), + @ApiImplicitParam( + name = "limit", + dataType = "int", + paramType = "query", + defaultValue = "100", + value = "Number of elements to return per page. To be used in conjunction with offset." + ) +}) +public @interface PaginatedApi { +} From f500aaba817461d3ef06ec70761d407527f1966b Mon Sep 17 00:00:00 2001 From: nscuro Date: Sat, 20 Apr 2024 19:56:15 +0200 Subject: [PATCH 083/412] Update Trivy docs with more instructions and known limitations Signed-off-by: nscuro --- docs/_docs/datasources/trivy.md | 80 ++++++++++++++++-- docs/_posts/2024-xx-xx-v4.11.0.md | 8 +- .../screenshots/trivy-configuration.png | Bin 232266 -> 70044 bytes 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/docs/_docs/datasources/trivy.md b/docs/_docs/datasources/trivy.md index 95e140fb21..50fcb0a818 100644 --- a/docs/_docs/datasources/trivy.md +++ b/docs/_docs/datasources/trivy.md @@ -5,22 +5,86 @@ chapter: 4 order: 6 --- -[Trivy](https://www.aquasec.com/products/trivy/) is a tool provided by aquas allowing you to scan for vulnerabilities. +[Trivy](https://www.aquasec.com/products/trivy/) is a *comprehensive and versatile security scanner* by [Aqua Security], +supporting most popular programming languages, operating systems, and platforms. More details on scanning coverage +can be found in [Trivy's documentation](https://aquasecurity.github.io/trivy/latest/docs/coverage/). -Dependency-Track integrates with Trivy using its undocumented REST API. +Dependency-Track integrates with Trivy using its [client/server mode]. -The Trivy integration is disabled by default. +The Trivy integration requires an external Trivy server and is disabled by default. ### Configuration To configure the Trivy integration, navigate to *Analyzers* -> *Trivy* in the administration panel. +The following options are available: -|:---|:----| -| Base URL | Base URL of the Trivy REST API. Defaults to `http://localhost:8081`. | -| API Token | Authentication token for the REST API. | +| Option | Description | Default | +|:---------------|:--------------------------------------------------------------------|:--------| +| Enable | Controls whether the Trivy integration is enabled | false | +| Base URL | Base URL of the Trivy REST API | - | +| API Token | Authentication token for the REST API | - | +| Ignore Unfixed | Whether to ignore vulnerabilities for which no fix is available yet | false | ![Trivy Configuration](../../images/screenshots/trivy-configuration.png) -### Run Trivy as Server +### Running Trivy in Server Mode -Trivy can be runned as a [server](https://github.com/aquasecurity/trivy/blob/b5874e3ad38e77ac86eedd7a65785b2933f3685f/docs/docs/references/configuration/cli/trivy_server.md) by executing the command `trivy server --listen localhost:8081 --token dummy -d` or by setting it up on a container. +To run Trivy in server mode, use the [server command]. For example: + +```shell +trivy server \ + --listen :8080 \ + --token yourAuthToken +``` + +> **Note** +> To prevent service disruption, you need to make sure that the Trivy server *keeps running*, and is automatically +> launched after the host system reboots. +> The easiest way to achieve this is to run Trivy with Docker, and using an appropriate [restart policy]. +> See below for an example with Docker Compose. + +When deploying Dependency-Track via Docker Compose, Trivy can simply be added as yet another service in `docker-compose.yml`: + +```yml +services: + trivy: + image: aquasec/trivy:latest + command: + - server + - --listen + - :8080 + - --token + - yourAuthToken + volumes: + - "trivy-cache:/root/.cache/trivy" + restart: unless-stopped + + # + +volumes: + trivy-cache: {} + # +``` + +Deploying Trivy in the same Compose project allows you to specify `http://trivy:8080` as *base URL* in the Trivy configuration. +Other deployment models will require you to configure the proper IP or hostname of the machine where Trivy is running. + +### Known Limitations + +* When scanning SBOMs, Trivy heavily relies on structures and properties that it populates when generating an SBOM. +This means that scanning SBOMs that were generated with tools other than Trivy may not yield as many results, +as when scanning an SBOM generated by Trivy itself. +* Trivy may report vulnerabilities from sources that Dependency-Track does not recognize. Because Trivy maintains +its own vulnerability database, it is inevitable that it deviates from Dependency-Track's. Vulnerabilities for which +this is the case will be labeled with source `UNKNOWN` in Dependency-Track. +* Vulnerability aliases can not be resolved for vulnerabilities of source `UNKNOWN`. +* Trivy may report vulnerabilities for which no official ID was assigned (yet). For example, the Debian project uses +[fake names] for certain vulnerabilities. Because Trivy integrates with Debian's vulnerability database directly, +it will report such vulnerabilities if applicable. The chance of encountering such vulnerabilities can be reduced by +enabling the *ignore unfixed vulnerabilities* option. + +[Aqua Security]: https://www.aquasec.com/ +[client/server mode]: https://aquasecurity.github.io/trivy/latest/docs/references/modes/client-server/ +[fake names]: https://security-tracker.debian.org/tracker/data/fake-names +[restart policy]: https://docs.docker.com/config/containers/start-containers-automatically/#use-a-restart-policy +[server command]: https://github.com/aquasecurity/trivy/blob/v0.50.1/docs/docs/references/configuration/cli/trivy_server.md \ No newline at end of file diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index 6b8ee1bcb9..5bf59700a2 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -13,7 +13,7 @@ Because the new implementation can have a big impact on how Dependency-Track beh it is disabled by default for this release. It may be enabled by setting the environment variable `BOM_PROCESSING_TASK_V2_ENABLED` to `true`. Users are highly encouraged to do so. * **BOM Validation**. Historically, Dependency-Track did not validate uploaded BOMs and VEXs against the CycloneDX -schema. While this allowed BOMs to be processed that did not strictly adhere to the schema, it could lead to confusion +schema. While this allowed BOMs to be processed that did not strictly adhere to the schema, it could also lead to confusion when uploaded files were accepted, but then failed to be ingested during asynchronous processing. Starting with this release, uploaded files will be rejected if they fail schema validation. Note that this may reveal issues in BOM generators that currently produce invalid CycloneDX documents. Validation may be turned off by setting the @@ -25,9 +25,11 @@ has explicit access to. It is possible to inspect individual findings, or aggreg making it possible to spot the most prevalent vulnerabilities. * *This feature was demoed in our April community meeting! Watch it [here](https://www.youtube.com/watch?v=3iIeajRJK8o&t=572s).* * **Trivy Analyzer Integration**. It is now possible to leverage [Trivy] in [server mode] for vulnerability analysis. + * Refer to the [analyzer's documentation]({{ site.baseurl }}{% link _docs/datasources/trivy.md %}) for further details, + in particular the [known limitations]({{ site.baseurl }}{% link _docs/datasources/trivy.md %}#known-limitations). * *This feature was demoed in our April community meeting! Watch it [here](https://www.youtube.com/watch?v=3iIeajRJK8o&t=725s).* * **Official Helm Chart**. The Dependency-Track project now offers an official Helm chart for Kubernetes deployments. -Community input and contributions are explicitly requested. The chart repository can be found at +Community input and contributions are highly requested. The chart repository can be found at [https://github.com/DependencyTrack/helm-charts](https://github.com/DependencyTrack/helm-charts) **Features:** @@ -295,6 +297,6 @@ Special thanks to everyone who contributed code to implement enhancements and fi [Mapped Diagnostic Context]: https://logback.qos.ch/manual/mdc.html [Trivy]: https://trivy.dev/ [component identity]: https://docs.dependencytrack.org/analysis-types/component-identity/ -[customized their logging configuration]: https://docs.dependencytrack.org/getting-started/monitoring/#custom-logging-configuration +[customized their logging configuration]: {{ site.baseurl }}{% link _docs/getting-started/monitoring.md %}#custom-logging-configuration [logback.xml]: https://github.com/DependencyTrack/dependency-track/blob/master/src/main/docker/logback.xml [server mode]: https://aquasecurity.github.io/trivy/v0.50/docs/references/modes/client-server/ \ No newline at end of file diff --git a/docs/images/screenshots/trivy-configuration.png b/docs/images/screenshots/trivy-configuration.png index 0f3e62e394ad0e44c7832880d37611230afb63d9..291f491f615945320696f5373c10bb1ff9a3a37b 100644 GIT binary patch literal 70044 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%0DTNT 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 m`$6P;ixhjPP Date: Sat, 20 Apr 2024 19:57:49 +0200 Subject: [PATCH 084/412] Have `trivy.base.url` default to `null` instead of `http://localhost:8081` Most users are deploying Dependency-Track in container environments, where `localhost` will never work for an external Trivy server. Better to not include a default at all instead of a misleading / broken one. Signed-off-by: nscuro --- .../java/org/dependencytrack/model/ConfigPropertyConstants.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index 5d6cc55848..3962e31d2a 100644 --- a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -61,7 +61,7 @@ public enum ConfigPropertyConstants { SCANNER_SNYK_BASE_URL("scanner", "snyk.base.url", "https://api.snyk.io", PropertyType.URL, "Base Url pointing to the hostname and path for Snyk analysis"), SCANNER_TRIVY_ENABLED("scanner", "trivy.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable Trivy Vulnerability Analysis"), SCANNER_TRIVY_API_TOKEN("scanner", "trivy.api.token", null, PropertyType.ENCRYPTEDSTRING, "The API token used for Trivy API authentication"), - SCANNER_TRIVY_BASE_URL("scanner", "trivy.base.url", "http://localhost:8081", PropertyType.URL, "Base Url pointing to the hostname and path for Trivy analysis"), + SCANNER_TRIVY_BASE_URL("scanner", "trivy.base.url", null, PropertyType.URL, "Base Url pointing to the hostname and path for Trivy analysis"), SCANNER_TRIVY_IGNORE_UNFIXED("scanner", "trivy.ignore.unfixed", "false", PropertyType.BOOLEAN, "Flag to ignore unfixed vulnerabilities"), VULNERABILITY_SOURCE_NVD_ENABLED("vuln-source", "nvd.enabled", "true", PropertyType.BOOLEAN, "Flag to enable/disable National Vulnerability Database"), VULNERABILITY_SOURCE_NVD_FEEDS_URL("vuln-source", "nvd.feeds.url", "https://nvd.nist.gov/feeds", PropertyType.URL, "A base URL pointing to the hostname and path of the NVD feeds"), From 5bf8eda95b26ce26734db7577a36e64c6a965c6f Mon Sep 17 00:00:00 2001 From: nscuro Date: Sat, 20 Apr 2024 19:58:10 +0200 Subject: [PATCH 085/412] Add dev Compose file for Trivy Signed-off-by: nscuro --- dev/docker-compose.trivy.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 dev/docker-compose.trivy.yml diff --git a/dev/docker-compose.trivy.yml b/dev/docker-compose.trivy.yml new file mode 100644 index 0000000000..6c96a7e3a0 --- /dev/null +++ b/dev/docker-compose.trivy.yml @@ -0,0 +1,31 @@ +# 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. +services: + trivy: + image: aquasec/trivy:latest + command: + - server + - --listen + - :8080 + - --token + - TrivyToken + volumes: + - "trivy-cache:/root/.cache/trivy" + restart: unless-stopped + +volumes: + trivy-cache: { } From ed5d37ab4ea69f047e97fa1abca377a2c17fa651 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 21 Apr 2024 12:43:39 +0200 Subject: [PATCH 086/412] Include sorting query parameters in OpenAPI spec These parameters are handled implicitly by Alpine and as such are not directly documented in the Dependency-Track codebase. The `sortName` and `sortOrder` parameters are now included in the OpenAPI spec for all endpoints that support them. Signed-off-by: nscuro --- .../resources/v1/openapi/PaginatedApi.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/org/dependencytrack/resources/v1/openapi/PaginatedApi.java b/src/main/java/org/dependencytrack/resources/v1/openapi/PaginatedApi.java index 1dbf7249e0..85444728fd 100644 --- a/src/main/java/org/dependencytrack/resources/v1/openapi/PaginatedApi.java +++ b/src/main/java/org/dependencytrack/resources/v1/openapi/PaginatedApi.java @@ -61,6 +61,19 @@ paramType = "query", defaultValue = "100", value = "Number of elements to return per page. To be used in conjunction with offset." + ), + @ApiImplicitParam( + name = "sortName", + dataType = "string", + paramType = "query", + value = "Name of the resource field to sort on." + ), + @ApiImplicitParam( + name = "sortOrder", + dataType = "string", + paramType = "query", + allowableValues = "asc, desc", + value = "Ordering of items when sorting with sortName." ) }) public @interface PaginatedApi { From 7840ca3fbd491aea85dcea57e392442e9b862608 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 21 Apr 2024 13:33:07 +0200 Subject: [PATCH 087/412] Remove default values from `offset` and `limit` in OpenAPI spec Including default values for both `pageNumber`/`pageSize` and `offset`/`limit` causes the request generated by Swagger UI to use them all, which is confusing. Signed-off-by: nscuro --- docs/_posts/2024-xx-xx-v4.11.0.md | 2 ++ .../org/dependencytrack/resources/v1/openapi/PaginatedApi.java | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index 5bf59700a2..a771ca9976 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -69,6 +69,7 @@ Community input and contributions are highly requested. The chart repository can * Ingest vulnerability alias information from VulnDB - [apiserver/#3588] * Properly validate UUID request parameters to prevent internal server errors - [apiserver/#3590] * Document pagination query parameters in OpenAPI specification - [apiserver/#3625] +* Document sorting query parameters in OpenAPI specification - [apiserver/#3631] * Show component count in projects list - [frontend/#683] * Add current *fail*, *warn*, and *info* values to bottom of policy violation metrics - [frontend/#707] * Remove unused policy violation widget - [frontend/#710] @@ -233,6 +234,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [apiserver/#3590]: https://github.com/DependencyTrack/dependency-track/pull/3590 [apiserver/#3595]: https://github.com/DependencyTrack/dependency-track/pull/3595 [apiserver/#3625]: https://github.com/DependencyTrack/dependency-track/pull/3625 +[apiserver/#3631]: https://github.com/DependencyTrack/dependency-track/pull/3631 [frontend/#682]: https://github.com/DependencyTrack/frontend/pull/682 [frontend/#683]: https://github.com/DependencyTrack/frontend/pull/683 diff --git a/src/main/java/org/dependencytrack/resources/v1/openapi/PaginatedApi.java b/src/main/java/org/dependencytrack/resources/v1/openapi/PaginatedApi.java index 85444728fd..64ea233efc 100644 --- a/src/main/java/org/dependencytrack/resources/v1/openapi/PaginatedApi.java +++ b/src/main/java/org/dependencytrack/resources/v1/openapi/PaginatedApi.java @@ -52,14 +52,12 @@ name = "offset", dataType = "int", paramType = "query", - defaultValue = "0", value = "Offset to start returning elements from. To be used in conjunction with limit." ), @ApiImplicitParam( name = "limit", dataType = "int", paramType = "query", - defaultValue = "100", value = "Number of elements to return per page. To be used in conjunction with offset." ), @ApiImplicitParam( From 3980ec6dc882cb6ae93b3a054d181f799d886580 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 08:13:58 +0000 Subject: [PATCH 088/412] Bump actions/upload-artifact from 4.3.1 to 4.3.2 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.1 to 4.3.2. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/5d5d22a31266ced268874388b861e4b58bb5c2f3...1746f4ab65b179e0ea60a494b83293b640dd5bba) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- .github/workflows/ci-test.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 8d213c6632..10a7cd4fa3 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -51,7 +51,7 @@ jobs: mvn cyclonedx:makeBom -Dservices.bom.merge.skip=false org.codehaus.mojo:exec-maven-plugin:exec@merge-services-bom - name: Upload Artifacts - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # tag=v4.3.1 + uses: actions/upload-artifact@1746f4ab65b179e0ea60a494b83293b640dd5bba # tag=v4.3.2 with: name: assembled-wars path: |- diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index 318967a927..9229d7855b 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -64,7 +64,7 @@ jobs: - name: Upload PR test coverage report if: ${{ github.event_name == 'pull_request' }} - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # tag=v4.3.1 + uses: actions/upload-artifact@1746f4ab65b179e0ea60a494b83293b640dd5bba # tag=v4.3.2 with: name: pr-test-coverage-report path: |- From e37ec0c4442f152c51a662ad2089503073ef95e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 08:14:04 +0000 Subject: [PATCH 089/412] Bump github/codeql-action from 3.24.10 to 3.25.1 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.24.10 to 3.25.1. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/4355270be187e1b672a7a1c7c7bae5afdc1ab94a...c7f9125735019aa87cfc361530512d50ea439c71) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 8d213c6632..fe793e0b13 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -133,6 +133,6 @@ jobs: - name: Upload Trivy Scan Results to GitHub Security Tab if: ${{ inputs.publish-container }} - uses: github/codeql-action/upload-sarif@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # tag=v3.24.10 + uses: github/codeql-action/upload-sarif@c7f9125735019aa87cfc361530512d50ea439c71 # tag=v3.25.1 with: sarif_file: 'trivy-results.sarif' From d29b85e930a9921276d16614f0ed63dd00ddb3df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 08:14:10 +0000 Subject: [PATCH 090/412] Bump actions/checkout from 4.1.2 to 4.1.3 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.2 to 4.1.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/9bb56186c3b09b4f86b1c65136769dd318469633...1d96c772d19495a3b5c517cd2bc0cb401ea0529f) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 4 ++-- .github/workflows/ci-publish.yaml | 4 ++-- .github/workflows/ci-release.yaml | 6 +++--- .github/workflows/ci-test.yaml | 2 +- .github/workflows/dependency-review.yaml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 8d213c6632..836aeacd7a 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 + uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 - name: Set up JDK uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v4.2.1 @@ -74,7 +74,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 + uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 - name: Download Artifacts uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # tag=v4.1.4 diff --git a/.github/workflows/ci-publish.yaml b/.github/workflows/ci-publish.yaml index 326f46ec7f..d9d7d2604f 100644 --- a/.github/workflows/ci-publish.yaml +++ b/.github/workflows/ci-publish.yaml @@ -23,7 +23,7 @@ jobs: exit 1 fi - name: Checkout Repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 + uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 - name: Parse Version from POM id: parse @@ -51,7 +51,7 @@ jobs: - call-build steps: - name: Checkout Repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 + uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 - name: Download Artifacts uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # tag=v4.1.4 diff --git a/.github/workflows/ci-release.yaml b/.github/workflows/ci-release.yaml index 626bfe8eb6..293a6acdcb 100644 --- a/.github/workflows/ci-release.yaml +++ b/.github/workflows/ci-release.yaml @@ -20,7 +20,7 @@ jobs: release-branch: ${{ steps.variables.outputs.release-branch }} steps: - name: Checkout Repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 + uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 - name: Setup Environment id: variables @@ -51,7 +51,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 + uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 - name: Set up JDK uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v4.2.1 @@ -118,7 +118,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 + uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 with: ref: ${{ needs.prepare-release.outputs.release-branch }} diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index 318967a927..481e729f4e 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 + uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 - name: Set up JDK uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v4.2.1 diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index 8499414f06..edee799dac 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag=v4.1.2 + uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 - name: Dependency Review uses: actions/dependency-review-action@5bbc3ba658137598168acb2ab73b21c432dd411b # tag=v4.2.5 From 578c0bbb37ae004c01f651963c8d7451326f23b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 08:59:09 +0000 Subject: [PATCH 091/412] Bump actions/download-artifact from 4.1.4 to 4.1.5 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.1.4 to 4.1.5. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/c850b930e6ba138125429b7e5c93fc707a7f8427...8caf195ad4b1dee92908e23f56eeb0696f1dd42d) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- .github/workflows/ci-publish.yaml | 2 +- .github/workflows/ci-test-pr-coverage.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index c371e26d05..253d702104 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -77,7 +77,7 @@ jobs: uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 - name: Download Artifacts - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # tag=v4.1.4 + uses: actions/download-artifact@8caf195ad4b1dee92908e23f56eeb0696f1dd42d # tag=v4.1.5 with: name: assembled-wars path: target diff --git a/.github/workflows/ci-publish.yaml b/.github/workflows/ci-publish.yaml index d9d7d2604f..aae27e9558 100644 --- a/.github/workflows/ci-publish.yaml +++ b/.github/workflows/ci-publish.yaml @@ -54,7 +54,7 @@ jobs: uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 - name: Download Artifacts - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # tag=v4.1.4 + uses: actions/download-artifact@8caf195ad4b1dee92908e23f56eeb0696f1dd42d # tag=v4.1.5 with: name: assembled-wars path: target diff --git a/.github/workflows/ci-test-pr-coverage.yml b/.github/workflows/ci-test-pr-coverage.yml index eb0f63ec5e..a0047f080e 100644 --- a/.github/workflows/ci-test-pr-coverage.yml +++ b/.github/workflows/ci-test-pr-coverage.yml @@ -18,7 +18,7 @@ jobs: && github.event.workflow_run.conclusion == 'success' steps: - name: Download PR test coverage report - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # tag=v4.1.4 + uses: actions/download-artifact@8caf195ad4b1dee92908e23f56eeb0696f1dd42d # tag=v4.1.5 with: name: pr-test-coverage-report github-token: ${{ secrets.GITHUB_TOKEN }} From 1200f788ae80a79061c713b07ed4ebe4466377d2 Mon Sep 17 00:00:00 2001 From: Marlon Pina Tojal Date: Mon, 22 Apr 2024 11:21:25 +0200 Subject: [PATCH 092/412] refactor Signed-off-by: Marlon Pina Tojal --- .../event/EventSubsystemInitializer.java | 26 ++----------------- .../resources/v1/ConfigPropertyResource.java | 19 -------------- .../tasks/BomUploadProcessingTaskV2.java | 9 +++++++ 3 files changed, 11 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java index 9b61ec13c9..d0f7ccc91b 100644 --- a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java +++ b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java @@ -24,9 +24,6 @@ import alpine.event.framework.SingleThreadedEventService; import alpine.server.tasks.LdapSyncTask; import org.dependencytrack.RequirementsVerifier; -import org.dependencytrack.model.ConfigPropertyConstants; -import org.dependencytrack.persistence.QueryManager; -import org.dependencytrack.tasks.BomUploadProcessingTask; import org.dependencytrack.tasks.BomUploadProcessingTaskV2; import org.dependencytrack.tasks.CallbackTask; import org.dependencytrack.tasks.ClearComponentAnalysisCacheTask; @@ -88,17 +85,7 @@ public void contextInitialized(final ServletContextEvent event) { return; } - //EXPERIMENTAL: FUTURE RELEASES SHOULD JUST REMOVE THE FOLLOWING BLOCK AND ENABLE THE COMMENT CODE - try (QueryManager qm = new QueryManager()) { - if (qm.isEnabled(ConfigPropertyConstants.BOM_PROCESSING_TASK_V2_ENABLED)) { - EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTaskV2.class); - } else { - EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTask.class); - } - } - // EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTaskV2.class); - //EXPERIMENTAL - + EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTaskV2.class); EVENT_SERVICE.subscribe(VexUploadEvent.class, VexUploadProcessingTask.class); EVENT_SERVICE.subscribe(LdapSyncEvent.class, LdapSyncTask.class); EVENT_SERVICE.subscribe(InternalAnalysisEvent.class, InternalAnalysisTask.class); @@ -142,17 +129,8 @@ public void contextDestroyed(final ServletContextEvent event) { LOGGER.info("Shutting down asynchronous event subsystem"); TaskScheduler.getInstance().shutdown(); - //EXPERIMENTAL: FUTURE RELEASES SHOULD JUST REMOVE THE FOLLOWING BLOCK AND ENABLE THE COMMENT CODE - try (QueryManager qm = new QueryManager()) { - if (qm.isEnabled(ConfigPropertyConstants.BOM_PROCESSING_TASK_V2_ENABLED)) { - EVENT_SERVICE.unsubscribe(BomUploadProcessingTaskV2.class); - } else { - EVENT_SERVICE.unsubscribe(BomUploadProcessingTask.class); - } - } - // EVENT_SERVICE.unsubscribe(BomUploadProcessingTaskV2.class); - //EXPERIMENTAL + EVENT_SERVICE.unsubscribe(BomUploadProcessingTaskV2.class); EVENT_SERVICE.unsubscribe(VexUploadProcessingTask.class); EVENT_SERVICE.unsubscribe(LdapSyncTask.class); EVENT_SERVICE.unsubscribe(InternalAnalysisTask.class); diff --git a/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java index f68d560887..4038b4916f 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java @@ -18,7 +18,6 @@ */ package org.dependencytrack.resources.v1; -import alpine.event.framework.EventService; import alpine.model.ConfigProperty; import alpine.server.auth.PermissionRequired; import io.swagger.annotations.Api; @@ -27,10 +26,7 @@ import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; import org.dependencytrack.auth.Permissions; -import org.dependencytrack.event.BomUploadEvent; import org.dependencytrack.persistence.QueryManager; -import org.dependencytrack.tasks.BomUploadProcessingTask; -import org.dependencytrack.tasks.BomUploadProcessingTaskV2; import javax.validation.Validator; import javax.ws.rs.Consumes; @@ -137,21 +133,6 @@ public Response updateConfigProperty(List list) { for (ConfigProperty item : list) { final ConfigProperty property = qm.getConfigProperty(item.getGroupName(), item.getPropertyName()); returnList.add(updatePropertyValue(qm, item, property).getEntity()); - - //EXPERIMENTAL: FUTURE RELEASES SHOULD REMOVE THIS BLOCK - if (item.getGroupName().equals("experimental") && - item.getPropertyName().equals("bom.processing.task.v2.enabled")) { - final EventService EVENT_SERVICE = EventService.getInstance(); - - if (Boolean.parseBoolean(item.getPropertyValue())) { - EVENT_SERVICE.unsubscribe(BomUploadProcessingTask.class); - EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTaskV2.class); - } else { - EVENT_SERVICE.unsubscribe(BomUploadProcessingTaskV2.class); - EVENT_SERVICE.subscribe(BomUploadEvent.class, BomUploadProcessingTask.class); - } - } - //EXPERIMENTAL } } return Response.ok(returnList).build(); diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java index 9c0fee24d3..6937ea5d21 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java @@ -41,6 +41,7 @@ import org.dependencytrack.model.Bom; import org.dependencytrack.model.Component; import org.dependencytrack.model.ComponentIdentity; +import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.DependencyMetrics; import org.dependencytrack.model.FindingAttribution; import org.dependencytrack.model.License; @@ -144,6 +145,14 @@ public void inform(final Event e) { return; } + //EXPERIMENTAL: FUTURE RELEASES SHOULD JUST REMOVE THE FOLLOWING BLOCK + try (QueryManager qm = new QueryManager()) { + if (!qm.isEnabled(ConfigPropertyConstants.BOM_PROCESSING_TASK_V2_ENABLED)) { + new BomUploadProcessingTask().inform(event); + } + } + //EXPERIMENTAL + final var ctx = new Context(event.getChainIdentifier(), event.getProject(), event.getBom()); try (var ignoredMdcProjectUuid = MDC.putCloseable(MDC_PROJECT_UUID, ctx.project.getUuid().toString()); var ignoredMdcProjectName = MDC.putCloseable(MDC_PROJECT_NAME, ctx.project.getName()); From 6457ecb8f83960f7df3a4275a551cdc32ca4c920 Mon Sep 17 00:00:00 2001 From: Marlon Pina Tojal Date: Mon, 22 Apr 2024 11:34:43 +0200 Subject: [PATCH 093/412] remove bom validation as experimental feature Signed-off-by: Marlon Pina Tojal --- .../java/org/dependencytrack/model/ConfigPropertyConstants.java | 2 +- src/main/java/org/dependencytrack/resources/v1/BomResource.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index c418f26d01..e1527d1cac 100644 --- a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -110,7 +110,7 @@ public enum ConfigPropertyConstants { SEARCH_INDEXES_CONSISTENCY_CHECK_CADENCE("search-indexes", "consistency.check.cadence", "4320", PropertyType.INTEGER, "Lucene indexes consistency check cadence (in minutes)"), SEARCH_INDEXES_CONSISTENCY_CHECK_DELTA_THRESHOLD("search-indexes", "consistency.check.delta.threshold", "20", PropertyType.INTEGER, "Threshold used to trigger an index rebuild when comparing database table and corresponding lucene index (in percentage). It must be an integer between 1 and 100"), BOM_PROCESSING_TASK_V2_ENABLED("experimental", "bom.processing.task.v2.enabled", "false", PropertyType.BOOLEAN, "Flag to enable BOM UPLOAD V2"), - BOM_VALIDATION_ENABLED("experimental", "bom.validation.enabled", "true", PropertyType.BOOLEAN, "Flag to control bom validation"); + BOM_VALIDATION_ENABLED("artifact", "bom.validation.enabled", "true", PropertyType.BOOLEAN, "Flag to control bom validation"); private String groupName; private String propertyName; diff --git a/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/src/main/java/org/dependencytrack/resources/v1/BomResource.java index c5c88f094a..7cf1febd4d 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -467,13 +467,11 @@ private Response process(QueryManager qm, Project project, List Date: Mon, 22 Apr 2024 11:49:36 +0200 Subject: [PATCH 094/412] fix test Signed-off-by: Marlon Pina Tojal --- .../dependencytrack/resources/v1/BomResourceTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index 9f9001e31d..1d62d4cf1d 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -64,6 +64,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.hamcrest.CoreMatchers.equalTo; +import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_ENABLED; public class BomResourceTest extends ResourceTest { @@ -871,6 +872,14 @@ public void uploadBomInvalidParentTest() throws Exception { public void uploadBomInvalidJsonTest() { initializeWithPermissions(Permissions.BOM_UPLOAD); + qm.createConfigProperty( + BOM_VALIDATION_ENABLED.getGroupName(), + BOM_VALIDATION_ENABLED.getPropertyName(), + "true", + BOM_VALIDATION_ENABLED.getPropertyType(), + null + ); + final var project = new Project(); project.setName("acme-app"); project.setVersion("1.0.0"); From cfba8696394861b78374b8f3d9a9a949665c6a82 Mon Sep 17 00:00:00 2001 From: Marlon Pina Tojal Date: Mon, 22 Apr 2024 12:51:58 +0200 Subject: [PATCH 095/412] fix more tests Signed-off-by: Marlon Pina Tojal --- .../resources/v1/BomResourceTest.java | 8 ++++++++ .../resources/v1/VexResourceTest.java | 17 +++++++++++++++++ .../tasks/BomUploadProcessingTaskTest.java | 10 ++++++++++ 3 files changed, 35 insertions(+) diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index 1d62d4cf1d..ab165db7f4 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -928,6 +928,14 @@ public void uploadBomInvalidJsonTest() { public void uploadBomInvalidXmlTest() { initializeWithPermissions(Permissions.BOM_UPLOAD); + qm.createConfigProperty( + BOM_VALIDATION_ENABLED.getGroupName(), + BOM_VALIDATION_ENABLED.getPropertyName(), + "true", + BOM_VALIDATION_ENABLED.getPropertyType(), + null + ); + final var project = new Project(); project.setName("acme-app"); project.setVersion("1.0.0"); diff --git a/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java index 7a74a6ec03..f061d07ca0 100644 --- a/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java @@ -49,6 +49,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.hamcrest.CoreMatchers.equalTo; +import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_ENABLED; public class VexResourceTest extends ResourceTest { @@ -219,6 +220,14 @@ public void exportProjectAsCycloneDxTest() { public void uploadVexInvalidJsonTest() { initializeWithPermissions(Permissions.BOM_UPLOAD); + qm.createConfigProperty( + BOM_VALIDATION_ENABLED.getGroupName(), + BOM_VALIDATION_ENABLED.getPropertyName(), + "true", + BOM_VALIDATION_ENABLED.getPropertyType(), + null + ); + final var project = new Project(); project.setName("acme-app"); project.setVersion("1.0.0"); @@ -267,6 +276,14 @@ public void uploadVexInvalidJsonTest() { public void uploadVexInvalidXmlTest() { initializeWithPermissions(Permissions.BOM_UPLOAD); + qm.createConfigProperty( + BOM_VALIDATION_ENABLED.getGroupName(), + BOM_VALIDATION_ENABLED.getPropertyName(), + "true", + BOM_VALIDATION_ENABLED.getPropertyType(), + null + ); + final var project = new Project(); project.setName("acme-app"); project.setVersion("1.0.0"); diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index ddf003722e..5f7e16bd0c 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -72,6 +72,7 @@ import static org.assertj.core.api.Assertions.fail; import static org.awaitility.Awaitility.await; import static org.dependencytrack.assertion.Assertions.assertConditionWithTimeout; +import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_ENABLED; @RunWith(Parameterized.class) public class BomUploadProcessingTaskTest extends PersistenceCapableTest { @@ -308,6 +309,15 @@ public void informWithEmptyBomTest() throws Exception { @Test public void informWithInvalidCycloneDxBomTest() throws Exception { + + qm.createConfigProperty( + BOM_VALIDATION_ENABLED.getGroupName(), + BOM_VALIDATION_ENABLED.getPropertyName(), + "true", + BOM_VALIDATION_ENABLED.getPropertyType(), + null + ); + final Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); final byte[] bomBytes = """ From dbeb6aef45479d3684a52a3cdf025009d55841d8 Mon Sep 17 00:00:00 2001 From: Marlon Pina Tojal Date: Mon, 22 Apr 2024 14:27:45 +0200 Subject: [PATCH 096/412] add missing return Signed-off-by: Marlon Pina Tojal --- .../org/dependencytrack/tasks/BomUploadProcessingTaskV2.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java index 6937ea5d21..b6b0619e04 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java @@ -149,6 +149,7 @@ public void inform(final Event e) { try (QueryManager qm = new QueryManager()) { if (!qm.isEnabled(ConfigPropertyConstants.BOM_PROCESSING_TASK_V2_ENABLED)) { new BomUploadProcessingTask().inform(event); + return; } } //EXPERIMENTAL From c54e0fb99e8e4c17d9543bd6f024b144c48ce8b4 Mon Sep 17 00:00:00 2001 From: Marlon Pina Tojal Date: Mon, 22 Apr 2024 18:05:34 +0200 Subject: [PATCH 097/412] attempt to fix tests Signed-off-by: Marlon Pina Tojal --- .../tasks/BomUploadProcessingTaskTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index 5f7e16bd0c..fd87006abc 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -73,6 +73,7 @@ import static org.awaitility.Awaitility.await; import static org.dependencytrack.assertion.Assertions.assertConditionWithTimeout; import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.BOM_PROCESSING_TASK_V2_ENABLED; @RunWith(Parameterized.class) public class BomUploadProcessingTaskTest extends PersistenceCapableTest { @@ -106,10 +107,13 @@ public static Collection data() { }); } + private final String ignoredBomUploadProcessingTaskName; private final Supplier bomUploadProcessingTaskSupplier; public BomUploadProcessingTaskTest(final String ignoredBomUploadProcessingTaskName, final Supplier bomUploadProcessingTaskSupplier) { + + this.ignoredBomUploadProcessingTaskName = ignoredBomUploadProcessingTaskName; this.bomUploadProcessingTaskSupplier = bomUploadProcessingTaskSupplier; } @@ -120,6 +124,15 @@ public void setUp() { EventService.getInstance().subscribe(VulnerabilityAnalysisEvent.class, EventSubscriber.class); NotificationService.getInstance().subscribe(new Subscription(NotificationSubscriber.class)); + + qm.createConfigProperty( + BOM_PROCESSING_TASK_V2_ENABLED.getGroupName(), + BOM_PROCESSING_TASK_V2_ENABLED.getPropertyName(), + (this.ignoredBomUploadProcessingTaskName == BomUploadProcessingTaskV2.class.getSimpleName()) ? "true" : "false", + BOM_PROCESSING_TASK_V2_ENABLED.getPropertyType(), + null + ); + // Enable processing of CycloneDX BOMs qm.createConfigProperty(ConfigPropertyConstants.ACCEPT_ARTIFACT_CYCLONEDX.getGroupName(), ConfigPropertyConstants.ACCEPT_ARTIFACT_CYCLONEDX.getPropertyName(), "true", @@ -310,6 +323,11 @@ public void informWithEmptyBomTest() throws Exception { @Test public void informWithInvalidCycloneDxBomTest() throws Exception { + // Known to now work with old task implementation. + if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTask) { + return; + } + qm.createConfigProperty( BOM_VALIDATION_ENABLED.getGroupName(), BOM_VALIDATION_ENABLED.getPropertyName(), From 2f4a9d9272ec0e30776df2856670fd452c274c42 Mon Sep 17 00:00:00 2001 From: Marlon Pina Tojal Date: Mon, 22 Apr 2024 20:50:01 +0200 Subject: [PATCH 098/412] fix tests Signed-off-by: Marlon Pina Tojal --- .../tasks/BomUploadProcessingTaskTest.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index fd87006abc..332a80520c 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -102,7 +102,7 @@ public void inform(final Notification notification) { @Parameterized.Parameters(name = "{index}: {0}") public static Collection data() { return Arrays.asList(new Object[][]{ - {BomUploadProcessingTask.class.getSimpleName(), (Supplier) BomUploadProcessingTask::new}, + {BomUploadProcessingTask.class.getSimpleName(), (Supplier) BomUploadProcessingTaskV2::new}, {BomUploadProcessingTaskV2.class.getSimpleName(), (Supplier) BomUploadProcessingTaskV2::new} }); } @@ -124,7 +124,6 @@ public void setUp() { EventService.getInstance().subscribe(VulnerabilityAnalysisEvent.class, EventSubscriber.class); NotificationService.getInstance().subscribe(new Subscription(NotificationSubscriber.class)); - qm.createConfigProperty( BOM_PROCESSING_TASK_V2_ENABLED.getGroupName(), BOM_PROCESSING_TASK_V2_ENABLED.getPropertyName(), @@ -301,7 +300,7 @@ public void informTest() throws Exception { @Test public void informWithEmptyBomTest() throws Exception { // Known to now work with old task implementation. - if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTask) { + if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { return; } @@ -324,9 +323,9 @@ public void informWithEmptyBomTest() throws Exception { public void informWithInvalidCycloneDxBomTest() throws Exception { // Known to now work with old task implementation. - if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTask) { + if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { return; - } + } qm.createConfigProperty( BOM_VALIDATION_ENABLED.getGroupName(), @@ -372,7 +371,7 @@ public void informWithInvalidCycloneDxBomTest() throws Exception { @Test public void informWithNonExistentProjectTest() throws Exception { // Known to now work with old task implementation. - if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTask) { + if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { return; } @@ -396,7 +395,7 @@ public void informWithNonExistentProjectTest() throws Exception { @Test public void informWithComponentsUnderMetadataBomTest() throws Exception { // Known to now work with old task implementation. - if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTask) { + if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { return; } @@ -432,7 +431,7 @@ public void informWithComponentsUnderMetadataBomTest() throws Exception { @Test public void informWithExistingDuplicateComponentsTest() { // Known to now work with old task implementation. - if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTask) { + if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { return; } @@ -518,7 +517,7 @@ public void informWithExistingDuplicateComponentsTest() { @Test public void informWithBloatedBomTest() throws Exception { // Known to now work with old task implementation. - if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTask) { + if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { return; } @@ -574,7 +573,7 @@ public void informWithBloatedBomTest() throws Exception { @Test public void informWithCustomLicenseResolutionTest() throws Exception { // Known to now work with old task implementation. - if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTask) { + if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { return; } @@ -1049,7 +1048,7 @@ public void informWithExistingComponentPropertiesAndBomWithComponentProperties() @Test // https://github.com/DependencyTrack/dependency-track/issues/1905 public void informIssue1905Test() throws Exception { // Known to now work with old task implementation. - if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTask) { + if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { return; } @@ -1088,7 +1087,7 @@ public void informIssue1905Test() throws Exception { @Test // https://github.com/DependencyTrack/dependency-track/issues/2519 public void informIssue2519Test() throws Exception { // Known to now work with old task implementation. - if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTask) { + if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { return; } @@ -1166,7 +1165,7 @@ public void informIssue3309Test() { @Test // https://github.com/DependencyTrack/dependency-track/issues/3371 public void informIssue3371Test() throws Exception { // Known to now work with old task implementation. - if (bomUploadProcessingTaskSupplier.get() instanceof BomUploadProcessingTask) { + if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { return; } From 9f4669b59ca9b3fd3dea0d6d3a9472ece9bd8739 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:01:04 +0000 Subject: [PATCH 099/412] Bump debian from `2c96e00` to `ff39497` in /src/main/docker Bumps debian from `2c96e00` to `ff39497`. --- updated-dependencies: - dependency-name: debian dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- src/main/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docker/Dockerfile b/src/main/docker/Dockerfile index 69ee7b7c19..163815bd16 100644 --- a/src/main/docker/Dockerfile +++ b/src/main/docker/Dockerfile @@ -1,6 +1,6 @@ FROM eclipse-temurin:21.0.2_13-jre-jammy@sha256:d9f7b8326b9d396d070432982a998015a04ffb8885b145e97777a5ae324a8df1 AS jre-build -FROM debian:stable-slim@sha256:2c96e0011b1b2855d8e482e6d4ced6d292ea50750e9e535d09510167ae4858f9 +FROM debian:stable-slim@sha256:ff394977014e94e9a7c67bb22f5014ea069d156b86e001174f4bae6f4618297a # Arguments that can be passed at build time # Directory names must end with / to avoid errors when ADDing and COPYing From afb5cca94b99dc9c2b59b901cd00113e24d4466b Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 28 Apr 2024 17:41:19 +0200 Subject: [PATCH 100/412] Fix `JDOFatalUserException` for long reference URLs from OSS Index When a reference URL won't fit in the respective database column, try to drop its query parameters first, and if that doesn't help (unlikely), make a hard cut at 255 characters. Example occurrence of the exception in question: ``` javax.jdo.JDOFatalUserException: Attempt to store value "https://ossindex.sonatype.org/vulnerability/CVE-2023-47108?component-type=golang&component-`name=go.opentelemetry.io%2Fcontrib%2Finstrumentation%2Fgoogle.golang.org%2Fgrpc%2Fotelgrpc&utm_source=dependency-track&utm_medium=integration&utm_content=v4.11.0-SNAPSHOT" in column "REFERENCE_URL" that has maximum length of 255. Please correct your data!at org.datanucleus.api.jdo.JDOAdapter.getJDOExceptionForNucleusException(JDOAdapter.java:678) at org.datanucleus.api.jdo.JDOTransaction.commit(JDOTransaction.java:160) at alpine.persistence.AbstractAlpineQueryManager.persist(AbstractAlpineQueryManager.java:428) at org.dependencytrack.persistence.VulnerabilityQueryManager.addVulnerability(VulnerabilityQueryManager.java:218) at org.dependencytrack.persistence.QueryManager.addVulnerability(QueryManager.java:740) at org.dependencytrack.tasks.scanners.OssIndexAnalysisTask.processResults(OssIndexAnalysisTask.java:303) ... ``` Signed-off-by: nscuro --- .../model/FindingAttribution.java | 18 +++- .../model/FindingAttributionTest.java | 99 +++++++++++++++++++ 2 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/dependencytrack/model/FindingAttributionTest.java diff --git a/src/main/java/org/dependencytrack/model/FindingAttribution.java b/src/main/java/org/dependencytrack/model/FindingAttribution.java index 05bc0efa60..f5e6a25559 100644 --- a/src/main/java/org/dependencytrack/model/FindingAttribution.java +++ b/src/main/java/org/dependencytrack/model/FindingAttribution.java @@ -100,7 +100,7 @@ public FindingAttribution(Component component, Vulnerability vulnerability, Anal this.analyzerIdentity = analyzerIdentity; this.attributedOn = new Date(); this.alternateIdentifier = alternateIdentifier; - this.referenceUrl = referenceUrl; + this.referenceUrl = maybeTrimUrl(referenceUrl); } public long getId() { @@ -157,7 +157,7 @@ public String getReferenceUrl() { } public void setReferenceUrl(String referenceUrl) { - this.referenceUrl = referenceUrl; + this.referenceUrl = maybeTrimUrl(referenceUrl); } public UUID getUuid() { @@ -167,4 +167,18 @@ public UUID getUuid() { public void setUuid(UUID uuid) { this.uuid = uuid; } + + private static String maybeTrimUrl(final String url) { + if (url == null || url.length() <= 255) { + return url; + } + + final String[] parts = url.split("\\?", 2); + if (parts.length == 2 && parts[0].length() <= 255) { + return parts[0]; + } + + return parts[0].substring(0, 255); + } + } diff --git a/src/test/java/org/dependencytrack/model/FindingAttributionTest.java b/src/test/java/org/dependencytrack/model/FindingAttributionTest.java new file mode 100644 index 0000000000..03ecde057a --- /dev/null +++ b/src/test/java/org/dependencytrack/model/FindingAttributionTest.java @@ -0,0 +1,99 @@ +/* + * 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.model; + +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.tasks.scanners.AnalyzerIdentity; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FindingAttributionTest extends PersistenceCapableTest { + + @Test + public void testReferenceUrlTrimQueryParams() { + final var vuln = new Vulnerability(); + vuln.setVulnId("INT-001"); + vuln.setSource(Vulnerability.Source.INTERNAL); + qm.persist(vuln); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + qm.addVulnerability(vuln, component, AnalyzerIdentity.INTERNAL_ANALYZER, null, """ + https://example.com/vulnerability/INT-001\ + ?component-type=golang\ + &component-name=go.opentelemetry.io%2Fcontrib%2Finstrumentation%2Fgoogle.golang.org%2Fgrpc%2Fotelgrpc\ + &utm_source=dependency-track\ + &utm_medium=integration\ + &utm_content=v4.11.0-SNAPSHOT\ + &foo=3d48d174-0dc8-4ca5-83bb-ad697dd79b0f\ + &bar=bdf18448-0a40-4974-9758-f7b74b799395"""); + + final FindingAttribution attribution = qm.getFindingAttribution(vuln, component); + assertThat(attribution).isNotNull(); + assertThat(attribution.getReferenceUrl()).isEqualTo("https://example.com/vulnerability/INT-001"); + } + + @Test + public void testReferenceUrlTrim() { + final var vuln = new Vulnerability(); + vuln.setVulnId("INT-001"); + vuln.setSource(Vulnerability.Source.INTERNAL); + qm.persist(vuln); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + qm.addVulnerability(vuln, component, AnalyzerIdentity.INTERNAL_ANALYZER, null, """ + https://example.com/vulnerability/foo\ + /bdf18448-0a40-4974-9758-f7b74b799395\ + /ed8e9597-321b-4a7c-a407-cf09d39960a2\ + /a6912180-f41f-4411-ae91-132d6d9cce66\ + /9f36c7fa-0137-4497-a59f-8af49f1dc30b\ + /48217aa0-06ac-4b2b-a568-cf2575920412\ + /6f641470-c5f3-4ebf-97f8-365dfd070d36\ + /11f41751-fa0b-4ee7-9689-1438f920159b\ + /fa50b01b-7de8-41e9-a84e-1ddea28a2749"""); + + final FindingAttribution attribution = qm.getFindingAttribution(vuln, component); + assertThat(attribution).isNotNull(); + assertThat(attribution.getReferenceUrl()).hasSize(255).isEqualTo(""" + https://example.com/vulnerability/foo\ + /bdf18448-0a40-4974-9758-f7b74b799395\ + /ed8e9597-321b-4a7c-a407-cf09d39960a2\ + /a6912180-f41f-4411-ae91-132d6d9cce66\ + /9f36c7fa-0137-4497-a59f-8af49f1dc30b\ + /48217aa0-06ac-4b2b-a568-cf2575920412\ + /6f641470-c5f3-4ebf-97f8-365dfd07"""); + } + +} \ No newline at end of file From 9ae124a8fbc36ffbb5298f91b5d3a13972fe5bb3 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 28 Apr 2024 16:54:46 +0200 Subject: [PATCH 101/412] Gracefully handle unique constraint violations When uploading many BOMs in bulk, it can happen that vulnerability and repository metadata analysis is performed multiple times for the same component identity in parallel. It is more likely to happen when the BOMs being uploaded have big overlap in terms of components they contain. This can lead to unique constraint violations when DT updates the `COMPONENTANALYSISCACHE` table. Occurrences of this are non-critical, and kind of expected. Instead of letting the task crash, simply "accept" the exception and consider the job done. Note that DataNucleus will log a warning even when we handle the exception. Signed-off-by: nscuro --- .../RepositoryMetaAnalyzerTask.java | 37 ++++++++++++++++++- .../scanners/BaseComponentAnalyzerTask.java | 18 ++++++++- .../dependencytrack/util/PersistenceUtil.java | 26 +++++++++++++ 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java b/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java index 1752e1d2ed..d855fb9836 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java @@ -50,6 +50,8 @@ import java.util.Map; import java.util.concurrent.Callable; +import static org.dependencytrack.util.PersistenceUtil.isUniqueConstraintViolation; + public class RepositoryMetaAnalyzerTask implements Subscriber { private static final Logger LOGGER = Logger.getLogger(RepositoryMetaAnalyzerTask.class); @@ -175,7 +177,23 @@ private void analyze(final QueryManager qm, final Component component, final IMe } analyzer.setRepositoryBaseUrl(repository.getUrl()); model = analyzer.analyze(component); - qm.updateComponentAnalysisCache(ComponentAnalysisCache.CacheType.REPOSITORY, repository.getUrl(), repository.getType().name(), PurlUtil.silentPurlCoordinatesOnly(component.getPurl()).toString(), new Date(), buildRepositoryComponentAnalysisCacheResult(model)); + + try { + qm.updateComponentAnalysisCache(ComponentAnalysisCache.CacheType.REPOSITORY, repository.getUrl(), repository.getType().name(), PurlUtil.silentPurlCoordinatesOnly(component.getPurl()).toString(), new Date(), buildRepositoryComponentAnalysisCacheResult(model)); + } catch (RuntimeException e) { + if (isUniqueConstraintViolation(e)) { + LOGGER.debug(""" + Encountered unique constraint violation while updating cache. \ + This happens when repository metadata analysis is executed for the same \ + component multiple times concurrently, and is safe to ignore. \ + [targetHost=%s, source=%s, target=%s]\ + """.formatted(repository.getUrl(), repository.getType(), PurlUtil.silentPurlCoordinatesOnly(component.getPurl())), e); + qm.ensureNoActiveTransaction(); // Workaround for https://github.com/DependencyTrack/dependency-track/issues/2677 + return; + } else { + throw e; + } + } } if (StringUtils.trimToNull(model.getLatestVersion()) != null) { @@ -187,7 +205,22 @@ private void analyze(final QueryManager qm, final Component component, final IMe metaComponent.setPublished(model.getPublishedTimestamp()); metaComponent.setLatestVersion(model.getLatestVersion()); metaComponent.setLastCheck(new Date()); - qm.synchronizeRepositoryMetaComponent(metaComponent); + try { + qm.synchronizeRepositoryMetaComponent(metaComponent); + } catch (RuntimeException e) { + if (isUniqueConstraintViolation(e)) { + LOGGER.debug(""" + Encountered unique constraint violation while synchronizing metadata. \ + This happens when repository metadata analysis is executed for the same \ + component multiple times concurrently, and is safe to ignore. \ + [targetHost=%s, source=%s, target=%s]\ + """.formatted(repository.getUrl(), repository.getType(), PurlUtil.silentPurlCoordinatesOnly(component.getPurl())), e); + qm.ensureNoActiveTransaction(); // Workaround for https://github.com/DependencyTrack/dependency-track/issues/2677 + return; + } else { + throw e; + } + } // Since the component metadata found and captured from this repository, return from this // method without attempting to query additional repositories. LOGGER.debug("Found component metadata for: " + component.getUuid() + " using repository: " diff --git a/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java b/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java index 4d1b8a6190..787b008c14 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java @@ -41,6 +41,8 @@ import javax.json.JsonObject; import java.util.Date; +import static org.dependencytrack.util.PersistenceUtil.isUniqueConstraintViolation; + /** * A base class that has logic common or useful to all classes that extend it. * @@ -112,7 +114,21 @@ protected void applyAnalysisFromCache(Vulnerability.Source source, String target protected synchronized void updateAnalysisCacheStats(QueryManager qm, Vulnerability.Source source, String targetHost, String target, JsonObject result) { - qm.updateComponentAnalysisCache(ComponentAnalysisCache.CacheType.VULNERABILITY, targetHost, source.name(), target, new Date(), result); + try { + qm.updateComponentAnalysisCache(ComponentAnalysisCache.CacheType.VULNERABILITY, targetHost, source.name(), target, new Date(), result); + } catch (RuntimeException e) { + if (isUniqueConstraintViolation(e)) { + LOGGER.debug(""" + Encountered unique constraint violation while updating cache. \ + This happens when vulnerability analysis is executed for the same \ + component identity multiple times concurrently, and is safe to ignore. \ + [targetHost=%s, source=%s, target=%s]\ + """.formatted(targetHost, source, target), e); + qm.ensureNoActiveTransaction(); // Workaround for https://github.com/DependencyTrack/dependency-track/issues/2677 + } else { + throw e; + } + } } protected void addVulnerabilityToCache(Component component, Vulnerability vulnerability) { diff --git a/src/main/java/org/dependencytrack/util/PersistenceUtil.java b/src/main/java/org/dependencytrack/util/PersistenceUtil.java index c49b7c7b3a..a2c0a73da2 100644 --- a/src/main/java/org/dependencytrack/util/PersistenceUtil.java +++ b/src/main/java/org/dependencytrack/util/PersistenceUtil.java @@ -18,14 +18,19 @@ */ package org.dependencytrack.util; +import com.mysql.cj.exceptions.MysqlErrorNumbers; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.datanucleus.enhancement.Persistable; import org.dependencytrack.persistence.QueryManager; +import org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException; +import org.postgresql.util.PSQLState; import javax.jdo.JDOHelper; import javax.jdo.ObjectState; import javax.jdo.PersistenceManager; import javax.jdo.PersistenceManagerFactory; +import java.sql.SQLException; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -260,4 +265,25 @@ private static Object getDataNucleusJdoObjectId(final Object object) { return objectId; } + public static boolean isUniqueConstraintViolation(final Throwable throwable) { + // NB: DataNucleus doesn't map constraint violation exceptions, + // so we have to depend on underlying JDBC driver's exception to + // tell us what happened. Leaky abstraction FTW. + final Throwable rootCause = ExceptionUtils.getRootCause(throwable); + + // H2 has a dedicated exception for this. + if (rootCause instanceof JdbcSQLIntegrityConstraintViolationException) { + return true; + } + + // Other RDBMSes use the SQL state to communicate errors. + if (rootCause instanceof final SQLException se) { + return MysqlErrorNumbers.SQL_STATE_INTEGRITY_CONSTRAINT_VIOLATION.equals(se.getSQLState()) // MySQL + || PSQLState.UNIQUE_VIOLATION.getState().equals(se.getSQLState()) // PostgreSQL + || "23000".equals(se.getSQLState()); // SQL Server + } + + return false; + } + } From 05afa080c6297fffe2c2e94eab6c83fc05efb68f Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 28 Apr 2024 20:01:59 +0200 Subject: [PATCH 102/412] Log debug information upon possible secret key corruption I was not able to reproduce the issue in question, so it's impossible to know why some users are experiencing secret decryption failures out of the blue. If for whatever reason the secret key in-memory gets corrupted, we now try to reload it from disk once, and log a warning about it. If on the other hand the encrypted value itself got corrupted (perhaps due to some bug in DataNucleus), we log a warning about that as well. In both cases, the warning logs contain a reference to issue #2366. Relates to #2366 Signed-off-by: nscuro --- .../fortifyssc/FortifySscUploader.java | 7 +- .../kenna/KennaSecurityUploader.java | 4 +- .../notification/publisher/JiraPublisher.java | 4 +- .../publisher/SendMailPublisher.java | 6 +- .../tasks/NistApiMirrorTask.java | 4 +- .../RepositoryMetaAnalyzerTask.java | 4 +- .../tasks/scanners/OssIndexAnalysisTask.java | 4 +- .../tasks/scanners/SnykAnalysisTask.java | 4 +- .../tasks/scanners/TrivyAnalysisTask.java | 4 +- .../tasks/scanners/VulnDbAnalysisTask.java | 5 +- .../util/DebugDataEncryption.java | 111 ++++++++++++++++++ .../util/DebugDataEncryptionTest.java | 56 +++++++++ 12 files changed, 190 insertions(+), 23 deletions(-) create mode 100644 src/main/java/org/dependencytrack/util/DebugDataEncryption.java create mode 100644 src/test/java/org/dependencytrack/util/DebugDataEncryptionTest.java diff --git a/src/main/java/org/dependencytrack/integrations/fortifyssc/FortifySscUploader.java b/src/main/java/org/dependencytrack/integrations/fortifyssc/FortifySscUploader.java index d2c1da448d..f6456f8b27 100644 --- a/src/main/java/org/dependencytrack/integrations/fortifyssc/FortifySscUploader.java +++ b/src/main/java/org/dependencytrack/integrations/fortifyssc/FortifySscUploader.java @@ -20,13 +20,13 @@ import alpine.common.logging.Logger; import alpine.model.ConfigProperty; -import alpine.security.crypto.DataEncryption; import org.dependencytrack.integrations.AbstractIntegrationPoint; import org.dependencytrack.integrations.FindingPackagingFormat; import org.dependencytrack.integrations.ProjectFindingUploader; import org.dependencytrack.model.Finding; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectProperty; +import org.dependencytrack.util.DebugDataEncryption; import org.json.JSONObject; import java.io.ByteArrayInputStream; @@ -35,9 +35,8 @@ import java.util.List; import static org.dependencytrack.model.ConfigPropertyConstants.FORTIFY_SSC_ENABLED; - -import static org.dependencytrack.model.ConfigPropertyConstants.FORTIFY_SSC_URL; import static org.dependencytrack.model.ConfigPropertyConstants.FORTIFY_SSC_TOKEN; +import static org.dependencytrack.model.ConfigPropertyConstants.FORTIFY_SSC_URL; public class FortifySscUploader extends AbstractIntegrationPoint implements ProjectFindingUploader { @@ -83,7 +82,7 @@ public void upload(final Project project, final InputStream payload) { } try { final FortifySscClient client = new FortifySscClient(this, new URL(sscUrl.getPropertyValue())); - final String token = client.generateOneTimeUploadToken(DataEncryption.decryptAsString(citoken.getPropertyValue())); + final String token = client.generateOneTimeUploadToken(DebugDataEncryption.decryptAsString(citoken.getPropertyValue())); if (token != null) { client.uploadDependencyTrackFindings(token, applicationId.getPropertyValue(), payload); } diff --git a/src/main/java/org/dependencytrack/integrations/kenna/KennaSecurityUploader.java b/src/main/java/org/dependencytrack/integrations/kenna/KennaSecurityUploader.java index 3f58b53276..99f4675fc9 100644 --- a/src/main/java/org/dependencytrack/integrations/kenna/KennaSecurityUploader.java +++ b/src/main/java/org/dependencytrack/integrations/kenna/KennaSecurityUploader.java @@ -20,7 +20,6 @@ import alpine.common.logging.Logger; import alpine.model.ConfigProperty; -import alpine.security.crypto.DataEncryption; import org.apache.http.HttpEntity; import org.apache.http.HttpStatus; import org.apache.http.NameValuePair; @@ -37,6 +36,7 @@ import org.dependencytrack.integrations.PortfolioFindingUploader; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectProperty; +import org.dependencytrack.util.DebugDataEncryption; import org.json.JSONObject; import java.io.ByteArrayInputStream; @@ -98,7 +98,7 @@ public void upload(final InputStream payload) { LOGGER.debug("Uploading payload to KennaSecurity"); final ConfigProperty tokenProperty = qm.getConfigProperty(KENNA_TOKEN.getGroupName(), KENNA_TOKEN.getPropertyName()); try { - final String token = DataEncryption.decryptAsString(tokenProperty.getPropertyValue()); + final String token = DebugDataEncryption.decryptAsString(tokenProperty.getPropertyValue()); HttpPost request = new HttpPost(String.format(CONNECTOR_UPLOAD_URL, connectorId)); request.addHeader("X-Risk-Token", token); request.addHeader("accept", "application/json"); diff --git a/src/main/java/org/dependencytrack/notification/publisher/JiraPublisher.java b/src/main/java/org/dependencytrack/notification/publisher/JiraPublisher.java index 1f272524b0..fbe6b2b354 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/JiraPublisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/JiraPublisher.java @@ -21,10 +21,10 @@ import alpine.common.logging.Logger; import alpine.model.ConfigProperty; import alpine.notification.Notification; -import alpine.security.crypto.DataEncryption; import io.pebbletemplates.pebble.PebbleEngine; import org.dependencytrack.exception.PublisherException; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.DebugDataEncryption; import javax.json.JsonObject; import java.util.Map; @@ -90,7 +90,7 @@ protected AuthCredentials getAuthCredentials() { final ConfigProperty jiraUsernameProp = qm.getConfigProperty(JIRA_USERNAME.getGroupName(), JIRA_USERNAME.getPropertyName()); final String jiraUsername = (jiraUsernameProp == null) ? null : jiraUsernameProp.getPropertyValue(); final ConfigProperty jiraPasswordProp = qm.getConfigProperty(JIRA_PASSWORD.getGroupName(), JIRA_PASSWORD.getPropertyName()); - final String jiraPassword = (jiraPasswordProp == null || jiraPasswordProp.getPropertyValue() == null) ? null : DataEncryption.decryptAsString(jiraPasswordProp.getPropertyValue()); + final String jiraPassword = (jiraPasswordProp == null || jiraPasswordProp.getPropertyValue() == null) ? null : DebugDataEncryption.decryptAsString(jiraPasswordProp.getPropertyValue()); return new AuthCredentials(jiraUsername, jiraPassword); } catch (final Exception e) { throw new PublisherException("An error occurred during the retrieval of Jira credentials", e); diff --git a/src/main/java/org/dependencytrack/notification/publisher/SendMailPublisher.java b/src/main/java/org/dependencytrack/notification/publisher/SendMailPublisher.java index c4349f09fe..31f136de22 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/SendMailPublisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/SendMailPublisher.java @@ -24,12 +24,12 @@ import alpine.model.OidcUser; import alpine.model.Team; import alpine.notification.Notification; -import alpine.security.crypto.DataEncryption; import alpine.server.mail.SendMail; import alpine.server.mail.SendMailException; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.DebugDataEncryption; import javax.json.JsonObject; import javax.json.JsonString; @@ -42,9 +42,9 @@ import java.util.function.Predicate; import java.util.stream.Stream; +import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_PREFIX; import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_ENABLED; import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_FROM_ADDR; -import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_PREFIX; import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_PASSWORD; import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_SERVER_HOSTNAME; import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_SERVER_PORT; @@ -130,7 +130,7 @@ private void sendNotification(final PublishContext ctx, Notification notificatio final boolean smtpAuth = (smtpUser != null && encryptedSmtpPassword != null); final String decryptedSmtpPassword; try { - decryptedSmtpPassword = (encryptedSmtpPassword != null) ? DataEncryption.decryptAsString(encryptedSmtpPassword) : null; + decryptedSmtpPassword = (encryptedSmtpPassword != null) ? DebugDataEncryption.decryptAsString(encryptedSmtpPassword) : null; } catch (Exception e) { LOGGER.error("Failed to decrypt SMTP password (%s)".formatted(ctx), e); return; diff --git a/src/main/java/org/dependencytrack/tasks/NistApiMirrorTask.java b/src/main/java/org/dependencytrack/tasks/NistApiMirrorTask.java index 9e55c430b5..6461c6b8ac 100644 --- a/src/main/java/org/dependencytrack/tasks/NistApiMirrorTask.java +++ b/src/main/java/org/dependencytrack/tasks/NistApiMirrorTask.java @@ -25,7 +25,6 @@ import alpine.event.framework.LoggableUncaughtExceptionHandler; import alpine.event.framework.Subscriber; import alpine.model.ConfigProperty; -import alpine.security.crypto.DataEncryption; import io.github.jeremylong.openvulnerability.client.HttpAsyncClientSupplier; import io.github.jeremylong.openvulnerability.client.nvd.CveItem; import io.github.jeremylong.openvulnerability.client.nvd.DefCveItem; @@ -53,6 +52,7 @@ import org.dependencytrack.model.Vulnerability.Source; import org.dependencytrack.model.VulnerableSoftware; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.DebugDataEncryption; import org.dependencytrack.util.PersistenceUtil.Diff; import org.dependencytrack.util.PersistenceUtil.Differ; @@ -126,7 +126,7 @@ public void inform(final Event e) { .map(StringUtils::trimToNull) .map(encryptedApiKey -> { try { - return DataEncryption.decryptAsString(encryptedApiKey); + return DebugDataEncryption.decryptAsString(encryptedApiKey); } catch (Exception ex) { LOGGER.warn("Failed to decrypt API key; Continuing without authentication", ex); return null; diff --git a/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java b/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java index 1752e1d2ed..532ad77f5b 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java @@ -24,7 +24,6 @@ import alpine.event.framework.Event; import alpine.event.framework.Subscriber; import alpine.model.ConfigProperty; -import alpine.security.crypto.DataEncryption; import io.micrometer.core.instrument.Timer; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.common.ConfigKey; @@ -38,6 +37,7 @@ import org.dependencytrack.model.RepositoryType; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.util.CacheStampedeBlocker; +import org.dependencytrack.util.DebugDataEncryption; import org.dependencytrack.util.PurlUtil; import javax.json.Json; @@ -166,7 +166,7 @@ private void analyze(final QueryManager qm, final Component component, final IMe try { String decryptedPassword = null; if (repository.getPassword() != null) { - decryptedPassword = DataEncryption.decryptAsString(repository.getPassword()); + decryptedPassword = DebugDataEncryption.decryptAsString(repository.getPassword()); } analyzer.setRepositoryUsernameAndPassword(repository.getUsername(), decryptedPassword); } catch (Exception e) { diff --git a/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java index c620a14a6e..37fba86f9d 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java @@ -25,7 +25,6 @@ import alpine.event.framework.Event; import alpine.event.framework.Subscriber; import alpine.model.ConfigProperty; -import alpine.security.crypto.DataEncryption; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; import io.github.resilience4j.micrometer.tagged.TaggedRetryMetrics; @@ -55,6 +54,7 @@ import org.dependencytrack.parser.ossindex.model.ComponentReport; import org.dependencytrack.parser.ossindex.model.ComponentReportVulnerability; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.DebugDataEncryption; import org.dependencytrack.util.HttpUtil; import org.dependencytrack.util.NotificationUtil; import org.dependencytrack.util.VulnerabilityUtil; @@ -156,7 +156,7 @@ public void inform(final Event e) { } else { try { apiUsername = apiUsernameProperty.getPropertyValue(); - apiToken = DataEncryption.decryptAsString(apiTokenProperty.getPropertyValue()); + apiToken = DebugDataEncryption.decryptAsString(apiTokenProperty.getPropertyValue()); } catch (Exception ex) { LOGGER.error("An error occurred decrypting the OSS Index API Token. Skipping", ex); return; diff --git a/src/main/java/org/dependencytrack/tasks/scanners/SnykAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/SnykAnalysisTask.java index 8b93155c93..d65fbbdc0b 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/SnykAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/SnykAnalysisTask.java @@ -28,7 +28,6 @@ import alpine.model.ConfigProperty; import alpine.notification.Notification; import alpine.notification.NotificationLevel; -import alpine.security.crypto.DataEncryption; import com.github.packageurl.PackageURL; import io.github.resilience4j.micrometer.tagged.TaggedRetryMetrics; import io.github.resilience4j.retry.Retry; @@ -58,6 +57,7 @@ import org.dependencytrack.parser.snyk.SnykParser; import org.dependencytrack.parser.snyk.model.SnykError; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.DebugDataEncryption; import org.dependencytrack.util.NotificationUtil; import org.dependencytrack.util.RoundRobinAccessor; import org.json.JSONArray; @@ -196,7 +196,7 @@ public void inform(final Event e) { apiVersion = apiVersionProperty.getPropertyValue(); try { - final String decryptedToken = DataEncryption.decryptAsString(apiTokenProperty.getPropertyValue()); + final String decryptedToken = DebugDataEncryption.decryptAsString(apiTokenProperty.getPropertyValue()); apiTokenSupplier = createTokenSupplier(decryptedToken); } catch (Exception ex) { LOGGER.error("An error occurred decrypting the Snyk API Token; Skipping", ex); diff --git a/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java index 709cffcba4..975c5daf45 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java @@ -25,7 +25,6 @@ import alpine.event.framework.Event; import alpine.event.framework.Subscriber; import alpine.model.ConfigProperty; -import alpine.security.crypto.DataEncryption; import com.github.packageurl.PackageURL; import com.google.gson.Gson; import io.github.resilience4j.micrometer.tagged.TaggedRetryMetrics; @@ -65,6 +64,7 @@ import org.dependencytrack.parser.trivy.model.ScanRequest; import org.dependencytrack.parser.trivy.model.TrivyResponse; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.DebugDataEncryption; import org.dependencytrack.util.NotificationUtil; import java.nio.charset.StandardCharsets; @@ -151,7 +151,7 @@ public void inform(final Event e) { apiBaseUrl = getApiBaseUrl().get(); try { - apiToken = DataEncryption.decryptAsString(apiTokenProperty.getPropertyValue()); + apiToken = DebugDataEncryption.decryptAsString(apiTokenProperty.getPropertyValue()); } catch (Exception ex) { LOGGER.error("An error occurred decrypting the Trivy API token; Skipping", ex); return; diff --git a/src/main/java/org/dependencytrack/tasks/scanners/VulnDbAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/VulnDbAnalysisTask.java index 52e994e294..1d9359100e 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/VulnDbAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/VulnDbAnalysisTask.java @@ -22,7 +22,6 @@ import alpine.event.framework.Event; import alpine.event.framework.Subscriber; import alpine.model.ConfigProperty; -import alpine.security.crypto.DataEncryption; import oauth.signpost.exception.OAuthCommunicationException; import oauth.signpost.exception.OAuthExpectationFailedException; import oauth.signpost.exception.OAuthMessageSignerException; @@ -35,7 +34,9 @@ import org.dependencytrack.parser.vulndb.VulnDbClient; import org.dependencytrack.parser.vulndb.model.Results; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.DebugDataEncryption; import org.dependencytrack.util.NotificationUtil; + import java.io.IOException; import java.net.URISyntaxException; import java.util.List; @@ -103,7 +104,7 @@ public void inform(final Event e) { } this.apiConsumerKey = apiConsumerKey.getPropertyValue(); try { - this.apiConsumerSecret = DataEncryption.decryptAsString(apiConsumerSecret.getPropertyValue()); + this.apiConsumerSecret = DebugDataEncryption.decryptAsString(apiConsumerSecret.getPropertyValue()); } catch (Exception ex) { LOGGER.error("An error occurred decrypting the VulnDB consumer secret. Skipping", ex); return; diff --git a/src/main/java/org/dependencytrack/util/DebugDataEncryption.java b/src/main/java/org/dependencytrack/util/DebugDataEncryption.java new file mode 100644 index 0000000000..4db625cebd --- /dev/null +++ b/src/main/java/org/dependencytrack/util/DebugDataEncryption.java @@ -0,0 +1,111 @@ +/* + * 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.util; + +import alpine.common.logging.Logger; +import alpine.security.crypto.DataEncryption; +import alpine.security.crypto.KeyManager; +import org.apache.commons.codec.digest.DigestUtils; + +import javax.crypto.BadPaddingException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @since 4.11.0 + */ +public class DebugDataEncryption { + + private static final Logger LOGGER = Logger.getLogger(DataEncryption.class); + private static final ReentrantLock RELOAD_LOCK = new ReentrantLock(); + + /** + * Wrapper around {@link DataEncryption#decryptAsString(String)} to help with debugging + * and / or handling of {@link BadPaddingException}s some users are experiencing. + * + * @param encryptedText the text to decrypt + * @return the decrypted string + * @throws Exception a number of exceptions may be thrown + * @see Issue 265 + * @see Issue 2366 + * @see Slack Thread + * @see DataEncryption#decryptAsString(String) + */ + public static String decryptAsString(final String encryptedText) throws Exception { + return retryDecryptAsString(encryptedText, 0); + } + + private static String retryDecryptAsString(final String encryptedText, final int attempt) throws Exception { + try { + return DataEncryption.decryptAsString(encryptedText); + } catch (BadPaddingException e) { + final byte[] currentKey = KeyManager.getInstance().getSecretKey().getEncoded(); + + RELOAD_LOCK.lock(); + try { + reloadKeys(); + } catch (NoSuchFieldException | NoSuchMethodException | InvocationTargetException + | IllegalAccessException | RuntimeException ex) { + LOGGER.warn("Failed to reload keys while handling %s".formatted(e), ex); + throw e; + } finally { + RELOAD_LOCK.unlock(); + } + + final byte[] reloadedKey = KeyManager.getInstance().getSecretKey().getEncoded(); + final boolean isKeyDifferent = !Arrays.equals(currentKey, reloadedKey); + + if (isKeyDifferent && attempt < 1) { + LOGGER.warn(""" + Failed to decrypt value, possibly because the secret key in memory got corrupted. \ + Reloaded key from disk and detected it being different from the previously loaded key. \ + Please report this to https://github.com/DependencyTrack/dependency-track/issues/2366 \ + so the root cause can be identified. Additional information about the keys: \ + length{previous=%d, reloaded=%d}, sha256={previous=%s, reloaded=%s}""" + .formatted(currentKey.length, reloadedKey.length, DigestUtils.sha256Hex(currentKey), DigestUtils.sha256Hex(reloadedKey))); + return retryDecryptAsString(encryptedText, attempt + 1); + } else if (!isKeyDifferent) { + LOGGER.warn(""" + Failed to decrypt value, possibly because it has been encrypted with a different key, \ + or the encrypted value is corrupted. To verify the latter, please check the respective \ + value in the database. For comparison, the base64-encoded value for which decryption \ + was attempted has a length of %d, and a sha256 digest of %s. If this is different to what's \ + stored in your database, please report this to https://github.com/DependencyTrack/dependency-track/issues/2366, \ + so the root cause can be identified.""".formatted(encryptedText.length(), DigestUtils.sha256Hex(encryptedText))); + } + + throw e; + } + } + + private static void reloadKeys() throws NoSuchFieldException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + final Field secretKeyField = KeyManager.class.getDeclaredField("secretKey"); + secretKeyField.setAccessible(true); + + final Method loadMethod = KeyManager.class.getDeclaredMethod("initialize"); + loadMethod.setAccessible(true); + + secretKeyField.set(KeyManager.getInstance(), null); + loadMethod.invoke(KeyManager.getInstance()); + } + +} diff --git a/src/test/java/org/dependencytrack/util/DebugDataEncryptionTest.java b/src/test/java/org/dependencytrack/util/DebugDataEncryptionTest.java new file mode 100644 index 0000000000..cdc40bae87 --- /dev/null +++ b/src/test/java/org/dependencytrack/util/DebugDataEncryptionTest.java @@ -0,0 +1,56 @@ +/* + * 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.util; + +import alpine.security.crypto.DataEncryption; +import alpine.security.crypto.KeyManager; +import org.junit.Test; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.lang.reflect.Field; +import java.security.SecureRandom; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DebugDataEncryptionTest { + + @Test + public void testReloadAndRetry() throws Exception { + final Field secretKeyField = KeyManager.class.getDeclaredField("secretKey"); + secretKeyField.setAccessible(true); + + // Encrypt a value with KeyManager's current secret key. + final String encryptedValue = DataEncryption.encryptAsString("foobarbaz"); + + // Generate a new secret key and replace KeyManager's current key with it. + final KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + final SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + keyGen.init(256, random); + final SecretKey newSecretKey = keyGen.generateKey(); + secretKeyField.set(KeyManager.getInstance(), newSecretKey); + + // Decrypt the value. This should work due to DebugDataEncryption + // reloading the secret key from disk upon decryption failure. + final String decryptedValue = DebugDataEncryption.decryptAsString(encryptedValue); + assertThat(decryptedValue).isEqualTo("foobarbaz"); + } + + +} \ No newline at end of file From cb94e618335c878e0345da05ae4c63092697a03e Mon Sep 17 00:00:00 2001 From: Niklas Date: Sun, 28 Apr 2024 22:49:13 +0200 Subject: [PATCH 103/412] Bump Temurin base image to `21.0.3_9` Signed-off-by: Niklas --- src/main/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docker/Dockerfile b/src/main/docker/Dockerfile index 163815bd16..b48899717b 100644 --- a/src/main/docker/Dockerfile +++ b/src/main/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM eclipse-temurin:21.0.2_13-jre-jammy@sha256:d9f7b8326b9d396d070432982a998015a04ffb8885b145e97777a5ae324a8df1 AS jre-build +FROM eclipse-temurin:21.0.3_9-jre-jammy@sha256:a56ee1f79cf57b2b31152cd471a4c85b6deb3057e4a1fbe8e50b57e7d2a1d7c9 AS jre-build FROM debian:stable-slim@sha256:ff394977014e94e9a7c67bb22f5014ea069d156b86e001174f4bae6f4618297a From c2ec16b7cf4bac432ebd8bd75f12294f052dd427 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 08:12:24 +0000 Subject: [PATCH 104/412] Bump actions/upload-artifact from 4.3.2 to 4.3.3 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.2 to 4.3.3. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/1746f4ab65b179e0ea60a494b83293b640dd5bba...65462800fd760344b1a7b4382951275a0abb4808) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- .github/workflows/ci-test.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 253d702104..c5ad034d9d 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -51,7 +51,7 @@ jobs: mvn cyclonedx:makeBom -Dservices.bom.merge.skip=false org.codehaus.mojo:exec-maven-plugin:exec@merge-services-bom - name: Upload Artifacts - uses: actions/upload-artifact@1746f4ab65b179e0ea60a494b83293b640dd5bba # tag=v4.3.2 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # tag=v4.3.3 with: name: assembled-wars path: |- diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index 1444cfa16c..ba1cbcec5c 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -64,7 +64,7 @@ jobs: - name: Upload PR test coverage report if: ${{ github.event_name == 'pull_request' }} - uses: actions/upload-artifact@1746f4ab65b179e0ea60a494b83293b640dd5bba # tag=v4.3.2 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # tag=v4.3.3 with: name: pr-test-coverage-report path: |- From ce05bb0c6dcf998a98eceda307f211f4a937b56e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 08:12:31 +0000 Subject: [PATCH 105/412] Bump actions/checkout from 4.1.3 to 4.1.4 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.3 to 4.1.4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/1d96c772d19495a3b5c517cd2bc0cb401ea0529f...0ad4b8fadaa221de15dcec353f45205ec38ea70b) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 4 ++-- .github/workflows/ci-publish.yaml | 4 ++-- .github/workflows/ci-release.yaml | 6 +++--- .github/workflows/ci-test.yaml | 2 +- .github/workflows/dependency-review.yaml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 253d702104..58914611a7 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 - name: Set up JDK uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v4.2.1 @@ -74,7 +74,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 - name: Download Artifacts uses: actions/download-artifact@8caf195ad4b1dee92908e23f56eeb0696f1dd42d # tag=v4.1.5 diff --git a/.github/workflows/ci-publish.yaml b/.github/workflows/ci-publish.yaml index aae27e9558..de04ca6270 100644 --- a/.github/workflows/ci-publish.yaml +++ b/.github/workflows/ci-publish.yaml @@ -23,7 +23,7 @@ jobs: exit 1 fi - name: Checkout Repository - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 - name: Parse Version from POM id: parse @@ -51,7 +51,7 @@ jobs: - call-build steps: - name: Checkout Repository - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 - name: Download Artifacts uses: actions/download-artifact@8caf195ad4b1dee92908e23f56eeb0696f1dd42d # tag=v4.1.5 diff --git a/.github/workflows/ci-release.yaml b/.github/workflows/ci-release.yaml index 293a6acdcb..b24b9adca9 100644 --- a/.github/workflows/ci-release.yaml +++ b/.github/workflows/ci-release.yaml @@ -20,7 +20,7 @@ jobs: release-branch: ${{ steps.variables.outputs.release-branch }} steps: - name: Checkout Repository - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 - name: Setup Environment id: variables @@ -51,7 +51,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 - name: Set up JDK uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v4.2.1 @@ -118,7 +118,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 with: ref: ${{ needs.prepare-release.outputs.release-branch }} diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index 1444cfa16c..6a900db60e 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 - name: Set up JDK uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v4.2.1 diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index edee799dac..06f005fa2c 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # tag=v4.1.3 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 - name: Dependency Review uses: actions/dependency-review-action@5bbc3ba658137598168acb2ab73b21c432dd411b # tag=v4.2.5 From 28da431b3703bc2df9be66d47617f340991e4dcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 08:12:44 +0000 Subject: [PATCH 106/412] Bump github/codeql-action from 3.25.1 to 3.25.3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.25.1 to 3.25.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/c7f9125735019aa87cfc361530512d50ea439c71...d39d31e687223d841ef683f52467bd88e9b21c14) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 253d702104..e1969b6ff8 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -133,6 +133,6 @@ jobs: - name: Upload Trivy Scan Results to GitHub Security Tab if: ${{ inputs.publish-container }} - uses: github/codeql-action/upload-sarif@c7f9125735019aa87cfc361530512d50ea439c71 # tag=v3.25.1 + uses: github/codeql-action/upload-sarif@d39d31e687223d841ef683f52467bd88e9b21c14 # tag=v3.25.3 with: sarif_file: 'trivy-results.sarif' From 494ac81f07c0f1dd72ffc510f74ae3f4a24ba954 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:08:55 +0000 Subject: [PATCH 107/412] Bump actions/download-artifact from 4.1.5 to 4.1.7 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.1.5 to 4.1.7. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/8caf195ad4b1dee92908e23f56eeb0696f1dd42d...65a9edc5881444af0b9093a5e628f2fe47ea3b2e) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- .github/workflows/ci-publish.yaml | 2 +- .github/workflows/ci-test-pr-coverage.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 5fd146d38b..877e620862 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -77,7 +77,7 @@ jobs: uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 - name: Download Artifacts - uses: actions/download-artifact@8caf195ad4b1dee92908e23f56eeb0696f1dd42d # tag=v4.1.5 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # tag=v4.1.7 with: name: assembled-wars path: target diff --git a/.github/workflows/ci-publish.yaml b/.github/workflows/ci-publish.yaml index de04ca6270..9c6c0a75bb 100644 --- a/.github/workflows/ci-publish.yaml +++ b/.github/workflows/ci-publish.yaml @@ -54,7 +54,7 @@ jobs: uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 - name: Download Artifacts - uses: actions/download-artifact@8caf195ad4b1dee92908e23f56eeb0696f1dd42d # tag=v4.1.5 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # tag=v4.1.7 with: name: assembled-wars path: target diff --git a/.github/workflows/ci-test-pr-coverage.yml b/.github/workflows/ci-test-pr-coverage.yml index a0047f080e..c133f1809b 100644 --- a/.github/workflows/ci-test-pr-coverage.yml +++ b/.github/workflows/ci-test-pr-coverage.yml @@ -18,7 +18,7 @@ jobs: && github.event.workflow_run.conclusion == 'success' steps: - name: Download PR test coverage report - uses: actions/download-artifact@8caf195ad4b1dee92908e23f56eeb0696f1dd42d # tag=v4.1.5 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # tag=v4.1.7 with: name: pr-test-coverage-report github-token: ${{ secrets.GITHUB_TOKEN }} From 7d28a1ae1d02e8229c42d82d88c866005d488e3c Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 29 Apr 2024 19:45:47 +0200 Subject: [PATCH 108/412] Add support for worker pool drain timeout This surfaces the functionality introduced in https://github.com/stevespringett/Alpine/pull/508 to Dependency-Track. It was previously only integrated into Hyades. Signed-off-by: nscuro --- docs/_docs/getting-started/configuration.md | 7 +++++++ .../java/org/dependencytrack/common/ConfigKey.java | 3 ++- .../event/EventSubsystemInitializer.java | 11 ++++++++--- .../NotificationSubsystemInitializer.java | 8 +++++++- src/main/resources/application.properties | 7 +++++++ 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/docs/_docs/getting-started/configuration.md b/docs/_docs/getting-started/configuration.md index 2df1f8f6d0..a3ac42f378 100644 --- a/docs/_docs/getting-started/configuration.md +++ b/docs/_docs/getting-started/configuration.md @@ -56,6 +56,13 @@ alpine.worker.threads=0 # 16 worker threads. Default value is 4. alpine.worker.thread.multiplier=4 +# Required +# Defines the maximum duration for which Dependency-Track will wait for queued +# events and notifications to be processed when shutting down. +# During shutdown, newly dispatched events will not be accepted. +# The duration must be specified in ISO 8601 notation (https://en.wikipedia.org/wiki/ISO_8601#Durations). +alpine.worker.pool.drain.timeout.duration=PT5S + # Required # Defines the path to the data directory. This directory will hold logs, keys, # and any database or index files along with application-specific files or diff --git a/src/main/java/org/dependencytrack/common/ConfigKey.java b/src/main/java/org/dependencytrack/common/ConfigKey.java index a4e8b4ed08..b813fdc1b3 100644 --- a/src/main/java/org/dependencytrack/common/ConfigKey.java +++ b/src/main/java/org/dependencytrack/common/ConfigKey.java @@ -39,7 +39,8 @@ public enum ConfigKey implements Config.Key { REPO_META_ANALYZER_CACHE_STAMPEDE_BLOCKER_ENABLED("repo.meta.analyzer.cacheStampedeBlocker.enabled", true), REPO_META_ANALYZER_CACHE_STAMPEDE_BLOCKER_LOCK_BUCKETS("repo.meta.analyzer.cacheStampedeBlocker.lock.buckets", 1000), REPO_META_ANALYZER_CACHE_STAMPEDE_BLOCKER_MAX_ATTEMPTS("repo.meta.analyzer.cacheStampedeBlocker.max.attempts", 10), - SYSTEM_REQUIREMENT_CHECK_ENABLED("system.requirement.check.enabled", true); + SYSTEM_REQUIREMENT_CHECK_ENABLED("system.requirement.check.enabled", true), + ALPINE_WORKER_POOL_DRAIN_TIMEOUT_DURATION("alpine.worker.pool.drain.timeout.duration", "PT5S"); private final String propertyName; private final Object defaultValue; diff --git a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java index d0f7ccc91b..7d6e73fc1b 100644 --- a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java +++ b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java @@ -18,12 +18,14 @@ */ package org.dependencytrack.event; +import alpine.Config; import alpine.common.logging.Logger; import alpine.event.LdapSyncEvent; import alpine.event.framework.EventService; import alpine.event.framework.SingleThreadedEventService; import alpine.server.tasks.LdapSyncTask; import org.dependencytrack.RequirementsVerifier; +import org.dependencytrack.common.ConfigKey; import org.dependencytrack.tasks.BomUploadProcessingTaskV2; import org.dependencytrack.tasks.CallbackTask; import org.dependencytrack.tasks.ClearComponentAnalysisCacheTask; @@ -57,6 +59,7 @@ import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; +import java.time.Duration; /** * Initializes the event subsystem and configures event subscribers. @@ -74,6 +77,9 @@ public class EventSubsystemInitializer implements ServletContextListener { // Starts the SingleThreadedEventService private static final SingleThreadedEventService EVENT_SERVICE_ST = SingleThreadedEventService.getInstance(); + private static final Duration DRAIN_TIMEOUT_DURATION = + Duration.parse(Config.getInstance().getProperty(ConfigKey.ALPINE_WORKER_POOL_DRAIN_TIMEOUT_DURATION)); + /** * {@inheritDoc} */ @@ -129,7 +135,6 @@ public void contextDestroyed(final ServletContextEvent event) { LOGGER.info("Shutting down asynchronous event subsystem"); TaskScheduler.getInstance().shutdown(); - EVENT_SERVICE.unsubscribe(BomUploadProcessingTaskV2.class); EVENT_SERVICE.unsubscribe(VexUploadProcessingTask.class); EVENT_SERVICE.unsubscribe(LdapSyncTask.class); @@ -158,9 +163,9 @@ public void contextDestroyed(final ServletContextEvent event) { EVENT_SERVICE.unsubscribe(NistMirrorTask.class); EVENT_SERVICE.unsubscribe(NistApiMirrorTask.class); EVENT_SERVICE.unsubscribe(EpssMirrorTask.class); - EVENT_SERVICE.shutdown(); + EVENT_SERVICE.shutdown(DRAIN_TIMEOUT_DURATION); EVENT_SERVICE_ST.unsubscribe(IndexTask.class); - EVENT_SERVICE_ST.shutdown(); + EVENT_SERVICE_ST.shutdown(DRAIN_TIMEOUT_DURATION); } } diff --git a/src/main/java/org/dependencytrack/notification/NotificationSubsystemInitializer.java b/src/main/java/org/dependencytrack/notification/NotificationSubsystemInitializer.java index 14bad56142..74f0527211 100644 --- a/src/main/java/org/dependencytrack/notification/NotificationSubsystemInitializer.java +++ b/src/main/java/org/dependencytrack/notification/NotificationSubsystemInitializer.java @@ -18,13 +18,16 @@ */ package org.dependencytrack.notification; +import alpine.Config; import alpine.common.logging.Logger; import alpine.notification.NotificationService; import alpine.notification.Subscription; import org.dependencytrack.RequirementsVerifier; +import org.dependencytrack.common.ConfigKey; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; +import java.time.Duration; /** * Initializes the notification subsystem and configures the notification router @@ -39,6 +42,9 @@ public class NotificationSubsystemInitializer implements ServletContextListener // Starts the NotificationService private static final NotificationService NOTIFICATION_SERVICE = NotificationService.getInstance(); + private static final Duration DRAIN_TIMEOUT_DURATION = + Duration.parse(Config.getInstance().getProperty(ConfigKey.ALPINE_WORKER_POOL_DRAIN_TIMEOUT_DURATION)); + /** * {@inheritDoc} */ @@ -57,6 +63,6 @@ public void contextInitialized(final ServletContextEvent event) { @Override public void contextDestroyed(final ServletContextEvent event) { LOGGER.info("Shutting down notification service"); - NOTIFICATION_SERVICE.shutdown(); + NOTIFICATION_SERVICE.shutdown(DRAIN_TIMEOUT_DURATION); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d03af8288c..9b3aba9a61 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -18,6 +18,13 @@ alpine.worker.threads=0 # 16 worker threads. Default value is 4. alpine.worker.thread.multiplier=4 +# Required +# Defines the maximum duration for which Dependency-Track will wait for queued +# events and notifications to be processed when shutting down. +# During shutdown, newly dispatched events will not be accepted. +# The duration must be specified in ISO 8601 notation (https://en.wikipedia.org/wiki/ISO_8601#Durations). +alpine.worker.pool.drain.timeout.duration=PT5S + # Required # Defines the path to the data directory. This directory will hold logs, # keys, and any database or index files along with application-specific From efe3205f85ba13c964951babbe214a83c1b53ab0 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 29 Apr 2024 20:24:37 +0200 Subject: [PATCH 109/412] Update v4.11 changelog with recent changes Signed-off-by: nscuro --- docs/_posts/2024-xx-xx-v4.11.0.md | 35 ++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index 3c27905c21..5abc8ddf17 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -10,12 +10,14 @@ overhauled to be more reliable and efficient. Further, BOM processing is now an occurring midway do not cause a partial state to be left behind. De-duplication of components and services is more predictable, and log messages emitted during processing contain additional context, making them easier to correlate. Because the new implementation can have a big impact on how Dependency-Track behaves regarding BOM uploads, -it is disabled by default for this release. It may be enabled in the experimental panel in administration. +it is disabled by default for this release. It may be enabled in the administration panel under +*Configuration* -> *Experimental*. * **BOM Validation**. Historically, Dependency-Track did not validate uploaded BOMs and VEXs against the CycloneDX schema. While this allowed BOMs to be processed that did not strictly adhere to the schema, it could also lead to confusion when uploaded files were accepted, but then failed to be ingested during asynchronous processing. Starting with this release, uploaded files will be rejected if they fail schema validation. Note that this may reveal issues in BOM -generators that currently produce invalid CycloneDX documents. Validation may be turned off by disabling it in the experimental panel in administration. +generators that currently produce invalid CycloneDX documents. Validation may be turned off in the +administration panel under *Configuration* -> *BOM Formats*. * *This feature was demoed in our April community meeting! Watch it [here](https://www.youtube.com/watch?v=3iIeajRJK8o&t=450s)* * **Global Vulnerability Audit View**. This new interface allows users to discover and filter vulnerabilities that affect their portfolio, across all projects. When portfolio access control is enabled, this view is limited to projects a user @@ -28,7 +30,8 @@ making it possible to spot the most prevalent vulnerabilities. * *This feature was demoed in our April community meeting! Watch it [here](https://www.youtube.com/watch?v=3iIeajRJK8o&t=725s).* * **Official Helm Chart**. The Dependency-Track project now offers an official Helm chart for Kubernetes deployments. Community input and contributions are highly requested. The chart repository can be found at -[https://github.com/DependencyTrack/helm-charts](https://github.com/DependencyTrack/helm-charts) +[https://github.com/DependencyTrack/helm-charts](https://github.com/DependencyTrack/helm-charts). +It is also available through [Artifact Hub](https://artifacthub.io/packages/helm/dependencytrack/dependency-track). **Features:** @@ -68,6 +71,9 @@ Community input and contributions are highly requested. The chart repository can * Properly validate UUID request parameters to prevent internal server errors - [apiserver/#3590] * Document pagination query parameters in OpenAPI specification - [apiserver/#3625] * Document sorting query parameters in OpenAPI specification - [apiserver/#3631] +* Gracefully handle unique constraint violations - [apiserver/#3648] +* Log debug information upon possible secret key corruption - [apiserver/#3651] +* Add support for worker pool drain timeout - [apiserver/#3657] * Show component count in projects list - [frontend/#683] * Add current *fail*, *warn*, and *info* values to bottom of policy violation metrics - [frontend/#707] * Remove unused policy violation widget - [frontend/#710] @@ -88,6 +94,9 @@ Community input and contributions are highly requested. The chart repository can * Use *concise* endpoint to populate license list - [frontend/#793] * Display *comment* field of external references - [frontend/#803] * Add support for localization based on browser language - [frontend/#805] +* Improve contrast ration on progress bars - [frontend/#816] +* Add language picker to profile dropdown - [frontend/#824] +* Display EPSS score and percentile on vulnerability view - [frontend/#832] **Fixes:** @@ -110,6 +119,7 @@ Community input and contributions are highly requested. The chart repository can * Fix inability to store PURLs longer than 255 characters - [apiserver/#3560] * Disable automatic API key generation for newly created teams - [apiserver/#3574] * Fix severity not being set for vulnerabilities from VulnDB - [apiserver/#3595] +* Fix `JDOFatalUserException` for long reference URLs from OSS Index - [apiserver/#3650] * Fix `VUE_APP_SERVER_URL` being ignored - [frontend/#682] * Fix visibility of "Vulnerabilities" and "Policy Violations" columns not being toggle-able individually - [frontend/#686] * Fix finding search routes - [frontend/#689] @@ -127,10 +137,11 @@ Community input and contributions are highly requested. The chart repository can **Upgrade Notes:** -* To enable the optimized BOM ingestion, set the environment variable `BOM_PROCESSING_TASK_V2_ENABLED` to `true` -* Validation of uploaded BOMs and VEXs is enabled per default, but can be disabled by setting the environment -variable `BOM_VALIDATION_ENABLED` to `false` -* The `CWE` table is dropped automatically upon upgrade +* To enable the optimized BOM ingestion, toggle the *BOM Processing V2* option in the administration panel +under *Configuration* -> *Experimental* +* Validation of uploaded BOMs and VEXs is enabled per default, but can be disabled in the administration panel +under *Configuration* -> *BOM Formats* -> *BOM Validation* +* The `CWE` table is dropped automatically upon upgrade, it has been unused since v4.5 * The default logging configuration ([logback.xml]) was updated to include the [Mapped Diagnostic Context] (MDC) * Users who [customized their logging configuration] are recommended to follow this change * Severities of vulnerabilities that previously had `NULL` severities in the database will be computed @@ -161,7 +172,7 @@ We thank all organizations and individuals who contributed to this release, from Special thanks to everyone who contributed code to implement enhancements and fix defects: [@AnthonyMastrean], [@LaVibeX], [@MangoIV], [@Robbilie], [@VithikaS], [@a5a351e7], [@acdha], [@aravindparappil46], [@baburkin], [@fnxpt], [@kepten], [@leec94], [@lukas-braune], [@malice00], [@mehab], [@mge-mm] -[@mikkeschiren], [@mykter], [@rbt-mm], [@rkesters], [@rkg-mm], [@sahibamittal], [@sebD], [@setchy] +[@mikkeschiren], [@mykter], [@rbt-mm], [@rkesters], [@rkg-mm], [@sahibamittal], [@sebD], [@setchy], [@validide] ###### dependency-track-apiserver.jar @@ -233,6 +244,10 @@ Special thanks to everyone who contributed code to implement enhancements and fi [apiserver/#3595]: https://github.com/DependencyTrack/dependency-track/pull/3595 [apiserver/#3625]: https://github.com/DependencyTrack/dependency-track/pull/3625 [apiserver/#3631]: https://github.com/DependencyTrack/dependency-track/pull/3631 +[apiserver/#3648]: https://github.com/DependencyTrack/dependency-track/pull/3648 +[apiserver/#3650]: https://github.com/DependencyTrack/dependency-track/pull/3650 +[apiserver/#3651]: https://github.com/DependencyTrack/dependency-track/pull/3651 +[apiserver/#3657]: https://github.com/DependencyTrack/dependency-track/pull/3657 [frontend/#682]: https://github.com/DependencyTrack/frontend/pull/682 [frontend/#683]: https://github.com/DependencyTrack/frontend/pull/683 @@ -267,6 +282,9 @@ Special thanks to everyone who contributed code to implement enhancements and fi [frontend/#803]: https://github.com/DependencyTrack/frontend/pull/803 [frontend/#805]: https://github.com/DependencyTrack/frontend/pull/805 [frontend/#812]: https://github.com/DependencyTrack/frontend/pull/812 +[frontend/#816]: https://github.com/DependencyTrack/frontend/pull/816 +[frontend/#824]: https://github.com/DependencyTrack/frontend/pull/824 +[frontend/#832]: https://github.com/DependencyTrack/frontend/pull/832 [@AnthonyMastrean]: https://github.com/AnthonyMastrean [@LaVibeX]: https://github.com/LaVibeX @@ -293,6 +311,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [@sahibamittal]: https://github.com/sahibamittal [@sebD]: https://github.com/sebD [@setchy]: https://github.com/setchy +[@validide]: https://github.com/validide [Mapped Diagnostic Context]: https://logback.qos.ch/manual/mdc.html [Trivy]: https://trivy.dev/ From 6538d252ee986bf6e27dd902d6efce3302fc5fb1 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 29 Apr 2024 23:35:32 +0200 Subject: [PATCH 110/412] Catch all unhandled `ClientErrorException`s `ClientErrorException` in JAX-RS corresponds to HTTP 4xx status codes. Those were previously handled by Alpine's `GlobalExceptionHandler`, resulting in an HTTP 500 response when it really should've been a 4xx. Fixes #3645 Signed-off-by: nscuro --- docs/_posts/2024-xx-xx-v4.11.0.md | 2 + ...r.java => ClientErrorExceptionMapper.java} | 10 +-- .../ClientErrorExceptionMapperTest.java | 73 +++++++++++++++++++ .../NotFoundExceptionMapperTest.java | 38 ---------- 4 files changed, 80 insertions(+), 43 deletions(-) rename src/main/java/org/dependencytrack/resources/v1/exception/{NotFoundExceptionMapper.java => ClientErrorExceptionMapper.java} (75%) create mode 100644 src/test/java/org/dependencytrack/resources/v1/exception/ClientErrorExceptionMapperTest.java delete mode 100644 src/test/java/org/dependencytrack/resources/v1/exception/NotFoundExceptionMapperTest.java diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index 5abc8ddf17..c0af5067f3 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -120,6 +120,7 @@ It is also available through [Artifact Hub](https://artifacthub.io/packages/helm * Disable automatic API key generation for newly created teams - [apiserver/#3574] * Fix severity not being set for vulnerabilities from VulnDB - [apiserver/#3595] * Fix `JDOFatalUserException` for long reference URLs from OSS Index - [apiserver/#3650] +* Fix unhandled `ClientErrorException`s causing a `HTTP 500` response - [apiserver/#3659] * Fix `VUE_APP_SERVER_URL` being ignored - [frontend/#682] * Fix visibility of "Vulnerabilities" and "Policy Violations" columns not being toggle-able individually - [frontend/#686] * Fix finding search routes - [frontend/#689] @@ -248,6 +249,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [apiserver/#3650]: https://github.com/DependencyTrack/dependency-track/pull/3650 [apiserver/#3651]: https://github.com/DependencyTrack/dependency-track/pull/3651 [apiserver/#3657]: https://github.com/DependencyTrack/dependency-track/pull/3657 +[apiserver/#3659]: https://github.com/DependencyTrack/dependency-track/pull/3659 [frontend/#682]: https://github.com/DependencyTrack/frontend/pull/682 [frontend/#683]: https://github.com/DependencyTrack/frontend/pull/683 diff --git a/src/main/java/org/dependencytrack/resources/v1/exception/NotFoundExceptionMapper.java b/src/main/java/org/dependencytrack/resources/v1/exception/ClientErrorExceptionMapper.java similarity index 75% rename from src/main/java/org/dependencytrack/resources/v1/exception/NotFoundExceptionMapper.java rename to src/main/java/org/dependencytrack/resources/v1/exception/ClientErrorExceptionMapper.java index 84a48d7abd..7ceda9176c 100644 --- a/src/main/java/org/dependencytrack/resources/v1/exception/NotFoundExceptionMapper.java +++ b/src/main/java/org/dependencytrack/resources/v1/exception/ClientErrorExceptionMapper.java @@ -20,23 +20,23 @@ import alpine.server.resources.GlobalExceptionHandler; -import javax.ws.rs.NotFoundException; +import javax.ws.rs.ClientErrorException; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; /** - * An {@link ExceptionMapper} to handle {@link NotFoundException}, that would otherwise be + * An {@link ExceptionMapper} to handle {@link ClientErrorException}s, that would otherwise be * handled by Alpine's {@link GlobalExceptionHandler}, resulting in a misleading {@code HTTP 500} response. * * @since 4.11.0 */ @Provider -public class NotFoundExceptionMapper implements ExceptionMapper { +public class ClientErrorExceptionMapper implements ExceptionMapper { @Override - public Response toResponse(final NotFoundException exception) { - return Response.status(Response.Status.NOT_FOUND).build(); + public Response toResponse(final ClientErrorException exception) { + return exception.getResponse(); } } diff --git a/src/test/java/org/dependencytrack/resources/v1/exception/ClientErrorExceptionMapperTest.java b/src/test/java/org/dependencytrack/resources/v1/exception/ClientErrorExceptionMapperTest.java new file mode 100644 index 0000000000..bcc1964c73 --- /dev/null +++ b/src/test/java/org/dependencytrack/resources/v1/exception/ClientErrorExceptionMapperTest.java @@ -0,0 +1,73 @@ +/* + * 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.resources.v1.exception; + +import org.dependencytrack.ResourceTest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.ServletDeploymentContext; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ClientErrorExceptionMapperTest extends ResourceTest { + + @Override + protected DeploymentContext configureDeployment() { + return ServletDeploymentContext.forServlet(new ServletContainer( + new ResourceConfig(TestResource.class) + .register(ClientErrorExceptionMapper.class))) + .build(); + } + + @Test + public void testNotFound() { + final Response response = target("/does/not/exist") + .request() + .get(); + + assertThat(response.getStatus()).isEqualTo(404); + } + + @Test + public void testMethodNotAllowed() { + final Response response = target("/test/foo") + .request() + .delete(); + + assertThat(response.getStatus()).isEqualTo(405); + } + + @Path("/test") + public static class TestResource { + + @GET + @Path("/foo") + public String foo() { + return "foo"; + } + + } + +} \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/resources/v1/exception/NotFoundExceptionMapperTest.java b/src/test/java/org/dependencytrack/resources/v1/exception/NotFoundExceptionMapperTest.java deleted file mode 100644 index 68e0aadedc..0000000000 --- a/src/test/java/org/dependencytrack/resources/v1/exception/NotFoundExceptionMapperTest.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.resources.v1.exception; - -import org.junit.Test; - -import javax.ws.rs.NotFoundException; -import javax.ws.rs.core.Response; - -import static org.assertj.core.api.Assertions.assertThat; - -public class NotFoundExceptionMapperTest { - - @Test - @SuppressWarnings("resource") - public void testToResponse() { - final Response response = new NotFoundExceptionMapper().toResponse(new NotFoundException()); - assertThat(response.getStatus()).isEqualTo(404); - assertThat(response.getEntity()).isNull(); - } - -} \ No newline at end of file From 21af7b219d94f3538d3e8e3e211a7a5fdf0f83b1 Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 1 May 2024 19:42:39 +0200 Subject: [PATCH 111/412] Fall back to no authentication when OSS Index API token decryption fails Instead of causing a full-blown service disruption when API token decryption fails, fall back to unauthenticated API usage to limit the impact. Relates to #2366 Signed-off-by: nscuro --- docs/_posts/2024-xx-xx-v4.11.0.md | 2 + .../tasks/scanners/OssIndexAnalysisTask.java | 68 ++++++------ .../scanners/OssIndexAnalysisTaskTest.java | 104 ++++++++++++++++++ 3 files changed, 142 insertions(+), 32 deletions(-) diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index c0af5067f3..242b226314 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -74,6 +74,7 @@ It is also available through [Artifact Hub](https://artifacthub.io/packages/helm * Gracefully handle unique constraint violations - [apiserver/#3648] * Log debug information upon possible secret key corruption - [apiserver/#3651] * Add support for worker pool drain timeout - [apiserver/#3657] +* Fall back to no authentication when OSS Index API token decryption fails - [apiserver/#3661] * Show component count in projects list - [frontend/#683] * Add current *fail*, *warn*, and *info* values to bottom of policy violation metrics - [frontend/#707] * Remove unused policy violation widget - [frontend/#710] @@ -250,6 +251,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [apiserver/#3651]: https://github.com/DependencyTrack/dependency-track/pull/3651 [apiserver/#3657]: https://github.com/DependencyTrack/dependency-track/pull/3657 [apiserver/#3659]: https://github.com/DependencyTrack/dependency-track/pull/3659 +[apiserver/#3661]: https://github.com/DependencyTrack/dependency-track/pull/3661 [frontend/#682]: https://github.com/DependencyTrack/frontend/pull/682 [frontend/#683]: https://github.com/DependencyTrack/frontend/pull/683 diff --git a/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java index 37fba86f9d..07e9b2f4f4 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java @@ -137,41 +137,45 @@ public AnalyzerIdentity getAnalyzerIdentity() { * {@inheritDoc} */ public void inform(final Event e) { - if (e instanceof OssIndexAnalysisEvent) { - if (!super.isEnabled(ConfigPropertyConstants.SCANNER_OSSINDEX_ENABLED)) { - return; - } - try (QueryManager qm = new QueryManager()) { - final ConfigProperty apiUsernameProperty = qm.getConfigProperty( - ConfigPropertyConstants.SCANNER_OSSINDEX_API_USERNAME.getGroupName(), - ConfigPropertyConstants.SCANNER_OSSINDEX_API_USERNAME.getPropertyName() - ); - final ConfigProperty apiTokenProperty = qm.getConfigProperty( - ConfigPropertyConstants.SCANNER_OSSINDEX_API_TOKEN.getGroupName(), - ConfigPropertyConstants.SCANNER_OSSINDEX_API_TOKEN.getPropertyName() - ); - if (apiUsernameProperty == null || apiUsernameProperty.getPropertyValue() == null - || apiTokenProperty == null || apiTokenProperty.getPropertyValue() == null) { - LOGGER.warn("An API username or token has not been specified for use with OSS Index. Using anonymous access"); - } else { - try { - apiUsername = apiUsernameProperty.getPropertyValue(); - apiToken = DebugDataEncryption.decryptAsString(apiTokenProperty.getPropertyValue()); - } catch (Exception ex) { - LOGGER.error("An error occurred decrypting the OSS Index API Token. Skipping", ex); - return; - } + if (!(e instanceof final OssIndexAnalysisEvent event)) { + return; + } + if (!super.isEnabled(ConfigPropertyConstants.SCANNER_OSSINDEX_ENABLED)) { + return; + } + + try (final var qm = new QueryManager()) { + final ConfigProperty apiUsernameProperty = qm.getConfigProperty( + ConfigPropertyConstants.SCANNER_OSSINDEX_API_USERNAME.getGroupName(), + ConfigPropertyConstants.SCANNER_OSSINDEX_API_USERNAME.getPropertyName() + ); + final ConfigProperty apiTokenProperty = qm.getConfigProperty( + ConfigPropertyConstants.SCANNER_OSSINDEX_API_TOKEN.getGroupName(), + ConfigPropertyConstants.SCANNER_OSSINDEX_API_TOKEN.getPropertyName() + ); + if (apiUsernameProperty == null || apiUsernameProperty.getPropertyValue() == null + || apiTokenProperty == null || apiTokenProperty.getPropertyValue() == null) { + LOGGER.warn("An API username or token has not been specified for use with OSS Index. Using anonymous access"); + } else { + try { + apiUsername = apiUsernameProperty.getPropertyValue(); + apiToken = DebugDataEncryption.decryptAsString(apiTokenProperty.getPropertyValue()); + } catch (Exception ex) { + // NB: OSS Index can be used without AuthN, however stricter rate limiting may apply. + // We favour "service degradation" over "service outage" here. Analysis will continue + // to work, although more retries may need to be performed until a new token is supplied. + LOGGER.error("An error occurred decrypting the OSS Index API Token; Continuing without authentication", ex); } - aliasSyncEnabled = super.isEnabled(ConfigPropertyConstants.SCANNER_OSSINDEX_ALIAS_SYNC_ENABLED); - } - final var event = (OssIndexAnalysisEvent) e; - LOGGER.info("Starting Sonatype OSS Index analysis task"); - vulnerabilityAnalysisLevel = event.getVulnerabilityAnalysisLevel(); - if (event.getComponents().size() > 0) { - analyze(event.getComponents()); } - LOGGER.info("Sonatype OSS Index analysis complete"); + aliasSyncEnabled = super.isEnabled(ConfigPropertyConstants.SCANNER_OSSINDEX_ALIAS_SYNC_ENABLED); + } + + LOGGER.info("Starting Sonatype OSS Index analysis task"); + vulnerabilityAnalysisLevel = event.getVulnerabilityAnalysisLevel(); + if (!event.getComponents().isEmpty()) { + analyze(event.getComponents()); } + LOGGER.info("Sonatype OSS Index analysis complete"); } /** diff --git a/src/test/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTaskTest.java b/src/test/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTaskTest.java index bb32c1cda8..414e208c4f 100644 --- a/src/test/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTaskTest.java @@ -1,6 +1,8 @@ package org.dependencytrack.tasks.scanners; +import alpine.security.crypto.DataEncryption; import com.github.packageurl.PackageURL; +import com.github.tomakehurst.wiremock.client.BasicCredentials; import com.github.tomakehurst.wiremock.junit.WireMockRule; import org.assertj.core.api.SoftAssertions; import org.dependencytrack.PersistenceCapableTest; @@ -30,6 +32,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_ANALYSIS_CACHE_VALIDITY_PERIOD; +import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_OSSINDEX_API_TOKEN; +import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_OSSINDEX_API_USERNAME; import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_OSSINDEX_ENABLED; public class OssIndexAnalysisTaskTest extends PersistenceCapableTest { @@ -185,4 +189,104 @@ public void testAnalyzeWithRateLimiting() { """))); } + @Test + public void testAnalyzeWithAuthentication() throws Exception { + qm.createConfigProperty( + SCANNER_OSSINDEX_API_USERNAME.getGroupName(), + SCANNER_OSSINDEX_API_USERNAME.getPropertyName(), + "foo", + SCANNER_OSSINDEX_API_USERNAME.getPropertyType(), + SCANNER_OSSINDEX_API_USERNAME.getDescription() + ); + qm.createConfigProperty( + SCANNER_OSSINDEX_API_TOKEN.getGroupName(), + SCANNER_OSSINDEX_API_TOKEN.getPropertyName(), + DataEncryption.encryptAsString("apiToken"), + SCANNER_OSSINDEX_API_TOKEN.getPropertyType(), + SCANNER_OSSINDEX_API_TOKEN.getDescription() + ); + + wireMock.stubFor(post(urlPathEqualTo("/api/v3/component-report")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/vnd.ossindex.component-report.v1+json") + .withBody("[]"))); + + var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + var component = new Component(); + component.setProject(project); + component.setGroup("com.fasterxml.jackson.core"); + component.setName("jackson-databind"); + component.setVersion("2.13.1"); + component.setPurl("pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1"); + qm.persist(component); + + assertThatNoException().isThrownBy(() -> analysisTask.inform(new OssIndexAnalysisEvent(component))); + + wireMock.verify(postRequestedFor(urlPathEqualTo("/api/v3/component-report")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo(ManagedHttpClientFactory.getUserAgent())) + .withBasicAuth(new BasicCredentials("foo", "apiToken")) + .withRequestBody(equalToJson(""" + { + "coordinates": [ + "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1" + ] + } + """))); + } + + @Test + public void testAnalyzeWithApiTokenDecryptionError() { + qm.createConfigProperty( + SCANNER_OSSINDEX_API_USERNAME.getGroupName(), + SCANNER_OSSINDEX_API_USERNAME.getPropertyName(), + "foo", + SCANNER_OSSINDEX_API_USERNAME.getPropertyType(), + SCANNER_OSSINDEX_API_USERNAME.getDescription() + ); + qm.createConfigProperty( + SCANNER_OSSINDEX_API_TOKEN.getGroupName(), + SCANNER_OSSINDEX_API_TOKEN.getPropertyName(), + "notAnEncryptedValue", + SCANNER_OSSINDEX_API_TOKEN.getPropertyType(), + SCANNER_OSSINDEX_API_TOKEN.getDescription() + ); + + wireMock.stubFor(post(urlPathEqualTo("/api/v3/component-report")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/vnd.ossindex.component-report.v1+json") + .withBody("[]"))); + + var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + var component = new Component(); + component.setProject(project); + component.setGroup("com.fasterxml.jackson.core"); + component.setName("jackson-databind"); + component.setVersion("2.13.1"); + component.setPurl("pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1"); + qm.persist(component); + + assertThatNoException().isThrownBy(() -> analysisTask.inform(new OssIndexAnalysisEvent(component))); + + wireMock.verify(postRequestedFor(urlPathEqualTo("/api/v3/component-report")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo(ManagedHttpClientFactory.getUserAgent())) + .withoutHeader("Authorization") + .withRequestBody(equalToJson(""" + { + "coordinates": [ + "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1" + ] + } + """))); + } + } \ No newline at end of file From acca75f5bd9ca20d98f22c8734a782dfd790fc0a Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 1 May 2024 20:05:56 +0200 Subject: [PATCH 112/412] Truncate `ComponentProperty` value at 1024 characters This is both to prevent the database's length constraint from being violated, and to avoid storing unnecessary long values. Some tools may misuse properties, and in those cases having the complete value is usually not of interest to users. Signed-off-by: nscuro --- .../java/org/dependencytrack/model/ComponentProperty.java | 3 ++- .../dependencytrack/tasks/BomUploadProcessingTaskTest.java | 7 +++++++ src/test/resources/unit/bom-1.xml | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/model/ComponentProperty.java b/src/main/java/org/dependencytrack/model/ComponentProperty.java index 170e0f4289..31651b1d47 100644 --- a/src/main/java/org/dependencytrack/model/ComponentProperty.java +++ b/src/main/java/org/dependencytrack/model/ComponentProperty.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.common.base.MoreObjects; +import org.apache.commons.lang3.StringUtils; import org.dependencytrack.model.validation.EnumValue; import javax.jdo.annotations.Column; @@ -150,7 +151,7 @@ public String getPropertyValue() { } public void setPropertyValue(final String propertyValue) { - this.propertyValue = propertyValue; + this.propertyValue = StringUtils.abbreviate(propertyValue, 1024); } public PropertyType getPropertyType() { diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index 332a80520c..f4722cd2c6 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -275,6 +275,13 @@ public void informTest() throws Exception { assertThat(property.getPropertyValue()).isEqualTo("qux"); assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); assertThat(property.getDescription()).isNull(); + }, + property -> { + assertThat(property.getGroupName()).isNull(); + assertThat(property.getPropertyName()).isEqualTo("long"); + assertThat(property.getPropertyValue()).isEqualTo("a".repeat(1021) + "..."); + assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING); + assertThat(property.getDescription()).isNull(); } ); diff --git a/src/test/resources/unit/bom-1.xml b/src/test/resources/unit/bom-1.xml index adde0b1861..3349fc1df3 100644 --- a/src/test/resources/unit/bom-1.xml +++ b/src/test/resources/unit/bom-1.xml @@ -92,6 +92,7 @@ baz qux qux + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
    From df1174d3a896b4a81eb8ba763383ec333ad4a5fa Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 1 May 2024 23:08:04 +0200 Subject: [PATCH 113/412] Fix unique constraint violation during NVD mirroring The fix is achieved by using the same logic for persisting `Vulnerability` and `VulnerableSoftware` records that `NistApiMirrorTask` was already using. It handles duplicate records. This should also yield a performance boost (did not benchmark because that wasn't the focus of this change), since the transaction commit frequency is reduced compared to the previous logic. Fixes #3663 Signed-off-by: nscuro --- docs/_posts/2024-xx-xx-v4.11.0.md | 2 + .../dependencytrack/parser/nvd/NvdParser.java | 225 +++++++-------- .../tasks/AbstractNistMirrorTask.java | 262 ++++++++++++++++++ .../tasks/NistApiMirrorTask.java | 252 +---------------- .../dependencytrack/tasks/NistMirrorTask.java | 31 ++- .../dependencytrack/util/PersistenceUtil.java | 16 ++ 6 files changed, 428 insertions(+), 360 deletions(-) create mode 100644 src/main/java/org/dependencytrack/tasks/AbstractNistMirrorTask.java diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index 242b226314..b6842470d8 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -122,6 +122,7 @@ It is also available through [Artifact Hub](https://artifacthub.io/packages/helm * Fix severity not being set for vulnerabilities from VulnDB - [apiserver/#3595] * Fix `JDOFatalUserException` for long reference URLs from OSS Index - [apiserver/#3650] * Fix unhandled `ClientErrorException`s causing a `HTTP 500` response - [apiserver/#3659] +* Fix unique constraint violation during NVD mirroring via feed files - [apiserver/#3664] * Fix `VUE_APP_SERVER_URL` being ignored - [frontend/#682] * Fix visibility of "Vulnerabilities" and "Policy Violations" columns not being toggle-able individually - [frontend/#686] * Fix finding search routes - [frontend/#689] @@ -252,6 +253,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [apiserver/#3657]: https://github.com/DependencyTrack/dependency-track/pull/3657 [apiserver/#3659]: https://github.com/DependencyTrack/dependency-track/pull/3659 [apiserver/#3661]: https://github.com/DependencyTrack/dependency-track/pull/3661 +[apiserver/#3664]: https://github.com/DependencyTrack/dependency-track/pull/3664 [frontend/#682]: https://github.com/DependencyTrack/frontend/pull/682 [frontend/#683]: https://github.com/DependencyTrack/frontend/pull/683 diff --git a/src/main/java/org/dependencytrack/parser/nvd/NvdParser.java b/src/main/java/org/dependencytrack/parser/nvd/NvdParser.java index 33fbfe855b..1c558dce2c 100644 --- a/src/main/java/org/dependencytrack/parser/nvd/NvdParser.java +++ b/src/main/java/org/dependencytrack/parser/nvd/NvdParser.java @@ -27,13 +27,11 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.commons.lang3.StringUtils; -import org.datanucleus.PropertyNames; import org.dependencytrack.event.IndexEvent; import org.dependencytrack.model.Cwe; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerableSoftware; import org.dependencytrack.parser.common.resolver.CweResolver; -import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.util.VulnerabilityUtil; import us.springett.cvss.Cvss; import us.springett.parsers.cpe.exceptions.CpeEncodingException; @@ -51,6 +49,7 @@ import java.util.Iterator; import java.util.List; import java.util.Optional; +import java.util.function.BiConsumer; /** * Parser and processor of NVD data feeds. @@ -71,6 +70,11 @@ private enum Operator { // https://github.com/DependencyTrack/dependency-track/pull/2520 // is merged. private final ObjectMapper objectMapper = new ObjectMapper(); + private final BiConsumer> vulnerabilityConsumer; + + public NvdParser(final BiConsumer> vulnerabilityConsumer) { + this.vulnerabilityConsumer = vulnerabilityConsumer; + } public void parse(final File file) { if (!file.getName().endsWith(".json")) { @@ -111,127 +115,115 @@ public void parse(final File file) { } private void parseCveItem(final ObjectNode cveItem) { - try (QueryManager qm = new QueryManager().withL2CacheDisabled()) { - qm.getPersistenceManager().setProperty(PropertyNames.PROPERTY_PERSISTENCE_BY_REACHABILITY_AT_COMMIT, "false"); - - final Vulnerability vulnerability = new Vulnerability(); - vulnerability.setSource(Vulnerability.Source.NVD); + final Vulnerability vulnerability = new Vulnerability(); + vulnerability.setSource(Vulnerability.Source.NVD); - // CVE ID - final var cve = (ObjectNode) cveItem.get("cve"); - final var meta0 = (ObjectNode) cve.get("CVE_data_meta"); - vulnerability.setVulnId(meta0.get("ID").asText()); + // CVE ID + final var cve = (ObjectNode) cveItem.get("cve"); + final var meta0 = (ObjectNode) cve.get("CVE_data_meta"); + vulnerability.setVulnId(meta0.get("ID").asText()); - // CVE Published and Modified dates - final String publishedDateString = cveItem.get("publishedDate").asText(); - final String lastModifiedDateString = cveItem.get("lastModifiedDate").asText(); - try { - if (StringUtils.isNotBlank(publishedDateString)) { - vulnerability.setPublished(Date.from(OffsetDateTime.parse(publishedDateString).toInstant())); - } - if (StringUtils.isNotBlank(lastModifiedDateString)) { - vulnerability.setUpdated(Date.from(OffsetDateTime.parse(lastModifiedDateString).toInstant())); - } - } catch (DateTimeParseException | NullPointerException | IllegalArgumentException e) { - LOGGER.error("Unable to parse dates from NVD data feed", e); + // CVE Published and Modified dates + final String publishedDateString = cveItem.get("publishedDate").asText(); + final String lastModifiedDateString = cveItem.get("lastModifiedDate").asText(); + try { + if (StringUtils.isNotBlank(publishedDateString)) { + vulnerability.setPublished(Date.from(OffsetDateTime.parse(publishedDateString).toInstant())); } + if (StringUtils.isNotBlank(lastModifiedDateString)) { + vulnerability.setUpdated(Date.from(OffsetDateTime.parse(lastModifiedDateString).toInstant())); + } + } catch (DateTimeParseException | NullPointerException | IllegalArgumentException e) { + LOGGER.error("Unable to parse dates from NVD data feed", e); + } - // CVE Description - final var descO = (ObjectNode) cve.get("description"); - final var desc1 = (ArrayNode) descO.get("description_data"); - final StringBuilder descriptionBuilder = new StringBuilder(); - for (int j = 0; j < desc1.size(); j++) { - final var desc2 = (ObjectNode) desc1.get(j); - if ("en".equals(desc2.get("lang").asText())) { - descriptionBuilder.append(desc2.get("value").asText()); - if (j < desc1.size() - 1) { - descriptionBuilder.append("\n\n"); - } + // CVE Description + final var descO = (ObjectNode) cve.get("description"); + final var desc1 = (ArrayNode) descO.get("description_data"); + final StringBuilder descriptionBuilder = new StringBuilder(); + for (int j = 0; j < desc1.size(); j++) { + final var desc2 = (ObjectNode) desc1.get(j); + if ("en".equals(desc2.get("lang").asText())) { + descriptionBuilder.append(desc2.get("value").asText()); + if (j < desc1.size() - 1) { + descriptionBuilder.append("\n\n"); } } - vulnerability.setDescription(descriptionBuilder.toString()); + } + vulnerability.setDescription(descriptionBuilder.toString()); - // CVE Impact - parseCveImpact(cveItem, vulnerability); + // CVE Impact + parseCveImpact(cveItem, vulnerability); - // CWE - final var prob0 = (ObjectNode) cve.get("problemtype"); - final var prob1 = (ArrayNode) prob0.get("problemtype_data"); - for (int j = 0; j < prob1.size(); j++) { - final var prob2 = (ObjectNode) prob1.get(j); - final var prob3 = (ArrayNode) prob2.get("description"); - for (int k = 0; k < prob3.size(); k++) { - final var prob4 = (ObjectNode) prob3.get(k); - if ("en".equals(prob4.get("lang").asText())) { - final String cweString = prob4.get("value").asText(); - if (cweString != null && cweString.startsWith("CWE-")) { - final Cwe cwe = CweResolver.getInstance().lookup(cweString); - if (cwe != null) { - vulnerability.addCwe(cwe); - } else { - LOGGER.warn("CWE " + cweString + " not found in Dependency-Track database. This could signify an issue with the NVD or with Dependency-Track not having advanced knowledge of this specific CWE identifier."); - } + // CWE + final var prob0 = (ObjectNode) cve.get("problemtype"); + final var prob1 = (ArrayNode) prob0.get("problemtype_data"); + for (int j = 0; j < prob1.size(); j++) { + final var prob2 = (ObjectNode) prob1.get(j); + final var prob3 = (ArrayNode) prob2.get("description"); + for (int k = 0; k < prob3.size(); k++) { + final var prob4 = (ObjectNode) prob3.get(k); + if ("en".equals(prob4.get("lang").asText())) { + final String cweString = prob4.get("value").asText(); + if (cweString != null && cweString.startsWith("CWE-")) { + final Cwe cwe = CweResolver.getInstance().lookup(cweString); + if (cwe != null) { + vulnerability.addCwe(cwe); + } else { + LOGGER.warn("CWE " + cweString + " not found in Dependency-Track database. This could signify an issue with the NVD or with Dependency-Track not having advanced knowledge of this specific CWE identifier."); } } } } + } - // References - final var ref0 = (ObjectNode) cve.get("references"); - final var ref1 = (ArrayNode) ref0.get("reference_data"); - final StringBuilder sb = new StringBuilder(); - for (int l = 0; l < ref1.size(); l++) { - final var ref2 = (ObjectNode) ref1.get(l); - final Iterator fieldNameIter = ref2.fieldNames(); - while (fieldNameIter.hasNext()) { - final String s = fieldNameIter.next(); - if ("url".equals(s)) { - // Convert reference to Markdown format - final String url = ref2.get("url").asText(); - sb.append("* [").append(url).append("](").append(url).append(")\n"); - } + // References + final var ref0 = (ObjectNode) cve.get("references"); + final var ref1 = (ArrayNode) ref0.get("reference_data"); + final StringBuilder sb = new StringBuilder(); + for (int l = 0; l < ref1.size(); l++) { + final var ref2 = (ObjectNode) ref1.get(l); + final Iterator fieldNameIter = ref2.fieldNames(); + while (fieldNameIter.hasNext()) { + final String s = fieldNameIter.next(); + if ("url".equals(s)) { + // Convert reference to Markdown format + final String url = ref2.get("url").asText(); + sb.append("* [").append(url).append("](").append(url).append(")\n"); } } - final String references = sb.toString(); - if (references.length() > 0) { - vulnerability.setReferences(references.substring(0, references.lastIndexOf("\n"))); - } - - // Update the vulnerability - LOGGER.debug("Synchronizing: " + vulnerability.getVulnId()); - final Vulnerability synchronizeVulnerability = qm.synchronizeVulnerability(vulnerability, false); - final List vsListOld = qm.detach(qm.getVulnerableSoftwareByVulnId(synchronizeVulnerability.getSource(), synchronizeVulnerability.getVulnId())); + } + final String references = sb.toString(); + if (!references.isEmpty()) { + vulnerability.setReferences(references.substring(0, references.lastIndexOf("\n"))); + } - // CPE - List vsList = new ArrayList<>(); - final var configurations = (ObjectNode) cveItem.get("configurations"); - final var nodes = (ArrayNode) configurations.get("nodes"); - for (int j = 0; j < nodes.size(); j++) { - final var node = (ObjectNode) nodes.get(j); - final List vulnerableSoftwareInNode = new ArrayList<>(); - final Operator nodeOperator = Operator.valueOf(node.get("operator").asText(Operator.NONE.name())); - if (node.has("children")) { - // https://github.com/DependencyTrack/dependency-track/issues/1033 - final var children = (ArrayNode) node.get("children"); - if (children.size() > 0) { - for (int l = 0; l < children.size(); l++) { - final var child = (ObjectNode) children.get(l); - vulnerableSoftwareInNode.addAll(parseCpes(qm, child)); - } - } else { - vulnerableSoftwareInNode.addAll(parseCpes(qm, node)); + // CPE + List vsList = new ArrayList<>(); + final var configurations = (ObjectNode) cveItem.get("configurations"); + final var nodes = (ArrayNode) configurations.get("nodes"); + for (int j = 0; j < nodes.size(); j++) { + final var node = (ObjectNode) nodes.get(j); + final List vulnerableSoftwareInNode = new ArrayList<>(); + final Operator nodeOperator = Operator.valueOf(node.get("operator").asText(Operator.NONE.name())); + if (node.has("children")) { + // https://github.com/DependencyTrack/dependency-track/issues/1033 + final var children = (ArrayNode) node.get("children"); + if (!children.isEmpty()) { + for (int l = 0; l < children.size(); l++) { + final var child = (ObjectNode) children.get(l); + vulnerableSoftwareInNode.addAll(parseCpes(child)); } } else { - vulnerableSoftwareInNode.addAll(parseCpes(qm, node)); + vulnerableSoftwareInNode.addAll(parseCpes(node)); } - vsList.addAll(reconcile(vulnerableSoftwareInNode, nodeOperator)); + } else { + vulnerableSoftwareInNode.addAll(parseCpes(node)); } - qm.persist(vsList); - qm.updateAffectedVersionAttributions(synchronizeVulnerability, vsList, Vulnerability.Source.NVD); - vsList = qm.reconcileVulnerableSoftware(synchronizeVulnerability, vsListOld, vsList, Vulnerability.Source.NVD); - synchronizeVulnerability.setVulnerableSoftware(vsList); - qm.persist(synchronizeVulnerability); + vsList.addAll(reconcile(vulnerableSoftwareInNode, nodeOperator)); } + + vulnerabilityConsumer.accept(vulnerability, vsList); } /** @@ -302,14 +294,14 @@ private void parseCveImpact(final ObjectNode cveItem, final Vulnerability vuln) )); } - private List parseCpes(final QueryManager qm, final ObjectNode node) { + private List parseCpes(final ObjectNode node) { final List vsList = new ArrayList<>(); if (node.has("cpe_match")) { final var cpeMatches = (ArrayNode) node.get("cpe_match"); for (int k = 0; k < cpeMatches.size(); k++) { final var cpeMatch = (ObjectNode) cpeMatches.get(k); if (cpeMatch.get("vulnerable").asBoolean(true)) { // only parse the CPEs marked as vulnerable - final VulnerableSoftware vs = generateVulnerableSoftware(qm, cpeMatch); + final VulnerableSoftware vs = generateVulnerableSoftware(cpeMatch); if (vs != null) { vsList.add(vs); } @@ -319,29 +311,26 @@ private List parseCpes(final QueryManager qm, final ObjectNo return vsList; } - private VulnerableSoftware generateVulnerableSoftware(final QueryManager qm, final ObjectNode cpeMatch) { + private VulnerableSoftware generateVulnerableSoftware(final ObjectNode cpeMatch) { final String cpe23Uri = cpeMatch.get("cpe23Uri").asText(); final String versionEndExcluding = Optional.ofNullable(cpeMatch.get("versionEndExcluding")).map(JsonNode::asText).orElse(null); final String versionEndIncluding = Optional.ofNullable(cpeMatch.get("versionEndIncluding")).map(JsonNode::asText).orElse(null); final String versionStartExcluding = Optional.ofNullable(cpeMatch.get("versionStartExcluding")).map(JsonNode::asText).orElse(null); final String versionStartIncluding = Optional.ofNullable(cpeMatch.get("versionStartIncluding")).map(JsonNode::asText).orElse(null); - VulnerableSoftware vs = qm.getVulnerableSoftwareByCpe23(cpe23Uri, versionEndExcluding, - versionEndIncluding, versionStartExcluding, versionStartIncluding); - if (vs != null) { - return vs; - } + + final VulnerableSoftware vs; try { vs = ModelConverter.convertCpe23UriToVulnerableSoftware(cpe23Uri); - vs.setVulnerable(cpeMatch.get("vulnerable").asBoolean(true)); - vs.setVersionEndExcluding(versionEndExcluding); - vs.setVersionEndIncluding(versionEndIncluding); - vs.setVersionStartExcluding(versionStartExcluding); - vs.setVersionStartIncluding(versionStartIncluding); - //Event.dispatch(new IndexEvent(IndexEvent.Action.CREATE, qm.detach(VulnerableSoftware.class, vs.getId()))); - return vs; } catch (CpeParsingException | CpeEncodingException e) { LOGGER.warn("An error occurred while parsing: " + cpe23Uri + " - The CPE is invalid and will be discarded."); + return null; } - return null; + + vs.setVulnerable(cpeMatch.get("vulnerable").asBoolean(true)); + vs.setVersionEndExcluding(versionEndExcluding); + vs.setVersionEndIncluding(versionEndIncluding); + vs.setVersionStartExcluding(versionStartExcluding); + vs.setVersionStartIncluding(versionStartIncluding); + return vs; } } diff --git a/src/main/java/org/dependencytrack/tasks/AbstractNistMirrorTask.java b/src/main/java/org/dependencytrack/tasks/AbstractNistMirrorTask.java new file mode 100644 index 0000000000..5676677fae --- /dev/null +++ b/src/main/java/org/dependencytrack/tasks/AbstractNistMirrorTask.java @@ -0,0 +1,262 @@ +/* + * 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.tasks; + +import alpine.common.logging.Logger; +import org.dependencytrack.model.AffectedVersionAttribution; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.VulnerableSoftware; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.PersistenceUtil; + +import javax.jdo.Query; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static java.util.stream.Collectors.groupingBy; +import static org.dependencytrack.util.PersistenceUtil.assertNonPersistentAll; +import static org.dependencytrack.util.PersistenceUtil.assertPersistent; + +/** + * @since 4.11.0 + */ +abstract class AbstractNistMirrorTask { + + private final Logger logger = Logger.getLogger(getClass()); + + Vulnerability synchronizeVulnerability(final QueryManager qm, final Vulnerability vuln) { + PersistenceUtil.assertNonPersistent(vuln, "vuln must not be persistent"); + + return qm.runInTransaction(trx -> { + trx.setSerializeRead(true); // SELECT ... FOR UPDATE + + Vulnerability persistentVuln = getVulnerabilityByCveId(qm, vuln.getVulnId()); + if (persistentVuln == null) { + persistentVuln = qm.getPersistenceManager().makePersistent(vuln); + } else { + final Map diffs = updateVulnerability(persistentVuln, vuln); + if (!diffs.isEmpty()) { + logger.debug("%s has changed: %s".formatted(vuln.getVulnId(), diffs)); + return persistentVuln; + } + + logger.debug("%s has not changed".formatted(vuln.getVulnId())); + } + + return persistentVuln; + }); + } + + void synchronizeVulnerableSoftware(final QueryManager qm, final Vulnerability persistentVuln, final List vsList) { + assertPersistent(persistentVuln, "vuln must be persistent"); + assertNonPersistentAll(vsList, "vsList must not be persistent"); + + qm.runInTransaction(tx -> { + tx.setSerializeRead(false); + + // Get all VulnerableSoftware records that are currently associated with the vulnerability. + // Note: For SOME ODD REASON, duplicate (as in, same database ID and all) VulnerableSoftware + // records are returned, when operating on data that was originally created by the feed-based + // NistMirrorTask. We thus have to deduplicate here. + final List vsOldList = persistentVuln.getVulnerableSoftware().stream().distinct().toList(); + logger.trace("%s: Existing VS: %d".formatted(persistentVuln.getVulnId(), vsOldList.size())); + + // Get attributions for all existing VulnerableSoftware records. + final Map> attributionsByVsId = + qm.getAffectedVersionAttributions(persistentVuln, vsOldList).stream() + .collect(groupingBy(attribution -> attribution.getVulnerableSoftware().getId())); + for (final VulnerableSoftware vsOld : vsOldList) { + vsOld.setAffectedVersionAttributions(attributionsByVsId.get(vsOld.getId())); + } + + // Based on the lists of currently reported, and previously reported VulnerableSoftware records, + // divide the previously reported ones into lists of records to keep, and records to remove. + // Records to keep are removed from vsList. Remaining records in vsList thus are entirely new. + final var vsListToRemove = new ArrayList(); + final var vsListToKeep = new ArrayList(); + for (final VulnerableSoftware vsOld : vsOldList) { + if (vsList.removeIf(vsOld::equalsIgnoringDatastoreIdentity)) { + vsListToKeep.add(vsOld); + } else { + final List attributions = vsOld.getAffectedVersionAttributions(); + if (attributions == null || attributions.isEmpty()) { + // DT versions prior to 4.7.0 did not record attributions. + // Drop the VulnerableSoftware for now. If it was previously + // reported by another source, it will be recorded and attributed + // whenever that source is mirrored again. + vsListToRemove.add(vsOld); + continue; + } + + final boolean previouslyReportedByNvd = attributions.stream() + .anyMatch(attr -> attr.getSource() == Vulnerability.Source.NVD); + final boolean previouslyReportedByOthers = !previouslyReportedByNvd; + + if (previouslyReportedByOthers) { + vsListToKeep.add(vsOld); + } else { + vsListToRemove.add(vsOld); + } + } + } + logger.trace("%s: vsListToKeep: %d".formatted(persistentVuln.getVulnId(), vsListToKeep.size())); + logger.trace("%s: vsListToRemove: %d".formatted(persistentVuln.getVulnId(), vsListToRemove.size())); + + // Remove attributions for VulnerableSoftware records that are no longer reported. + if (!vsListToRemove.isEmpty()) { + qm.deleteAffectedVersionAttributions(persistentVuln, vsListToRemove, Vulnerability.Source.NVD); + } + + final var attributionDate = new Date(); + + // For VulnerableSoftware records that existed before, update the lastSeen timestamp. + for (final VulnerableSoftware oldVs : vsListToKeep) { + oldVs.getAffectedVersionAttributions().stream() + .filter(attribution -> attribution.getSource() == Vulnerability.Source.NVD) + .findAny() + .ifPresent(attribution -> attribution.setLastSeen(attributionDate)); + } + + // For VulnerableSoftware records that are newly reported for this vulnerability, check if any matching + // records exist in the database that are currently associated with other (or no) vulnerabilities. + for (final VulnerableSoftware vs : vsList) { + final VulnerableSoftware existingVs = qm.getVulnerableSoftwareByCpe23( + vs.getCpe23(), + vs.getVersionEndExcluding(), + vs.getVersionEndIncluding(), + vs.getVersionStartExcluding(), + vs.getVersionStartIncluding() + ); + if (existingVs != null) { + final boolean hasAttribution = qm.hasAffectedVersionAttribution(persistentVuln, existingVs, Vulnerability.Source.NVD); + if (!hasAttribution) { + logger.trace("%s: Adding attribution".formatted(persistentVuln.getVulnId())); + final AffectedVersionAttribution attribution = createAttribution(persistentVuln, existingVs, attributionDate); + qm.getPersistenceManager().makePersistent(attribution); + } else { + logger.debug("%s: Encountered dangling attribution; Re-using by updating firstSeen and lastSeen timestamps".formatted(persistentVuln.getVulnId())); + final AffectedVersionAttribution existingAttribution = qm.getAffectedVersionAttribution(persistentVuln, existingVs, Vulnerability.Source.NVD); + existingAttribution.setFirstSeen(attributionDate); + existingAttribution.setLastSeen(attributionDate); + } + vsListToKeep.add(existingVs); + } else { + logger.trace("%s: Creating new VS".formatted(persistentVuln.getVulnId())); + final VulnerableSoftware persistentVs = qm.getPersistenceManager().makePersistent(vs); + final AffectedVersionAttribution attribution = createAttribution(persistentVuln, persistentVs, attributionDate); + qm.getPersistenceManager().makePersistent(attribution); + vsListToKeep.add(persistentVs); + } + } + + logger.trace("%s: Final vsList: %d".formatted(persistentVuln.getVulnId(), vsListToKeep.size())); + if (!Objects.equals(persistentVuln.getVulnerableSoftware(), vsListToKeep)) { + logger.trace("%s: vsList has changed: %s".formatted(persistentVuln.getVulnId(), new PersistenceUtil.Diff(persistentVuln.getVulnerableSoftware(), vsListToKeep))); + persistentVuln.setVulnerableSoftware(vsListToKeep); + } + }); + } + + private static AffectedVersionAttribution createAttribution(final Vulnerability vuln, final VulnerableSoftware vs, + final Date attributionDate) { + final var attribution = new AffectedVersionAttribution(); + attribution.setSource(Vulnerability.Source.NVD); + attribution.setVulnerability(vuln); + attribution.setVulnerableSoftware(vs); + attribution.setFirstSeen(attributionDate); + attribution.setLastSeen(attributionDate); + return attribution; + } + + /** + * Get a {@link Vulnerability} by its CVE ID (implying the source {@link Vulnerability.Source#NVD}). + *

    + * It differs from {@link QueryManager#getVulnerabilityByVulnId(String, String)} in that it does not fetch any + * adjacent relationships (e.g. affected components and aliases). + * + * @param qm The {@link QueryManager} to use + * @param cveId The CVE ID to look for + * @return The {@link Vulnerability} matching the CVE ID, or {@code null} when no match was found + */ + private static Vulnerability getVulnerabilityByCveId(final QueryManager qm, final String cveId) { + final Query query = qm.getPersistenceManager().newQuery(Vulnerability.class); + query.setFilter("source == :source && vulnId == :cveId"); + query.setNamedParameters(Map.of( + "source", Vulnerability.Source.NVD.name(), + "cveId", cveId + )); + try { + return query.executeUnique(); + } finally { + query.closeAll(); + } + } + + /** + * Update an existing, persistent {@link Vulnerability} with data as reported by the NVD. + *

    + * It differs from {@link QueryManager#updateVulnerability(Vulnerability, boolean)} in that it keeps track of + * which fields are modified, and assumes the to-be-updated {@link Vulnerability} to be persistent, and enrolled + * in an active {@link javax.jdo.Transaction}. + * + * @param existingVuln The existing {@link Vulnerability} to update + * @param reportedVuln The {@link Vulnerability} as reported by the NVD + * @return A {@link Map} holding the differences of all updated fields + */ + private static Map updateVulnerability(final Vulnerability existingVuln, final Vulnerability reportedVuln) { + assertPersistent(existingVuln, "existingVuln must be persistent in order for changes to be effective"); + + final var differ = new PersistenceUtil.Differ<>(existingVuln, reportedVuln); + differ.applyIfChanged("title", Vulnerability::getTitle, existingVuln::setTitle); + differ.applyIfChanged("subTitle", Vulnerability::getSubTitle, existingVuln::setSubTitle); + differ.applyIfChanged("description", Vulnerability::getDescription, existingVuln::setDescription); + differ.applyIfChanged("detail", Vulnerability::getDetail, existingVuln::setDetail); + differ.applyIfChanged("recommendation", Vulnerability::getRecommendation, existingVuln::setRecommendation); + differ.applyIfChanged("references", Vulnerability::getReferences, existingVuln::setReferences); + differ.applyIfChanged("credits", Vulnerability::getCredits, existingVuln::setCredits); + differ.applyIfChanged("created", Vulnerability::getCreated, existingVuln::setCreated); + differ.applyIfChanged("published", Vulnerability::getPublished, existingVuln::setPublished); + differ.applyIfChanged("updated", Vulnerability::getUpdated, existingVuln::setUpdated); + differ.applyIfNonEmptyAndChanged("cwes", Vulnerability::getCwes, existingVuln::setCwes); + differ.applyIfChanged("severity", Vulnerability::getSeverity, existingVuln::setSeverity); + differ.applyIfChanged("cvssV2BaseScore", Vulnerability::getCvssV2BaseScore, existingVuln::setCvssV2BaseScore); + differ.applyIfChanged("cvssV2ImpactSubScore", Vulnerability::getCvssV2ImpactSubScore, existingVuln::setCvssV2ImpactSubScore); + differ.applyIfChanged("cvssV2ExploitabilitySubScore", Vulnerability::getCvssV2ExploitabilitySubScore, existingVuln::setCvssV2ExploitabilitySubScore); + differ.applyIfChanged("cvssV2Vector", Vulnerability::getCvssV2Vector, existingVuln::setCvssV2Vector); + differ.applyIfChanged("cvssV3BaseScore", Vulnerability::getCvssV3BaseScore, existingVuln::setCvssV3BaseScore); + differ.applyIfChanged("cvssV3ImpactSubScore", Vulnerability::getCvssV3ImpactSubScore, existingVuln::setCvssV3ImpactSubScore); + differ.applyIfChanged("cvssV3ExploitabilitySubScore", Vulnerability::getCvssV3ExploitabilitySubScore, existingVuln::setCvssV3ExploitabilitySubScore); + differ.applyIfChanged("cvssV3Vector", Vulnerability::getCvssV3Vector, existingVuln::setCvssV3Vector); + differ.applyIfChanged("owaspRRLikelihoodScore", Vulnerability::getOwaspRRLikelihoodScore, existingVuln::setOwaspRRLikelihoodScore); + differ.applyIfChanged("owaspRRTechnicalImpactScore", Vulnerability::getOwaspRRTechnicalImpactScore, existingVuln::setOwaspRRTechnicalImpactScore); + differ.applyIfChanged("owaspRRBusinessImpactScore", Vulnerability::getOwaspRRBusinessImpactScore, existingVuln::setOwaspRRBusinessImpactScore); + differ.applyIfChanged("owaspRRVector", Vulnerability::getOwaspRRVector, existingVuln::setOwaspRRVector); + differ.applyIfChanged("vulnerableVersions", Vulnerability::getVulnerableVersions, existingVuln::setVulnerableVersions); + differ.applyIfChanged("patchedVersions", Vulnerability::getPatchedVersions, existingVuln::setPatchedVersions); + // EPSS is an additional enrichment that no source currently provides natively. We don't want EPSS scores of CVEs to be purged. + differ.applyIfNonNullAndChanged("epssScore", Vulnerability::getEpssScore, existingVuln::setEpssScore); + differ.applyIfNonNullAndChanged("epssPercentile", Vulnerability::getEpssPercentile, existingVuln::setEpssPercentile); + + return differ.getDiffs(); + } + +} diff --git a/src/main/java/org/dependencytrack/tasks/NistApiMirrorTask.java b/src/main/java/org/dependencytrack/tasks/NistApiMirrorTask.java index 6461c6b8ac..d82ac2ae8e 100644 --- a/src/main/java/org/dependencytrack/tasks/NistApiMirrorTask.java +++ b/src/main/java/org/dependencytrack/tasks/NistApiMirrorTask.java @@ -33,7 +33,6 @@ import io.github.jeremylong.openvulnerability.client.nvd.NvdCveClientBuilder; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.concurrent.BasicThreadFactory; -import org.apache.commons.lang3.tuple.Pair; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.NTCredentials; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; @@ -45,27 +44,20 @@ import org.dependencytrack.common.AlpineHttpProxySelector; import org.dependencytrack.event.EpssMirrorEvent; import org.dependencytrack.event.IndexEvent; -import org.dependencytrack.event.IndexEvent.Action; import org.dependencytrack.event.NistApiMirrorEvent; import org.dependencytrack.model.AffectedVersionAttribution; import org.dependencytrack.model.Vulnerability; -import org.dependencytrack.model.Vulnerability.Source; import org.dependencytrack.model.VulnerableSoftware; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.persistence.listener.IndexingInstanceLifecycleListener; +import org.dependencytrack.persistence.listener.L2CacheEvictingInstanceLifecycleListener; import org.dependencytrack.util.DebugDataEncryption; -import org.dependencytrack.util.PersistenceUtil.Diff; -import org.dependencytrack.util.PersistenceUtil.Differ; -import javax.jdo.Query; import java.time.Duration; import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Date; import java.util.List; -import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -73,22 +65,21 @@ import java.util.concurrent.atomic.AtomicInteger; import static io.github.jeremylong.openvulnerability.client.nvd.NvdCveClientBuilder.aNvdCveApi; -import static java.util.stream.Collectors.groupingBy; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.datanucleus.PropertyNames.PROPERTY_PERSISTENCE_BY_REACHABILITY_AT_COMMIT; +import static org.datanucleus.PropertyNames.PROPERTY_RETAIN_VALUES; import static org.dependencytrack.model.ConfigPropertyConstants.VULNERABILITY_SOURCE_NVD_API_KEY; import static org.dependencytrack.model.ConfigPropertyConstants.VULNERABILITY_SOURCE_NVD_API_LAST_MODIFIED_EPOCH_SECONDS; import static org.dependencytrack.model.ConfigPropertyConstants.VULNERABILITY_SOURCE_NVD_API_URL; import static org.dependencytrack.parser.nvd.api20.ModelConverter.convert; import static org.dependencytrack.parser.nvd.api20.ModelConverter.convertConfigurations; -import static org.dependencytrack.util.PersistenceUtil.assertPersistent; /** * A {@link Subscriber} that mirrors the content of the NVD through the NVD API 2.0. * * @since 4.10.0 */ -public class NistApiMirrorTask implements Subscriber { +public class NistApiMirrorTask extends AbstractNistMirrorTask implements Subscriber { private static final Logger LOGGER = Logger.getLogger(NistApiMirrorTask.class); @@ -175,12 +166,15 @@ public void inform(final Event e) { executor.submit(() -> { try (final var qm = new QueryManager().withL2CacheDisabled()) { qm.getPersistenceManager().setProperty(PROPERTY_PERSISTENCE_BY_REACHABILITY_AT_COMMIT, "false"); + qm.getPersistenceManager().setProperty(PROPERTY_RETAIN_VALUES, "true"); + qm.getPersistenceManager().addInstanceLifecycleListener(new IndexingInstanceLifecycleListener(Event::dispatch), + Vulnerability.class, VulnerableSoftware.class); + qm.getPersistenceManager().addInstanceLifecycleListener(new L2CacheEvictingInstanceLifecycleListener(qm), + AffectedVersionAttribution.class, Vulnerability.class, VulnerableSoftware.class); - // Note: persistentVuln is in HOLLOW state (all fields except ID are unloaded). - // https://www.datanucleus.org/products/accessplatform_6_0/jdo/persistence.html#lifecycle final Vulnerability persistentVuln = synchronizeVulnerability(qm, vuln); synchronizeVulnerableSoftware(qm, persistentVuln, vsList); - } catch (Exception ex) { + } catch (RuntimeException ex) { LOGGER.error("An unexpected error occurred while processing %s".formatted(vuln.getVulnId()), ex); } finally { final int currentNumMirrored = numMirrored.incrementAndGet(); @@ -225,149 +219,13 @@ public void inform(final Event e) { } if (updateLastModified(lastModified)) { - Event.dispatch(new IndexEvent(Action.COMMIT, Vulnerability.class)); + Event.dispatch(new IndexEvent(IndexEvent.Action.COMMIT, Vulnerability.class)); + Event.dispatch(new IndexEvent(IndexEvent.Action.COMMIT, VulnerableSoftware.class)); } Event.dispatch(new EpssMirrorEvent()); } - private static Vulnerability synchronizeVulnerability(final QueryManager qm, final Vulnerability vuln) { - final Pair vulnIndexEventPair = qm.runInTransaction(trx -> { - trx.setSerializeRead(true); // SELECT ... FOR UPDATE - - Vulnerability persistentVuln = getVulnerabilityByCveId(qm, vuln.getVulnId()); - if (persistentVuln == null) { - persistentVuln = qm.getPersistenceManager().makePersistent(vuln); - return Pair.of(persistentVuln, new IndexEvent(Action.CREATE, persistentVuln)); - } else { - final Map diffs = updateVulnerability(persistentVuln, vuln); - if (!diffs.isEmpty()) { - LOGGER.debug("%s has changed: %s".formatted(vuln.getVulnId(), diffs)); - return Pair.of(persistentVuln, new IndexEvent(Action.UPDATE, persistentVuln)); - } - - LOGGER.debug("%s has not changed".formatted(vuln.getVulnId())); - return Pair.of(persistentVuln, null); - } - }); - - final IndexEvent indexEvent = vulnIndexEventPair.getRight(); - final Vulnerability persistentVuln = vulnIndexEventPair.getLeft(); - - if (indexEvent != null) { - Event.dispatch(indexEvent); - } - - return persistentVuln; - } - - private static void synchronizeVulnerableSoftware(final QueryManager qm, final Vulnerability persistentVuln, final List vsList) { - qm.runInTransaction(tx -> { - tx.setSerializeRead(false); - - // Get all VulnerableSoftware records that are currently associated with the vulnerability. - // Note: For SOME ODD REASON, duplicate (as in, same database ID and all) VulnerableSoftware - // records are returned, when operating on data that was originally created by the feed-based - // NistMirrorTask. We thus have to deduplicate here. - final List vsOldList = persistentVuln.getVulnerableSoftware().stream().distinct().toList(); - LOGGER.trace("%s: Existing VS: %d".formatted(persistentVuln.getVulnId(), vsOldList.size())); - - // Get attributions for all existing VulnerableSoftware records. - final Map> attributionsByVsId = - qm.getAffectedVersionAttributions(persistentVuln, vsOldList).stream() - .collect(groupingBy(attribution -> attribution.getVulnerableSoftware().getId())); - for (final VulnerableSoftware vsOld : vsOldList) { - vsOld.setAffectedVersionAttributions(attributionsByVsId.get(vsOld.getId())); - } - - // Based on the lists of currently reported, and previously reported VulnerableSoftware records, - // divide the previously reported ones into lists of records to keep, and records to remove. - // Records to keep are removed from vsList. Remaining records in vsList thus are entirely new. - final var vsListToRemove = new ArrayList(); - final var vsListToKeep = new ArrayList(); - for (final VulnerableSoftware vsOld : vsOldList) { - if (vsList.removeIf(vsOld::equalsIgnoringDatastoreIdentity)) { - vsListToKeep.add(vsOld); - } else { - final List attributions = vsOld.getAffectedVersionAttributions(); - if (attributions == null || attributions.isEmpty()) { - // DT versions prior to 4.7.0 did not record attributions. - // Drop the VulnerableSoftware for now. If it was previously - // reported by another source, it will be recorded and attributed - // whenever that source is mirrored again. - vsListToRemove.add(vsOld); - continue; - } - - final boolean previouslyReportedByNvd = attributions.stream() - .anyMatch(attr -> attr.getSource() == Source.NVD); - final boolean previouslyReportedByOthers = !previouslyReportedByNvd; - - if (previouslyReportedByOthers) { - vsListToKeep.add(vsOld); - } else { - vsListToRemove.add(vsOld); - } - } - } - LOGGER.trace("%s: vsListToKeep: %d".formatted(persistentVuln.getVulnId(), vsListToKeep.size())); - LOGGER.trace("%s: vsListToRemove: %d".formatted(persistentVuln.getVulnId(), vsListToRemove.size())); - - // Remove attributions for VulnerableSoftware records that are no longer reported. - if (!vsListToRemove.isEmpty()) { - qm.deleteAffectedVersionAttributions(persistentVuln, vsListToRemove, Source.NVD); - } - - final var attributionDate = new Date(); - - // For VulnerableSoftware records that existed before, update the lastSeen timestamp. - for (final VulnerableSoftware oldVs : vsListToKeep) { - oldVs.getAffectedVersionAttributions().stream() - .filter(attribution -> attribution.getSource() == Source.NVD) - .findAny() - .ifPresent(attribution -> attribution.setLastSeen(attributionDate)); - } - - // For VulnerableSoftware records that are newly reported for this vulnerability, check if any matching - // records exist in the database that are currently associated with other (or no) vulnerabilities. - for (final VulnerableSoftware vs : vsList) { - final VulnerableSoftware existingVs = qm.getVulnerableSoftwareByCpe23( - vs.getCpe23(), - vs.getVersionEndExcluding(), - vs.getVersionEndIncluding(), - vs.getVersionStartExcluding(), - vs.getVersionStartIncluding() - ); - if (existingVs != null) { - final boolean hasAttribution = qm.hasAffectedVersionAttribution(persistentVuln, existingVs, Source.NVD); - if (!hasAttribution) { - LOGGER.trace("%s: Adding attribution".formatted(persistentVuln.getVulnId())); - final AffectedVersionAttribution attribution = createAttribution(persistentVuln, existingVs, attributionDate); - qm.getPersistenceManager().makePersistent(attribution); - } else { - LOGGER.debug("%s: Encountered dangling attribution; Re-using by updating firstSeen and lastSeen timestamps".formatted(persistentVuln.getVulnId())); - final AffectedVersionAttribution existingAttribution = qm.getAffectedVersionAttribution(persistentVuln, existingVs, Source.NVD); - existingAttribution.setFirstSeen(attributionDate); - existingAttribution.setLastSeen(attributionDate); - } - vsListToKeep.add(existingVs); - } else { - LOGGER.trace("%s: Creating new VS".formatted(persistentVuln.getVulnId())); - final VulnerableSoftware persistentVs = qm.getPersistenceManager().makePersistent(vs); - final AffectedVersionAttribution attribution = createAttribution(persistentVuln, persistentVs, attributionDate); - qm.getPersistenceManager().makePersistent(attribution); - vsListToKeep.add(persistentVs); - } - } - - LOGGER.trace("%s: Final vsList: %d".formatted(persistentVuln.getVulnId(), vsListToKeep.size())); - if (!Objects.equals(persistentVuln.getVulnerableSoftware(), vsListToKeep)) { - LOGGER.trace("%s: vsList has changed: %s".formatted(persistentVuln.getVulnId(), new Diff(persistentVuln.getVulnerableSoftware(), vsListToKeep))); - persistentVuln.setVulnerableSoftware(vsListToKeep); - } - }); - } - private static NvdCveClient createApiClient(final String apiUrl, final String apiKey, final long lastModifiedEpochSeconds) { final NvdCveClientBuilder clientBuilder = aNvdCveApi() .withHttpClientSupplier(new HttpClientSupplier(apiKey != null)) @@ -409,92 +267,6 @@ private static boolean updateLastModified(final ZonedDateTime lastModifiedDateTi return true; } - private static AffectedVersionAttribution createAttribution(final Vulnerability vuln, final VulnerableSoftware vs, - final Date attributionDate) { - final var attribution = new AffectedVersionAttribution(); - attribution.setSource(Source.NVD); - attribution.setVulnerability(vuln); - attribution.setVulnerableSoftware(vs); - attribution.setFirstSeen(attributionDate); - attribution.setLastSeen(attributionDate); - return attribution; - } - - /** - * Get a {@link Vulnerability} by its CVE ID (implying the source {@link Source#NVD}). - *

    - * It differs from {@link QueryManager#getVulnerabilityByVulnId(String, String)} in that it does not fetch any - * adjacent relationships (e.g. affected components and aliases). - * - * @param qm The {@link QueryManager} to use - * @param cveId The CVE ID to look for - * @return The {@link Vulnerability} matching the CVE ID, or {@code null} when no match was found - */ - private static Vulnerability getVulnerabilityByCveId(final QueryManager qm, final String cveId) { - final Query query = qm.getPersistenceManager().newQuery(Vulnerability.class); - query.setFilter("source == :source && vulnId == :cveId"); - query.setNamedParameters(Map.of( - "source", Source.NVD.name(), - "cveId", cveId - )); - try { - return query.executeUnique(); - } finally { - query.closeAll(); - } - } - - /** - * Update an existing, persistent {@link Vulnerability} with data as reported by the NVD. - *

    - * It differs from {@link QueryManager#updateVulnerability(Vulnerability, boolean)} in that it keeps track of - * which fields are modified, and assumes the to-be-updated {@link Vulnerability} to be persistent, and enrolled - * in an active {@link javax.jdo.Transaction}. - * - * @param existingVuln The existing {@link Vulnerability} to update - * @param reportedVuln The {@link Vulnerability} as reported by the NVD - * @return A {@link Map} holding the differences of all updated fields - */ - private static Map updateVulnerability(final Vulnerability existingVuln, final Vulnerability reportedVuln) { - assertPersistent(existingVuln, "existingVuln must be persistent in order for changes to be effective"); - - final var differ = new Differ<>(existingVuln, reportedVuln); - differ.applyIfChanged("title", Vulnerability::getTitle, existingVuln::setTitle); - differ.applyIfChanged("subTitle", Vulnerability::getSubTitle, existingVuln::setSubTitle); - differ.applyIfChanged("description", Vulnerability::getDescription, existingVuln::setDescription); - differ.applyIfChanged("detail", Vulnerability::getDetail, existingVuln::setDetail); - differ.applyIfChanged("recommendation", Vulnerability::getRecommendation, existingVuln::setRecommendation); - differ.applyIfChanged("references", Vulnerability::getReferences, existingVuln::setReferences); - differ.applyIfChanged("credits", Vulnerability::getCredits, existingVuln::setCredits); - differ.applyIfChanged("created", Vulnerability::getCreated, existingVuln::setCreated); - differ.applyIfChanged("published", Vulnerability::getPublished, existingVuln::setPublished); - differ.applyIfChanged("updated", Vulnerability::getUpdated, existingVuln::setUpdated); - differ.applyIfNonEmptyAndChanged("cwes", Vulnerability::getCwes, existingVuln::setCwes); - // Calling setSeverity nulls all CVSS and OWASP RR fields. getSeverity calculates the severity on-the-fly, - // and will return UNASSIGNED even when no severity is set explicitly. Thus, calling setSeverity - // must happen before CVSS and OWASP RR fields are set, to avoid null-ing them again. - differ.applyIfChanged("severity", Vulnerability::getSeverity, existingVuln::setSeverity); - differ.applyIfChanged("cvssV2BaseScore", Vulnerability::getCvssV2BaseScore, existingVuln::setCvssV2BaseScore); - differ.applyIfChanged("cvssV2ImpactSubScore", Vulnerability::getCvssV2ImpactSubScore, existingVuln::setCvssV2ImpactSubScore); - differ.applyIfChanged("cvssV2ExploitabilitySubScore", Vulnerability::getCvssV2ExploitabilitySubScore, existingVuln::setCvssV2ExploitabilitySubScore); - differ.applyIfChanged("cvssV2Vector", Vulnerability::getCvssV2Vector, existingVuln::setCvssV2Vector); - differ.applyIfChanged("cvssV3BaseScore", Vulnerability::getCvssV3BaseScore, existingVuln::setCvssV3BaseScore); - differ.applyIfChanged("cvssV3ImpactSubScore", Vulnerability::getCvssV3ImpactSubScore, existingVuln::setCvssV3ImpactSubScore); - differ.applyIfChanged("cvssV3ExploitabilitySubScore", Vulnerability::getCvssV3ExploitabilitySubScore, existingVuln::setCvssV3ExploitabilitySubScore); - differ.applyIfChanged("cvssV3Vector", Vulnerability::getCvssV3Vector, existingVuln::setCvssV3Vector); - differ.applyIfChanged("owaspRRLikelihoodScore", Vulnerability::getOwaspRRLikelihoodScore, existingVuln::setOwaspRRLikelihoodScore); - differ.applyIfChanged("owaspRRTechnicalImpactScore", Vulnerability::getOwaspRRTechnicalImpactScore, existingVuln::setOwaspRRTechnicalImpactScore); - differ.applyIfChanged("owaspRRBusinessImpactScore", Vulnerability::getOwaspRRBusinessImpactScore, existingVuln::setOwaspRRBusinessImpactScore); - differ.applyIfChanged("owaspRRVector", Vulnerability::getOwaspRRVector, existingVuln::setOwaspRRVector); - differ.applyIfChanged("vulnerableVersions", Vulnerability::getVulnerableVersions, existingVuln::setVulnerableVersions); - differ.applyIfChanged("patchedVersions", Vulnerability::getPatchedVersions, existingVuln::setPatchedVersions); - // EPSS is an additional enrichment that no source currently provides natively. We don't want EPSS scores of CVEs to be purged. - differ.applyIfNonNullAndChanged("epssScore", Vulnerability::getEpssScore, existingVuln::setEpssScore); - differ.applyIfNonNullAndChanged("epssPercentile", Vulnerability::getEpssPercentile, existingVuln::setEpssPercentile); - - return differ.getDiffs(); - } - private static final class HttpClientSupplier implements HttpAsyncClientSupplier { private final boolean isApiKeyProvided; diff --git a/src/main/java/org/dependencytrack/tasks/NistMirrorTask.java b/src/main/java/org/dependencytrack/tasks/NistMirrorTask.java index 85d1ab6d04..58901c4e94 100644 --- a/src/main/java/org/dependencytrack/tasks/NistMirrorTask.java +++ b/src/main/java/org/dependencytrack/tasks/NistMirrorTask.java @@ -41,13 +41,19 @@ import org.apache.http.conn.ConnectTimeoutException; import org.dependencytrack.common.HttpClientPool; import org.dependencytrack.event.EpssMirrorEvent; +import org.dependencytrack.event.IndexEvent; import org.dependencytrack.event.NistApiMirrorEvent; import org.dependencytrack.event.NistMirrorEvent; +import org.dependencytrack.model.AffectedVersionAttribution; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.VulnerableSoftware; import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.parser.nvd.NvdParser; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.persistence.listener.IndexingInstanceLifecycleListener; +import org.dependencytrack.persistence.listener.L2CacheEvictingInstanceLifecycleListener; import java.io.BufferedReader; import java.io.Closeable; @@ -64,9 +70,12 @@ import java.time.Duration; import java.util.Calendar; import java.util.Date; +import java.util.List; import java.util.zip.GZIPInputStream; import static io.github.resilience4j.core.IntervalFunction.ofExponentialBackoff; +import static org.datanucleus.PropertyNames.PROPERTY_PERSISTENCE_BY_REACHABILITY_AT_COMMIT; +import static org.datanucleus.PropertyNames.PROPERTY_RETAIN_VALUES; import static org.dependencytrack.model.ConfigPropertyConstants.VULNERABILITY_SOURCE_NVD_API_DOWNLOAD_FEEDS; import static org.dependencytrack.model.ConfigPropertyConstants.VULNERABILITY_SOURCE_NVD_API_ENABLED; import static org.dependencytrack.model.ConfigPropertyConstants.VULNERABILITY_SOURCE_NVD_ENABLED; @@ -78,7 +87,7 @@ * @author Steve Springett * @since 3.0.0 */ -public class NistMirrorTask implements LoggableSubscriber { +public class NistMirrorTask extends AbstractNistMirrorTask implements LoggableSubscriber { private enum ResourceType { CVE_YEAR_DATA, @@ -363,8 +372,10 @@ private void uncompress(final File file, final ResourceType resourceType) { final long start = System.currentTimeMillis(); if (ResourceType.CVE_YEAR_DATA == resourceType || ResourceType.CVE_MODIFIED_DATA == resourceType) { if (!isApiEnabled) { - final NvdParser parser = new NvdParser(); + final NvdParser parser = new NvdParser(this::processVulnerability); parser.parse(uncompressedFile); + Event.dispatch(new IndexEvent(IndexEvent.Action.COMMIT, Vulnerability.class)); + Event.dispatch(new IndexEvent(IndexEvent.Action.COMMIT, VulnerableSoftware.class)); } else { LOGGER.debug(""" %s was successfully downloaded and uncompressed, but will not be parsed because \ @@ -378,6 +389,22 @@ private void uncompress(final File file, final ResourceType resourceType) { metricParseTime += end - start; } + private void processVulnerability(final Vulnerability vuln, final List vsList) { + try (final var qm = new QueryManager().withL2CacheDisabled()) { + qm.getPersistenceManager().setProperty(PROPERTY_PERSISTENCE_BY_REACHABILITY_AT_COMMIT, "false"); + qm.getPersistenceManager().setProperty(PROPERTY_RETAIN_VALUES, "true"); + qm.getPersistenceManager().addInstanceLifecycleListener(new IndexingInstanceLifecycleListener(Event::dispatch), + Vulnerability.class, VulnerableSoftware.class); + qm.getPersistenceManager().addInstanceLifecycleListener(new L2CacheEvictingInstanceLifecycleListener(qm), + AffectedVersionAttribution.class, Vulnerability.class, VulnerableSoftware.class); + + final Vulnerability persistentVuln = synchronizeVulnerability(qm, vuln); + synchronizeVulnerableSoftware(qm, persistentVuln, vsList); + } catch (RuntimeException e) { + LOGGER.error("An unexpected error occurred while processing %s".formatted(vuln.getVulnId()), e); + } + } + /** * Closes a closable object. * @param object the object to close diff --git a/src/main/java/org/dependencytrack/util/PersistenceUtil.java b/src/main/java/org/dependencytrack/util/PersistenceUtil.java index a2c0a73da2..f92ac11abd 100644 --- a/src/main/java/org/dependencytrack/util/PersistenceUtil.java +++ b/src/main/java/org/dependencytrack/util/PersistenceUtil.java @@ -201,6 +201,22 @@ public static void assertNonPersistent(final Object object, final String message } } + /** + * Utility method to ensure that a given {@link Collection} is not in a persistent state. + * + * @param objects The {@link Collection} to check the state of + * @param message Message to use for the exception, if object is persistent + * @see #assertNonPersistent(Object, String) + * @since 4.11.0 + */ + public static void assertNonPersistentAll(final Collection objects, final String message) { + if (objects == null || objects.isEmpty()) { + return; + } + + objects.forEach(object -> assertNonPersistent(object, message)); + } + private static boolean isPersistent(final Object object) { final ObjectState objectState = JDOHelper.getObjectState(object); return objectState == PERSISTENT_CLEAN From 063c2b385ce50c753b8a8b2c325d2c9775a680aa Mon Sep 17 00:00:00 2001 From: Ross Murphy Date: Thu, 2 May 2024 14:23:26 +0100 Subject: [PATCH 114/412] Add the project name and project URL to bom processing notifications Previously, the user was only notified of the result of the BOM processing. Now the user is told the name of the project for each notificication, and for errors, they are also given the project URL Signed-off-by: Ross Murphy --- .../tasks/BomUploadProcessingTask.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java index c8d72843da..7841209f50 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java @@ -19,8 +19,10 @@ package org.dependencytrack.tasks; import alpine.common.logging.Logger; +import alpine.common.util.UrlUtil; import alpine.event.framework.Event; import alpine.event.framework.Subscriber; +import alpine.model.ConfigProperty; import alpine.notification.Notification; import alpine.notification.NotificationLevel; import org.cyclonedx.BomParserFactory; @@ -220,7 +222,7 @@ public void inform(final Event e) { Notification.dispatch(new Notification() .scope(NotificationScope.PORTFOLIO) .group(NotificationGroup.BOM_PROCESSED) - .title(NotificationConstants.Title.BOM_PROCESSED) + .title(NotificationConstants.Title.BOM_PROCESSED + " on Project: [" + event.getProject().getName() + "]") .level(NotificationLevel.INFORMATIONAL) .content("A " + bomFormat.getFormatShortName() + " BOM was processed") .subject(new BomConsumedOrProcessed(detachedProject, Base64.getEncoder().encodeToString(bomBytes), bomFormat, bomSpecVersion))); @@ -229,12 +231,16 @@ public void inform(final Event e) { if (bomProcessingFailedProject != null) { bomProcessingFailedProject = qm.detach(Project.class, bomProcessingFailedProject.getId()); } + final ConfigProperty baseUrlProperty = qm.getConfigProperty( + ConfigPropertyConstants.GENERAL_BASE_URL.getGroupName(), + ConfigPropertyConstants.GENERAL_BASE_URL.getPropertyName() + ); Notification.dispatch(new Notification() .scope(NotificationScope.PORTFOLIO) .group(NotificationGroup.BOM_PROCESSING_FAILED) - .title(NotificationConstants.Title.BOM_PROCESSING_FAILED) + .title(NotificationConstants.Title.BOM_PROCESSING_FAILED + " on Project: [" + event.getProject().getName() + "]") .level(NotificationLevel.ERROR) - .content("An error occurred while processing a BOM") + .content("An error occurred while processing a BOM: " + UrlUtil.normalize(baseUrlProperty.getPropertyValue()) + "/projects/" + event.getProject().getUuid()) .subject(new BomProcessingFailed(bomProcessingFailedProject, Base64.getEncoder().encodeToString(bomBytes), ex.getMessage(), bomProcessingFailedBomFormat, bomProcessingFailedBomVersion))); } finally { qm.commitSearchIndex(true, Component.class); From 8120cf7370cf90bb6ce23462f6d70f5b944c6b09 Mon Sep 17 00:00:00 2001 From: nscuro Date: Thu, 2 May 2024 19:30:24 +0200 Subject: [PATCH 115/412] Add test for `NistMirrorTask` Signed-off-by: nscuro --- .../servlets/NvdMirrorServlet.java | 2 +- .../dependencytrack/tasks/NistMirrorTask.java | 14 +- .../tasks/NistMirrorTaskTest.java | 175 + .../unit/nvd/feed/nvdcve-1.1-2022.json | 16675 ++++++++++++++++ .../unit/nvd/feed/nvdcve-1.1-2022.meta | 5 + 5 files changed, 16867 insertions(+), 4 deletions(-) create mode 100644 src/test/java/org/dependencytrack/tasks/NistMirrorTaskTest.java create mode 100644 src/test/resources/unit/nvd/feed/nvdcve-1.1-2022.json create mode 100644 src/test/resources/unit/nvd/feed/nvdcve-1.1-2022.meta diff --git a/src/main/java/org/dependencytrack/servlets/NvdMirrorServlet.java b/src/main/java/org/dependencytrack/servlets/NvdMirrorServlet.java index 8144906651..01bd083447 100644 --- a/src/main/java/org/dependencytrack/servlets/NvdMirrorServlet.java +++ b/src/main/java/org/dependencytrack/servlets/NvdMirrorServlet.java @@ -36,7 +36,7 @@ public class NvdMirrorServlet extends FileSystemResourceServlet { public void init(final ServletConfig config) throws ServletException { LOGGER.info("Initializing NVD mirror"); super.init(config); - super.setDirectory(NistMirrorTask.NVD_MIRROR_DIR); + super.setDirectory(NistMirrorTask.DEFAULT_NVD_MIRROR_DIR.toString()); super.setAbsolute(true); } diff --git a/src/main/java/org/dependencytrack/tasks/NistMirrorTask.java b/src/main/java/org/dependencytrack/tasks/NistMirrorTask.java index 58901c4e94..9f7ebd6093 100644 --- a/src/main/java/org/dependencytrack/tasks/NistMirrorTask.java +++ b/src/main/java/org/dependencytrack/tasks/NistMirrorTask.java @@ -66,6 +66,7 @@ import java.net.SocketTimeoutException; import java.net.URL; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.time.Duration; import java.util.Calendar; @@ -98,7 +99,7 @@ private enum ResourceType { NONE // DO NOT PARSE THIS TYPE } - public static final String NVD_MIRROR_DIR = Config.getInstance().getDataDirectorty().getAbsolutePath() + File.separator + "nist"; + public static final Path DEFAULT_NVD_MIRROR_DIR = Config.getInstance().getDataDirectorty().toPath().resolve("nist").toAbsolutePath(); private static final String CVE_JSON_11_MODIFIED_URL = "/json/cve/1.1/nvdcve-1.1-modified.json.gz"; private static final String CVE_JSON_11_BASE_URL = "/json/cve/1.1/nvdcve-1.1-%d.json.gz"; private static final String CVE_JSON_11_MODIFIED_META = "/json/cve/1.1/nvdcve-1.1-modified.meta"; @@ -142,9 +143,16 @@ private enum ResourceType { .bindTo(Metrics.getRegistry()); } + private final Path mirrorDirPath; private boolean mirroredWithoutErrors = true; public NistMirrorTask() { + this(DEFAULT_NVD_MIRROR_DIR); + } + + NistMirrorTask(final Path mirrorDirPath) { + this.mirrorDirPath = mirrorDirPath; + try (final QueryManager qm = new QueryManager()) { this.isEnabled = qm.isEnabled(VULNERABILITY_SOURCE_NVD_ENABLED); this.isApiEnabled = qm.isEnabled(VULNERABILITY_SOURCE_NVD_API_ENABLED); @@ -156,7 +164,7 @@ public NistMirrorTask() { if (this.nvdFeedsUrl.endsWith("/")) { this.nvdFeedsUrl = this.nvdFeedsUrl.substring(0, this.nvdFeedsUrl.length()-1); } - } + } } /** @@ -183,7 +191,7 @@ public void inform(final Event e) { final long start = System.currentTimeMillis(); LOGGER.info("Starting NIST mirroring task"); - final File mirrorPath = new File(NVD_MIRROR_DIR); + final File mirrorPath = mirrorDirPath.toFile(); setOutputDir(mirrorPath.getAbsolutePath()); getAllFiles(); final long end = System.currentTimeMillis(); diff --git a/src/test/java/org/dependencytrack/tasks/NistMirrorTaskTest.java b/src/test/java/org/dependencytrack/tasks/NistMirrorTaskTest.java new file mode 100644 index 0000000000..d9d1284dd8 --- /dev/null +++ b/src/test/java/org/dependencytrack/tasks/NistMirrorTaskTest.java @@ -0,0 +1,175 @@ +/* + * 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.tasks; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.event.NistMirrorEvent; +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Vulnerability; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.zip.GZIPOutputStream; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.apache.commons.io.IOUtils.resourceToByteArray; +import static org.assertj.core.api.Assertions.assertThat; +import static org.dependencytrack.model.ConfigPropertyConstants.VULNERABILITY_SOURCE_NVD_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.VULNERABILITY_SOURCE_NVD_FEEDS_URL; + +public class NistMirrorTaskTest extends PersistenceCapableTest { + + @Rule + public WireMockRule wireMock = new WireMockRule(options().dynamicPort()); + + @Before + public void setUp() { + qm.createConfigProperty( + VULNERABILITY_SOURCE_NVD_ENABLED.getGroupName(), + VULNERABILITY_SOURCE_NVD_ENABLED.getPropertyName(), + "true", + VULNERABILITY_SOURCE_NVD_ENABLED.getPropertyType(), + VULNERABILITY_SOURCE_NVD_ENABLED.getDescription() + ); + qm.createConfigProperty( + VULNERABILITY_SOURCE_NVD_FEEDS_URL.getGroupName(), + VULNERABILITY_SOURCE_NVD_FEEDS_URL.getPropertyName(), + wireMock.baseUrl(), + VULNERABILITY_SOURCE_NVD_FEEDS_URL.getPropertyType(), + VULNERABILITY_SOURCE_NVD_FEEDS_URL.getDescription() + ); + } + + @Test + public void test() throws Exception { + // Gzip the JSON feed file to match the format returned by the NVD. + // NB: The file is a truncated version of an actual feed file. + // It only contains the first three CVEs. Truncation was done with jq: + // jq 'del(.CVE_Items[3:])' ~/.dependency-track/nist/nvdcve-1.1-2022.json > nvdcve-1.1-2022.json + final var byteArrayOutputStream = new ByteArrayOutputStream(); + try (final var gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) { + gzipOutputStream.write(resourceToByteArray("/unit/nvd/feed/nvdcve-1.1-2022.json")); + } + + wireMock.stubFor(get(anyUrl()) + .willReturn(aResponse() + .withStatus(404))); + wireMock.stubFor(get(urlPathEqualTo("/json/cve/1.1/nvdcve-1.1-2022.json.gz")) + .willReturn(aResponse() + .withBody(byteArrayOutputStream.toByteArray()))); + wireMock.stubFor(get(urlPathEqualTo("/json/cve/1.1/nvdcve-1.1-2022.meta")) + .willReturn(aResponse() + .withBody(resourceToByteArray("/unit/nvd/feed/nvdcve-1.1-2022.meta")))); + + final Path mirrorDirPath = Files.createTempDirectory(null); + mirrorDirPath.toFile().deleteOnExit(); + + new NistMirrorTask(mirrorDirPath).inform(new NistMirrorEvent()); + + assertThat(mirrorDirPath.resolve("nvdcve-1.1-2022.json.gz")).exists(); + assertThat(mirrorDirPath.resolve("nvdcve-1.1-2022.json")).exists(); + assertThat(mirrorDirPath.resolve("nvdcve-1.1-2022.meta")).exists(); + + final List vulns = qm.getVulnerabilities().getList(Vulnerability.class); + assertThat(vulns).satisfiesExactlyInAnyOrder( + vuln -> { + assertThat(vuln.getVulnId()).isEqualTo("CVE-2022-0001"); + assertThat(vuln.getSource()).isEqualTo("NVD"); + assertThat(vuln.getDescription()).isEqualTo(""" + Non-transparent sharing of branch predictor selectors between contexts \ + in some Intel(R) Processors may allow an authorized user to potentially \ + enable information disclosure via local access."""); + assertThat(vuln.getReferences()).isEqualTo(""" + * [https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00598.html](https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00598.html) + * [http://www.openwall.com/lists/oss-security/2022/03/18/2](http://www.openwall.com/lists/oss-security/2022/03/18/2) + * [https://www.oracle.com/security-alerts/cpujul2022.html](https://www.oracle.com/security-alerts/cpujul2022.html) + * [https://security.netapp.com/advisory/ntap-20220818-0004/](https://security.netapp.com/advisory/ntap-20220818-0004/) + * [https://www.kb.cert.org/vuls/id/155143](https://www.kb.cert.org/vuls/id/155143)"""); + assertThat(vuln.getPublished()).isInSameMinuteAs("2022-03-11T18:15:00Z"); + assertThat(vuln.getUpdated()).isInSameMinuteAs("2024-04-09T15:15:00Z"); + assertThat(vuln.getCvssV2BaseScore()).isEqualByComparingTo("2.1"); + assertThat(vuln.getCvssV2ExploitabilitySubScore()).isEqualByComparingTo("3.9"); + assertThat(vuln.getCvssV2ImpactSubScore()).isEqualByComparingTo("2.9"); + assertThat(vuln.getCvssV2Vector()).isEqualTo("(AV:L/AC:L/Au:N/C:P/I:N/A:N)"); + assertThat(vuln.getCvssV3BaseScore()).isEqualByComparingTo("6.5"); + assertThat(vuln.getCvssV3ExploitabilitySubScore()).isEqualByComparingTo("2.0"); + assertThat(vuln.getCvssV3ImpactSubScore()).isEqualByComparingTo("4.0"); + assertThat(vuln.getCvssV3Vector()).isEqualTo("CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N"); + assertThat(vuln.getSeverity()).isEqualTo(Severity.MEDIUM); + }, + vuln -> { + assertThat(vuln.getVulnId()).isEqualTo("CVE-2022-0002"); + assertThat(vuln.getSource()).isEqualTo("NVD"); + assertThat(vuln.getDescription()).isEqualTo(""" + Non-transparent sharing of branch predictor within a context in some \ + Intel(R) Processors may allow an authorized user to potentially enable \ + information disclosure via local access."""); + assertThat(vuln.getReferences()).isEqualTo(""" + * [https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00598.html](https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00598.html) + * [http://www.openwall.com/lists/oss-security/2022/03/18/2](http://www.openwall.com/lists/oss-security/2022/03/18/2) + * [https://www.oracle.com/security-alerts/cpujul2022.html](https://www.oracle.com/security-alerts/cpujul2022.html) + * [https://security.netapp.com/advisory/ntap-20220818-0004/](https://security.netapp.com/advisory/ntap-20220818-0004/)"""); + assertThat(vuln.getPublished()).isInSameMinuteAs("2022-03-11T18:15:00Z"); + assertThat(vuln.getUpdated()).isInSameMinuteAs("2022-08-19T12:28:00Z"); + assertThat(vuln.getCvssV2BaseScore()).isEqualByComparingTo("2.1"); + assertThat(vuln.getCvssV2ExploitabilitySubScore()).isEqualByComparingTo("3.9"); + assertThat(vuln.getCvssV2ImpactSubScore()).isEqualByComparingTo("2.9"); + assertThat(vuln.getCvssV2Vector()).isEqualTo("(AV:L/AC:L/Au:N/C:P/I:N/A:N)"); + assertThat(vuln.getCvssV3BaseScore()).isEqualByComparingTo("6.5"); + assertThat(vuln.getCvssV3ExploitabilitySubScore()).isEqualByComparingTo("2.0"); + assertThat(vuln.getCvssV3ImpactSubScore()).isEqualByComparingTo("4.0"); + assertThat(vuln.getCvssV3Vector()).isEqualTo("CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N"); + assertThat(vuln.getSeverity()).isEqualTo(Severity.MEDIUM); + }, + vuln -> { + assertThat(vuln.getVulnId()).isEqualTo("CVE-2022-0004"); + assertThat(vuln.getSource()).isEqualTo("NVD"); + assertThat(vuln.getDescription()).isEqualTo(""" + Hardware debug modes and processor INIT setting that allow override of \ + locks for some Intel(R) Processors in Intel(R) Boot Guard and Intel(R) \ + TXT may allow an unauthenticated user to potentially enable escalation \ + of privilege via physical access."""); + assertThat(vuln.getReferences()).isEqualTo(""" + * [https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00613.html](https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00613.html)"""); + assertThat(vuln.getPublished()).isInSameMinuteAs("2022-05-12T17:15:00Z"); + assertThat(vuln.getUpdated()).isInSameMinuteAs("2022-06-10T20:52:00Z"); + assertThat(vuln.getCvssV2BaseScore()).isEqualByComparingTo("7.2"); + assertThat(vuln.getCvssV2ExploitabilitySubScore()).isEqualByComparingTo("3.9"); + assertThat(vuln.getCvssV2ImpactSubScore()).isEqualByComparingTo("10.0"); + assertThat(vuln.getCvssV2Vector()).isEqualTo("(AV:L/AC:L/Au:N/C:C/I:C/A:C)"); + assertThat(vuln.getCvssV3BaseScore()).isEqualByComparingTo("6.8"); + assertThat(vuln.getCvssV3ExploitabilitySubScore()).isEqualByComparingTo("0.9"); + assertThat(vuln.getCvssV3ImpactSubScore()).isEqualByComparingTo("5.9"); + assertThat(vuln.getCvssV3Vector()).isEqualTo("CVSS:3.0/AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"); + assertThat(vuln.getSeverity()).isEqualTo(Severity.MEDIUM); + } + ); + } + +} \ No newline at end of file diff --git a/src/test/resources/unit/nvd/feed/nvdcve-1.1-2022.json b/src/test/resources/unit/nvd/feed/nvdcve-1.1-2022.json new file mode 100644 index 0000000000..4d1835e44b --- /dev/null +++ b/src/test/resources/unit/nvd/feed/nvdcve-1.1-2022.json @@ -0,0 +1,16675 @@ +{ + "CVE_data_type": "CVE", + "CVE_data_format": "MITRE", + "CVE_data_version": "4.0", + "CVE_data_numberOfCVEs": "24682", + "CVE_data_timestamp": "2024-05-01T07:00Z", + "CVE_Items": [ + { + "cve": { + "data_type": "CVE", + "data_format": "MITRE", + "data_version": "4.0", + "CVE_data_meta": { + "ID": "CVE-2022-0001", + "ASSIGNER": "secure@intel.com" + }, + "problemtype": { + "problemtype_data": [ + { + "description": [ + { + "lang": "en", + "value": "NVD-CWE-noinfo" + } + ] + } + ] + }, + "references": { + "reference_data": [ + { + "url": "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00598.html", + "name": "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00598.html", + "refsource": "MISC", + "tags": [ + "Vendor Advisory" + ] + }, + { + "url": "http://www.openwall.com/lists/oss-security/2022/03/18/2", + "name": "[oss-security] 20220318 Xen Security Advisory 398 v2 - Multiple speculative security issues", + "refsource": "MLIST", + "tags": [ + "Mailing List", + "Third Party Advisory" + ] + }, + { + "url": "https://www.oracle.com/security-alerts/cpujul2022.html", + "name": "N/A", + "refsource": "N/A", + "tags": [ + "Patch", + "Third Party Advisory" + ] + }, + { + "url": "https://security.netapp.com/advisory/ntap-20220818-0004/", + "name": "https://security.netapp.com/advisory/ntap-20220818-0004/", + "refsource": "CONFIRM", + "tags": [ + "Third Party Advisory" + ] + }, + { + "url": "https://www.kb.cert.org/vuls/id/155143", + "name": "VU#155143", + "refsource": "", + "tags": [] + } + ] + }, + "description": { + "description_data": [ + { + "lang": "en", + "value": "Non-transparent sharing of branch predictor selectors between contexts in some Intel(R) Processors may allow an authorized user to potentially enable information disclosure via local access." + } + ] + } + }, + "configurations": { + "CVE_data_version": "4.0", + "nodes": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j4005:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4100:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4000:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j4105:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_j5005:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_n5000:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10110u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1005g1:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10210u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10310y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10210y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1035g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1035g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1035g1:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-9300h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-9400h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-9400:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-9600k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8265u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8200y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10510u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10510y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10710u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1065g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-9850h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-9700k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8565u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8500y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_m3-8100y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-7960x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-7940x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-7920x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-7900x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-7820x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-7800x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_9282:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_9242:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_9222:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_9221:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8280l:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8280:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8276l:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8276:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8270:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8268:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8260y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8260l:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8260:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8256:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8253:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6262v:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6254:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6252n:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6252:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6248:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6246:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6244:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6242:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6240:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6238t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6238l:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6238:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6234:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6230t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6230n:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6230:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6226:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6222v:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5222:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5220s:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5220:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5218n:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5218b:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5218:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5217:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5215l:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5215:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4216:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4215:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4214y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4214:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4210:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4209t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4208:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_bronze_3204:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2288g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2278g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2275:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2295:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2265:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2255:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2223:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2245:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2225:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2235:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3265m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3245m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3275:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3245:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3275m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3223:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3265:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3225:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6210u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6212u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6240y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6240l:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5218t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5220t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6209u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2286m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9880h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8365u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8665u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9900k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2278gel:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2278ge:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9980hk:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9900kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-9750hf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-9700kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8210y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8310y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10110y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3235:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-9600kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-9400f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_5305u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10920x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9900x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9920x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9960x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9940x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_j5040:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_n5030:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j4125:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j4025:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4120:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4020:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1030g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1030g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1000g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1000g1:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1060g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10940x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9820x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9800x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-7740x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-7640x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5220r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4210r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4214r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_bronze_3206r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10875h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10850h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10810u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10750h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700te:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10610u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10870h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6258r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6256:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6250l:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6250:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6248r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6246r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6242r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6240r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6238r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6230r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6226r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6208u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5218r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4215r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4210t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10980hk:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900te:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10850k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10600t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10600kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10600k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10600:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10500te:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10500t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10500:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10400h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10400f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10400:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10300h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10200h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10320:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10300t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10300:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10100te:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10100t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10100f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10100:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10885h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-l16g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-l13g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-10885m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-10855m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1290te:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1290t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1290p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1290e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1290:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1270p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1270:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1250p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1250:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5305u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5205u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6405u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1185g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1165g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1135g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1115g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1125g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1160g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1130g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1120g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1110g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_j6425:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_n6415:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j6413:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n6211:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1350:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1350p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1370:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1370p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1390:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1390p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1390t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11955m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11855m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11370h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11375h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1185g7e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1185gre:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1180g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11850h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11800h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1195g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5318h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8380h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6328h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5320h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6330h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8353h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8354h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6348h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8376h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8356h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8376hl:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8380hl:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6328hl:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8360hl:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8358:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8352y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6338:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6330n:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8380:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8351n:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8368q:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8352s:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8358p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8352v:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8368:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6348:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6346:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6330:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8360y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6354:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6314u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6338n:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4314:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4316:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5318y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5317:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6334:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6326:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4309y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6342:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4310:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6338t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5318s:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6336y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5318n:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6312u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4310t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5320t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5320:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5315y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8352m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8362:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4500:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n5100:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n5105:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_n6005:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_n6000:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1115gre:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1115g4e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10105t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10305:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10325:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10105:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10105f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10305t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11390h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10505:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11300h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1145g7e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1145gre:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11400:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11400f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1140g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1145g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11500t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11600:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11600k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11260h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11320h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11400h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11500:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11500h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1155g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11600kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11600t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11950h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11980hk:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_7505:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6400:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6405:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6405t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6500:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6500t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6505:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6505t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6600:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6605:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_6305e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_6305:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6413e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6425re:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_p5942b:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6200fe:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6211e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6212re:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6425e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6427fe:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_p5931b:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_p5962b:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_p5921b:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8360h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11155mle:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11155mre:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11555mle:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11555mre:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11865mre:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2386g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2388g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2378g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2378:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2374g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2314:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2334:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2356g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2324g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2336:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11850he:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-11100he:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d1700:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d2700:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g7400:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g7400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1300:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_6600he:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-10855:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10850h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5920:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5900:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5900t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5925:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5905:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5905t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4504:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12400:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12400f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12500:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-12300:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-12100:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-12100f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12500t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-12300t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-12100t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g6900:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g6900t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900hk:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12800h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12650h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12500h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12450h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1280p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1270p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1260p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1250p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1240p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1220p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11865mld:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:a:oracle:communications_cloud_native_core_binding_support_function:22.1.3:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:a:oracle:communications_cloud_native_core_policy:22.2.0:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:a:oracle:communications_cloud_native_core_network_exposure_function:22.1.1:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ] + }, + "impact": { + "baseMetricV3": { + "cvssV3": { + "version": "3.1", + "vectorString": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N", + "attackVector": "LOCAL", + "attackComplexity": "LOW", + "privilegesRequired": "LOW", + "userInteraction": "NONE", + "scope": "CHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "NONE", + "availabilityImpact": "NONE", + "baseScore": 6.5, + "baseSeverity": "MEDIUM" + }, + "exploitabilityScore": 2.0, + "impactScore": 4.0 + }, + "baseMetricV2": { + "cvssV2": { + "version": "2.0", + "vectorString": "AV:L/AC:L/Au:N/C:P/I:N/A:N", + "accessVector": "LOCAL", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "NONE", + "availabilityImpact": "NONE", + "baseScore": 2.1 + }, + "severity": "LOW", + "exploitabilityScore": 3.9, + "impactScore": 2.9, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + }, + "publishedDate": "2022-03-11T18:15Z", + "lastModifiedDate": "2024-04-09T15:15Z" + }, + { + "cve": { + "data_type": "CVE", + "data_format": "MITRE", + "data_version": "4.0", + "CVE_data_meta": { + "ID": "CVE-2022-0002", + "ASSIGNER": "secure@intel.com" + }, + "problemtype": { + "problemtype_data": [ + { + "description": [ + { + "lang": "en", + "value": "NVD-CWE-noinfo" + } + ] + } + ] + }, + "references": { + "reference_data": [ + { + "url": "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00598.html", + "name": "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00598.html", + "refsource": "MISC", + "tags": [ + "Vendor Advisory" + ] + }, + { + "url": "http://www.openwall.com/lists/oss-security/2022/03/18/2", + "name": "[oss-security] 20220318 Xen Security Advisory 398 v2 - Multiple speculative security issues", + "refsource": "MLIST", + "tags": [ + "Mailing List", + "Third Party Advisory" + ] + }, + { + "url": "https://www.oracle.com/security-alerts/cpujul2022.html", + "name": "N/A", + "refsource": "N/A", + "tags": [ + "Patch", + "Third Party Advisory" + ] + }, + { + "url": "https://security.netapp.com/advisory/ntap-20220818-0004/", + "name": "https://security.netapp.com/advisory/ntap-20220818-0004/", + "refsource": "CONFIRM", + "tags": [ + "Third Party Advisory" + ] + } + ] + }, + "description": { + "description_data": [ + { + "lang": "en", + "value": "Non-transparent sharing of branch predictor within a context in some Intel(R) Processors may allow an authorized user to potentially enable information disclosure via local access." + } + ] + } + }, + "configurations": { + "CVE_data_version": "4.0", + "nodes": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j4005:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4100:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4000:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j4105:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j3355:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n3350:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j3455:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n3450:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x5-e3930:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x5-e3940:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x7-e3950:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_j5005:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_n5000:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10110u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1005g1:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10210u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10310y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10210y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1035g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1035g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1035g1:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-9300h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-9400h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-9400:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-9600k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8265u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8200y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10510u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10510y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10710u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1065g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-9850h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-9700k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8565u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8500y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_m3-8100y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-7960x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-7940x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-7920x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-7900x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-7820x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-7800x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_9282:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_9242:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_9222:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_9221:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8280l:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8280:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8276l:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8276:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8270:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8268:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8260y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8260l:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8260:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8256:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8253:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6262v:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6254:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6252n:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6252:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6248:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6246:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6244:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6242:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6240:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6238t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6238l:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6238:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6234:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6230t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6230n:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6230:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6226:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6222v:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5222:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5220s:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5220:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5218n:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5218b:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5218:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5217:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5215l:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5215:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4216:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4215:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4214y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4214:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4210:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4209t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4208:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_bronze_3204:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2288g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2278g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2275:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2295:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2265:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2255:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2223:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2245:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2225:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-2235:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3265m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3245m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3275:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3245:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3275m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3223:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3265:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3225:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6210u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6212u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6240y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6240l:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5218t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5220t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6209u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3308:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3336:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3338:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3508:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3538:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3558:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3708:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3750:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3758:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3808:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3850:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3858:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3830:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3950:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3955:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3958:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2286m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9880h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8365u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8665u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9900k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2278gel:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2278ge:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9980hk:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9900kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-9750hf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-9700kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8210y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8310y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10110y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-3235:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-9600kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-9400f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_5305u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10920x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9900x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9920x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9960x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9940x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_j5040:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_n5030:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_j4205:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j4125:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j4025:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j3355e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4120:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4020:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n3350e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_n4200:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x5-a3930:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x5-a3940:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1030g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1030g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1000g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1000g1:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1060g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x5-z8330:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x5-z8500:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x7-z8700:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x5-z8300:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10940x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9820x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-9800x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-7740x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-7640x:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x5-z8550:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x5-z8350:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x7-z8750:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5220r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4210r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4214r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_bronze_3206r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10875h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10850h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10810u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10750h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700te:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10610u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10870h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6258r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6256:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6250l:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6250:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6248r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6246r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6242r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6240r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6238r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6230r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6226r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6208u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5218r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4215r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4210t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10980hk:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900te:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10850k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10600t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10600kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10600k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10600:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10500te:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10500t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10500:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10400h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10400f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10400:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10300h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10200h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10320:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10300t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10300:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10100te:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10100t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10100f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10100:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10885h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-l16g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-l13g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-10885m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-10855m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1290te:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1290t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1290p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1290e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1290:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1270p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1270:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1250p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1250:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5305u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5205u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6405u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1185g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1165g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1135g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1115g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1125g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1160g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1130g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1120g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1110g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3436l:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3558r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3758r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3338r:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_j6425:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_n6415:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_n4200e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j6413:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j3455e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n6211:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1350:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1350p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1370:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1370p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1390:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1390p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1390t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11955m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11855m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11370h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11375h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1185g7e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1185gre:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1180g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11850h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11800h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1195g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5318h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8380h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6328h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5320h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6330h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8353h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8354h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6348h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8376h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8356h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8376hl:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8380hl:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6328hl:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8360hl:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8358:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8352y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6338:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6330n:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8380:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8351n:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8368q:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8352s:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8358p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8352v:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8368:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6348:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6346:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6330:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8360y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6354:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6314u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6338n:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4314:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4316:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5318y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5317:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6334:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6326:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4309y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6342:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4310:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6338t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5318s:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6336y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5318n:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_6312u:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_silver_4310t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5320t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5320:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_gold_5315y:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8352m:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8362:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4500:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n5100:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n5105:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_n6005:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_n6000:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1115gre:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1115g4e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10105t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10305:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10325:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10105:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10105f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10305t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11390h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10505:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11300h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1145g7e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1145gre:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11400:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11400f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1140g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1145g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11500t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11600:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11600k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11260h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11320h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11400h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11500:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11500h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1155g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11600kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11600t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11950h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11980hk:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_7505:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6400:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6405:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6405t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6500:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6500t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6505:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6505t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6600:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6605:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_6305e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_6305:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x5-a3950:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x5-a3960:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_c3558rc:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6413e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6425re:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_p5942b:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6200fe:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6211e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6212re:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6425e:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6427fe:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_p5931b:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_p5962b:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:atom_p5921b:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_platinum_8360h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11155mle:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11155mre:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11555mle:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11555mre:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11865mre:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2386g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2388g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2378g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2378:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2374g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2314:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2334:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2356g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2324g:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_e-2336:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11850he:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-11100he:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d1700:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d2700:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g7400:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g7400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1300:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_6600he:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-10855:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10850h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5920:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5900:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5900t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5925:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5905:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g5905t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4504:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600k:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12400:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12400f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12500:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-12300:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-12100:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-12100f:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12500t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-12300t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-12100t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g6900:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:celeron_g6900t:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900hk:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12800h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12650h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12500h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12450h:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1280p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1270p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-1260p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1250p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-1240p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-1220p:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-11865mld:-:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:h:intel:puma_7:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:a:oracle:communications_cloud_native_core_binding_support_function:22.1.3:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:a:oracle:communications_cloud_native_core_policy:22.2.0:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:a:oracle:communications_cloud_native_core_network_exposure_function:22.1.1:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ] + }, + "impact": { + "baseMetricV3": { + "cvssV3": { + "version": "3.1", + "vectorString": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N", + "attackVector": "LOCAL", + "attackComplexity": "LOW", + "privilegesRequired": "LOW", + "userInteraction": "NONE", + "scope": "CHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "NONE", + "availabilityImpact": "NONE", + "baseScore": 6.5, + "baseSeverity": "MEDIUM" + }, + "exploitabilityScore": 2.0, + "impactScore": 4.0 + }, + "baseMetricV2": { + "cvssV2": { + "version": "2.0", + "vectorString": "AV:L/AC:L/Au:N/C:P/I:N/A:N", + "accessVector": "LOCAL", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "NONE", + "availabilityImpact": "NONE", + "baseScore": 2.1 + }, + "severity": "LOW", + "exploitabilityScore": 3.9, + "impactScore": 2.9, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + }, + "publishedDate": "2022-03-11T18:15Z", + "lastModifiedDate": "2022-08-19T12:28Z" + }, + { + "cve": { + "data_type": "CVE", + "data_format": "MITRE", + "data_version": "4.0", + "CVE_data_meta": { + "ID": "CVE-2022-0004", + "ASSIGNER": "secure@intel.com" + }, + "problemtype": { + "problemtype_data": [ + { + "description": [ + { + "lang": "en", + "value": "NVD-CWE-noinfo" + } + ] + } + ] + }, + "references": { + "reference_data": [ + { + "url": "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00613.html", + "name": "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00613.html", + "refsource": "MISC", + "tags": [ + "Vendor Advisory" + ] + } + ] + }, + "description": { + "description_data": [ + { + "lang": "en", + "value": "Hardware debug modes and processor INIT setting that allow override of locks for some Intel(R) Processors in Intel(R) Boot Guard and Intel(R) TXT may allow an unauthenticated user to potentially enable escalation of privilege via physical access." + } + ] + } + }, + "configurations": { + "CVE_data_version": "4.0", + "nodes": [ + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-12100_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-12100:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-12100f_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-12100f:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-12100t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-12100t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-12300t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-12300t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-12300_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-12300:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-12600t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-12600kf_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-12600hx_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600hx:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-12600k_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600k:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-12600h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-12600_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12600:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-12500t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12500t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-12500h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12500h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-12500_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12500:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-12450hx_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12450hx:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-12450h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12450h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-12400t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-12400f_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12400f:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-12400_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-12400:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-12700t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-12700kf_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-12700k_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700k:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-12700h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-12700f_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700f:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-12700_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12700:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-12850hx_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12850hx:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-12800hx_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12800hx:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-12800h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12800h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-12650hx_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12650hx:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-12650h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-12650h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-12950hx_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12950hx:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-12900t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-12900kf_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-12900k_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900k:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-12900hx_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900hx:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-12900hk_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900hk:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-12900h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-12900f_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900f:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-12900_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "16.0.15", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-12900:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-11100b_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-11100b:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-11100he_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-11100he:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11260h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11260h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11300h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11300h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11320h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11320h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11400_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11400:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11400f_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11400f:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11400h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11400h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11400t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11500_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11500:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11500b_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11500b:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11500h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11500h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11500he_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11500he:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11500t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11500t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11600_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11600:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11600k_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11600k:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11600kf_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11600kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-11600t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-11600t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-11370h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11370h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-11375h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11375h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-11390h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11390h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-11600h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11600h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-11700_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-11700b_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700b:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-11700f_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700f:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-11700k_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700k:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-11700kf_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-11700t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11700t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-11800h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11800h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-11850h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11850h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-11850he_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-11850he:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-11900_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-11900f_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900f:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-11900h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-11900k_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900k:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-11900kb_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900kb:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-11900kf_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-11900t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11900t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-11950h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11950h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-11980hk_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-11980hk:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10100y_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10100y:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10105_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10105:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10105f_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10105f:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10105t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10105t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10110u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10110u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10110y_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10110y:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10300_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10300:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10300t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10300t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10305_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10305:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10305t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10305t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10320_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10320:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10325_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10325:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10980xe_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10980xe:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10980hk_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10980hk:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10940x_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10940x:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10920x_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10920x:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10910_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10910:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10900x_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900x:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10900te_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900te:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10900t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10900kf_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10900k_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900k:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10900f_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900f:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10900e_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900e:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10900_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10900:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10885h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10885h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10850k_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10850k:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-10850h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-10850h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-10875h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10875h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-10870h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10870h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-10850h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10850h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-10810u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10810u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-10750h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10750h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-10710u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10710u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-10700te_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700te:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-10700t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-10700kf_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-10700k_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700k:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-10700f_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700f:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-10510u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10510u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-10510y_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10510y:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-10700_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-10700e_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-10700e:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10600k_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10600k:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10610u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10610u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10600t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10600t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10600kf_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10600kf:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10600_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10600:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10505_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10505:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10500te_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10500te:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10500t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10500t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10500h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10500h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10500e_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10500e:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10500_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10500:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10400t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10400h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10400h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10400f_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10400f:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10400_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10400:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10310y_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10310y:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10310u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10310u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10300h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10300h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10210y_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10210y:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10210u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10210u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10200h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10200h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-10110y_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-10110y:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10100_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10100:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10100e_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10100e:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10100f_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10100f:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10100te_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10100te:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-10100t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.0.60", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-10100t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8000_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8000:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8000t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8000t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8020_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8020:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8100_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8100:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8100b_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8100b:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8100h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8100h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8100t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8100t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8109u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8109u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8120_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8120:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8121u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8121u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8130u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8130u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8140u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8140u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8145u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8145u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8145ue_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8145ue:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8300_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8300:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8300t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8300t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-8350k_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-8350k:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8200y_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8200y:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8210y_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8210y:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8250u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8250u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8257u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8257u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8259u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8259u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8260u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8260u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8265u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8265u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8269u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8269u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8279u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8279u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8300h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8300h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8305g_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8305g:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8310y_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8310y:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8350u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8350u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8365u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8365u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8365ue_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8365ue:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8400_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8400:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8400b_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8400b:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8400h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8400h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8400t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8420_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8420:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8420t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8420t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8500_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8500:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8500b_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8500b:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8500t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8500t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8550_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8550:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8600_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8600:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8600k_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8600k:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8600t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8600t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8650_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8650:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8650k_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8650k:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-8700b_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-8700b:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8086k_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8086k:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8500y_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8500y:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8510y_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8510y:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8550u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8550u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8557u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8557u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8559u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8559u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8560u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8560u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8565u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8565u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8569u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8569u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8650u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8650u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8665u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8665u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8665ue_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8665ue:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8670_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8670:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8670t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8670t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8700_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8700:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8700b_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8700b:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8700k_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8700k:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8700t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8700t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8705g_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8705g:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8706g_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8706g:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8706g__firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8706g_:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8709g_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8709g:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8750h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8750h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8750hf_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8750hf:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8809g_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8809g:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i7-8850h_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i7-8850h:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i9-8950hk_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i9-8950hk:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_6305_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_6305:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_6305e_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_6305e:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_6600he_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_6600he:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_4205u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "12.0.90", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_4205u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_4305u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "12.0.90", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_4305u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_4305ue_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "12.0.90", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_4305ue:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_j3060_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j3060:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_j3160_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j3160:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_j3355_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j3355:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_j3355e_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j3355e:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_j3455_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j3455:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_j3455e_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j3455e:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n3000_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n3000:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n3010_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n3010:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n3050_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n3050:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n3060_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n3060:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n3150_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n3150:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n3160_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n3160:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n3350_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n3350:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n3350e_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n3350e:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n3450_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n3450:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n3520_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n3520:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n3700_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n3700:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_j4005_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "4.0.45", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j4005:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_j4025_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "4.0.45", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j4025:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_j4105_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "4.0.45", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j4105:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_j4115_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "4.0.45", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j4115:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_j4125_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "4.0.45", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_j4125:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n4000_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "4.0.45", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4000:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n4000c_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "4.0.45", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4000c:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n4020_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "4.0.45", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4020:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n4020c_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "4.0.45", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4020c:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n4100_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "4.0.45", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4100:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n4120_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "4.0.45", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4120:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n4500_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "4.0.45", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4500:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n4504_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "4.0.45", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4504:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n4505_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "4.0.45", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n4505:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n2805_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n2805:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n2806_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n2806:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n2807_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n2807:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n2808_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n2808:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n2810_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n2810:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n2815_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n2815:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n2820_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n2820:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n2830_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n2830:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n2840_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n2840:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n2910_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n2910:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n2920_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n2920:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n2930_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n2930:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n2940_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n2940:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n5095_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n5095:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n5100_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n5100:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n5105_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n5105:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n6210_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n6210:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:celeron_n6211_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:celeron_n6211:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:atom_x7-e3950_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:atom_x7-e3950:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:atom_x5-e3940_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:atom_x5-e3940:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:atom_x5-e3930_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "3.1.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:atom_x5-e3930:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:atom_x6200fe_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.40.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6200fe:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:atom_x6211e_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.40.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6211e:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:atom_x6212re_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.40.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6212re:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:atom_x6413e_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.40.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6413e:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:atom_x6425e_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.40.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6425e:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:atom_x6425re_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.40.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6425re:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:atom_x6427fe_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.40.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:atom_x6427fe:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c232_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c232:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c236_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.8.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c236:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c242_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "12.0.90", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c242:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c246_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "12.0.90", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c246:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c629_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.22.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c629:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c628_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.22.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c628:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c627_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.22.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c627:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c626_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.22.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c626:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c625_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.22.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c625:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c624_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.22.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c624:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c622_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.22.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c622:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c621_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.22.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c621:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c420_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.12.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c420:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c422_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.12.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c422:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:x299_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "11.12.92", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:x299:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_w-1300_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1300:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_w-1350_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1350:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_w-1350p_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1350p:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_w-1370_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1370:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_w-1370p_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1370p:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_w-1390_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1390:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_w-1390p_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1390p:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_w-1390t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_w-1390t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_4410y_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_4410y:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_4415u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_4415u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_4415y_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_4415y:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_4417u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_4417u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_4425y_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_4425y:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_5405u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_5405u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_6405u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_6405u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_6500y_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_6500y:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_7505_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_7505:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g4560_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g4560:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g4600_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g4600:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g4620_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g4620:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g5400_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g5400:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g5400t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g5400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g5420_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g5420:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g5420t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g5420t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g5500_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g5500:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g5500t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g5500t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g5600_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g5600:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g5600t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g5600t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g5620_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g5620:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g6400_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6400:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g6400e_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6400e:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g6400t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g6400te_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6400te:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g6405_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6405:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g6405t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6405t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g6405u_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6405u:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g6500_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6500:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g6500t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6500t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g6505_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6505:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g6505t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6505t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g6600_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6600:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g6605_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g6605:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g7400_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g7400:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g7400e_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g7400e:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g7400t_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g7400t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_gold_g7400te_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "15.0.40", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_gold_g7400te:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_silver_a1030_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_a1030:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_silver_j5005_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_j5005:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_silver_j5040_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_j5040:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_silver_n5000_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_n5000:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_silver_n5030_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_n5030:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_silver_n6000_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_n6000:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:pentium_silver_n6005_firmware:*:*:*:*:*:*:*:*", + "versionEndExcluding": "13.50.20", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:pentium_silver_n6005:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:atom_p5921b_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:atom_p5921b:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:atom_p5931b_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:atom_p5931b:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:atom_p5942b_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:atom_p5942b:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:atom_p5962b_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:atom_p5962b:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c621a_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c621a:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c624a_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c624a:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c627a_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c627a:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:c629a_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:c629a:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i3-l13g4_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i3-l13g4:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:core_i5-l16g7_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:core_i5-l16g7:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2123it_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2123it:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2141i_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2141i:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2142it_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2142it:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2143it_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2143it:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2145nt_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2145nt:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2146nt_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2146nt:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2161i_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2161i:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2163it_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2163it:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2166nt_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2166nt:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2173it_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2173it:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2177nt_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2177nt:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2183it_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2183it:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2187nt_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2187nt:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2191_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2191:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2712t_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2712t:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2733nt_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2733nt:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2738_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2738:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2752nte_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2752nte:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2752ter_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2752ter:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2753nt_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2753nt:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2766nt_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2766nt:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2775te_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2775te:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2776nt_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2776nt:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2779_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2779:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2786nte_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2786nte:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2795nt_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2795nt:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2796nt_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2796nt:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2796te_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2796te:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2798nt_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2798nt:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d-2799_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d-2799:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:xeon_d2700_firmware:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:xeon_d2700:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + } + ] + }, + "impact": { + "baseMetricV3": { + "cvssV3": { + "version": "3.1", + "vectorString": "CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "attackVector": "PHYSICAL", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "HIGH", + "availabilityImpact": "HIGH", + "baseScore": 6.8, + "baseSeverity": "MEDIUM" + }, + "exploitabilityScore": 0.9, + "impactScore": 5.9 + }, + "baseMetricV2": { + "cvssV2": { + "version": "2.0", + "vectorString": "AV:L/AC:L/Au:N/C:C/I:C/A:C", + "accessVector": "LOCAL", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "COMPLETE", + "integrityImpact": "COMPLETE", + "availabilityImpact": "COMPLETE", + "baseScore": 7.2 + }, + "severity": "HIGH", + "exploitabilityScore": 3.9, + "impactScore": 10.0, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + }, + "publishedDate": "2022-05-12T17:15Z", + "lastModifiedDate": "2022-06-10T20:52Z" + } + ] +} diff --git a/src/test/resources/unit/nvd/feed/nvdcve-1.1-2022.meta b/src/test/resources/unit/nvd/feed/nvdcve-1.1-2022.meta new file mode 100644 index 0000000000..de2efde961 --- /dev/null +++ b/src/test/resources/unit/nvd/feed/nvdcve-1.1-2022.meta @@ -0,0 +1,5 @@ +lastModifiedDate:2024-05-01T03:00:46-04:00 +size:127566675 +zipSize:6495621 +gzSize:6495485 +sha256:6FDD92D7B4CA495E786B3D01CD787BC6148201E4E15D9555727B05FB07799922 From 7d7f0eda9197a6f28c70e4cbb63aa835ab7f65b8 Mon Sep 17 00:00:00 2001 From: nscuro Date: Thu, 2 May 2024 21:45:03 +0200 Subject: [PATCH 116/412] De-duplicate CPEs in NVD feed file parsing Some CVE records contain duplicate CPEs. The original parsing logic did not de-duplicate those, consequently causing duplicate `VulnerableSoftware` records in the database. De-duplication was already handled in `NistApiMirrorTask`, but not `NistMirrorTask`. Relates to #3663 Signed-off-by: nscuro --- .../dependencytrack/parser/nvd/NvdParser.java | 8 +- .../parser/nvd/api20/ModelConverter.java | 2 +- .../tasks/NistMirrorTaskTest.java | 61 +++++- .../feed/nvdcve-1.1-2021_duplicate-cpes.json | 187 ++++++++++++++++++ 4 files changed, 247 insertions(+), 11 deletions(-) create mode 100644 src/test/resources/unit/nvd/feed/nvdcve-1.1-2021_duplicate-cpes.json diff --git a/src/main/java/org/dependencytrack/parser/nvd/NvdParser.java b/src/main/java/org/dependencytrack/parser/nvd/NvdParser.java index 1c558dce2c..5df81dde37 100644 --- a/src/main/java/org/dependencytrack/parser/nvd/NvdParser.java +++ b/src/main/java/org/dependencytrack/parser/nvd/NvdParser.java @@ -50,6 +50,9 @@ import java.util.List; import java.util.Optional; import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +import static org.dependencytrack.parser.nvd.api20.ModelConverter.distinctIgnoringDatastoreIdentity; /** * Parser and processor of NVD data feeds. @@ -223,7 +226,10 @@ private void parseCveItem(final ObjectNode cveItem) { vsList.addAll(reconcile(vulnerableSoftwareInNode, nodeOperator)); } - vulnerabilityConsumer.accept(vulnerability, vsList); + final List uniqueVsList = vsList.stream() + .filter(distinctIgnoringDatastoreIdentity()) + .collect(Collectors.toList()); + vulnerabilityConsumer.accept(vulnerability, uniqueVsList); } /** diff --git a/src/main/java/org/dependencytrack/parser/nvd/api20/ModelConverter.java b/src/main/java/org/dependencytrack/parser/nvd/api20/ModelConverter.java index d1babfcb7d..169a8750c7 100644 --- a/src/main/java/org/dependencytrack/parser/nvd/api20/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/nvd/api20/ModelConverter.java @@ -183,7 +183,7 @@ public static List convertConfigurations(final String cveId, .collect(Collectors.toList()); } - private static Predicate distinctIgnoringDatastoreIdentity() { + public static Predicate distinctIgnoringDatastoreIdentity() { final var seen = new HashSet(); return vs -> seen.add(vs.hashCodeWithoutDatastoreIdentity()); } diff --git a/src/test/java/org/dependencytrack/tasks/NistMirrorTaskTest.java b/src/test/java/org/dependencytrack/tasks/NistMirrorTaskTest.java index d9d1284dd8..38af88c635 100644 --- a/src/test/java/org/dependencytrack/tasks/NistMirrorTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/NistMirrorTaskTest.java @@ -68,21 +68,14 @@ public void setUp() { @Test public void test() throws Exception { - // Gzip the JSON feed file to match the format returned by the NVD. - // NB: The file is a truncated version of an actual feed file. - // It only contains the first three CVEs. Truncation was done with jq: - // jq 'del(.CVE_Items[3:])' ~/.dependency-track/nist/nvdcve-1.1-2022.json > nvdcve-1.1-2022.json - final var byteArrayOutputStream = new ByteArrayOutputStream(); - try (final var gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) { - gzipOutputStream.write(resourceToByteArray("/unit/nvd/feed/nvdcve-1.1-2022.json")); - } + final byte[] gzippedFeedFileBytes = gzipResource("/unit/nvd/feed/nvdcve-1.1-2022.json"); wireMock.stubFor(get(anyUrl()) .willReturn(aResponse() .withStatus(404))); wireMock.stubFor(get(urlPathEqualTo("/json/cve/1.1/nvdcve-1.1-2022.json.gz")) .willReturn(aResponse() - .withBody(byteArrayOutputStream.toByteArray()))); + .withBody(gzippedFeedFileBytes))); wireMock.stubFor(get(urlPathEqualTo("/json/cve/1.1/nvdcve-1.1-2022.meta")) .willReturn(aResponse() .withBody(resourceToByteArray("/unit/nvd/feed/nvdcve-1.1-2022.meta")))); @@ -172,4 +165,54 @@ locks for some Intel(R) Processors in Intel(R) Boot Guard and Intel(R) \ ); } + @Test + public void testWithDuplicateCpes() throws Exception { + final byte[] gzippedFeedFileBytes = gzipResource("/unit/nvd/feed/nvdcve-1.1-2021_duplicate-cpes.json"); + + wireMock.stubFor(get(anyUrl()) + .willReturn(aResponse() + .withStatus(404))); + wireMock.stubFor(get(urlPathEqualTo("/json/cve/1.1/nvdcve-1.1-2021.json.gz")) + .willReturn(aResponse() + .withBody(gzippedFeedFileBytes))); + + final Path mirrorDirPath = Files.createTempDirectory(null); + mirrorDirPath.toFile().deleteOnExit(); + + new NistMirrorTask(mirrorDirPath).inform(new NistMirrorEvent()); + + final List vulns = qm.getVulnerabilities().getList(Vulnerability.class); + assertThat(vulns).hasSize(1); + + final Vulnerability vuln = vulns.get(0); + assertThat(vuln.getVulnerableSoftware()).satisfiesExactlyInAnyOrder( + vs -> { + assertThat(vs.getCpe22()).isEqualTo("cpe:/o:intel:ethernet_controller_e810_firmware:::~~~linux~~"); + assertThat(vs.getCpe23()).isEqualTo("cpe:2.3:o:intel:ethernet_controller_e810_firmware:*:*:*:*:*:linux:*:*"); + }, + vs -> { + assertThat(vs.getCpe22()).isEqualTo("cpe:/o:fedoraproject:fedora:33"); + assertThat(vs.getCpe23()).isEqualTo("cpe:2.3:o:fedoraproject:fedora:33:*:*:*:*:*:*:*"); + }, + vs -> { + assertThat(vs.getCpe22()).isEqualTo("cpe:/o:fedoraproject:fedora:34"); + assertThat(vs.getCpe23()).isEqualTo("cpe:2.3:o:fedoraproject:fedora:34:*:*:*:*:*:*:*"); + }, + vs -> { + // This CPE appears twice in the feed file. We must only record it once. + assertThat(vs.getCpe22()).isEqualTo("cpe:/o:fedoraproject:fedora:35"); + assertThat(vs.getCpe23()).isEqualTo("cpe:2.3:o:fedoraproject:fedora:35:*:*:*:*:*:*:*"); + } + ); + } + + private byte[] gzipResource(final String resourcePath) throws Exception { + final var byteArrayOutputStream = new ByteArrayOutputStream(); + try (final var gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) { + gzipOutputStream.write(resourceToByteArray(resourcePath)); + } + + return byteArrayOutputStream.toByteArray(); + } + } \ No newline at end of file diff --git a/src/test/resources/unit/nvd/feed/nvdcve-1.1-2021_duplicate-cpes.json b/src/test/resources/unit/nvd/feed/nvdcve-1.1-2021_duplicate-cpes.json new file mode 100644 index 0000000000..8a90154c28 --- /dev/null +++ b/src/test/resources/unit/nvd/feed/nvdcve-1.1-2021_duplicate-cpes.json @@ -0,0 +1,187 @@ +{ + "CVE_data_type": "CVE", + "CVE_data_format": "MITRE", + "CVE_data_version": "4.0", + "CVE_data_numberOfCVEs": "22423", + "CVE_data_timestamp": "2024-04-30T07:00Z", + "CVE_Items": [ + { + "cve": { + "data_type": "CVE", + "data_format": "MITRE", + "data_version": "4.0", + "CVE_data_meta": { + "ID": "CVE-2021-0002", + "ASSIGNER": "secure@intel.com" + }, + "problemtype": { + "problemtype_data": [ + { + "description": [ + { + "lang": "en", + "value": "CWE-754" + } + ] + } + ] + }, + "references": { + "reference_data": [ + { + "url": "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00515.html", + "name": "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00515.html", + "refsource": "MISC", + "tags": [ + "Vendor Advisory" + ] + }, + { + "url": "https://security.netapp.com/advisory/ntap-20210827-0008/", + "name": "https://security.netapp.com/advisory/ntap-20210827-0008/", + "refsource": "CONFIRM", + "tags": [ + "Third Party Advisory" + ] + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/EUZYFCI7N4TFZSIGA7WGZ4Q7V3EK76GH/", + "name": "FEDORA-2021-9807b754d9", + "refsource": "", + "tags": [] + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/LKMUMLUH6ENNMLGTJ5AFRF6764ILEMYJ/", + "name": "FEDORA-2021-cbad295a90", + "refsource": "", + "tags": [] + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/MFLYHRQPDF6ZMESCI3HRNOP6D6GELPFR/", + "name": "FEDORA-2021-9818cabe0d", + "refsource": "", + "tags": [] + } + ] + }, + "description": { + "description_data": [ + { + "lang": "en", + "value": "Improper conditions check in some Intel(R) Ethernet Controllers 800 series Linux drivers before version 1.4.11 may allow an authenticated user to potentially enable information disclosure or denial of service via local access." + } + ] + } + }, + "configurations": { + "CVE_data_version": "4.0", + "nodes": [ + { + "operator": "AND", + "children": [ + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:intel:ethernet_controller_e810_firmware:*:*:*:*:*:linux:*:*", + "versionEndExcluding": "1.4.11", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": false, + "cpe23Uri": "cpe:2.3:h:intel:ethernet_controller_e810:-:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ], + "cpe_match": [] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:fedoraproject:fedora:33:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:fedoraproject:fedora:34:*:*:*:*:*:*:*", + "cpe_name": [] + }, + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:fedoraproject:fedora:35:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + }, + { + "operator": "OR", + "children": [], + "cpe_match": [ + { + "vulnerable": true, + "cpe23Uri": "cpe:2.3:o:fedoraproject:fedora:35:*:*:*:*:*:*:*", + "cpe_name": [] + } + ] + } + ] + }, + "impact": { + "baseMetricV3": { + "cvssV3": { + "version": "3.1", + "vectorString": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:H", + "attackVector": "LOCAL", + "attackComplexity": "LOW", + "privilegesRequired": "LOW", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "NONE", + "availabilityImpact": "HIGH", + "baseScore": 7.1, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 1.8, + "impactScore": 5.2 + }, + "baseMetricV2": { + "cvssV2": { + "version": "2.0", + "vectorString": "AV:L/AC:L/Au:N/C:P/I:N/A:P", + "accessVector": "LOCAL", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "NONE", + "availabilityImpact": "PARTIAL", + "baseScore": 3.6 + }, + "severity": "LOW", + "exploitabilityScore": 3.9, + "impactScore": 4.9, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + }, + "publishedDate": "2021-08-11T13:15Z", + "lastModifiedDate": "2023-11-07T03:27Z" + } + ] +} \ No newline at end of file From 93f05eb3480576654a7fce9770f58b74ec201db3 Mon Sep 17 00:00:00 2001 From: nscuro Date: Thu, 2 May 2024 23:26:59 +0200 Subject: [PATCH 117/412] Start Jersey `TestContainer` once per class vs. once per test method Reduces the time it takes to execute the entire test suite, since the Jersey context is not torn down and rebuilt from scratch as often. Signed-off-by: nscuro --- .../org/dependencytrack/JerseyTestRule.java | 87 ++++++++++++++ .../org/dependencytrack/ResourceTest.java | 21 +--- .../resources/v1/AnalysisResourceTest.java | 60 +++++----- .../resources/v1/BadgeResourceTest.java | 44 ++++--- .../resources/v1/BomResourceTest.java | 64 +++++----- .../resources/v1/CalculatorResourceTest.java | 28 ++--- .../v1/ComponentPropertyResourceTest.java | 34 +++--- .../resources/v1/ComponentResourceTest.java | 80 ++++++------- .../v1/ConfigPropertyResourceTest.java | 44 ++++--- .../resources/v1/CweResourceTest.java | 22 ++-- .../v1/DependencyGraphResourceTest.java | 46 ++++---- .../resources/v1/FindingResourceTest.java | 52 ++++----- .../resources/v1/IntegrationResourceTest.java | 22 ++-- .../resources/v1/LdapResourceTest.java | 32 +++-- .../resources/v1/LicenseResourceTest.java | 36 +++--- .../v1/NotificationPublisherResourceTest.java | 55 ++++----- .../v1/NotificationRuleResourceTest.java | 72 ++++++------ .../v1/OidcResourceAuthenticatedTest.java | 66 +++++------ .../v1/OidcResourceUnauthenticatedTest.java | 18 ++- .../resources/v1/PermissionResourceTest.java | 56 +++++---- .../resources/v1/PolicyResourceTest.java | 50 ++++---- .../v1/PolicyViolationResourceTest.java | 38 +++--- .../v1/ProjectPropertyResourceTest.java | 38 +++--- .../resources/v1/ProjectResourceTest.java | 110 +++++++++--------- .../resources/v1/RepositoryResourceTest.java | 46 ++++---- .../resources/v1/SearchResourceTest.java | 45 ++++--- .../resources/v1/TagResourceTest.java | 22 ++-- .../resources/v1/TeamResourceTest.java | 60 +++++----- .../v1/UserResourceAuthenticatedTest.java | 88 +++++++------- .../v1/UserResourceUnauthenticatedTest.java | 38 +++--- .../resources/v1/VexResourceTest.java | 32 +++-- .../v1/ViolationAnalysisResourceTest.java | 50 ++++---- .../v1/VulnerabilityResourceTest.java | 84 +++++++------ .../ClientErrorExceptionMapperTest.java | 20 ++-- ...onstraintViolationExceptionMapperTest.java | 18 ++- 35 files changed, 812 insertions(+), 866 deletions(-) create mode 100644 src/test/java/org/dependencytrack/JerseyTestRule.java diff --git a/src/test/java/org/dependencytrack/JerseyTestRule.java b/src/test/java/org/dependencytrack/JerseyTestRule.java new file mode 100644 index 0000000000..a30cfe0625 --- /dev/null +++ b/src/test/java/org/dependencytrack/JerseyTestRule.java @@ -0,0 +1,87 @@ +/* + * 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; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.grizzly.connector.GrizzlyConnectorProvider; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.ServletDeploymentContext; +import org.glassfish.jersey.test.spi.TestContainerException; +import org.glassfish.jersey.test.spi.TestContainerFactory; +import org.junit.rules.ExternalResource; + +import javax.ws.rs.client.WebTarget; + +/** + * @since 4.11.0 + */ +public class JerseyTestRule extends ExternalResource { + + private final JerseyTest jerseyTest; + + public JerseyTestRule(final ResourceConfig resourceConfig) { + this.jerseyTest = new JerseyTest() { + + @Override + protected TestContainerFactory getTestContainerFactory() throws TestContainerException { + return new DTGrizzlyWebTestContainerFactory(); + } + + @Override + protected void configureClient(final ClientConfig config) { + // Prevent InaccessibleObjectException with JDK >= 16 when performing PATCH requests + // using the default HttpUrlConnection connector provider. + // See https://github.com/eclipse-ee4j/jersey/issues/4825 + config.connectorProvider(new GrizzlyConnectorProvider()); + } + + @Override + protected DeploymentContext configureDeployment() { + return ServletDeploymentContext.forServlet(new ServletContainer(resourceConfig)).build(); + } + + }; + } + + @Override + protected void before() throws Throwable { + jerseyTest.setUp(); + } + + @Override + protected void after() { + try { + jerseyTest.tearDown(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public WebTarget target() { + return jerseyTest.target(); + } + + public final WebTarget target(final String path) { + return jerseyTest.target(path); + } + +} diff --git a/src/test/java/org/dependencytrack/ResourceTest.java b/src/test/java/org/dependencytrack/ResourceTest.java index 1568808bc2..05e7995128 100644 --- a/src/test/java/org/dependencytrack/ResourceTest.java +++ b/src/test/java/org/dependencytrack/ResourceTest.java @@ -27,10 +27,6 @@ import alpine.server.persistence.PersistenceManagerFactory; import org.dependencytrack.auth.Permissions; import org.dependencytrack.persistence.QueryManager; -import org.glassfish.jersey.client.ClientConfig; -import org.glassfish.jersey.grizzly.connector.GrizzlyConnectorProvider; -import org.glassfish.jersey.test.JerseyTest; -import org.glassfish.jersey.test.spi.TestContainerFactory; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; @@ -44,7 +40,7 @@ import java.util.ArrayList; import java.util.List; -public abstract class ResourceTest extends JerseyTest { +public abstract class ResourceTest { protected final String V1_ANALYSIS = "/v1/analysis"; protected final String V1_BADGE = "/v1/badge"; @@ -110,7 +106,7 @@ public void before() throws Exception { } @After - public void after() { + public void after() throws Exception { // PersistenceManager will refuse to close when there's an active transaction // that was neither committed nor rolled back. Unfortunately some areas of the // code base can leave such a broken state behind if they run into unexpected @@ -123,19 +119,6 @@ public void after() { PersistenceManagerFactory.tearDown(); } - @Override - protected TestContainerFactory getTestContainerFactory() { - return new DTGrizzlyWebTestContainerFactory(); - } - - @Override - protected void configureClient(final ClientConfig config) { - // Prevent InaccessibleObjectException with JDK >= 16 when performing PATCH requests - // using the default HttpUrlConnection connector provider. - // See https://github.com/eclipse-ee4j/jersey/issues/4825 - config.connectorProvider(new GrizzlyConnectorProvider()); - } - public void initializeWithPermissions(Permissions... permissions) { List permissionList = new ArrayList<>(); for (Permissions permission: permissions) { diff --git a/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java index f68ba8287a..08ca069344 100644 --- a/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java @@ -28,6 +28,7 @@ import alpine.server.filters.AuthorizationFilter; import net.jcip.annotations.NotThreadSafe; import org.apache.http.HttpStatus; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Analysis; @@ -44,11 +45,10 @@ import org.dependencytrack.resources.v1.vo.AnalysisRequest; import org.dependencytrack.util.NotificationUtil; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; +import org.junit.After; import org.junit.AfterClass; import org.junit.BeforeClass; +import org.junit.ClassRule; import org.junit.Test; import javax.json.Json; @@ -68,15 +68,12 @@ @NotThreadSafe public class AnalysisResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(AnalysisResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class) - .register(AuthorizationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(AnalysisResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class) + .register(AuthorizationFilter.class)); public static class NotificationSubscriber implements Subscriber { @@ -99,10 +96,11 @@ public static void tearDownClass() { NotificationService.getInstance().unsubscribe(new Subscription(NotificationSubscriber.class)); } + @After @Override - public void tearDown() throws Exception { + public void after() throws Exception { NOTIFICATIONS.clear(); - super.tearDown(); + super.after(); } @Test @@ -128,7 +126,7 @@ public void retrieveAnalysisTest() { AnalysisJustification.CODE_NOT_REACHABLE, AnalysisResponse.WILL_NOT_FIX, "Analysis details here", true); qm.makeAnalysisComment(analysis, "Analysis comment here", "Jane Doe"); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .queryParam("project", project.getUuid()) .queryParam("component", component.getUuid()) .queryParam("vulnerability", vulnerability.getUuid()) @@ -170,7 +168,7 @@ public void retrieveAnalysisWithoutExistingAnalysisTest() { vulnerability.setComponents(List.of(component)); vulnerability = qm.createVulnerability(vulnerability, false); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .queryParam("project", project.getUuid()) .queryParam("component", component.getUuid()) .queryParam("vulnerability", vulnerability.getUuid()) @@ -200,7 +198,7 @@ public void noAnalysisExists() { vulnerability.setComponents(List.of(component)); vulnerability = qm.createVulnerability(vulnerability, false); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .queryParam("component", component.getUuid()) .queryParam("vulnerability", vulnerability.getUuid()) .request() @@ -229,7 +227,7 @@ public void retrieveAnalysisWithProjectNotFoundTest() { vulnerability.setComponents(List.of(component)); vulnerability = qm.createVulnerability(vulnerability, false); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .queryParam("project", UUID.randomUUID()) .queryParam("component", component.getUuid()) .queryParam("vulnerability", vulnerability.getUuid()) @@ -260,7 +258,7 @@ public void retrieveAnalysisWithComponentNotFoundTest() { vulnerability.setComponents(List.of(component)); vulnerability = qm.createVulnerability(vulnerability, false); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .queryParam("project", project.getUuid()) .queryParam("component", UUID.randomUUID()) .queryParam("vulnerability", vulnerability.getUuid()) @@ -291,7 +289,7 @@ public void retrieveAnalysisWithVulnerabilityNotFoundTest() { vulnerability.setComponents(List.of(component)); qm.createVulnerability(vulnerability, false); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .queryParam("project", project.getUuid()) .queryParam("component", component.getUuid()) .queryParam("vulnerability", UUID.randomUUID()) @@ -305,7 +303,7 @@ public void retrieveAnalysisWithVulnerabilityNotFoundTest() { @Test public void retrieveAnalysisUnauthorizedTest() { - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .queryParam("project", UUID.randomUUID()) .queryParam("component", UUID.randomUUID()) .queryParam("vulnerability", UUID.randomUUID()) @@ -339,7 +337,7 @@ public void updateAnalysisCreateNewTest() throws Exception { vulnerability.getUuid().toString(), AnalysisState.NOT_AFFECTED, AnalysisJustification.CODE_NOT_REACHABLE, AnalysisResponse.WILL_NOT_FIX, "Analysis details here", "Analysis comment here", true); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(analysisRequest, MediaType.APPLICATION_JSON)); @@ -395,7 +393,7 @@ public void updateAnalysisCreateNewWithEmptyRequestTest() throws Exception { final var analysisRequest = new AnalysisRequest(project.getUuid().toString(), component.getUuid().toString(), vulnerability.getUuid().toString(), null, null, null, null, null, null); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(analysisRequest, MediaType.APPLICATION_JSON)); @@ -450,7 +448,7 @@ public void updateAnalysisUpdateExistingTest() throws Exception { vulnerability.getUuid().toString(), AnalysisState.EXPLOITABLE, AnalysisJustification.NOT_SET, AnalysisResponse.UPDATE, "New analysis details here", "New analysis comment here", false); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(analysisRequest, MediaType.APPLICATION_JSON)); @@ -528,7 +526,7 @@ public void updateAnalysisWithNoChangesTest() throws Exception{ vulnerability.getUuid().toString(), AnalysisState.NOT_AFFECTED, AnalysisJustification.CODE_NOT_REACHABLE, AnalysisResponse.WILL_NOT_FIX, "Analysis details here", null, true); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(analysisRequest, MediaType.APPLICATION_JSON)); @@ -577,7 +575,7 @@ public void updateAnalysisUpdateExistingWithEmptyRequestTest() throws Exception final var analysisRequest = new AnalysisRequest(project.getUuid().toString(), component.getUuid().toString(), vulnerability.getUuid().toString(), null, null, null, null, null, null); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(analysisRequest, MediaType.APPLICATION_JSON)); @@ -641,7 +639,7 @@ public void updateAnalysisWithProjectNotFoundTest() { vulnerability.getUuid().toString(), AnalysisState.NOT_AFFECTED, AnalysisJustification.CODE_NOT_REACHABLE, AnalysisResponse.WILL_NOT_FIX, "Analysis details here", "Analysis comment here", true); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(analysisRequest, MediaType.APPLICATION_JSON)); @@ -673,7 +671,7 @@ public void updateAnalysisWithComponentNotFoundTest() { vulnerability.getUuid().toString(), AnalysisState.NOT_AFFECTED, AnalysisJustification.CODE_NOT_REACHABLE, AnalysisResponse.WILL_NOT_FIX, "Analysis details here", "Analysis comment here", true); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(analysisRequest, MediaType.APPLICATION_JSON)); @@ -705,7 +703,7 @@ public void updateAnalysisWithVulnerabilityNotFoundTest() { UUID.randomUUID().toString(), AnalysisState.NOT_AFFECTED, AnalysisJustification.CODE_NOT_REACHABLE, AnalysisResponse.WILL_NOT_FIX, "Analysis details here", "Analysis comment here", true); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(analysisRequest, MediaType.APPLICATION_JSON)); @@ -743,7 +741,7 @@ public void updateAnalysisIssue1409Test() throws InterruptedException { vulnerability.getUuid().toString(), AnalysisState.NOT_AFFECTED, AnalysisJustification.PROTECTED_BY_MITIGATING_CONTROL, AnalysisResponse.UPDATE, "New analysis details here", "New analysis comment here", false); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(analysisRequest, MediaType.APPLICATION_JSON)); @@ -794,7 +792,7 @@ public void updateAnalysisUnauthorizedTest() { UUID.randomUUID().toString(), AnalysisState.NOT_AFFECTED, AnalysisJustification.PROTECTED_BY_MITIGATING_CONTROL, AnalysisResponse.UPDATE, "Analysis details here", "Analysis comment here", false); - final Response response = target(V1_ANALYSIS) + final Response response = jersey.target(V1_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(analysisRequest, MediaType.APPLICATION_JSON)); diff --git a/src/test/java/org/dependencytrack/resources/v1/BadgeResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BadgeResourceTest.java index 76b13e959e..a2435e065a 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BadgeResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BadgeResourceTest.java @@ -20,13 +20,12 @@ import alpine.model.IConfigProperty; import alpine.server.filters.ApiFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.Project; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import javax.ws.rs.core.Response; @@ -41,13 +40,10 @@ public class BadgeResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(BadgeResource.class) - .register(ApiFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(BadgeResource.class) + .register(ApiFilter.class)); @Override public void before() throws Exception { @@ -58,7 +54,7 @@ public void before() throws Exception { @Test public void projectVulnerabilitiesByUuidTest() { Project project = qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); - Response response = target(V1_BADGE + "/vulns/project/" + project.getUuid()).request() + Response response = jersey.target(V1_BADGE + "/vulns/project/" + project.getUuid()).request() .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); @@ -68,14 +64,14 @@ public void projectVulnerabilitiesByUuidTest() { @Test public void projectVulnerabilitiesByUuidProjectDisabledTest() { disableBadge(); - Response response = target(V1_BADGE + "/vulns/project/" + UUID.randomUUID()).request() + Response response = jersey.target(V1_BADGE + "/vulns/project/" + UUID.randomUUID()).request() .get(Response.class); Assert.assertEquals(204, response.getStatus(), 0); } @Test public void projectVulnerabilitiesByUuidProjectNotFoundTest() { - Response response = target(V1_BADGE + "/vulns/project/" + UUID.randomUUID()).request() + Response response = jersey.target(V1_BADGE + "/vulns/project/" + UUID.randomUUID()).request() .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); } @@ -83,7 +79,7 @@ public void projectVulnerabilitiesByUuidProjectNotFoundTest() { @Test public void projectVulnerabilitiesByNameAndVersionTest() { qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); - Response response = target(V1_BADGE + "/vulns/project/Acme%20Example/1.0.0").request() + Response response = jersey.target(V1_BADGE + "/vulns/project/Acme%20Example/1.0.0").request() .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); @@ -93,14 +89,14 @@ public void projectVulnerabilitiesByNameAndVersionTest() { @Test public void projectVulnerabilitiesByNameAndVersionDisabledTest() { disableBadge(); - Response response = target(V1_BADGE + "/vulns/project/ProjectNameDoesNotExist/1.0.0").request() + Response response = jersey.target(V1_BADGE + "/vulns/project/ProjectNameDoesNotExist/1.0.0").request() .get(Response.class); Assert.assertEquals(204, response.getStatus(), 0); } @Test public void projectVulnerabilitiesByNameAndVersionProjectNotFoundTest() { - Response response = target(V1_BADGE + "/vulns/project/ProjectNameDoesNotExist/1.0.0").request() + Response response = jersey.target(V1_BADGE + "/vulns/project/ProjectNameDoesNotExist/1.0.0").request() .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); } @@ -108,7 +104,7 @@ public void projectVulnerabilitiesByNameAndVersionProjectNotFoundTest() { @Test public void projectVulnerabilitiesByNameAndVersionVersionNotFoundTest() { qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); - Response response = target(V1_BADGE + "/vulns/project/Acme%20Example/1.2.0").request() + Response response = jersey.target(V1_BADGE + "/vulns/project/Acme%20Example/1.2.0").request() .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); } @@ -116,7 +112,7 @@ public void projectVulnerabilitiesByNameAndVersionVersionNotFoundTest() { @Test public void projectPolicyViolationsByUuidTest() { Project project = qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); - Response response = target(V1_BADGE + "/violations/project/" + project.getUuid()).request() + Response response = jersey.target(V1_BADGE + "/violations/project/" + project.getUuid()).request() .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); @@ -126,14 +122,14 @@ public void projectPolicyViolationsByUuidTest() { @Test public void projectPolicyViolationsByUuidProjectDisabledTest() { disableBadge(); - Response response = target(V1_BADGE + "/violations/project/" + UUID.randomUUID()).request() + Response response = jersey.target(V1_BADGE + "/violations/project/" + UUID.randomUUID()).request() .get(Response.class); Assert.assertEquals(204, response.getStatus(), 0); } @Test public void projectPolicyViolationsByUuidProjectNotFoundTest() { - Response response = target(V1_BADGE + "/violations/project/" + UUID.randomUUID()).request() + Response response = jersey.target(V1_BADGE + "/violations/project/" + UUID.randomUUID()).request() .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); } @@ -141,7 +137,7 @@ public void projectPolicyViolationsByUuidProjectNotFoundTest() { @Test public void projectPolicyViolationsByNameAndVersionTest() { qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); - Response response = target(V1_BADGE + "/violations/project/Acme%20Example/1.0.0").request() + Response response = jersey.target(V1_BADGE + "/violations/project/Acme%20Example/1.0.0").request() .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); @@ -151,14 +147,14 @@ public void projectPolicyViolationsByNameAndVersionTest() { @Test public void projectPolicyViolationsByNameAndVersionDisabledTest() { disableBadge(); - Response response = target(V1_BADGE + "/violations/project/ProjectNameDoesNotExist/1.0.0").request() + Response response = jersey.target(V1_BADGE + "/violations/project/ProjectNameDoesNotExist/1.0.0").request() .get(Response.class); Assert.assertEquals(204, response.getStatus(), 0); } @Test public void projectPolicyViolationsByNameAndVersionProjectNotFoundTest() { - Response response = target(V1_BADGE + "/violations/project/ProjectNameDoesNotExist/1.0.0").request() + Response response = jersey.target(V1_BADGE + "/violations/project/ProjectNameDoesNotExist/1.0.0").request() .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); } @@ -166,7 +162,7 @@ public void projectPolicyViolationsByNameAndVersionProjectNotFoundTest() { @Test public void projectPolicyViolationsByNameAndVersionVersionNotFoundTest() { qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); - Response response = target(V1_BADGE + "/violations/project/Acme%20Example/1.2.0").request() + Response response = jersey.target(V1_BADGE + "/violations/project/Acme%20Example/1.2.0").request() .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); } diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index ab165db7f4..1258943ad4 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -24,6 +24,7 @@ import alpine.server.filters.AuthenticationFilter; import com.fasterxml.jackson.core.StreamReadConstraints; import org.apache.http.HttpStatus; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.AnalysisResponse; @@ -44,10 +45,8 @@ import org.dependencytrack.tasks.scanners.AnalyzerIdentity; import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonObject; @@ -63,21 +62,18 @@ import static org.apache.commons.io.IOUtils.resourceToByteArray; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.hamcrest.CoreMatchers.equalTo; import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_ENABLED; +import static org.hamcrest.CoreMatchers.equalTo; public class BomResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(BomResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class) - .register(MultiPartFeature.class) - .register(JsonMappingExceptionMapper.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(BomResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class) + .register(MultiPartFeature.class) + .register(JsonMappingExceptionMapper.class)); @Test public void exportProjectAsCycloneDxTest() { @@ -87,7 +83,7 @@ public void exportProjectAsCycloneDxTest() { c.setName("sample-component"); c.setVersion("1.0"); Component component = qm.createComponent(c, false); - Response response = target(V1_BOM + "/cyclonedx/project/" + project.getUuid()).request() + Response response = jersey.target(V1_BOM + "/cyclonedx/project/" + project.getUuid()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -98,7 +94,7 @@ public void exportProjectAsCycloneDxTest() { @Test public void exportProjectAsCycloneDxInvalidTest() { - Response response = target(V1_BOM + "/cyclonedx/project/" + UUID.randomUUID()).request() + Response response = jersey.target(V1_BOM + "/cyclonedx/project/" + UUID.randomUUID()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); @@ -202,7 +198,7 @@ public void exportProjectAsCycloneDxInventoryTest() { )); qm.persist(project); - final Response response = target(V1_BOM + "/cyclonedx/project/" + project.getUuid()) + final Response response = jersey.target(V1_BOM + "/cyclonedx/project/" + project.getUuid()) .queryParam("variant", "inventory") .request() .header(X_API_KEY, apiKey) @@ -374,7 +370,7 @@ public void exportProjectAsCycloneDxInventoryWithVulnerabilitiesTest() { )); qm.persist(project); - final Response response = target(V1_BOM + "/cyclonedx/project/" + project.getUuid()) + final Response response = jersey.target(V1_BOM + "/cyclonedx/project/" + project.getUuid()) .queryParam("variant", "withVulnerabilities") .request() .header(X_API_KEY, apiKey) @@ -568,7 +564,7 @@ public void exportProjectAsCycloneDxVdrTest() { )); qm.persist(project); - final Response response = target(V1_BOM + "/cyclonedx/project/" + project.getUuid()) + final Response response = jersey.target(V1_BOM + "/cyclonedx/project/" + project.getUuid()) .queryParam("variant", "vdr") .request() .header(X_API_KEY, apiKey) @@ -705,7 +701,7 @@ public void exportComponentAsCycloneDx() { c.setName("sample-component"); c.setVersion("1.0"); Component component = qm.createComponent(c, false); - Response response = target(V1_BOM + "/cyclonedx/component/" + component.getUuid()).request() + Response response = jersey.target(V1_BOM + "/cyclonedx/component/" + component.getUuid()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -716,7 +712,7 @@ public void exportComponentAsCycloneDx() { @Test public void exportComponentAsCycloneDxInvalid() { - Response response = target(V1_BOM + "/cyclonedx/component/" + UUID.randomUUID()).request() + Response response = jersey.target(V1_BOM + "/cyclonedx/component/" + UUID.randomUUID()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); @@ -731,7 +727,7 @@ public void uploadBomTest() throws Exception { Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); String bomString = Base64.getEncoder().encodeToString(resourceToByteArray("/unit/bom-1.xml")); BomSubmitRequest request = new BomSubmitRequest(project.getUuid().toString(), null, null, false, bomString); - Response response = target(V1_BOM).request() + Response response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -746,7 +742,7 @@ public void uploadBomInvalidProjectTest() throws Exception { initializeWithPermissions(Permissions.BOM_UPLOAD); String bomString = Base64.getEncoder().encodeToString(resourceToByteArray("/unit/bom-1.xml")); BomSubmitRequest request = new BomSubmitRequest(UUID.randomUUID().toString(), null, null, false, bomString); - Response response = target(V1_BOM).request() + Response response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -760,7 +756,7 @@ public void uploadBomAutoCreateTest() throws Exception { initializeWithPermissions(Permissions.BOM_UPLOAD, Permissions.PROJECT_CREATION_UPLOAD); String bomString = Base64.getEncoder().encodeToString(resourceToByteArray("/unit/bom-1.xml")); BomSubmitRequest request = new BomSubmitRequest(null, "Acme Example", "1.0", true, bomString); - Response response = target(V1_BOM).request() + Response response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -776,7 +772,7 @@ public void uploadBomAutoCreateTest() throws Exception { public void uploadBomUnauthorizedTest() throws Exception { String bomString = Base64.getEncoder().encodeToString(resourceToByteArray("/unit/bom-1.xml")); BomSubmitRequest request = new BomSubmitRequest(null, "Acme Example", "1.0", true, bomString); - Response response = target(V1_BOM).request() + Response response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(401, response.getStatus(), 0); @@ -790,7 +786,7 @@ public void uploadBomAutoCreateTestWithParentTest() throws Exception { String bomString = Base64.getEncoder().encodeToString(resourceToByteArray("/unit/bom-1.xml")); // Upload parent project BomSubmitRequest request = new BomSubmitRequest(null, "Acme Parent", "1.0", true, bomString); - Response response = target(V1_BOM).request() + Response response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -802,7 +798,7 @@ public void uploadBomAutoCreateTestWithParentTest() throws Exception { // Upload first child, search parent by UUID request = new BomSubmitRequest(null, "Acme Example", "1.0", true, parentUUID, null, null, bomString); - response = target(V1_BOM).request() + response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -818,7 +814,7 @@ public void uploadBomAutoCreateTestWithParentTest() throws Exception { // Upload second child, search parent by name+ver request = new BomSubmitRequest(null, "Acme Example", "2.0", true, null, "Acme Parent", "1.0", bomString); - response = target(V1_BOM).request() + response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -833,7 +829,7 @@ public void uploadBomAutoCreateTestWithParentTest() throws Exception { // Upload third child, specify parent's UUID, name, ver. Name and ver are ignored when UUID is specified. request = new BomSubmitRequest(null, "Acme Example", "3.0", true, parentUUID, "Non-existent parent", "1.0", bomString); - response = target(V1_BOM).request() + response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -852,7 +848,7 @@ public void uploadBomInvalidParentTest() throws Exception { initializeWithPermissions(Permissions.BOM_UPLOAD, Permissions.PROJECT_CREATION_UPLOAD); String bomString = Base64.getEncoder().encodeToString(resourceToByteArray("/unit/bom-1.xml")); BomSubmitRequest request = new BomSubmitRequest(null, "Acme Example", "1.0", true, UUID.randomUUID().toString(), null, null, bomString); - Response response = target(V1_BOM).request() + Response response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -860,7 +856,7 @@ public void uploadBomInvalidParentTest() throws Exception { Assert.assertEquals("The parent component could not be found.", body); request = new BomSubmitRequest(null, "Acme Example", "2.0", true, null, "Non-existent parent", null, bomString); - response = target(V1_BOM).request() + response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -901,7 +897,7 @@ public void uploadBomInvalidJsonTest() { } """.getBytes()); - final Response response = target(V1_BOM).request() + final Response response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(""" { @@ -953,7 +949,7 @@ public void uploadBomInvalidXmlTest() { """.getBytes()); - final Response response = target(V1_BOM).request() + final Response response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(""" { @@ -988,7 +984,7 @@ public void uploadBomTooLargeViaPutTest() { final String bom = "a".repeat(StreamReadConstraints.DEFAULT_MAX_STRING_LEN + 1); - final Response response = target(V1_BOM).request() + final Response response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(""" { diff --git a/src/test/java/org/dependencytrack/resources/v1/CalculatorResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/CalculatorResourceTest.java index d1032a2a50..07ffdd7a23 100644 --- a/src/test/java/org/dependencytrack/resources/v1/CalculatorResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/CalculatorResourceTest.java @@ -20,12 +20,11 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import us.springett.owasp.riskrating.Level; @@ -34,18 +33,15 @@ public class CalculatorResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(CalculatorResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(CalculatorResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Test public void getCvssScoresV3Test() { - Response response = target(V1_CALCULATOR + "/cvss") + Response response = jersey.target(V1_CALCULATOR + "/cvss") .queryParam("vector", "AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H") .request() .header(X_API_KEY, apiKey) @@ -61,7 +57,7 @@ public void getCvssScoresV3Test() { @Test public void getCvssScoresV2Test() { - Response response = target(V1_CALCULATOR + "/cvss") + Response response = jersey.target(V1_CALCULATOR + "/cvss") .queryParam("vector", "(AV:N/AC:L/Au:N/C:P/I:P/A:P)") .request() .header(X_API_KEY, apiKey) @@ -77,7 +73,7 @@ public void getCvssScoresV2Test() { @Test public void getCvssScoresInvalidTest() { - Response response = target(V1_CALCULATOR + "/cvss") + Response response = jersey.target(V1_CALCULATOR + "/cvss") .queryParam("vector", "foobar") .request() .header(X_API_KEY, apiKey) @@ -90,7 +86,7 @@ public void getCvssScoresInvalidTest() { @Test public void getOwaspRRScoresTest() { - Response response = target(V1_CALCULATOR + "/owasp") + Response response = jersey.target(V1_CALCULATOR + "/owasp") .queryParam("vector", "SL:1/M:1/O:0/S:2/ED:1/EE:1/A:1/ID:1/LC:2/LI:1/LAV:1/LAC:1/FD:1/RD:1/NC:2/PV:3") .request() .header(X_API_KEY, apiKey) @@ -109,7 +105,7 @@ public void getOwaspRRScoresTest() { @Test public void getOwaspScoresInvalidTest() { - Response response = target(V1_CALCULATOR + "/owasp") + Response response = jersey.target(V1_CALCULATOR + "/owasp") .queryParam("vector", "foobar") .request() .header(X_API_KEY, apiKey) diff --git a/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java index 9756832a42..e5f3910d21 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java @@ -21,14 +21,13 @@ import alpine.model.IConfigProperty.PropertyType; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.Component; import org.dependencytrack.model.ComponentProperty; import org.dependencytrack.model.Project; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; +import org.junit.ClassRule; import org.junit.Test; import javax.ws.rs.client.Entity; @@ -42,14 +41,11 @@ public class ComponentPropertyResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(ComponentPropertyResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(ComponentPropertyResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Test public void getPropertiesTest() { @@ -80,7 +76,7 @@ public void getPropertiesTest() { propertyB.setDescription("qux-b"); qm.persist(propertyB); - final Response response = target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() + final Response response = jersey.target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() .header(X_API_KEY, apiKey) .get(); @@ -113,7 +109,7 @@ public void getPropertiesTest() { @Test public void getPropertiesInvalidTest() { - final Response response = target("%s/%s/property".formatted(V1_COMPONENT, UUID.randomUUID())).request() + final Response response = jersey.target("%s/%s/property".formatted(V1_COMPONENT, UUID.randomUUID())).request() .header(X_API_KEY, apiKey) .get(Response.class); @@ -133,7 +129,7 @@ public void createPropertyTest() { component.setName("acme-lib"); qm.persist(component); - final Response response = target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() + final Response response = jersey.target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() .header(X_API_KEY, apiKey) .put(Entity.entity(""" { @@ -170,7 +166,7 @@ public void createPropertyWithoutGroupTest() { component.setName("acme-lib"); qm.persist(component); - final Response response = target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() + final Response response = jersey.target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() .header(X_API_KEY, apiKey) .put(Entity.entity(""" { @@ -213,7 +209,7 @@ public void createPropertyDuplicateTest() { property.setPropertyType(PropertyType.STRING); qm.persist(property); - final Response response = target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() + final Response response = jersey.target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() .header(X_API_KEY, apiKey) .put(Entity.entity(""" { @@ -242,7 +238,7 @@ public void createPropertyDisallowedPropertyTypeTest() { component.setName("acme-lib"); qm.persist(component); - final Response response = target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() + final Response response = jersey.target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() .header(X_API_KEY, apiKey) .put(Entity.entity(""" { @@ -279,7 +275,7 @@ public void createPropertyComponentNotFoundTest() { component.setName("acme-lib"); qm.persist(component); - final Response response = target("%s/%s/property".formatted(V1_COMPONENT, UUID.randomUUID())).request() + final Response response = jersey.target("%s/%s/property".formatted(V1_COMPONENT, UUID.randomUUID())).request() .header(X_API_KEY, apiKey) .put(Entity.entity(""" { @@ -315,7 +311,7 @@ public void deletePropertyTest() { property.setPropertyType(PropertyType.STRING); qm.persist(property); - final Response response = target("%s/%s/property/%s".formatted(V1_COMPONENT, component.getUuid(), property.getUuid())).request() + final Response response = jersey.target("%s/%s/property/%s".formatted(V1_COMPONENT, component.getUuid(), property.getUuid())).request() .header(X_API_KEY, apiKey) .delete(); diff --git a/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java index b3aa3a6d06..bfa2d17f3d 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java @@ -24,6 +24,7 @@ import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; import org.apache.http.HttpStatus; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; @@ -31,10 +32,8 @@ import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -52,18 +51,15 @@ public class ComponentResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(ComponentResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(ComponentResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Test public void getComponentsDefaultRequestTest() { - Response response = target(V1_COMPONENT).request() + Response response = jersey.target(V1_COMPONENT).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(405, response.getStatus()); // No longer prohibited in DT 4.0+ @@ -125,7 +121,7 @@ private Project prepareProject() throws MalformedPackageURLException { public void getOutdatedComponentsTest() throws MalformedPackageURLException { final Project project = prepareProject(); - final Response response = target(V1_COMPONENT + "/project/" + project.getUuid()) + final Response response = jersey.target(V1_COMPONENT + "/project/" + project.getUuid()) .queryParam("onlyOutdated", true) .queryParam("onlyDirect", false) .request() @@ -142,7 +138,7 @@ public void getOutdatedComponentsTest() throws MalformedPackageURLException { public void getOutdatedDirectComponentsTest() throws MalformedPackageURLException { final Project project = prepareProject(); - final Response response = target(V1_COMPONENT + "/project/" + project.getUuid()) + final Response response = jersey.target(V1_COMPONENT + "/project/" + project.getUuid()) .queryParam("onlyOutdated", true) .queryParam("onlyDirect", true) .request() @@ -159,7 +155,7 @@ public void getOutdatedDirectComponentsTest() throws MalformedPackageURLExceptio public void getAllComponentsTest() throws MalformedPackageURLException { final Project project = prepareProject(); - final Response response = target(V1_COMPONENT + "/project/" + project.getUuid()) + final Response response = jersey.target(V1_COMPONENT + "/project/" + project.getUuid()) .request() .header(X_API_KEY, apiKey) .get(Response.class); @@ -174,7 +170,7 @@ public void getAllComponentsTest() throws MalformedPackageURLException { public void getAllDirectComponentsTest() throws MalformedPackageURLException { final Project project = prepareProject(); - final Response response = target(V1_COMPONENT + "/project/" + project.getUuid()) + final Response response = jersey.target(V1_COMPONENT + "/project/" + project.getUuid()) .queryParam("onlyDirect", true) .request() .header(X_API_KEY, apiKey) @@ -193,7 +189,7 @@ public void getComponentByUuidTest() { component.setProject(project); component.setName("ABC"); component = qm.createComponent(component, false); - Response response = target(V1_COMPONENT + "/" + component.getUuid()) + Response response = jersey.target(V1_COMPONENT + "/" + component.getUuid()) .request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); @@ -204,7 +200,7 @@ public void getComponentByUuidTest() { @Test public void getComponentByInvalidUuidTest() { - Response response = target(V1_COMPONENT + "/" + UUID.randomUUID()) + Response response = jersey.target(V1_COMPONENT + "/" + UUID.randomUUID()) .request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); @@ -228,7 +224,7 @@ public void getComponentByUuidWithRepositoryMetaDataTest() { meta.setRepositoryType(RepositoryType.MAVEN); qm.persist(meta); component = qm.createComponent(component, false); - Response response = target(V1_COMPONENT + "/" + component.getUuid()) + Response response = jersey.target(V1_COMPONENT + "/" + component.getUuid()) .queryParam("includeRepositoryMetaData", true) .request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -265,7 +261,7 @@ public void getComponentByIdentityWithCoordinatesTest() { componentA.setPurl("pkg:maven/groupB/nameB@versionB?baz=qux"); componentB = qm.createComponent(componentB, false); - final Response response = target(V1_COMPONENT + "/identity") + final Response response = jersey.target(V1_COMPONENT + "/identity") .queryParam("group", "groupB") .queryParam("name", "nameB") .queryParam("version", "versionB") @@ -305,7 +301,7 @@ public void getComponentByIdentityWithPurlTest() { componentB.setPurl("pkg:maven/groupB/nameB@versionB?baz=qux"); componentB = qm.createComponent(componentB, false); - final Response response = target(V1_COMPONENT + "/identity") + final Response response = jersey.target(V1_COMPONENT + "/identity") .queryParam("purl", "pkg:maven/groupB/nameB@versionB") .request() .header(X_API_KEY, apiKey) @@ -343,7 +339,7 @@ public void getComponentByIdentityWithCpeTest() { componentB.setPurl("pkg:maven/groupB/nameB@versionB?baz=qux"); componentB = qm.createComponent(componentB, false); - final Response response = target(V1_COMPONENT + "/identity") + final Response response = jersey.target(V1_COMPONENT + "/identity") .queryParam("cpe", "cpe:2.3:a:groupB:nameB:versionB") .request() .header(X_API_KEY, apiKey) @@ -379,7 +375,7 @@ public void getComponentByIdentityWithProjectTest() { componentB.setPurl("pkg:maven/group/name@version?foo=bar"); componentB = qm.createComponent(componentB, false); - final Response response = target(V1_COMPONENT + "/identity") + final Response response = jersey.target(V1_COMPONENT + "/identity") .queryParam("purl", "pkg:maven/group/name@version") .queryParam("project", projectB.getUuid().toString()) .request() @@ -398,7 +394,7 @@ public void getComponentByIdentityWithProjectTest() { @Test public void getComponentByIdentityWithProjectWhenProjectDoesNotExistTest() { - final Response response = target(V1_COMPONENT + "/identity") + final Response response = jersey.target(V1_COMPONENT + "/identity") .queryParam("purl", "pkg:maven/group/name@version") .queryParam("project", UUID.randomUUID()) .request() @@ -417,7 +413,7 @@ public void getComponentByHashTest() { component.setName("ABC"); component.setSha1("da39a3ee5e6b4b0d3255bfef95601890afd80709"); component = qm.createComponent(component, false); - Response response = target(V1_COMPONENT + "/hash/" + component.getSha1()) + Response response = jersey.target(V1_COMPONENT + "/hash/" + component.getSha1()) .request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertEquals(response.getHeaderString(TOTAL_COUNT_HEADER), "1"); @@ -428,7 +424,7 @@ public void getComponentByHashTest() { @Test public void getComponentByInvalidHashTest() { - Response response = target(V1_COMPONENT + "/hash/c5a8829aa3da800216b933e265dd0b97eb6f9341") + Response response = jersey.target(V1_COMPONENT + "/hash/c5a8829aa3da800216b933e265dd0b97eb6f9341") .request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertEquals(response.getHeaderString(TOTAL_COUNT_HEADER), "0"); @@ -459,7 +455,7 @@ public void getComponentByHashWithAclTest() { component.setSha1("da39a3ee5e6b4b0d3255bfef95601890afd80709"); qm.persist(component); - final Response response = target("%s/hash/%s".formatted(V1_COMPONENT, component.getSha1())) + final Response response = jersey.target("%s/hash/%s".formatted(V1_COMPONENT, component.getSha1())) .request() .header(X_API_KEY, apiKey) .get(Response.class); @@ -477,7 +473,7 @@ public void createComponentTest() { component.setProject(project); component.setName("My Component"); component.setVersion("1.0"); - Response response = target(V1_COMPONENT + "/project/" + project.getUuid().toString()).request() + Response response = jersey.target(V1_COMPONENT + "/project/" + project.getUuid().toString()).request() .header(X_API_KEY, apiKey) .put(Entity.entity(component, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus(), 0); @@ -503,7 +499,7 @@ public void createComponentUpperCaseHashTest() { component.setSha512("c6ee9e33cf5c6715a1d148fd73f7318884b41adcb916021e2bc0e800a5c5dd97f5142178f6ae88c8fdd98e1afb0ce4c8d2c54b5f37b30b7da1997bb33b0b8a31".toUpperCase()); component.setSha3_512("301bb421c971fbb7ed01dcc3a9976ce53df034022ba982b97d0f27d48c4f03883aabf7c6bc778aa7c383062f6823045a6d41b8a720afbb8a9607690f89fbe1a7".toUpperCase()); component.setMd5("0cbc6611f5540bd0809a388dc95a615b".toUpperCase()); - Response response = target(V1_COMPONENT + "/project/" + project.getUuid().toString()).request() + Response response = jersey.target(V1_COMPONENT + "/project/" + project.getUuid().toString()).request() .header(X_API_KEY, apiKey) .put(Entity.entity(component, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus(), 0); @@ -531,7 +527,7 @@ public void updateComponentTest() { component.setVersion("1.0"); component = qm.createComponent(component, false); component.setDescription("Test component"); - Response response = target(V1_COMPONENT).request() + Response response = jersey.target(V1_COMPONENT).request() .header(X_API_KEY, apiKey) .post(Entity.entity(component, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -551,7 +547,7 @@ public void updateComponentEmptyNameTest() { component.setVersion("1.0"); component = qm.createComponent(component, false); component.setName(" "); - Response response = target(V1_COMPONENT).request() + Response response = jersey.target(V1_COMPONENT).request() .header(X_API_KEY, apiKey) .post(Entity.entity(component, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -574,7 +570,7 @@ public void updateComponentInvalidLicenseExpressionTest() { jsonComponent.setVersion("1.0.0"); jsonComponent.setLicenseExpression("(invalid"); - final Response response = target(V1_COMPONENT).request() + final Response response = jersey.target(V1_COMPONENT).request() .header(X_API_KEY, apiKey) .post(Entity.entity(""" { @@ -607,7 +603,7 @@ public void deleteComponentTest() { component.setName("My Component"); component.setVersion("1.0"); component = qm.createComponent(component, false); - Response response = target(V1_COMPONENT + "/" + component.getUuid().toString()) + Response response = jersey.target(V1_COMPONENT + "/" + component.getUuid().toString()) .request().header(X_API_KEY, apiKey).delete(); Assert.assertEquals(204, response.getStatus(), 0); } @@ -620,14 +616,14 @@ public void deleteComponentInvalidUuidTest() { component.setName("My Component"); component.setVersion("1.0"); qm.createComponent(component, false); - Response response = target(V1_COMPONENT + "/" + UUID.randomUUID()) + Response response = jersey.target(V1_COMPONENT + "/" + UUID.randomUUID()) .request().header(X_API_KEY, apiKey).delete(); Assert.assertEquals(404, response.getStatus(), 0); } @Test public void internalComponentIdentificationTest() { - Response response = target(V1_COMPONENT + "/internal/identify") + Response response = jersey.target(V1_COMPONENT + "/internal/identify") .request().header(X_API_KEY, apiKey).get(); Assert.assertEquals(204, response.getStatus(), 0); } @@ -678,7 +674,7 @@ public void getDependencyGraphForComponentTest() { component2_1.setDirectDependencies("[{\"uuid\":\"" + component2_1_1.getUuid() + "\"}]"); component2_1_1.setDirectDependencies("[{\"uuid\":\"" + component2_1_1_1.getUuid() + "\"}]"); - Response response = target(V1_COMPONENT + "/project/" + project.getUuid() + "/dependencyGraph/" + component1_1_1.getUuid()) + Response response = jersey.target(V1_COMPONENT + "/project/" + project.getUuid() + "/dependencyGraph/" + component1_1_1.getUuid()) .request().header(X_API_KEY, apiKey).get(); JsonObject json = parseJsonObject(response); Assert.assertEquals(200, response.getStatus(), 0); @@ -744,7 +740,7 @@ public void getDependencyGraphForComponentTestWithRepositoryMetaData() { component1.setDirectDependencies("[{\"uuid\":\"" + component1_1.getUuid() + "\"}]"); component1_1.setDirectDependencies("[{\"uuid\":\"" + component1_1_1.getUuid() + "\"}]"); - Response response = target(V1_COMPONENT + "/project/" + project.getUuid() + "/dependencyGraph/" + component1_1_1.getUuid()) + Response response = jersey.target(V1_COMPONENT + "/project/" + project.getUuid() + "/dependencyGraph/" + component1_1_1.getUuid()) .request().header(X_API_KEY, apiKey).get(); JsonObject json = parseJsonObject(response); Assert.assertEquals(200, response.getStatus(), 0); @@ -765,7 +761,7 @@ public void getDependencyGraphForComponentInvalidProjectUuidTest() { component.setName("My Component"); component.setVersion("1.0"); component = qm.createComponent(component, false); - Response response = target(V1_COMPONENT + "/project/" + UUID.randomUUID() + "/dependencyGraph/" + component.getUuid()) + Response response = jersey.target(V1_COMPONENT + "/project/" + UUID.randomUUID() + "/dependencyGraph/" + component.getUuid()) .request().header(X_API_KEY, apiKey).get(); Assert.assertEquals(404, response.getStatus(), 0); } @@ -773,7 +769,7 @@ public void getDependencyGraphForComponentInvalidProjectUuidTest() { @Test public void getDependencyGraphForComponentInvalidComponentUuidTest() { Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false); - Response response = target(V1_COMPONENT + "/project/" + project.getUuid() + "/dependencyGraph/" + UUID.randomUUID()) + Response response = jersey.target(V1_COMPONENT + "/project/" + project.getUuid() + "/dependencyGraph/" + UUID.randomUUID()) .request().header(X_API_KEY, apiKey).get(); Assert.assertEquals(404, response.getStatus(), 0); } @@ -786,7 +782,7 @@ public void getDependencyGraphForComponentNoDependencyGraphTest() { component.setName("My Component"); component.setVersion("1.0"); component = qm.createComponent(component, false); - Response response = target(V1_COMPONENT + "/project/" + project.getUuid() + "/dependencyGraph/" + component.getUuid()) + Response response = jersey.target(V1_COMPONENT + "/project/" + project.getUuid() + "/dependencyGraph/" + component.getUuid()) .request().header(X_API_KEY, apiKey).get(); JsonObject json = parseJsonObject(response); Assert.assertEquals(200, response.getStatus(), 0); @@ -803,12 +799,12 @@ public void getDependencyGraphForComponentIsNotComponentOfProject() { component = qm.createComponent(component, false); projectWithComponent.setDirectDependencies("[{\"uuid\":\"" + component.getUuid() + "\"}]"); Project projectWithoutComponent = qm.createProject("Acme Library", null, null, null, null, null, true, false); - Response responseWithComponent = target(V1_COMPONENT + "/project/" + projectWithComponent.getUuid() + "/dependencyGraph/" + component.getUuid()) + Response responseWithComponent = jersey.target(V1_COMPONENT + "/project/" + projectWithComponent.getUuid() + "/dependencyGraph/" + component.getUuid()) .request().header(X_API_KEY, apiKey).get(); JsonObject jsonWithComponent = parseJsonObject(responseWithComponent); Assert.assertEquals(200, responseWithComponent.getStatus(), 0); Assert.assertEquals(1, jsonWithComponent.size()); - Response responseWithoutComponent = target(V1_COMPONENT + "/project/" + projectWithoutComponent.getUuid() + "/dependencyGraph/" + component.getUuid()) + Response responseWithoutComponent = jersey.target(V1_COMPONENT + "/project/" + projectWithoutComponent.getUuid() + "/dependencyGraph/" + component.getUuid()) .request().header(X_API_KEY, apiKey).get(); JsonObject jsonWithoutComponent = parseJsonObject(responseWithoutComponent); Assert.assertEquals(200, responseWithoutComponent.getStatus(), 0); diff --git a/src/test/java/org/dependencytrack/resources/v1/ConfigPropertyResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ConfigPropertyResourceTest.java index 30e140970a..516a7a3577 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ConfigPropertyResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ConfigPropertyResourceTest.java @@ -18,17 +18,16 @@ */ package org.dependencytrack.resources.v1; -import alpine.server.filters.ApiFilter; -import alpine.server.filters.AuthenticationFilter; import alpine.model.ConfigProperty; import alpine.model.IConfigProperty; +import alpine.server.filters.ApiFilter; +import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.ConfigPropertyConstants; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -40,21 +39,18 @@ public class ConfigPropertyResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(ConfigPropertyResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(ConfigPropertyResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Test public void getConfigPropertiesTest() { qm.createConfigProperty("my.group", "my.string", "ABC", IConfigProperty.PropertyType.STRING, "A string"); qm.createConfigProperty("my.group", "my.integer", "1", IConfigProperty.PropertyType.INTEGER, "A integer"); qm.createConfigProperty("my.group", "my.password", "password", IConfigProperty.PropertyType.ENCRYPTEDSTRING, "A password"); - Response response = target(V1_CONFIG_PROPERTY).request() + Response response = jersey.target(V1_CONFIG_PROPERTY).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -83,7 +79,7 @@ public void updateConfigPropertyStringTest() { ConfigProperty property = qm.createConfigProperty("my.group", "my.string", "ABC", IConfigProperty.PropertyType.STRING, "A string"); ConfigProperty request = qm.detach(ConfigProperty.class, property.getId()); request.setPropertyValue("DEF"); - Response response = target(V1_CONFIG_PROPERTY).request() + Response response = jersey.target(V1_CONFIG_PROPERTY).request() .header(X_API_KEY, apiKey) .post(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -101,7 +97,7 @@ public void updateConfigPropertyBooleanTest() { ConfigProperty property = qm.createConfigProperty("my.group", "my.boolean", "false", IConfigProperty.PropertyType.BOOLEAN, "A boolean"); ConfigProperty request = qm.detach(ConfigProperty.class, property.getId()); request.setPropertyValue("true"); - Response response = target(V1_CONFIG_PROPERTY).request() + Response response = jersey.target(V1_CONFIG_PROPERTY).request() .header(X_API_KEY, apiKey) .post(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -119,7 +115,7 @@ public void updateConfigPropertyNumberTest() { ConfigProperty property = qm.createConfigProperty("my.group", "my.number", "7.75", IConfigProperty.PropertyType.NUMBER, "A number"); ConfigProperty request = qm.detach(ConfigProperty.class, property.getId()); request.setPropertyValue("5.50"); - Response response = target(V1_CONFIG_PROPERTY).request() + Response response = jersey.target(V1_CONFIG_PROPERTY).request() .header(X_API_KEY, apiKey) .post(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -137,7 +133,7 @@ public void updateBadTaskSchedulerCadenceConfigPropertyTest() { ConfigProperty property = qm.createConfigProperty(ConfigPropertyConstants.TASK_SCHEDULER_LDAP_SYNC_CADENCE.getGroupName(), "my.cadence", "24", IConfigProperty.PropertyType.INTEGER, "A cadence"); ConfigProperty request = qm.detach(ConfigProperty.class, property.getId()); request.setPropertyValue("-2"); - Response response = target(V1_CONFIG_PROPERTY).request() + Response response = jersey.target(V1_CONFIG_PROPERTY).request() .header(X_API_KEY, apiKey) .post(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -150,7 +146,7 @@ public void updateBadIndexConsistencyThresholdConfigPropertyTest() { ConfigProperty property = qm.createConfigProperty(ConfigPropertyConstants.SEARCH_INDEXES_CONSISTENCY_CHECK_DELTA_THRESHOLD.getGroupName(), ConfigPropertyConstants.SEARCH_INDEXES_CONSISTENCY_CHECK_DELTA_THRESHOLD.getPropertyName(), "24", IConfigProperty.PropertyType.INTEGER, ConfigPropertyConstants.SEARCH_INDEXES_CONSISTENCY_CHECK_DELTA_THRESHOLD.getDescription()); ConfigProperty request = qm.detach(ConfigProperty.class, property.getId()); request.setPropertyValue("-1"); - Response response = target(V1_CONFIG_PROPERTY).request() + Response response = jersey.target(V1_CONFIG_PROPERTY).request() .header(X_API_KEY, apiKey) .post(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -163,7 +159,7 @@ public void updateConfigPropertyUrlTest() { ConfigProperty property = qm.createConfigProperty("my.group", "my.url", "http://localhost", IConfigProperty.PropertyType.URL, "A url"); ConfigProperty request = qm.detach(ConfigProperty.class, property.getId()); request.setPropertyValue("http://localhost/path"); - Response response = target(V1_CONFIG_PROPERTY).request() + Response response = jersey.target(V1_CONFIG_PROPERTY).request() .header(X_API_KEY, apiKey) .post(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -181,7 +177,7 @@ public void updateConfigPropertyUuidTest() { ConfigProperty property = qm.createConfigProperty("my.group", "my.uuid", "a496cabc-749d-4751-b9e5-3b49b656d018", IConfigProperty.PropertyType.UUID, "A uuid"); ConfigProperty request = qm.detach(ConfigProperty.class, property.getId()); request.setPropertyValue("fe03c401-b5a1-4b86-bc3b-1b7a68f0f78d"); - Response response = target(V1_CONFIG_PROPERTY).request() + Response response = jersey.target(V1_CONFIG_PROPERTY).request() .header(X_API_KEY, apiKey) .post(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -199,7 +195,7 @@ public void updateConfigPropertyEncryptedStringTest() { ConfigProperty property = qm.createConfigProperty("my.group", "my.encryptedString", "aaaaa", IConfigProperty.PropertyType.ENCRYPTEDSTRING, "A encrypted string"); ConfigProperty request = qm.detach(ConfigProperty.class, property.getId()); request.setPropertyValue("bbbbb"); - Response response = target(V1_CONFIG_PROPERTY).request() + Response response = jersey.target(V1_CONFIG_PROPERTY).request() .header(X_API_KEY, apiKey) .post(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -224,7 +220,7 @@ public void updateConfigPropertiesAggregateTest() { prop4 = qm.detach(ConfigProperty.class, prop4.getId()); prop3.setPropertyValue("XYZ"); prop4.setPropertyValue("-2"); - Response response = target(V1_CONFIG_PROPERTY+"/aggregate").request() + Response response = jersey.target(V1_CONFIG_PROPERTY+"/aggregate").request() .header(X_API_KEY, apiKey) .post(Entity.entity(Arrays.asList(prop1, prop2, prop3, prop4), MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -245,7 +241,7 @@ public void updateConfigPropertyOsvEcosystemTest() { ConfigProperty property = qm.createConfigProperty("my.group", ConfigPropertyConstants.VULNERABILITY_SOURCE_GOOGLE_OSV_ENABLED.getPropertyName(), "maven;npm;maven", IConfigProperty.PropertyType.STRING, "List of ecosystems"); ConfigProperty request = qm.detach(ConfigProperty.class, property.getId()); request.setPropertyValue("maven;npm;maven"); - Response response = target(V1_CONFIG_PROPERTY).request() + Response response = jersey.target(V1_CONFIG_PROPERTY).request() .header(X_API_KEY, apiKey) .post(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); diff --git a/src/test/java/org/dependencytrack/resources/v1/CweResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/CweResourceTest.java index af02631aad..4feeb8542f 100644 --- a/src/test/java/org/dependencytrack/resources/v1/CweResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/CweResourceTest.java @@ -20,12 +20,11 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -34,18 +33,15 @@ public class CweResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(CweResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(CweResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Test public void getCwesTest() { - Response response = target(V1_CWE).request() + Response response = jersey.target(V1_CWE).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -59,7 +55,7 @@ public void getCwesTest() { @Test public void getCweTest() { - Response response = target(V1_CWE + "/79").request() + Response response = jersey.target(V1_CWE + "/79").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); diff --git a/src/test/java/org/dependencytrack/resources/v1/DependencyGraphResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/DependencyGraphResourceTest.java index 2d4f7b9832..5ea6a06a32 100644 --- a/src/test/java/org/dependencytrack/resources/v1/DependencyGraphResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/DependencyGraphResourceTest.java @@ -21,21 +21,23 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; -import com.github.packageurl.PackageURL; import net.javacrumbs.jsonunit.core.Option; import org.apache.http.HttpStatus; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; -import org.dependencytrack.model.*; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.ComponentIdentity; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.RepositoryMetaComponent; +import org.dependencytrack.model.RepositoryType; +import org.dependencytrack.model.ServiceComponent; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.json.JSONArray; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; import javax.ws.rs.core.Response; - import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -47,15 +49,11 @@ public class DependencyGraphResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(DependencyGraphResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } - + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(DependencyGraphResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Test public void getComponentsAndServicesByComponentUuidTests() { @@ -91,7 +89,7 @@ public void getComponentsAndServicesByComponentUuidTests() { jsonArray.put(new ComponentIdentity(component).toJSON()); } - for(ServiceComponent serviceComponent : serviceComponents) { + for (ServiceComponent serviceComponent : serviceComponents) { jsonArray.put(new ComponentIdentity(serviceComponent).toJSON()); } @@ -99,7 +97,7 @@ public void getComponentsAndServicesByComponentUuidTests() { final UUID rootUuid = qm.createComponent(rootComponent, false).getUuid(); - final Response response = target(V1_DEPENDENCY_GRAPH + "/component/" + rootUuid.toString() + "/directDependencies") + final Response response = jersey.target(V1_DEPENDENCY_GRAPH + "/component/" + rootUuid.toString() + "/directDependencies") .request() .header(X_API_KEY, apiKey) .get(); @@ -173,7 +171,7 @@ public void getComponentsAndServicesByComponentUuidWithRepositoryMetaTests() { jsonArray.put(new ComponentIdentity(component).toJSON()); } - for(ServiceComponent serviceComponent : serviceComponents) { + for (ServiceComponent serviceComponent : serviceComponents) { jsonArray.put(new ComponentIdentity(serviceComponent).toJSON()); } @@ -181,7 +179,7 @@ public void getComponentsAndServicesByComponentUuidWithRepositoryMetaTests() { final UUID rootUuid = qm.createComponent(rootComponent, false).getUuid(); - final Response response = target(V1_DEPENDENCY_GRAPH + "/component/" + rootUuid.toString() + "/directDependencies") + final Response response = jersey.target(V1_DEPENDENCY_GRAPH + "/component/" + rootUuid.toString() + "/directDependencies") .request() .header(X_API_KEY, apiKey) .get(); @@ -222,14 +220,14 @@ public void getComponentsAndServicesByProjectUuidTests() { jsonArray.put(new ComponentIdentity(component).toJSON()); } - for(ServiceComponent serviceComponent : serviceComponents) { + for (ServiceComponent serviceComponent : serviceComponents) { jsonArray.put(new ComponentIdentity(serviceComponent).toJSON()); } project.setDirectDependencies(jsonArray.toString()); qm.updateProject(project, false); - final Response response = target(V1_DEPENDENCY_GRAPH + "/project/" + project.getUuid().toString() + "/directDependencies") + final Response response = jersey.target(V1_DEPENDENCY_GRAPH + "/project/" + project.getUuid().toString() + "/directDependencies") .request() .header(X_API_KEY, apiKey) .get(); @@ -298,14 +296,14 @@ public void getComponentsAndServicesByProjectUuidWithRepositoryMetaTests() { jsonArray.put(new ComponentIdentity(component).toJSON()); } - for(ServiceComponent serviceComponent : serviceComponents) { + for (ServiceComponent serviceComponent : serviceComponents) { jsonArray.put(new ComponentIdentity(serviceComponent).toJSON()); } project.setDirectDependencies(jsonArray.toString()); qm.updateProject(project, false); - final Response response = target(V1_DEPENDENCY_GRAPH + "/project/" + project.getUuid().toString() + "/directDependencies") + final Response response = jersey.target(V1_DEPENDENCY_GRAPH + "/project/" + project.getUuid().toString() + "/directDependencies") .request() .header(X_API_KEY, apiKey) .get(); @@ -353,7 +351,7 @@ public void getComponentsAndServicesByProjectUuidWithComponentsWithoutPurlTest() """.formatted(componentWithPurl.getUuid(), componentWithoutPurl.getUuid())); qm.persist(project); - final Response response = target("%s/project/%s/directDependencies".formatted(V1_DEPENDENCY_GRAPH, project.getUuid())) + final Response response = jersey.target("%s/project/%s/directDependencies".formatted(V1_DEPENDENCY_GRAPH, project.getUuid())) .request() .header(X_API_KEY, apiKey) .get(); diff --git a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java index 380ddc1e67..e058c30a76 100644 --- a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java @@ -18,18 +18,13 @@ */ package org.dependencytrack.resources.v1; -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; -import static org.dependencytrack.resources.v1.FindingResource.MEDIA_TYPE_SARIF_JSON; -import static org.hamcrest.CoreMatchers.equalTo; - import alpine.Config; import alpine.model.About; import alpine.model.ConfigProperty; import alpine.model.Team; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; -import javax.ws.rs.core.HttpHeaders; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; @@ -40,29 +35,30 @@ import org.dependencytrack.model.Vulnerability; import org.dependencytrack.tasks.scanners.AnalyzerIdentity; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; import javax.json.JsonObject; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import java.util.Date; import java.util.List; import java.util.UUID; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; +import static org.dependencytrack.resources.v1.FindingResource.MEDIA_TYPE_SARIF_JSON; +import static org.hamcrest.CoreMatchers.equalTo; + public class FindingResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(FindingResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(FindingResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Test public void getFindingsByProjectTest() { @@ -82,7 +78,7 @@ public void getFindingsByProjectTest() { qm.addVulnerability(v2, c1, AnalyzerIdentity.NONE); qm.addVulnerability(v3, c2, AnalyzerIdentity.NONE); qm.addVulnerability(v4, c5, AnalyzerIdentity.NONE); - Response response = target(V1_FINDING + "/project/" + p1.getUuid().toString()).request() + Response response = jersey.target(V1_FINDING + "/project/" + p1.getUuid().toString()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -124,7 +120,7 @@ public void getFindingsByProjectTest() { @Test public void getFindingsByProjectInvalidTest() { - Response response = target(V1_FINDING + "/project/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_FINDING + "/project/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); @@ -151,7 +147,7 @@ public void exportFindingsByProjectTest() { qm.addVulnerability(v2, c1, AnalyzerIdentity.NONE); qm.addVulnerability(v3, c2, AnalyzerIdentity.NONE); qm.addVulnerability(v4, c5, AnalyzerIdentity.NONE); - Response response = target(V1_FINDING + "/project/" + p1.getUuid().toString() + "/export").request() + Response response = jersey.target(V1_FINDING + "/project/" + p1.getUuid().toString() + "/export").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -201,7 +197,7 @@ public void exportFindingsByProjectTest() { @Test public void exportFindingsByProjectInvalidTest() { - Response response = target(V1_FINDING + "/project/" + UUID.randomUUID().toString() + "/export").request() + Response response = jersey.target(V1_FINDING + "/project/" + UUID.randomUUID().toString() + "/export").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); @@ -259,7 +255,7 @@ public void getFindingsByProjectWithComponentLatestVersionTest() { qm.addVulnerability(v2, c1, AnalyzerIdentity.NONE); qm.addVulnerability(v3, c2, AnalyzerIdentity.NONE); qm.addVulnerability(v4, c5, AnalyzerIdentity.NONE); - Response response = target(V1_FINDING + "/project/" + p1.getUuid().toString()).request() + Response response = jersey.target(V1_FINDING + "/project/" + p1.getUuid().toString()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -310,7 +306,7 @@ public void getFindingsByProjectWithComponentLatestVersionWithoutRepositoryMetaC Vulnerability v1 = createVulnerability("Vuln-1", Severity.CRITICAL); qm.addVulnerability(v1, c1, AnalyzerIdentity.NONE); - Response response = target(V1_FINDING + "/project/" + p1.getUuid().toString()).request() + Response response = jersey.target(V1_FINDING + "/project/" + p1.getUuid().toString()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -356,7 +352,7 @@ public void getAllFindings() { qm.addVulnerability(v2, c3, AnalyzerIdentity.NONE); qm.addVulnerability(v3, c2, AnalyzerIdentity.NONE); qm.addVulnerability(v4, c5, AnalyzerIdentity.NONE); - Response response = target(V1_FINDING) + Response response = jersey.target(V1_FINDING) .queryParam("sortName", "component.projectName") .queryParam("sortOrder", "asc") .request() @@ -423,7 +419,7 @@ public void getAllFindingsWithAclEnabled() { aclToggle.setPropertyValue("true"); qm.persist(aclToggle); } - Response response = target(V1_FINDING).request() + Response response = jersey.target(V1_FINDING).request() .header(X_API_KEY, team.getApiKeys().get(0).getKey()) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -472,7 +468,7 @@ public void getAllFindingsGroupedByVulnerability() { qm.addVulnerability(v3, c2, AnalyzerIdentity.NONE); qm.addVulnerability(v3, c6, AnalyzerIdentity.NONE); qm.addVulnerability(v4, c5, AnalyzerIdentity.NONE); - Response response = target(V1_FINDING + "/grouped").request() + Response response = jersey.target(V1_FINDING + "/grouped").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -557,7 +553,7 @@ public void getAllFindingsGroupedByVulnerabilityWithAclEnabled() { aclToggle.setPropertyValue("true"); qm.persist(aclToggle); } - Response response = target(V1_FINDING + "/grouped").request() + Response response = jersey.target(V1_FINDING + "/grouped").request() .header(X_API_KEY, team.getApiKeys().get(0).getKey()) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -616,7 +612,7 @@ public void getSARIFFindingsByProjectTest() { qm.addVulnerability(v3, c1, AnalyzerIdentity.NONE); qm.addVulnerability(v3, c2, AnalyzerIdentity.NONE); - Response response = target(V1_FINDING + "/project/" + project.getUuid().toString()).request() + Response response = jersey.target(V1_FINDING + "/project/" + project.getUuid().toString()).request() .header(HttpHeaders.ACCEPT, MEDIA_TYPE_SARIF_JSON) .header(X_API_KEY, apiKey) .get(Response.class); diff --git a/src/test/java/org/dependencytrack/resources/v1/IntegrationResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/IntegrationResourceTest.java index b7867b221c..2d59768a4e 100644 --- a/src/test/java/org/dependencytrack/resources/v1/IntegrationResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/IntegrationResourceTest.java @@ -21,13 +21,12 @@ import alpine.model.IConfigProperty; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -38,14 +37,11 @@ public class IntegrationResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(IntegrationResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(IntegrationResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Before public void before() throws Exception { @@ -64,7 +60,7 @@ public void before() throws Exception { @Test public void getEcosystemsTest() { - Response response = target(V1_OSV_ECOSYSTEM).request() + Response response = jersey.target(V1_OSV_ECOSYSTEM).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -74,7 +70,7 @@ public void getEcosystemsTest() { Assert.assertFalse(json.isEmpty()); var total = json.size(); - response = target(V1_OSV_ECOSYSTEM + "/inactive").request() + response = jersey.target(V1_OSV_ECOSYSTEM + "/inactive").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); diff --git a/src/test/java/org/dependencytrack/resources/v1/LdapResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/LdapResourceTest.java index 4731e85394..b759a8d381 100644 --- a/src/test/java/org/dependencytrack/resources/v1/LdapResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/LdapResourceTest.java @@ -18,16 +18,15 @@ */ package org.dependencytrack.resources.v1; +import alpine.model.MappedLdapGroup; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; -import alpine.model.MappedLdapGroup; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.resources.v1.vo.MappedLdapGroupRequest; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -39,18 +38,15 @@ public class LdapResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(LdapResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(LdapResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Test public void retrieveLdapGroupsNotEnabledTest() { - Response response = target(V1_LDAP + "/groups").request() + Response response = jersey.target(V1_LDAP + "/groups").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -65,7 +61,7 @@ public void retrieveLdapGroupsIsEnabledTest() { public void retrieveLdapGroupsTest() { qm.createMappedLdapGroup(team, "CN=Developers,OU=R&D,O=Acme"); qm.createMappedLdapGroup(team, "CN=QA,OU=R&D,O=Acme"); - Response response = target(V1_LDAP + "/team/" + team.getUuid().toString()).request() + Response response = jersey.target(V1_LDAP + "/team/" + team.getUuid().toString()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -80,7 +76,7 @@ public void retrieveLdapGroupsTest() { @Test public void addMappingTest() { MappedLdapGroupRequest request = new MappedLdapGroupRequest(team.getUuid().toString(), "CN=Administrators,OU=R&D,O=Acme"); - Response response = target(V1_LDAP + "/mapping").request() + Response response = jersey.target(V1_LDAP + "/mapping").request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -93,7 +89,7 @@ public void addMappingTest() { @Test public void addMappingInvalidTest() { MappedLdapGroupRequest request = new MappedLdapGroupRequest(UUID.randomUUID().toString(), "CN=Administrators,OU=R&D,O=Acme"); - Response response = target(V1_LDAP + "/mapping").request() + Response response = jersey.target(V1_LDAP + "/mapping").request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -105,7 +101,7 @@ public void addMappingInvalidTest() { @Test public void deleteMappingTest() { MappedLdapGroup mapping = qm.createMappedLdapGroup(team, "CN=Finance,OU=R&D,O=Acme"); - Response response = target(V1_LDAP + "/mapping/" + mapping.getUuid().toString()).request() + Response response = jersey.target(V1_LDAP + "/mapping/" + mapping.getUuid().toString()).request() .header(X_API_KEY, apiKey) .delete(Response.class); Assert.assertEquals(204, response.getStatus(), 0); @@ -114,7 +110,7 @@ public void deleteMappingTest() { @Test public void deleteMappingInvalidTest() { - Response response = target(V1_LDAP + "/mapping/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_LDAP + "/mapping/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .delete(Response.class); Assert.assertEquals(404, response.getStatus(), 0); diff --git a/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java index a6d6206662..3b184ee16b 100644 --- a/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java @@ -21,15 +21,14 @@ import alpine.common.util.UuidUtil; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.License; import org.dependencytrack.persistence.DefaultObjectGenerator; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -40,14 +39,11 @@ public class LicenseResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(LicenseResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(LicenseResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Before public void loadDefaultLicenses() { @@ -57,7 +53,7 @@ public void loadDefaultLicenses() { @Test public void getLicensesTest() { - Response response = target(V1_LICENSE).request() + Response response = jersey.target(V1_LICENSE).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -73,7 +69,7 @@ public void getLicensesTest() { @Test public void getLicensesConciseTest() { - Response response = target(V1_LICENSE + "/concise").request() + Response response = jersey.target(V1_LICENSE + "/concise").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -89,7 +85,7 @@ public void getLicensesConciseTest() { @Test public void getLicense() { - Response response = target(V1_LICENSE + "/Apache-2.0").request() + Response response = jersey.target(V1_LICENSE + "/Apache-2.0").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -104,7 +100,7 @@ public void getLicense() { @Test public void getLicenseInvalid() { - Response response = target(V1_LICENSE + "/blah").request() + Response response = jersey.target(V1_LICENSE + "/blah").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); @@ -118,7 +114,7 @@ public void createCustomLicense() { License license = new License(); license.setName("Acme Example"); license.setLicenseId("Acme-Example-License"); - Response response = target(V1_LICENSE) + Response response = jersey.target(V1_LICENSE) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(license, MediaType.APPLICATION_JSON)); @@ -139,7 +135,7 @@ public void createCustomLicenseDuplicate() { License license = new License(); license.setName("Apache License 2.0"); license.setLicenseId("Apache-2.0"); - Response response = target(V1_LICENSE) + Response response = jersey.target(V1_LICENSE) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(license, MediaType.APPLICATION_JSON)); @@ -153,7 +149,7 @@ public void createCustomLicenseDuplicate() { public void createCustomLicenseWithoutLicenseId() { License license = new License(); license.setName("Acme Example"); - Response response = target(V1_LICENSE) + Response response = jersey.target(V1_LICENSE) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(license, MediaType.APPLICATION_JSON)); @@ -169,7 +165,7 @@ public void deleteCustomLicense() { license.setCustomLicense(true); qm.createCustomLicense(license, false); - Response response = target(V1_LICENSE + "/" + license.getLicenseId()) + Response response = jersey.target(V1_LICENSE + "/" + license.getLicenseId()) .request() .header(X_API_KEY, apiKey) .delete(); @@ -184,7 +180,7 @@ public void deleteNotCustomLicense() { license1.setName("Acme Example"); License license2 = qm.createCustomLicense(license1, false); license1.setCustomLicense(false); - Response response = target(V1_LICENSE + "/" + license1.getLicenseId()) + Response response = jersey.target(V1_LICENSE + "/" + license1.getLicenseId()) .request() .header(X_API_KEY, apiKey) .delete(); diff --git a/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java index e0d5d8a678..b734044880 100644 --- a/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java @@ -23,6 +23,7 @@ import alpine.notification.NotificationLevel; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.NotificationPublisher; @@ -32,13 +33,10 @@ import org.dependencytrack.notification.publisher.Publisher; import org.dependencytrack.notification.publisher.SendMailPublisher; import org.dependencytrack.persistence.DefaultObjectGenerator; -import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -51,14 +49,11 @@ public class NotificationPublisherResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(NotificationPublisherResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(NotificationPublisherResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Before public void before() throws Exception { @@ -69,7 +64,7 @@ public void before() throws Exception { @Test public void getAllNotificationPublishersTest() { - Response response = target(V1_NOTIFICATION_PUBLISHER).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -94,7 +89,7 @@ public void createNotificationPublisherTest() { publisher.setTemplateMimeType("application/json"); publisher.setPublisherClass(SendMailPublisher.class.getName()); publisher.setDefaultPublisher(false); - Response response = target(V1_NOTIFICATION_PUBLISHER).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER).request() .header(X_API_KEY, apiKey) .put(Entity.entity(publisher, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus(), 0); @@ -118,7 +113,7 @@ public void createNotificationPublisherWithDefaultFlagTest() { publisher.setTemplateMimeType("application/json"); publisher.setPublisherClass(SendMailPublisher.class.getName()); publisher.setDefaultPublisher(true); - Response response = target(V1_NOTIFICATION_PUBLISHER).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER).request() .header(X_API_KEY, apiKey) .put(Entity.entity(publisher, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -135,7 +130,7 @@ public void createNotificationPublisherWithExistingNameTest() { publisher.setTemplateMimeType("application/json"); publisher.setPublisherClass(SendMailPublisher.class.getName()); publisher.setDefaultPublisher(true); - Response response = target(V1_NOTIFICATION_PUBLISHER).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER).request() .header(X_API_KEY, apiKey) .put(Entity.entity(publisher, MediaType.APPLICATION_JSON)); Assert.assertEquals(409, response.getStatus(), 0); @@ -152,7 +147,7 @@ public void createNotificationPublisherWithClassNotImplementingPublisherInterfac publisher.setTemplateMimeType("application/json"); publisher.setPublisherClass(NotificationPublisherResource.class.getName()); publisher.setDefaultPublisher(false); - Response response = target(V1_NOTIFICATION_PUBLISHER).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER).request() .header(X_API_KEY, apiKey) .put(Entity.entity(publisher, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -169,7 +164,7 @@ public void createNotificationPublisherClassNotFoundTest() { publisher.setTemplateMimeType("application/json"); publisher.setPublisherClass("invalidClassFqcn"); publisher.setDefaultPublisher(false); - Response response = target(V1_NOTIFICATION_PUBLISHER).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER).request() .header(X_API_KEY, apiKey) .put(Entity.entity(publisher, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -185,7 +180,7 @@ public void updateNotificationPublisherTest() { false ); notificationPublisher.setName("Updated Publisher name"); - Response response = target(V1_NOTIFICATION_PUBLISHER).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER).request() .header(X_API_KEY, apiKey) .post(Entity.entity(notificationPublisher, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -209,7 +204,7 @@ public void updateUnknownNotificationPublisherTest() { ); notificationPublisher = qm.detach(NotificationPublisher.class, notificationPublisher.getId()); notificationPublisher.setUuid(UUID.randomUUID()); - Response response = target(V1_NOTIFICATION_PUBLISHER).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER).request() .header(X_API_KEY, apiKey) .post(Entity.entity(notificationPublisher, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -222,7 +217,7 @@ public void updateUnknownNotificationPublisherTest() { public void updateExistingDefaultNotificationPublisherTest() { NotificationPublisher notificationPublisher = qm.getDefaultNotificationPublisher(SendMailPublisher.class); notificationPublisher.setName(notificationPublisher.getName() + " Updated"); - Response response = target(V1_NOTIFICATION_PUBLISHER).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER).request() .header(X_API_KEY, apiKey) .post(Entity.entity(notificationPublisher, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -240,7 +235,7 @@ public void updateNotificationPublisherWithNameOfAnotherNotificationPublisherTes ); notificationPublisher = qm.detach(NotificationPublisher.class, notificationPublisher.getId()); notificationPublisher.setName(DefaultNotificationPublishers.MS_TEAMS.getPublisherName()); - Response response = target(V1_NOTIFICATION_PUBLISHER).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER).request() .header(X_API_KEY, apiKey) .post(Entity.entity(notificationPublisher, MediaType.APPLICATION_JSON)); Assert.assertEquals(409, response.getStatus(), 0); @@ -257,7 +252,7 @@ public void updateNotificationPublisherWithInvalidClassTest() { false ); notificationPublisher.setPublisherClass("unknownClass"); - Response response = target(V1_NOTIFICATION_PUBLISHER).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER).request() .header(X_API_KEY, apiKey) .post(Entity.entity(notificationPublisher, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -274,7 +269,7 @@ public void updateNotificationPublisherWithClassNotImplementingPublisherInterfac false ); notificationPublisher.setPublisherClass(NotificationPublisherResource.class.getName()); - Response response = target(V1_NOTIFICATION_PUBLISHER).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER).request() .header(X_API_KEY, apiKey) .post(Entity.entity(notificationPublisher, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -290,7 +285,7 @@ public void deleteNotificationPublisherWithNoRulesTest() { (Class) SendMailPublisher.class, "template", "text/html", false ); - Response response = target(V1_NOTIFICATION_PUBLISHER + "/" + publisher.getUuid()).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER + "/" + publisher.getUuid()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(204, response.getStatus(), 0); @@ -306,7 +301,7 @@ public void deleteNotificationPublisherWithLinkedNotificationRulesTest() { ); NotificationRule firstRule = qm.createNotificationRule("Example Rule 1", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); NotificationRule secondRule = qm.createNotificationRule("Example Rule 2", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); - Response response = target(V1_NOTIFICATION_PUBLISHER + "/" + publisher.getUuid()).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER + "/" + publisher.getUuid()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(204, response.getStatus(), 0); @@ -317,7 +312,7 @@ public void deleteNotificationPublisherWithLinkedNotificationRulesTest() { @Test public void deleteUnknownNotificationPublisherTest() { - Response response = target(V1_NOTIFICATION_PUBLISHER + "/" + UUID.randomUUID()).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER + "/" + UUID.randomUUID()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(404, response.getStatus(), 0); @@ -326,7 +321,7 @@ public void deleteUnknownNotificationPublisherTest() { @Test public void deleteDefaultNotificationPublisherTest() { NotificationPublisher notificationPublisher = qm.getDefaultNotificationPublisher((Class) SendMailPublisher.class); - Response response = target(V1_NOTIFICATION_PUBLISHER + "/" + notificationPublisher.getUuid()).request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER + "/" + notificationPublisher.getUuid()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(400, response.getStatus(), 0); @@ -339,7 +334,7 @@ public void deleteDefaultNotificationPublisherTest() { public void testSmtpPublisherConfigTest() { Form form = new Form(); form.param("destination", "test@example.com"); - Response response = target(V1_NOTIFICATION_PUBLISHER + "/test/smtp").request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER + "/test/smtp").request() .header(X_API_KEY, apiKey) .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); Assert.assertEquals(200, response.getStatus(), 0); @@ -358,7 +353,7 @@ public void restoreDefaultTemplatesTest() { property.setPropertyValue("true"); qm.persist(property); qm.detach(ConfigProperty.class, property.getId()); - Response response = target(V1_NOTIFICATION_PUBLISHER + "/restoreDefaultTemplates").request() + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER + "/restoreDefaultTemplates").request() .header(X_API_KEY, apiKey) .post(Entity.json("")); qm.getPersistenceManager().refreshAll(); diff --git a/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java index 774333148a..ed9884b17f 100644 --- a/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java @@ -23,6 +23,7 @@ import alpine.notification.NotificationLevel; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.NotificationPublisher; import org.dependencytrack.model.NotificationRule; @@ -34,11 +35,9 @@ import org.dependencytrack.persistence.DefaultObjectGenerator; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -57,14 +56,11 @@ public class NotificationRuleResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(NotificationRuleResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(NotificationRuleResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Before public void before() throws Exception { @@ -79,7 +75,7 @@ public void getAllNotificationRulesTest() { NotificationRule r1 = qm.createNotificationRule("Rule 1", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); NotificationRule r2 = qm.createNotificationRule("Rule 2", NotificationScope.PORTFOLIO, NotificationLevel.WARNING, publisher); NotificationRule r3 = qm.createNotificationRule("Rule 3", NotificationScope.SYSTEM, NotificationLevel.ERROR, publisher); - Response response = target(V1_NOTIFICATION_RULE).request() + Response response = jersey.target(V1_NOTIFICATION_RULE).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -107,7 +103,7 @@ public void createNotificationRuleTest() { rule.setNotificationLevel(NotificationLevel.WARNING); rule.setScope(NotificationScope.SYSTEM); rule.setPublisher(publisher); - Response response = target(V1_NOTIFICATION_RULE).request() + Response response = jersey.target(V1_NOTIFICATION_RULE).request() .header(X_API_KEY, apiKey) .put(Entity.entity(rule, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus(), 0); @@ -134,7 +130,7 @@ public void createNotificationRuleInvalidPublisherTest() { rule.setNotificationLevel(NotificationLevel.WARNING); rule.setScope(NotificationScope.SYSTEM); rule.setPublisher(publisher); - Response response = target(V1_NOTIFICATION_RULE).request() + Response response = jersey.target(V1_NOTIFICATION_RULE).request() .header(X_API_KEY, apiKey) .put(Entity.entity(rule, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -149,7 +145,7 @@ public void updateNotificationRuleTest() { NotificationRule rule = qm.createNotificationRule("Rule 1", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); rule.setName("Example Rule"); rule.setNotifyOn(Collections.singleton(NotificationGroup.NEW_VULNERABILITY)); - Response response = target(V1_NOTIFICATION_RULE).request() + Response response = jersey.target(V1_NOTIFICATION_RULE).request() .header(X_API_KEY, apiKey) .post(Entity.entity(rule, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -170,7 +166,7 @@ public void updateNotificationRuleInvalidTest() { NotificationRule rule = qm.createNotificationRule("Rule 1", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); rule = qm.detach(NotificationRule.class, rule.getId()); rule.setUuid(UUID.randomUUID()); - Response response = target(V1_NOTIFICATION_RULE).request() + Response response = jersey.target(V1_NOTIFICATION_RULE).request() .header(X_API_KEY, apiKey) .post(Entity.entity(rule, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -184,7 +180,7 @@ public void deleteNotificationRuleTest() { NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); NotificationRule rule = qm.createNotificationRule("Rule 1", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); rule.setName("Example Rule"); - Response response = target(V1_NOTIFICATION_RULE).request() + Response response = jersey.target(V1_NOTIFICATION_RULE).request() .header(X_API_KEY, apiKey) .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) // HACK .method("DELETE", Entity.entity(rule, MediaType.APPLICATION_JSON)); // HACK @@ -197,7 +193,7 @@ public void addProjectToRuleTest() { Project project = qm.createProject("Acme Example", null, null, null, null, null, true, false); NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); NotificationRule rule = qm.createNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.json("")); Assert.assertEquals(200, response.getStatus(), 0); @@ -213,7 +209,7 @@ public void addProjectToRuleTest() { public void addProjectToRuleInvalidRuleTest() { Project project = qm.createProject("Acme Example", null, null, null, null, null, true, false); NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); - Response response = target(V1_NOTIFICATION_RULE + "/" + UUID.randomUUID().toString() + "/project/" + project.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + UUID.randomUUID().toString() + "/project/" + project.getUuid().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.json("")); Assert.assertEquals(404, response.getStatus(), 0); @@ -227,7 +223,7 @@ public void addProjectToRuleInvalidScopeTest() { Project project = qm.createProject("Acme Example", null, null, null, null, null, true, false); NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); NotificationRule rule = qm.createNotificationRule("Example Rule", NotificationScope.SYSTEM, NotificationLevel.INFORMATIONAL, publisher); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.json("")); Assert.assertEquals(406, response.getStatus(), 0); @@ -240,7 +236,7 @@ public void addProjectToRuleInvalidScopeTest() { public void addProjectToRuleInvalidProjectTest() { NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); NotificationRule rule = qm.createNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.json("")); Assert.assertEquals(404, response.getStatus(), 0); @@ -258,7 +254,7 @@ public void addProjectToRuleDuplicateProjectTest() { projects.add(project); rule.setProjects(projects); qm.persist(rule); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.json("")); Assert.assertEquals(304, response.getStatus(), 0); @@ -274,7 +270,7 @@ public void removeProjectFromRuleTest() { projects.add(project); rule.setProjects(projects); qm.persist(rule); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(200, response.getStatus(), 0); @@ -285,7 +281,7 @@ public void removeProjectFromRuleTest() { public void removeProjectFromRuleInvalidRuleTest() { Project project = qm.createProject("Acme Example", null, null, null, null, null, true, false); NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); - Response response = target(V1_NOTIFICATION_RULE + "/" + UUID.randomUUID().toString() + "/project/" + project.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + UUID.randomUUID().toString() + "/project/" + project.getUuid().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(404, response.getStatus(), 0); @@ -299,7 +295,7 @@ public void removeProjectFromRuleInvalidScopeTest() { Project project = qm.createProject("Acme Example", null, null, null, null, null, true, false); NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); NotificationRule rule = qm.createNotificationRule("Example Rule", NotificationScope.SYSTEM, NotificationLevel.INFORMATIONAL, publisher); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(406, response.getStatus(), 0); @@ -312,7 +308,7 @@ public void removeProjectFromRuleInvalidScopeTest() { public void removeProjectFromRuleInvalidProjectTest() { NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); NotificationRule rule = qm.createNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(404, response.getStatus(), 0); @@ -326,7 +322,7 @@ public void removeProjectFromRuleDuplicateProjectTest() { Project project = qm.createProject("Acme Example", null, null, null, null, null, true, false); NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); NotificationRule rule = qm.createNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(304, response.getStatus(), 0); @@ -338,7 +334,7 @@ public void addTeamToRuleTest(){ Team team = qm.createTeam("Team Example", false); NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.EMAIL.getPublisherName()); NotificationRule rule = qm.createNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.json("")); Assert.assertEquals(200, response.getStatus(), 0); @@ -354,7 +350,7 @@ public void addTeamToRuleTest(){ public void addTeamToRuleInvalidRuleTest(){ Team team = qm.createTeam("Team Example", false); NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.EMAIL.getPublisherName()); - Response response = target(V1_NOTIFICATION_RULE + "/" + UUID.randomUUID().toString() + "/team/" + team.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + UUID.randomUUID().toString() + "/team/" + team.getUuid().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.json("")); Assert.assertEquals(404, response.getStatus(), 0); @@ -367,7 +363,7 @@ public void addTeamToRuleInvalidRuleTest(){ public void addTeamToRuleInvalidTeamTest() { NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.EMAIL.getPublisherName()); NotificationRule rule = qm.createNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.json("")); Assert.assertEquals(404, response.getStatus(), 0); @@ -385,7 +381,7 @@ public void addTeamToRuleDuplicateTeamTest() { teams.add(team); rule.setTeams(teams); qm.persist(rule); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.json("")); Assert.assertEquals(304, response.getStatus(), 0); @@ -397,7 +393,7 @@ public void addTeamToRuleInvalidPublisherTest(){ Team team = qm.createTeam("Team Example", false); NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); NotificationRule rule = qm.createNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.json("")); Assert.assertEquals(406, response.getStatus(), 0); @@ -411,7 +407,7 @@ public void addTeamToRuleWithCustomEmailPublisherTest() { final Team team = qm.createTeam("Team Example", false); final NotificationPublisher publisher = qm.createNotificationPublisher("foo", "description", SendMailPublisher.class, "template", "templateMimeType", false); final NotificationRule rule = qm.createNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); - final Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid() + "/team/" + team.getUuid()).request() + final Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid() + "/team/" + team.getUuid()).request() .header(X_API_KEY, apiKey) .post(Entity.json("")); assertThat(response.getStatus()).isEqualTo(200); @@ -458,7 +454,7 @@ public void removeTeamFromRuleTest() { teams.add(team); rule.setTeams(teams); qm.persist(rule); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(200, response.getStatus(), 0); @@ -469,7 +465,7 @@ public void removeTeamFromRuleTest() { public void removeTeamFromRuleInvalidRuleTest() { Team team = qm.createTeam("Team Example", false); NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.EMAIL.getPublisherName()); - Response response = target(V1_NOTIFICATION_RULE + "/" + UUID.randomUUID().toString() + "/team/" + team.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + UUID.randomUUID().toString() + "/team/" + team.getUuid().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(404, response.getStatus(), 0); @@ -482,7 +478,7 @@ public void removeTeamFromRuleInvalidRuleTest() { public void removeTeamFromRuleInvalidTeamTest() { NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.EMAIL.getPublisherName()); NotificationRule rule = qm.createNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(404, response.getStatus(), 0); @@ -496,7 +492,7 @@ public void removeTeamFromRuleDuplicateTeamTest() { Team team = qm.createTeam("Team Example", false); NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.EMAIL.getPublisherName()); NotificationRule rule = qm.createNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(304, response.getStatus(), 0); @@ -508,7 +504,7 @@ public void removeTeamToRuleInvalidPublisherTest(){ Team team = qm.createTeam("Team Example", false); NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); NotificationRule rule = qm.createNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); - Response response = target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() + Response response = jersey.target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(406, response.getStatus(), 0); diff --git a/src/test/java/org/dependencytrack/resources/v1/OidcResourceAuthenticatedTest.java b/src/test/java/org/dependencytrack/resources/v1/OidcResourceAuthenticatedTest.java index 9fc277e561..ad21029915 100644 --- a/src/test/java/org/dependencytrack/resources/v1/OidcResourceAuthenticatedTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/OidcResourceAuthenticatedTest.java @@ -17,17 +17,16 @@ */ package org.dependencytrack.resources.v1; -import alpine.server.filters.ApiFilter; -import alpine.server.filters.AuthenticationFilter; import alpine.model.MappedOidcGroup; import alpine.model.OidcGroup; import alpine.model.Team; +import alpine.server.filters.ApiFilter; +import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.resources.v1.vo.MappedOidcGroupRequest; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -41,12 +40,11 @@ public class OidcResourceAuthenticatedTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer(new ResourceConfig(OidcResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))).build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(OidcResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Test public void retrieveGroupsShouldReturnListOfGroups() { @@ -54,7 +52,7 @@ public void retrieveGroupsShouldReturnListOfGroups() { oidcGroup.setName("groupName"); qm.persist(oidcGroup); - final Response response = target(V1_OIDC + "/group") + final Response response = jersey.target(V1_OIDC + "/group") .request().header(X_API_KEY, apiKey).get(); assertThat(response.getStatus()).isEqualTo(200); @@ -66,7 +64,7 @@ public void retrieveGroupsShouldReturnListOfGroups() { @Test public void retrieveGroupsShouldReturnEmptyListWhenNoGroupsWhereFound() { - final Response response = target(V1_OIDC + "/group") + final Response response = jersey.target(V1_OIDC + "/group") .request().header(X_API_KEY, apiKey).get(); assertThat(response.getStatus()).isEqualTo(200); @@ -80,7 +78,7 @@ public void createGroupShouldReturnCreatedGroup() { final OidcGroup oidcGroup = new OidcGroup(); oidcGroup.setName("groupName"); - final Response response = target(V1_OIDC + "/group") + final Response response = jersey.target(V1_OIDC + "/group") .request() .header(X_API_KEY, apiKey) .put(Entity.entity(oidcGroup, MediaType.APPLICATION_JSON)); @@ -100,7 +98,7 @@ public void createGroupShouldIndicateConflictWhenGroupAlreadyExists() { final OidcGroup oidcGroup = new OidcGroup(); oidcGroup.setName("groupName"); - final Response response = target(V1_OIDC + "/group") + final Response response = jersey.target(V1_OIDC + "/group") .request() .header(X_API_KEY, apiKey) .put(Entity.entity(oidcGroup, MediaType.APPLICATION_JSON)); @@ -113,7 +111,7 @@ public void createGroupShouldIndicateBadRequestWhenRequestIsInvalid() { final OidcGroup oidcGroup = new OidcGroup(); oidcGroup.setName(" "); - final Response response = target(V1_OIDC + "/group") + final Response response = jersey.target(V1_OIDC + "/group") .request() .header(X_API_KEY, apiKey) .put(Entity.entity(oidcGroup, MediaType.APPLICATION_JSON)); @@ -129,7 +127,7 @@ public void updateGroupShouldUpdateAndReturnGroup() { jsonGroup.setUuid(existingGroup.getUuid()); jsonGroup.setName("newGroupName"); - final Response response = target(V1_OIDC + "/group").request() + final Response response = jersey.target(V1_OIDC + "/group").request() .header(X_API_KEY, apiKey) .post(Entity.entity(jsonGroup, MediaType.APPLICATION_JSON)); @@ -144,7 +142,7 @@ public void updateGroupShouldUpdateAndReturnGroup() { public void updateGroupShouldIndicateBadRequestWhenRequestBodyIsInvalid() { final OidcGroup jsonGroup = new OidcGroup(); - final Response response = target(V1_OIDC + "/group").request() + final Response response = jersey.target(V1_OIDC + "/group").request() .header(X_API_KEY, apiKey) .post(Entity.entity(jsonGroup, MediaType.APPLICATION_JSON)); @@ -157,7 +155,7 @@ public void updateGroupShouldIndicateNotFoundWhenGroupDoesNotExist() { jsonGroup.setUuid(UUID.randomUUID()); jsonGroup.setName("groupName"); - final Response response = target(V1_OIDC + "/group").request() + final Response response = jersey.target(V1_OIDC + "/group").request() .header(X_API_KEY, apiKey) .post(Entity.entity(jsonGroup, MediaType.APPLICATION_JSON)); @@ -168,7 +166,7 @@ public void updateGroupShouldIndicateNotFoundWhenGroupDoesNotExist() { public void deleteGroupShouldDeleteGroupAndIndicateNoContent() { final OidcGroup existingOidcGroup = qm.createOidcGroup("groupName"); - final Response response = target(V1_OIDC + "/group/" + existingOidcGroup.getUuid()) + final Response response = jersey.target(V1_OIDC + "/group/" + existingOidcGroup.getUuid()) .request() .header(X_API_KEY, apiKey) .delete(); @@ -179,7 +177,7 @@ public void deleteGroupShouldDeleteGroupAndIndicateNoContent() { @Test public void deleteGroupShouldIndicateNotFoundWhenGroupDoesNotExist() { - final Response response = target(V1_OIDC + "/group/" + UUID.randomUUID()) + final Response response = jersey.target(V1_OIDC + "/group/" + UUID.randomUUID()) .request() .header(X_API_KEY, apiKey) .delete(); @@ -193,7 +191,7 @@ public void retrieveTeamsMappedToGroupShouldReturnTeamsMappedToSpecifiedGroup() final Team team = qm.createTeam("teamName", false); qm.createMappedOidcGroup(team, oidcGroup); - final Response response = target(V1_OIDC + "/group/" + oidcGroup.getUuid() + "/team") + final Response response = jersey.target(V1_OIDC + "/group/" + oidcGroup.getUuid() + "/team") .request().header(X_API_KEY, apiKey).get(); assertThat(response.getStatus()).isEqualTo(200); @@ -205,7 +203,7 @@ public void retrieveTeamsMappedToGroupShouldReturnTeamsMappedToSpecifiedGroup() @Test public void retrieveTeamsMappedToGroupShouldIndicateNotFoundWhenGroupDoesNotExit() { - final Response response = target(V1_OIDC + "/group/" + UUID.randomUUID() + "/team") + final Response response = jersey.target(V1_OIDC + "/group/" + UUID.randomUUID() + "/team") .request().header(X_API_KEY, apiKey).get(); assertThat(response.getStatus()).isEqualTo(404); @@ -215,7 +213,7 @@ public void retrieveTeamsMappedToGroupShouldIndicateNotFoundWhenGroupDoesNotExit public void addMappingShouldIndicateBadRequestWhenRequestIsInvalid() { final MappedOidcGroupRequest request = new MappedOidcGroupRequest("not-a-uuid", "not-a-uuid"); - final Response response = target(V1_OIDC + "/mapping") + final Response response = jersey.target(V1_OIDC + "/mapping") .request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -229,7 +227,7 @@ public void addMappingShouldIndicateNotFoundWhenTeamDoesNotExist() { final MappedOidcGroupRequest request = new MappedOidcGroupRequest(UUID.randomUUID().toString(), group.getUuid().toString()); - final Response response = target(V1_OIDC + "/mapping") + final Response response = jersey.target(V1_OIDC + "/mapping") .request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -243,7 +241,7 @@ public void addMappingShouldIndicateNotFoundWhenGroupDoesNotExist() { final MappedOidcGroupRequest request = new MappedOidcGroupRequest(team.getUuid().toString(), UUID.randomUUID().toString()); - final Response response = target(V1_OIDC + "/mapping") + final Response response = jersey.target(V1_OIDC + "/mapping") .request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -259,7 +257,7 @@ public void addMappingShouldIndicateConflictWhenMappingAlreadyExists() { final MappedOidcGroupRequest request = new MappedOidcGroupRequest(team.getUuid().toString(), group.getUuid().toString()); - final Response response = target(V1_OIDC + "/mapping") + final Response response = jersey.target(V1_OIDC + "/mapping") .request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -274,7 +272,7 @@ public void addMappingShouldReturnCreatedMapping() { final MappedOidcGroupRequest request = new MappedOidcGroupRequest(team.getUuid().toString(), group.getUuid().toString()); - final Response response = target(V1_OIDC + "/mapping") + final Response response = jersey.target(V1_OIDC + "/mapping") .request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -294,7 +292,7 @@ public void deleteMappingByUuidShouldDeleteMappingAndIndicateNoContent() { final OidcGroup group = qm.createOidcGroup("groupName"); final MappedOidcGroup mapping = qm.createMappedOidcGroup(team, group); - final Response response = target(V1_OIDC + "/mapping/" + mapping.getUuid()) + final Response response = jersey.target(V1_OIDC + "/mapping/" + mapping.getUuid()) .request() .header(X_API_KEY, apiKey) .delete(); @@ -305,7 +303,7 @@ public void deleteMappingByUuidShouldDeleteMappingAndIndicateNoContent() { @Test public void deleteMappingByUuidShouldIndicateNotFoundWhenMappingDoesNotExist() { - final Response response = target(V1_OIDC + "/mapping/" + UUID.randomUUID()) + final Response response = jersey.target(V1_OIDC + "/mapping/" + UUID.randomUUID()) .request() .header(X_API_KEY, apiKey) .delete(); @@ -319,7 +317,7 @@ public void deleteMappingShouldDeleteMappingAndIndicateNoContent() { final Team team = qm.createTeam("teamName", false); final MappedOidcGroup mapping = qm.createMappedOidcGroup(team, oidcGroup); - final Response response = target(V1_OIDC + "/group/" + oidcGroup.getUuid() + "/team/" + team.getUuid() + "/mapping").request() + final Response response = jersey.target(V1_OIDC + "/group/" + oidcGroup.getUuid() + "/team/" + team.getUuid() + "/mapping").request() .header(X_API_KEY, apiKey) .delete(); @@ -331,7 +329,7 @@ public void deleteMappingShouldDeleteMappingAndIndicateNoContent() { public void deleteMappingShouldIndicateNotFoundWhenTeamDoesNotExist() { final OidcGroup oidcGroup = qm.createOidcGroup("groupName"); - final Response response = target(V1_OIDC + "/group/" + oidcGroup.getUuid() + "/team/" + UUID.randomUUID() + "/mapping").request() + final Response response = jersey.target(V1_OIDC + "/group/" + oidcGroup.getUuid() + "/team/" + UUID.randomUUID() + "/mapping").request() .header(X_API_KEY, apiKey) .delete(); @@ -342,7 +340,7 @@ public void deleteMappingShouldIndicateNotFoundWhenTeamDoesNotExist() { public void deleteMappingShouldIndicateNotFoundWhenGroupDoesNotExist() { final Team team = qm.createTeam("teamName", false); - final Response response = target(V1_OIDC + "/group/" + UUID.randomUUID() + "/team/" + team.getUuid() + "/mapping").request() + final Response response = jersey.target(V1_OIDC + "/group/" + UUID.randomUUID() + "/team/" + team.getUuid() + "/mapping").request() .header(X_API_KEY, apiKey) .delete(); @@ -354,7 +352,7 @@ public void deleteMappingShouldIndicateNotFoundWhenMappingDoesNotExist() { final OidcGroup oidcGroup = qm.createOidcGroup("groupName"); final Team team = qm.createTeam("teamName", false); - final Response response = target(V1_OIDC + "/group/" + oidcGroup.getUuid() + "/team/" + team.getUuid() + "/mapping").request() + final Response response = jersey.target(V1_OIDC + "/group/" + oidcGroup.getUuid() + "/team/" + team.getUuid() + "/mapping").request() .header(X_API_KEY, apiKey) .delete(); diff --git a/src/test/java/org/dependencytrack/resources/v1/OidcResourceUnauthenticatedTest.java b/src/test/java/org/dependencytrack/resources/v1/OidcResourceUnauthenticatedTest.java index 3953d9a32c..45cd0d7031 100644 --- a/src/test/java/org/dependencytrack/resources/v1/OidcResourceUnauthenticatedTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/OidcResourceUnauthenticatedTest.java @@ -18,11 +18,10 @@ package org.dependencytrack.resources.v1; import alpine.server.filters.ApiFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; +import org.junit.ClassRule; import org.junit.Test; import javax.ws.rs.core.Response; @@ -31,17 +30,14 @@ public class OidcResourceUnauthenticatedTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(OidcResource.class) - .register(ApiFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(OidcResource.class) + .register(ApiFilter.class)); @Test public void isAvailableShouldReturnFalseWhenOidcIsNotAvailable() { - final Response response = target(V1_OIDC + "/available") + final Response response = jersey.target(V1_OIDC + "/available") .request().get(); assertThat(getPlainTextBody(response)).isEqualTo("false"); diff --git a/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java index 330ed8c3a0..9a0ad96db3 100644 --- a/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java @@ -18,21 +18,20 @@ */ package org.dependencytrack.resources.v1; -import alpine.server.filters.ApiFilter; -import alpine.server.filters.AuthenticationFilter; import alpine.model.ManagedUser; import alpine.model.Permission; import alpine.model.Team; import alpine.server.auth.PasswordService; +import alpine.server.filters.ApiFilter; +import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; import org.dependencytrack.persistence.DefaultObjectGenerator; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -44,14 +43,11 @@ public class PermissionResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(PermissionResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(PermissionResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Before public void before() throws Exception { @@ -62,7 +58,7 @@ public void before() throws Exception { @Test public void getAllPermissionsTest() { - Response response = target(V1_PERMISSION).request() + Response response = jersey.target(V1_PERMISSION).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -79,7 +75,7 @@ public void addPermissionToUserTest() { ManagedUser user = qm.createManagedUser("user1", new String(PasswordService.createHash("password".toCharArray()))); String username = user.getUsername(); qm.close(); - Response response = target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/user/" + username).request() + Response response = jersey.target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/user/" + username).request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -92,7 +88,7 @@ public void addPermissionToUserTest() { @Test public void addPermissionToUserInvalidUserTest() { - Response response = target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/user/blah").request() + Response response = jersey.target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/user/blah").request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -106,7 +102,7 @@ public void addPermissionToUserInvalidPermissionTest() { ManagedUser user = qm.createManagedUser("user1", new String(PasswordService.createHash("password".toCharArray()))); String username = user.getUsername(); qm.close(); - Response response = target(V1_PERMISSION + "/BLAH/user/" + username).request() + Response response = jersey.target(V1_PERMISSION + "/BLAH/user/" + username).request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -123,7 +119,7 @@ public void addPermissionToUserDuplicateTest() { user.getPermissions().add(permission); qm.persist(user); qm.close(); - Response response = target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/user/" + username).request() + Response response = jersey.target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/user/" + username).request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(304, response.getStatus(), 0); @@ -138,7 +134,7 @@ public void removePermissionFromUserTest() { user.getPermissions().add(permission); qm.persist(user); qm.close(); - Response response = target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/user/" + username).request() + Response response = jersey.target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/user/" + username).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(200, response.getStatus(), 0); @@ -150,7 +146,7 @@ public void removePermissionFromUserTest() { @Test public void removePermissionFromUserInvalidUserTest() { - Response response = target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/user/blah").request() + Response response = jersey.target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/user/blah").request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(404, response.getStatus(), 0); @@ -164,7 +160,7 @@ public void removePermissionFromUserInvalidPermissionTest() { ManagedUser user = qm.createManagedUser("user1", new String(PasswordService.createHash("password".toCharArray()))); String username = user.getUsername(); qm.close(); - Response response = target(V1_PERMISSION + "/BLAH/user/" + username).request() + Response response = jersey.target(V1_PERMISSION + "/BLAH/user/" + username).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(404, response.getStatus(), 0); @@ -177,7 +173,7 @@ public void removePermissionFromUserInvalidPermissionTest() { public void removePermissionFromUserNoChangesTest() { ManagedUser user = qm.createManagedUser("user1", new String(PasswordService.createHash("password".toCharArray()))); String username = user.getUsername(); - Response response = target(V1_PERMISSION + "/BOM_UPLOAD/user/" + username).request() + Response response = jersey.target(V1_PERMISSION + "/BOM_UPLOAD/user/" + username).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(304, response.getStatus(), 0); @@ -189,7 +185,7 @@ public void addPermissionToTeamTest() { Team team = qm.createTeam("team1", false); String teamUuid = team.getUuid().toString(); qm.close(); - Response response = target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/team/" + teamUuid).request() + Response response = jersey.target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/team/" + teamUuid).request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -202,7 +198,7 @@ public void addPermissionToTeamTest() { @Test public void addPermissionToTeamInvalidTeamTest() { - Response response = target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/team/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/team/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -216,7 +212,7 @@ public void addPermissionToTeamInvalidPermissionTest() { Team team = qm.createTeam("team1", false); String teamUuid = team.getUuid().toString(); qm.close(); - Response response = target(V1_PERMISSION + "/BLAH/team/" + teamUuid).request() + Response response = jersey.target(V1_PERMISSION + "/BLAH/team/" + teamUuid).request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -233,7 +229,7 @@ public void addPermissionToTeamDuplicateTest() { team.getPermissions().add(permission); qm.persist(team); qm.close(); - Response response = target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/team/" + teamUuid).request() + Response response = jersey.target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/team/" + teamUuid).request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(304, response.getStatus(), 0); @@ -248,7 +244,7 @@ public void removePermissionFromTeamTest() { team.getPermissions().add(permission); qm.persist(team); qm.close(); - Response response = target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/team/" + teamUuid).request() + Response response = jersey.target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/team/" + teamUuid).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(200, response.getStatus(), 0); @@ -260,7 +256,7 @@ public void removePermissionFromTeamTest() { @Test public void removePermissionFromTeamInvalidTeamTest() { - Response response = target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/team/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/team/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(404, response.getStatus(), 0); @@ -274,7 +270,7 @@ public void removePermissionFromTeamInvalidPermissionTest() { Team team = qm.createTeam("team1", false); String teamUuid = team.getUuid().toString(); qm.close(); - Response response = target(V1_PERMISSION + "/BLAH/team/" + teamUuid).request() + Response response = jersey.target(V1_PERMISSION + "/BLAH/team/" + teamUuid).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(404, response.getStatus(), 0); @@ -287,7 +283,7 @@ public void removePermissionFromTeamInvalidPermissionTest() { public void removePermissionFromTeamNoChangesTest() { Team team = qm.createTeam("team1", false); String teamUuid = team.getUuid().toString(); - Response response = target(V1_PERMISSION + "/BOM_UPLOAD/team/" + teamUuid).request() + Response response = jersey.target(V1_PERMISSION + "/BOM_UPLOAD/team/" + teamUuid).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(304, response.getStatus(), 0); diff --git a/src/test/java/org/dependencytrack/resources/v1/PolicyResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/PolicyResourceTest.java index a4c6f3544d..ee1fba255c 100644 --- a/src/test/java/org/dependencytrack/resources/v1/PolicyResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/PolicyResourceTest.java @@ -22,6 +22,7 @@ import alpine.common.util.UuidUtil; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.Component; import org.dependencytrack.model.Policy; @@ -30,9 +31,7 @@ import org.dependencytrack.model.Project; import org.dependencytrack.model.Tag; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -47,14 +46,11 @@ public class PolicyResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(PolicyResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(PolicyResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Test public void getPoliciesTest() { @@ -62,7 +58,7 @@ public void getPoliciesTest() { qm.createPolicy("policy" + i, Policy.Operator.ANY, Policy.ViolationState.INFO); } - final Response response = target(V1_POLICY) + final Response response = jersey.target(V1_POLICY) .request() .header(X_API_KEY, apiKey) .get(); @@ -80,7 +76,7 @@ public void getPoliciesTest() { public void getPolicyByUuidTest() { final Policy policy = qm.createPolicy("policy", Policy.Operator.ANY, Policy.ViolationState.INFO); - final Response response = target(V1_POLICY + "/" + policy.getUuid()) + final Response response = jersey.target(V1_POLICY + "/" + policy.getUuid()) .request() .header(X_API_KEY, apiKey) .get(); @@ -100,7 +96,7 @@ public void createPolicyTest() { policy.setOperator(Policy.Operator.ANY); policy.setViolationState(Policy.ViolationState.INFO); - final Response response = target(V1_POLICY) + final Response response = jersey.target(V1_POLICY) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(policy, MediaType.APPLICATION_JSON)); @@ -123,7 +119,7 @@ public void createPolicySpecifyOperatorAndViolationStateTest() { policy.setOperator(Policy.Operator.ALL); policy.setViolationState(Policy.ViolationState.FAIL); - final Response response = target(V1_POLICY) + final Response response = jersey.target(V1_POLICY) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(policy, MediaType.APPLICATION_JSON)); @@ -144,7 +140,7 @@ public void createPolicyUseDefaultValueTest() { final Policy policy = new Policy(); policy.setName("policy"); - final Response response = target(V1_POLICY) + final Response response = jersey.target(V1_POLICY) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(policy, MediaType.APPLICATION_JSON)); @@ -166,7 +162,7 @@ public void updatePolicyTest() { policy.setViolationState(Policy.ViolationState.FAIL); policy.setIncludeChildren(true); - final Response response = target(V1_POLICY) + final Response response = jersey.target(V1_POLICY) .request() .header(X_API_KEY, apiKey) .post(Entity.entity(policy, MediaType.APPLICATION_JSON)); @@ -185,7 +181,7 @@ public void updatePolicyTest() { public void deletePolicyTest() { final Policy policy = qm.createPolicy("policy", Policy.Operator.ANY, Policy.ViolationState.INFO); - final Response response = target(V1_POLICY + "/" + policy.getUuid()) + final Response response = jersey.target(V1_POLICY + "/" + policy.getUuid()) .request() .header(X_API_KEY, apiKey) .delete(); @@ -218,7 +214,7 @@ public void deletePolicyCascadingTest() { qm.reconcilePolicyViolations(component, singletonList(violation)); - final Response response = target(V1_POLICY + "/" + policy.getUuid()) + final Response response = jersey.target(V1_POLICY + "/" + policy.getUuid()) .request() .header(X_API_KEY, apiKey) .delete(); @@ -233,7 +229,7 @@ public void addProjectToPolicyTest() { final Policy policy = qm.createPolicy("policy", Policy.Operator.ANY, Policy.ViolationState.INFO); final Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false); - final Response response = target(V1_POLICY + "/" + policy.getUuid() + "/project/" + project.getUuid()) + final Response response = jersey.target(V1_POLICY + "/" + policy.getUuid() + "/project/" + project.getUuid()) .request() .header(X_API_KEY, apiKey) .post(null); @@ -253,7 +249,7 @@ public void addProjectToPolicyProjectAlreadyAddedTest() { policy.setProjects(singletonList(project)); qm.persist(policy); - final Response response = target(V1_POLICY + "/" + policy.getUuid() + "/project/" + project.getUuid()) + final Response response = jersey.target(V1_POLICY + "/" + policy.getUuid() + "/project/" + project.getUuid()) .request() .header(X_API_KEY, apiKey) .post(null); @@ -269,7 +265,7 @@ public void removeProjectFromPolicyTest() { policy.setProjects(singletonList(project)); qm.persist(policy); - final Response response = target(V1_POLICY + "/" + policy.getUuid() + "/project/" + project.getUuid()) + final Response response = jersey.target(V1_POLICY + "/" + policy.getUuid() + "/project/" + project.getUuid()) .request() .header(X_API_KEY, apiKey) .delete(); @@ -282,7 +278,7 @@ public void removeProjectFromPolicyProjectAlreadyRemovedTest() { final Policy policy = qm.createPolicy("policy", Policy.Operator.ANY, Policy.ViolationState.INFO); final Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false); - final Response response = target(V1_POLICY + "/" + policy.getUuid() + "/project/" + project.getUuid()) + final Response response = jersey.target(V1_POLICY + "/" + policy.getUuid() + "/project/" + project.getUuid()) .request() .header(X_API_KEY, apiKey) .delete(); @@ -296,7 +292,7 @@ public void addTagToPolicyTest() { final Tag tag = qm.createTag("Policy Tag"); System.out.println("Tag being created is "+qm.getTagByName("Policy Tag")); - final Response response = target(V1_POLICY + "/" + policy.getUuid() + "/tag/" + tag.getName()) + final Response response = jersey.target(V1_POLICY + "/" + policy.getUuid() + "/tag/" + tag.getName()) .request() .header(X_API_KEY, apiKey) .post(null); @@ -316,7 +312,7 @@ public void addTagToPolicyTagAlreadyAddedTest() { policy.setTags(singletonList(tag)); qm.persist(policy); - final Response response = target(V1_POLICY + "/" + policy.getUuid() + "/tag/" + tag.getName()) + final Response response = jersey.target(V1_POLICY + "/" + policy.getUuid() + "/tag/" + tag.getName()) .request() .header(X_API_KEY, apiKey) .post(null); @@ -332,7 +328,7 @@ public void removeTagFromPolicyTest() { policy.setTags(singletonList(tag)); qm.persist(policy); - final Response response = target(V1_POLICY + "/" + policy.getUuid() + "/tag/" + tag.getName()) + final Response response = jersey.target(V1_POLICY + "/" + policy.getUuid() + "/tag/" + tag.getName()) .request() .header(X_API_KEY, apiKey) .delete(); @@ -345,7 +341,7 @@ public void removeTagFromPolicyTagDoesNotExistTest() { final Policy policy = qm.createPolicy("policy", Policy.Operator.ANY, Policy.ViolationState.INFO); final Tag tag = qm.createTag("Policy Tag"); - final Response response = target(V1_POLICY + "/" + policy.getUuid() + "/tag/" + tag.getName()) + final Response response = jersey.target(V1_POLICY + "/" + policy.getUuid() + "/tag/" + tag.getName()) .request() .header(X_API_KEY, apiKey) .delete(); diff --git a/src/test/java/org/dependencytrack/resources/v1/PolicyViolationResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/PolicyViolationResourceTest.java index cfc0464c3b..c78ca186e3 100644 --- a/src/test/java/org/dependencytrack/resources/v1/PolicyViolationResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/PolicyViolationResourceTest.java @@ -21,6 +21,7 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import alpine.server.filters.AuthorizationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Component; @@ -29,9 +30,7 @@ import org.dependencytrack.model.PolicyViolation; import org.dependencytrack.model.Project; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -45,15 +44,12 @@ public class PolicyViolationResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(PolicyViolationResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class) - .register(AuthorizationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(PolicyViolationResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class) + .register(AuthorizationFilter.class)); @Test public void getViolationsTest() { @@ -77,7 +73,7 @@ public void getViolationsTest() { violation.setTimestamp(new Date()); violation = qm.persist(violation); - final Response response = target(V1_POLICY_VIOLATION) + final Response response = jersey.target(V1_POLICY_VIOLATION) .request() .header(X_API_KEY, apiKey) .get(); @@ -99,7 +95,7 @@ public void getViolationsTest() { @Test public void getViolationsUnauthorizedTest() { - final Response response = target(V1_POLICY_VIOLATION) + final Response response = jersey.target(V1_POLICY_VIOLATION) .request() .header(X_API_KEY, apiKey) .get(); @@ -148,7 +144,7 @@ public void getViolationsByProjectTest() { } } - final Response response = target(V1_POLICY_VIOLATION) + final Response response = jersey.target(V1_POLICY_VIOLATION) .queryParam("searchText", "0") .path("/project/" + project.getUuid()) .request() @@ -207,7 +203,7 @@ public void getViolationsByProjectIssue2766() { qm.persist(violation); // Requesting violations for projectB must not yield violations for projectA. - final Response response = target(V1_POLICY_VIOLATION) + final Response response = jersey.target(V1_POLICY_VIOLATION) .path("/project/" + projectB.getUuid()) .request() .header(X_API_KEY, apiKey) @@ -221,7 +217,7 @@ public void getViolationsByProjectIssue2766() { @Test public void getViolationsByProjectUnauthorizedTest() { - final Response response = target(V1_POLICY_VIOLATION) + final Response response = jersey.target(V1_POLICY_VIOLATION) .path("/project/" + UUID.randomUUID()) .request() .header(X_API_KEY, apiKey) @@ -234,7 +230,7 @@ public void getViolationsByProjectUnauthorizedTest() { public void getViolationsByProjectNotFoundTest() { initializeWithPermissions(Permissions.VIEW_POLICY_VIOLATION); - final Response response = target(V1_POLICY_VIOLATION) + final Response response = jersey.target(V1_POLICY_VIOLATION) .path("/project/" + UUID.randomUUID()) .request() .header(X_API_KEY, apiKey) @@ -266,7 +262,7 @@ public void getViolationsByComponentTest() { violation.setTimestamp(new Date()); violation = qm.persist(violation); - final Response response = target(V1_POLICY_VIOLATION) + final Response response = jersey.target(V1_POLICY_VIOLATION) .path("/component/" + component.getUuid()) .request() .header(X_API_KEY, apiKey) @@ -288,7 +284,7 @@ public void getViolationsByComponentTest() { @Test public void getViolationsByComponentUnauthorizedTest() { - final Response response = target(V1_POLICY_VIOLATION) + final Response response = jersey.target(V1_POLICY_VIOLATION) .path("/component/" + UUID.randomUUID()) .request() .header(X_API_KEY, apiKey) @@ -301,7 +297,7 @@ public void getViolationsByComponentUnauthorizedTest() { public void getViolationsByComponentNotFoundTest() { initializeWithPermissions(Permissions.VIEW_POLICY_VIOLATION); - final Response response = target(V1_POLICY_VIOLATION) + final Response response = jersey.target(V1_POLICY_VIOLATION) .path("/component/" + UUID.randomUUID()) .request() .header(X_API_KEY, apiKey) diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectPropertyResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectPropertyResourceTest.java index 399b6b7605..961b2d36c1 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectPropertyResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectPropertyResourceTest.java @@ -18,18 +18,17 @@ */ package org.dependencytrack.resources.v1; +import alpine.model.IConfigProperty; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; -import alpine.model.IConfigProperty; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectProperty; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -41,21 +40,18 @@ public class ProjectPropertyResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(ProjectPropertyResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(ProjectPropertyResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Test public void getPropertiesTest() { Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); qm.createProjectProperty(project, "mygroup", "prop1", "value1", IConfigProperty.PropertyType.STRING, "Test Property 1"); qm.createProjectProperty(project, "mygroup", "prop2", "value2", IConfigProperty.PropertyType.ENCRYPTEDSTRING, "Test Property 2"); - Response response = target(V1_PROJECT + "/" + project.getUuid().toString() + "/property").request() + Response response = jersey.target(V1_PROJECT + "/" + project.getUuid().toString() + "/property").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -77,7 +73,7 @@ public void getPropertiesTest() { @Test public void getPropertiesInvalidTest() { - Response response = target(V1_PROJECT + "/" + UUID.randomUUID().toString() + "/property").request() + Response response = jersey.target(V1_PROJECT + "/" + UUID.randomUUID().toString() + "/property").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); @@ -96,7 +92,7 @@ public void createPropertyTest() { property.setPropertyValue("value1"); property.setPropertyType(IConfigProperty.PropertyType.STRING); property.setDescription("Test Property 1"); - Response response = target(V1_PROJECT + "/" + project.getUuid().toString() + "/property").request() + Response response = jersey.target(V1_PROJECT + "/" + project.getUuid().toString() + "/property").request() .header(X_API_KEY, apiKey) .put(Entity.entity(property, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus(), 0); @@ -119,7 +115,7 @@ public void createPropertyEncryptedTest() { property.setPropertyValue("value1"); property.setPropertyType(IConfigProperty.PropertyType.ENCRYPTEDSTRING); property.setDescription("Test Property 1"); - Response response = target(V1_PROJECT + "/" + project.getUuid().toString() + "/property").request() + Response response = jersey.target(V1_PROJECT + "/" + project.getUuid().toString() + "/property").request() .header(X_API_KEY, apiKey) .put(Entity.entity(property, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus(), 0); @@ -145,7 +141,7 @@ public void createPropertyDuplicateTest() { property.setPropertyValue("value1"); property.setPropertyType(IConfigProperty.PropertyType.STRING); property.setDescription("Test Property 1"); - Response response = target(V1_PROJECT + "/" + uuid + "/property").request() + Response response = jersey.target(V1_PROJECT + "/" + uuid + "/property").request() .header(X_API_KEY, apiKey) .put(Entity.entity(property, MediaType.APPLICATION_JSON)); Assert.assertEquals(409, response.getStatus(), 0); @@ -164,7 +160,7 @@ public void createPropertyInvalidTest() { property.setPropertyValue("value1"); property.setPropertyType(IConfigProperty.PropertyType.STRING); property.setDescription("Test Property 1"); - Response response = target(V1_PROJECT + "/" + UUID.randomUUID() + "/property").request() + Response response = jersey.target(V1_PROJECT + "/" + UUID.randomUUID() + "/property").request() .header(X_API_KEY, apiKey) .put(Entity.entity(property, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -181,7 +177,7 @@ public void updatePropertyTest() { qm.getPersistenceManager().detachCopy(property); qm.close(); property.setPropertyValue("updatedValue"); - Response response = target(V1_PROJECT + "/" + uuid + "/property").request() + Response response = jersey.target(V1_PROJECT + "/" + uuid + "/property").request() .header(X_API_KEY, apiKey) .post(Entity.entity(property, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -203,7 +199,7 @@ public void updatePropertyInvalidTest() { property.setPropertyValue("value1"); property.setPropertyType(IConfigProperty.PropertyType.STRING); property.setDescription("Test Property 1"); - Response response = target(V1_PROJECT + "/" + UUID.randomUUID().toString() + "/property").request() + Response response = jersey.target(V1_PROJECT + "/" + UUID.randomUUID().toString() + "/property").request() .header(X_API_KEY, apiKey) .post(Entity.entity(property, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -219,7 +215,7 @@ public void deletePropertyTest() { String uuid = project.getUuid().toString(); qm.getPersistenceManager().detachCopy(property); qm.close(); - Response response = target(V1_PROJECT + "/" + uuid + "/property").request() + Response response = jersey.target(V1_PROJECT + "/" + uuid + "/property").request() .header(X_API_KEY, apiKey) .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) // HACK .method("DELETE", Entity.entity(property, MediaType.APPLICATION_JSON)); // HACK diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index b3dc5852d9..dd3229bb7a 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -24,6 +24,7 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import org.cyclonedx.model.ExternalReference.Type; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.event.CloneProjectEvent; import org.dependencytrack.model.Analysis; @@ -45,12 +46,10 @@ import org.dependencytrack.tasks.scanners.AnalyzerIdentity; import org.glassfish.jersey.client.HttpUrlConnectorProvider; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.hamcrest.CoreMatchers; import org.junit.After; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import javax.json.Json; @@ -74,20 +73,17 @@ public class ProjectResourceTest extends ResourceTest { - @After - public void tearDown() throws Exception { - EventService.getInstance().unsubscribe(CloneProjectTask.class); - - super.tearDown(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(ProjectResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); + @After @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(ProjectResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); + public void after() throws Exception { + EventService.getInstance().unsubscribe(CloneProjectTask.class); + super.after(); } @Test @@ -95,7 +91,7 @@ public void getProjectsDefaultRequestTest() { for (int i=0; i<1000; i++) { qm.createProject("Acme Example", null, String.valueOf(i), null, null, null, true, false); } - Response response = target(V1_PROJECT) + Response response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .get(Response.class); @@ -127,7 +123,7 @@ public void getProjectsWithAclEnabledTest() { // Create a second project that the current principal has no access to. qm.createProject("acme-app-b", null, "2.0.0", null, null, null, true, false); - final Response response = target(V1_PROJECT) + final Response response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .get(Response.class); @@ -145,7 +141,7 @@ public void getProjectsByNameRequestTest() { for (int i=0; i<1000; i++) { qm.createProject("Acme Example", null, String.valueOf(i), null, null, null, true, false); } - Response response = target(V1_PROJECT) + Response response = jersey.target(V1_PROJECT) .queryParam("name", "Acme Example") .request() .header(X_API_KEY, apiKey) @@ -164,7 +160,7 @@ public void getProjectsByInvalidNameRequestTest() { for (int i=0; i<1000; i++) { qm.createProject("Acme Example", null, String.valueOf(i), null, null, null, true, false); } - Response response = target(V1_PROJECT) + Response response = jersey.target(V1_PROJECT) .queryParam("name", "blah") .request() .header(X_API_KEY, apiKey) @@ -184,7 +180,7 @@ public void getProjectsByNameActiveOnlyRequestTest() { for (int i=500; i<1000; i++) { qm.createProject("Acme Example", null, String.valueOf(i), null, null, null, false, false); } - Response response = target(V1_PROJECT) + Response response = jersey.target(V1_PROJECT) .queryParam("name", "Acme Example") .queryParam("excludeInactive", "true") .request() @@ -202,7 +198,7 @@ public void getProjectLookupTest() { for (int i=0; i<500; i++) { qm.createProject("Acme Example", null, String.valueOf(i), null, null, null, false, false); } - Response response = target(V1_PROJECT+"/lookup") + Response response = jersey.target(V1_PROJECT+"/lookup") .queryParam("name", "Acme Example") .queryParam("version", "10") .request() @@ -224,7 +220,7 @@ public void getProjectLookupTest() { public void getProjectsAscOrderedRequestTest() { qm.createProject("ABC", null, "1.0", null, null, null, true, false); qm.createProject("DEF", null, "1.0", null, null, null, true, false); - Response response = target(V1_PROJECT) + Response response = jersey.target(V1_PROJECT) .queryParam(ORDER_BY, "name") .queryParam(SORT, SORT_ASC) .request() @@ -241,7 +237,7 @@ public void getProjectsAscOrderedRequestTest() { public void getProjectsDescOrderedRequestTest() { qm.createProject("ABC", null, "1.0", null, null, null, true, false); qm.createProject("DEF", null, "1.0", null, null, null, true, false); - Response response = target(V1_PROJECT) + Response response = jersey.target(V1_PROJECT) .queryParam(ORDER_BY, "name") .queryParam(SORT, SORT_DESC) .request() @@ -257,7 +253,7 @@ public void getProjectsDescOrderedRequestTest() { @Test public void getProjectByUuidTest() { Project project = qm.createProject("ABC", null, "1.0", null, null, null, true, false); - Response response = target(V1_PROJECT + "/" + project.getUuid()) + Response response = jersey.target(V1_PROJECT + "/" + project.getUuid()) .request() .header(X_API_KEY, apiKey) .get(Response.class); @@ -274,7 +270,7 @@ public void getProjectByUuidTest() { @Test public void getProjectByInvalidUuidTest() { qm.createProject("ABC", null, "1.0", null, null, null, true, false); - Response response = target(V1_PROJECT + "/" + UUID.randomUUID()) + Response response = jersey.target(V1_PROJECT + "/" + UUID.randomUUID()) .request() .header(X_API_KEY, apiKey) .get(Response.class); @@ -291,7 +287,7 @@ public void getProjectByTagTest() { tags.add(tag); qm.createProject("ABC", null, "1.0", tags, null, null, true, false); qm.createProject("DEF", null, "1.0", null, null, null, true, false); - Response response = target(V1_PROJECT + "/tag/" + "production") + Response response = jersey.target(V1_PROJECT + "/tag/" + "production") .request() .header(X_API_KEY, apiKey) .get(Response.class); @@ -309,7 +305,7 @@ public void getProjectByCaseInsensitiveTagTest() { tags.add(tag); qm.createProject("ABC", null, "1.0", tags, null, null, true, false); qm.createProject("DEF", null, "1.0", null, null, null, true, false); - Response response = target(V1_PROJECT + "/tag/" + "production") + Response response = jersey.target(V1_PROJECT + "/tag/" + "production") .request() .header(X_API_KEY, apiKey) .get(Response.class); @@ -327,7 +323,7 @@ public void getProjectByUnknownTagTest() { tags.add(tag); qm.createProject("ABC", null, "1.0", tags, null, null, true, false); qm.createProject("DEF", null, "1.0", null, null, null, true, false); - Response response = target(V1_PROJECT + "/tag/" + "stable") + Response response = jersey.target(V1_PROJECT + "/tag/" + "stable") .request() .header(X_API_KEY, apiKey) .get(Response.class); @@ -344,7 +340,7 @@ public void createProjectTest(){ project.setName("Acme Example"); project.setVersion("1.0"); project.setDescription("Test project"); - Response response = target(V1_PROJECT) + Response response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(project, MediaType.APPLICATION_JSON)); @@ -363,12 +359,12 @@ public void createProjectDuplicateTest() { Project project = new Project(); project.setName("Acme Example"); project.setVersion("1.0"); - Response response = target(V1_PROJECT) + Response response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(project, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus(), 0); - response = target(V1_PROJECT) + response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(project, MediaType.APPLICATION_JSON)); @@ -381,12 +377,12 @@ public void createProjectDuplicateTest() { public void createProjectWithoutVersionDuplicateTest() { Project project = new Project(); project.setName("Acme Example"); - Response response = target(V1_PROJECT) + Response response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(project, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus(), 0); - response = target(V1_PROJECT) + response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(project, MediaType.APPLICATION_JSON)); @@ -399,7 +395,7 @@ public void createProjectWithoutVersionDuplicateTest() { public void createProjectEmptyTest() { Project project = new Project(); project.setName(" "); - Response response = target(V1_PROJECT) + Response response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(project, MediaType.APPLICATION_JSON)); @@ -410,7 +406,7 @@ public void createProjectEmptyTest() { public void updateProjectTest() { Project project = qm.createProject("ABC", null, "1.0", null, null, null, true, false); project.setDescription("Test project"); - Response response = target(V1_PROJECT) + Response response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .post(Entity.entity(project, MediaType.APPLICATION_JSON)); @@ -428,7 +424,7 @@ public void updateProjectTestIsActiveEqualsNull() { project.setDescription("Test project"); project.setActive(null); Assert.assertNull(project.isActive()); - Response response = target(V1_PROJECT) + Response response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .post(Entity.entity(project, MediaType.APPLICATION_JSON)); @@ -456,7 +452,7 @@ public void updateProjectTagsTest() { }).collect(Collectors.toList())); // update the 1st time and add another tag - var response = target(V1_PROJECT) + var response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .post(Entity.entity(jsonProject, MediaType.APPLICATION_JSON)); @@ -473,7 +469,7 @@ public void updateProjectTagsTest() { Assert.assertEquals("tag3", jsonTags.get(2).asJsonObject().getString("name")); // and update again with the same tags ... issue #1165 - response = target(V1_PROJECT) + response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .post(Entity.entity(jsonProject, MediaType.APPLICATION_JSON)); @@ -486,7 +482,7 @@ public void updateProjectTagsTest() { // and finally delete one of the tags jsonProject.getTags().remove(0); - response = target(V1_PROJECT) + response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .post(Entity.entity(jsonProject, MediaType.APPLICATION_JSON)); @@ -501,7 +497,7 @@ public void updateProjectTagsTest() { public void updateProjectEmptyNameTest() { Project project = qm.createProject("ABC", null, "1.0", null, null, null, true, false); project.setName(" "); - Response response = target(V1_PROJECT) + Response response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .post(Entity.entity(project, MediaType.APPLICATION_JSON)); @@ -513,7 +509,7 @@ public void updateProjectDuplicateTest() { qm.createProject("ABC", null, "1.0", null, null, null, true, false); Project project = qm.createProject("DEF", null, "1.0", null, null, null, true, false); project.setName("ABC"); - Response response = target(V1_PROJECT) + Response response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .post(Entity.entity(project, MediaType.APPLICATION_JSON)); @@ -525,7 +521,7 @@ public void updateProjectDuplicateTest() { @Test public void deleteProjectTest() { Project project = qm.createProject("ABC", null, "1.0", null, null, null, true, false); - Response response = target(V1_PROJECT + "/" + project.getUuid().toString()) + Response response = jersey.target(V1_PROJECT + "/" + project.getUuid().toString()) .request() .header(X_API_KEY, apiKey) .delete(); @@ -535,7 +531,7 @@ public void deleteProjectTest() { @Test public void deleteProjectInvalidUuidTest() { qm.createProject("ABC", null, "1.0", null, null, null, true, false); - Response response = target(V1_PROJECT + "/" + UUID.randomUUID().toString()) + Response response = jersey.target(V1_PROJECT + "/" + UUID.randomUUID().toString()) .request() .header(X_API_KEY, apiKey) .delete(); @@ -549,7 +545,7 @@ public void patchProjectNotModifiedTest() { final var jsonProject = new Project(); jsonProject.setDescription(p1.getDescription()); - final var response = target(V1_PROJECT + "/" + p1.getUuid()) + final var response = jersey.target(V1_PROJECT + "/" + p1.getUuid()) .request() .header(X_API_KEY, apiKey) .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true) @@ -565,7 +561,7 @@ public void patchProjectNameVersionConflictTest() { qm.createProject("ABC", "Test project", "0.9", null, null, null, false, false); final var jsonProject = new Project(); jsonProject.setVersion("0.9"); - final var response = target(V1_PROJECT + "/" + p1.getUuid()) + final var response = jersey.target(V1_PROJECT + "/" + p1.getUuid()) .request() .header(X_API_KEY, apiKey) .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true) @@ -576,7 +572,7 @@ public void patchProjectNameVersionConflictTest() { @Test public void patchProjectNotFoundTest() { - final var response = target(V1_PROJECT + "/" + UUID.randomUUID()) + final var response = jersey.target(V1_PROJECT + "/" + UUID.randomUUID()) .request() .header(X_API_KEY, apiKey) .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true) @@ -626,7 +622,7 @@ public void patchProjectSuccessfullyPatchedTest() { jsonProjectSupplier.setUrls(new String[]{"https://supplier.example.com"}); jsonProjectSupplier.setContacts(List.of(jsonProjectSupplierContact)); jsonProject.setSupplier(jsonProjectSupplier); - final var response = target(V1_PROJECT + "/" + p1.getUuid()) + final var response = jersey.target(V1_PROJECT + "/" + p1.getUuid()) .request() .header(X_API_KEY, apiKey) .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true) @@ -688,7 +684,7 @@ public void patchProjectExternalReferencesTest() { final var jsonProject = new Project(); jsonProject.setExternalReferences(externalReferences); - final var response = target(V1_PROJECT + "/" + project.getUuid()) + final var response = jersey.target(V1_PROJECT + "/" + project.getUuid()) .request() .header(X_API_KEY, apiKey) .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true) @@ -718,7 +714,7 @@ public void patchProjectParentTest() { .add("uuid", newParent.getUuid().toString())) .build(); - final Response response = target(V1_PROJECT + "/" + project.getUuid()) + final Response response = jersey.target(V1_PROJECT + "/" + project.getUuid()) .request() .header(X_API_KEY, apiKey) .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true) @@ -761,7 +757,7 @@ public void patchProjectParentNotFoundTest() { .add("uuid", UUID.randomUUID().toString())) .build(); - final Response response = target(V1_PROJECT + "/" + project.getUuid()) + final Response response = jersey.target(V1_PROJECT + "/" + project.getUuid()) .request() .header(X_API_KEY, apiKey) .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true) @@ -781,7 +777,7 @@ public void getRootProjectsTest() { Project parent = qm.createProject("ABC", null, "1.0", null, null, null, true, false); Project child = qm.createProject("DEF", null, "1.0", null, parent, null, true, false); qm.createProject("GHI", null, "1.0", null, child, null, true, false); - Response response = target(V1_PROJECT) + Response response = jersey.target(V1_PROJECT) .queryParam("onlyRoot", true) .request() .header(X_API_KEY, apiKey) @@ -800,7 +796,7 @@ public void getChildrenProjectsTest() { Project child = qm.createProject("DEF", null, "1.0", null, parent, null, true, false); qm.createProject("GHI", null, "1.0", null, parent, null, true, false); qm.createProject("JKL", null, "1.0", null, child, null, true, false); - Response response = target(V1_PROJECT + "/" + parent.getUuid().toString() + "/children") + Response response = jersey.target(V1_PROJECT + "/" + parent.getUuid().toString() + "/children") .request() .header(X_API_KEY, apiKey) .get(Response.class); @@ -862,7 +858,7 @@ public void getProjectsWithoutDescendantsOfTest() { Project child = qm.createProject("GHI", null, "1.0", null, parent, null, true, false); qm.createProject("JKL", null, "1.0", null, child, null, true, false); - Response response = target(V1_PROJECT + "/withoutDescendantsOf/" + parent.getUuid()) + Response response = jersey.target(V1_PROJECT + "/withoutDescendantsOf/" + parent.getUuid()) .request() .header(X_API_KEY, apiKey) .get(Response.class); @@ -934,7 +930,7 @@ public void cloneProjectTest() { AnalysisJustification.REQUIRES_ENVIRONMENT, AnalysisResponse.WILL_NOT_FIX, "details", false); qm.makeAnalysisComment(analysis, "comment", "commenter"); - final Response response = target("%s/clone".formatted(V1_PROJECT)).request() + final Response response = jersey.target("%s/clone".formatted(V1_PROJECT)).request() .header(X_API_KEY, apiKey) .put(Entity.json(""" { @@ -1022,7 +1018,7 @@ public void cloneProjectConflictTest() { project.setVersion("1.0.0"); qm.persist(project); - final Response response = target("%s/clone".formatted(V1_PROJECT)).request() + final Response response = jersey.target("%s/clone".formatted(V1_PROJECT)).request() .header(X_API_KEY, apiKey) .put(Entity.json(""" { @@ -1056,7 +1052,7 @@ public void cloneProjectWithAclTest() { noAccessProject.setVersion("2.0.0"); qm.persist(noAccessProject); - Response response = target("%s/clone".formatted(V1_PROJECT)).request() + Response response = jersey.target("%s/clone".formatted(V1_PROJECT)).request() .header(X_API_KEY, apiKey) .put(Entity.json(""" { @@ -1067,7 +1063,7 @@ public void cloneProjectWithAclTest() { assertThat(response.getStatus()).isEqualTo(403); assertThat(getPlainTextBody(response)).isEqualTo("Access to the specified project is forbidden"); - response = target("%s/clone".formatted(V1_PROJECT)).request() + response = jersey.target("%s/clone".formatted(V1_PROJECT)).request() .header(X_API_KEY, apiKey) .put(Entity.json(""" { diff --git a/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java index 0de81d2f4b..51b943193d 100644 --- a/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java @@ -20,6 +20,7 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.Repository; import org.dependencytrack.model.RepositoryMetaComponent; @@ -27,11 +28,9 @@ import org.dependencytrack.persistence.DefaultObjectGenerator; import org.dependencytrack.persistence.QueryManager; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -44,14 +43,11 @@ public class RepositoryResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(RepositoryResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(RepositoryResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Before public void before() throws Exception { @@ -62,7 +58,7 @@ public void before() throws Exception { @Test public void getRepositoriesTest() { - Response response = target(V1_REPOSITORY).request() + Response response = jersey.target(V1_REPOSITORY).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -81,7 +77,7 @@ public void getRepositoriesTest() { @Test public void getRepositoriesByTypeTest() { - Response response = target(V1_REPOSITORY + "/MAVEN").request() + Response response = jersey.target(V1_REPOSITORY + "/MAVEN").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -108,7 +104,7 @@ public void getRepositoryMetaComponentTest() { meta.setLatestVersion("2.0.0"); meta.setRepositoryType(RepositoryType.MAVEN); qm.persist(meta); - Response response = target(V1_REPOSITORY + "/latest") + Response response = jersey.target(V1_REPOSITORY + "/latest") .queryParam("purl", "pkg:/maven/org.acme/example-component@1.0.0") .request() .header(X_API_KEY, apiKey) @@ -134,7 +130,7 @@ public void getRepositoryMetaComponentInvalidRepoTypeTest() { meta.setLatestVersion("2.0.0"); meta.setRepositoryType(RepositoryType.MAVEN); qm.persist(meta); - Response response = target(V1_REPOSITORY + "/latest") + Response response = jersey.target(V1_REPOSITORY + "/latest") .queryParam("purl", "pkg:/generic/org.acme/example-component@1.0.0") .request() .header(X_API_KEY, apiKey) @@ -153,7 +149,7 @@ public void getRepositoryMetaComponentInvalidPurlTest() { meta.setLatestVersion("2.0.0"); meta.setRepositoryType(RepositoryType.MAVEN); qm.persist(meta); - Response response = target(V1_REPOSITORY + "/latest") + Response response = jersey.target(V1_REPOSITORY + "/latest") .queryParam("purl", "g:/g/g/g") .request() .header(X_API_KEY, apiKey) @@ -164,7 +160,7 @@ public void getRepositoryMetaComponentInvalidPurlTest() { @Test public void getRepositoryMetaUntrackedComponentTest() { - Response response = target(V1_REPOSITORY + "/latest") + Response response = jersey.target(V1_REPOSITORY + "/latest") .queryParam("purl", "pkg:/maven/org.acme/example-component@1.0.0") .request() .header(X_API_KEY, apiKey) @@ -187,12 +183,12 @@ public void createRepositoryTest() { repository.setIdentifier("test"); repository.setUrl("www.foobar.com"); repository.setType(RepositoryType.MAVEN); - Response response = target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) + Response response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) .put(Entity.entity(repository, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus()); - response = target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class); + response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertEquals(String.valueOf(16), response.getHeaderString(TOTAL_COUNT_HEADER)); JsonArray json = parseJsonArray(response); @@ -219,12 +215,12 @@ public void createNonInternalRepositoryTest() { repository.setUrl("www.foobar.com"); repository.setType(RepositoryType.MAVEN); RepositoryResource repositoryResource = new RepositoryResource(); - Response response = target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) + Response response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) .put(Entity.entity(repository, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus()); - response = target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class); + response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertEquals(String.valueOf(16), response.getHeaderString(TOTAL_COUNT_HEADER)); JsonArray json = parseJsonArray(response); @@ -249,12 +245,12 @@ public void createRepositoryAuthFalseTest() { repository.setIdentifier("test"); repository.setUrl("www.foobar.com"); repository.setType(RepositoryType.MAVEN); - Response response = target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) + Response response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) .put(Entity.entity(repository, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus()); - response = target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class); + response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertEquals(String.valueOf(16), response.getHeaderString(TOTAL_COUNT_HEADER)); JsonArray json = parseJsonArray(response); @@ -280,7 +276,7 @@ public void updateRepositoryTest() throws Exception { repository.setIdentifier("test"); repository.setUrl("www.foobar.com"); repository.setType(RepositoryType.MAVEN); - Response response = target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) + Response response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) .put(Entity.entity(repository, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus()); try (QueryManager qm = new QueryManager()) { @@ -288,7 +284,7 @@ public void updateRepositoryTest() throws Exception { for (Repository repository1 : repositoryList) { if (repository1.getIdentifier().equals("test")) { repository1.setAuthenticationRequired(false); - response = target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) + response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) .post(Entity.entity(repository1, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus()); break; diff --git a/src/test/java/org/dependencytrack/resources/v1/SearchResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/SearchResourceTest.java index 9cf1cdb47f..9a3142dacc 100644 --- a/src/test/java/org/dependencytrack/resources/v1/SearchResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/SearchResourceTest.java @@ -23,18 +23,17 @@ import alpine.event.framework.Subscriber; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.event.IndexEvent; import org.dependencytrack.model.License; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.search.IndexManager; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonObject; @@ -47,14 +46,11 @@ public class SearchResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(SearchResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(SearchResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); private static final ConcurrentLinkedQueue EVENTS = new ConcurrentLinkedQueue<>(); @@ -68,23 +64,24 @@ public void inform(final Event event) { } @Before - public void setUp() throws Exception { - super.setUp(); + @Override + public void before() throws Exception { + super.before(); SingleThreadedEventService.getInstance().subscribe(IndexEvent.class, EventSubscriber.class); } @After - public void tearDown() throws Exception { - super.tearDown(); - + @Override + public void after() throws Exception { SingleThreadedEventService.getInstance().unsubscribe(EventSubscriber.class); EVENTS.clear(); + super.after(); } @Test public void searchTest() { - Response response = target(V1_SEARCH).queryParam("query", "tomcat").request() + Response response = jersey.target(V1_SEARCH).queryParam("query", "tomcat").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -95,7 +92,7 @@ public void searchTest() { @Test public void searchProjectTest() { - Response response = target(V1_SEARCH + "/project").queryParam("query", "acme").request() + Response response = jersey.target(V1_SEARCH + "/project").queryParam("query", "acme").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -106,7 +103,7 @@ public void searchProjectTest() { @Test public void searchComponentTest() { - Response response = target(V1_SEARCH + "/component").queryParam("query", "bootstrap").request() + Response response = jersey.target(V1_SEARCH + "/component").queryParam("query", "bootstrap").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -117,7 +114,7 @@ public void searchComponentTest() { @Test public void searchServiceComponentTest() { - Response response = target(V1_SEARCH + "/service").queryParam("query", "stock-ticker").request() + Response response = jersey.target(V1_SEARCH + "/service").queryParam("query", "stock-ticker").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -128,7 +125,7 @@ public void searchServiceComponentTest() { @Test public void searchLicenseTest() { - Response response = target(V1_SEARCH + "/license").queryParam("query", "Apache").request() + Response response = jersey.target(V1_SEARCH + "/license").queryParam("query", "Apache").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -139,7 +136,7 @@ public void searchLicenseTest() { @Test public void searchVulnerabilityTest() { - Response response = target(V1_SEARCH + "/vulnerability").queryParam("query", "CVE-2020").request() + Response response = jersey.target(V1_SEARCH + "/vulnerability").queryParam("query", "CVE-2020").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -150,7 +147,7 @@ public void searchVulnerabilityTest() { @Test public void reindexWithBadIndexTypes() { - Response response = target(V1_SEARCH + "/reindex").queryParam("type", "BAD_TYPE_1", "BAD_TYPE_2").request() + Response response = jersey.target(V1_SEARCH + "/reindex").queryParam("type", "BAD_TYPE_1", "BAD_TYPE_2").request() .header(X_API_KEY, apiKey) .post(null, Response.class); Assert.assertEquals(400, response.getStatus(), 0); @@ -161,7 +158,7 @@ public void reindexWithBadIndexTypes() { @Test public void reindexWithMixedIndexTypes() { - Response response = target(V1_SEARCH + "/reindex").queryParam("type", "BAD_TYPE_1", IndexManager.IndexType.VULNERABILITY.name(), IndexManager.IndexType.LICENSE).request() + Response response = jersey.target(V1_SEARCH + "/reindex").queryParam("type", "BAD_TYPE_1", IndexManager.IndexType.VULNERABILITY.name(), IndexManager.IndexType.LICENSE).request() .header(X_API_KEY, apiKey) .post(null, Response.class); Assert.assertEquals(200, response.getStatus(), 0); diff --git a/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java index f4851a82f0..f417caabc8 100644 --- a/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java @@ -2,13 +2,12 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.Policy; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -17,14 +16,11 @@ public class TagResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(TagResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(TagResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Test public void getAllTagsWithOrderingTest() { @@ -35,7 +31,7 @@ public void getAllTagsWithOrderingTest() { qm.createProject("Project B", null, "1", List.of(qm.getTagByName("Tag 2"), qm.getTagByName("Tag 3"), qm.getTagByName("Tag 4")), null, null, true, false); Policy policy = qm.createPolicy("Test Policy", Policy.Operator.ANY, Policy.ViolationState.INFO); - Response response = target(V1_TAG + "/" + policy.getUuid()) + Response response = jersey.target(V1_TAG + "/" + policy.getUuid()) .request() .header(X_API_KEY, apiKey) .get(); @@ -60,7 +56,7 @@ public void getTagsWithPolicyProjectsFilterTest() { Policy policy = qm.createPolicy("Test Policy", Policy.Operator.ANY, Policy.ViolationState.INFO); policy.setProjects(List.of(qm.getProject("Project A", "1"), qm.getProject("Project C", "1"))); - Response response = target(V1_TAG + "/" + policy.getUuid()) + Response response = jersey.target(V1_TAG + "/" + policy.getUuid()) .request() .header(X_API_KEY, apiKey) .get(); diff --git a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java index 2ef8b62f95..076e848337 100644 --- a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java @@ -24,16 +24,15 @@ import alpine.model.Team; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Project; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -49,21 +48,18 @@ public class TeamResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(TeamResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(TeamResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Test public void getTeamsTest() { for (int i=0; i<1000; i++) { qm.createTeam("Team " + i, false); } - Response response = target(V1_TEAM).request() + Response response = jersey.target(V1_TEAM).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -77,7 +73,7 @@ public void getTeamsTest() { @Test public void getTeamTest() { Team team = qm.createTeam("ABC", false); - Response response = target(V1_TEAM + "/" + team.getUuid()) + Response response = jersey.target(V1_TEAM + "/" + team.getUuid()) .request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); @@ -88,7 +84,7 @@ public void getTeamTest() { @Test public void getTeamByInvalidUuidTest() { - Response response = target(V1_TEAM + "/" + UUID.randomUUID()) + Response response = jersey.target(V1_TEAM + "/" + UUID.randomUUID()) .request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); @@ -99,7 +95,7 @@ public void getTeamByInvalidUuidTest() { @Test public void getTeamSelfTest() { initializeWithPermissions(Permissions.BOM_UPLOAD, Permissions.PROJECT_CREATION_UPLOAD); - var response = target(V1_TEAM + "/self").request().header(X_API_KEY, apiKey).get(Response.class); + var response = jersey.target(V1_TEAM + "/self").request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(200, response.getStatus()); final var json = parseJsonObject(response); Assert.assertEquals(team.getName(), json.getString("name")); @@ -110,15 +106,15 @@ public void getTeamSelfTest() { Assert.assertEquals(Permissions.PROJECT_CREATION_UPLOAD.toString(), permissions.get(1).asJsonObject().getString("name")); // missing api-key - response = target(V1_TEAM + "/self").request().get(Response.class); + response = jersey.target(V1_TEAM + "/self").request().get(Response.class); Assert.assertEquals(401, response.getStatus()); // wrong api-key - response = target(V1_TEAM + "/self").request().header(X_API_KEY, "5ce9b8a5-5f18-4c1f-9eda-1611b83e8915").get(Response.class); + response = jersey.target(V1_TEAM + "/self").request().header(X_API_KEY, "5ce9b8a5-5f18-4c1f-9eda-1611b83e8915").get(Response.class); Assert.assertEquals(401, response.getStatus()); // not an api-key - response = target(V1_TEAM + "/self").request().header("Authorization", "Bearer " + jwt).get(Response.class); + response = jersey.target(V1_TEAM + "/self").request().header("Authorization", "Bearer " + jwt).get(Response.class); Assert.assertEquals(400, response.getStatus()); } @@ -126,7 +122,7 @@ public void getTeamSelfTest() { public void createTeamTest() { Team team = new Team(); team.setName("My Team"); - Response response = target(V1_TEAM).request() + Response response = jersey.target(V1_TEAM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(team, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus(), 0); @@ -141,7 +137,7 @@ public void createTeamTest() { public void updateTeamTest() { Team team = qm.createTeam("My Team", false); team.setName("My New Teams Name"); - Response response = target(V1_TEAM).request() + Response response = jersey.target(V1_TEAM).request() .header(X_API_KEY, apiKey) .post(Entity.entity(team, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -154,7 +150,7 @@ public void updateTeamTest() { public void updateTeamEmptyNameTest() { Team team = qm.createTeam("My Team", false); team.setName(" "); - Response response = target(V1_TEAM).request() + Response response = jersey.target(V1_TEAM).request() .header(X_API_KEY, apiKey) .post(Entity.entity(team, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -165,7 +161,7 @@ public void updateTeamInvalidTest() { Team team = new Team(); team.setName("My Team"); team.setUuid(UUID.randomUUID()); - Response response = target(V1_TEAM).request() + Response response = jersey.target(V1_TEAM).request() .header(X_API_KEY, apiKey) .post(Entity.entity(team, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -177,7 +173,7 @@ public void updateTeamInvalidTest() { @Test public void deleteTeamTest() { Team team = qm.createTeam("My Team", false); - Response response = target(V1_TEAM).request() + Response response = jersey.target(V1_TEAM).request() .header(X_API_KEY, apiKey) .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) // HACK .method("DELETE", Entity.entity(team, MediaType.APPLICATION_JSON)); // HACK @@ -198,7 +194,7 @@ public void deleteTeamWithAclTest() { Project project = qm.createProject("Acme Example", null, "1", null, null, null, true, false); project.addAccessTeam(team); qm.persist(project); - Response response = target(V1_TEAM).request() + Response response = jersey.target(V1_TEAM).request() .header(X_API_KEY, apiKey) .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) // HACK .method("DELETE", Entity.entity(team, MediaType.APPLICATION_JSON)); // HACK @@ -210,7 +206,7 @@ public void deleteTeamWithAclTest() { public void generateApiKeyTest() { Team team = qm.createTeam("My Team", false); Assert.assertEquals(0, team.getApiKeys().size()); - Response response = target(V1_TEAM + "/" + team.getUuid().toString() + "/key").request() + Response response = jersey.target(V1_TEAM + "/" + team.getUuid().toString() + "/key").request() .header(X_API_KEY, apiKey) .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) .put(Entity.entity(null, MediaType.APPLICATION_JSON)); @@ -221,7 +217,7 @@ public void generateApiKeyTest() { @Test public void generateApiKeyInvalidTest() { - Response response = target(V1_TEAM + "/" + UUID.randomUUID().toString() + "/key").request() + Response response = jersey.target(V1_TEAM + "/" + UUID.randomUUID().toString() + "/key").request() .header(X_API_KEY, apiKey) .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) .put(Entity.entity(null, MediaType.APPLICATION_JSON)); @@ -235,7 +231,7 @@ public void generateApiKeyInvalidTest() { public void regenerateApiKeyTest() { Team team = qm.createTeam("My Team", true); Assert.assertEquals(1, team.getApiKeys().size()); - Response response = target(V1_TEAM + "/key/" + team.getApiKeys().get(0).getKey()).request() + Response response = jersey.target(V1_TEAM + "/key/" + team.getApiKeys().get(0).getKey()).request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -247,7 +243,7 @@ public void regenerateApiKeyTest() { @Test public void regenerateApiKeyInvalidTest() { - Response response = target(V1_TEAM + "/key/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_TEAM + "/key/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -260,7 +256,7 @@ public void regenerateApiKeyInvalidTest() { public void deleteApiKeyTest() { Team team = qm.createTeam("My Team", true); Assert.assertEquals(1, team.getApiKeys().size()); - Response response = target(V1_TEAM + "/key/" + team.getApiKeys().get(0).getKey()).request() + Response response = jersey.target(V1_TEAM + "/key/" + team.getApiKeys().get(0).getKey()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(204, response.getStatus(), 0); @@ -268,7 +264,7 @@ public void deleteApiKeyTest() { @Test public void deleteApiKeyInvalidTest() { - Response response = target(V1_TEAM + "/key/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_TEAM + "/key/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(404, response.getStatus(), 0); @@ -286,7 +282,7 @@ public void updateApiKeyCommentTest() { assertThat(apiKey.getLastUsed()).isNull(); assertThat(apiKey.getComment()).isNull(); - final Response response = target("%s/key/%s/comment".formatted(V1_TEAM, apiKey.getKey())).request() + final Response response = jersey.target("%s/key/%s/comment".formatted(V1_TEAM, apiKey.getKey())).request() .header(X_API_KEY, this.apiKey) .post(Entity.entity("Some comment 123", MediaType.TEXT_PLAIN)); @@ -307,7 +303,7 @@ public void updateApiKeyCommentTest() { @Test public void updateApiKeyCommentNotFoundTest() { - final Response response = target("%s/key/does-not-exist/comment".formatted(V1_TEAM)).request() + final Response response = jersey.target("%s/key/does-not-exist/comment".formatted(V1_TEAM)).request() .header(X_API_KEY, this.apiKey) .post(Entity.entity("Some comment 123", MediaType.TEXT_PLAIN)); diff --git a/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java b/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java index b7c8b2f80c..322a1e7fc1 100644 --- a/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java @@ -18,21 +18,20 @@ */ package org.dependencytrack.resources.v1; -import alpine.server.filters.ApiFilter; -import alpine.server.filters.AuthenticationFilter; import alpine.model.LdapUser; import alpine.model.ManagedUser; import alpine.model.OidcUser; import alpine.model.Team; import alpine.server.auth.PasswordService; +import alpine.server.filters.ApiFilter; +import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.IdentifiableObject; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import javax.json.JsonArray; @@ -44,14 +43,11 @@ public class UserResourceAuthenticatedTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(UserResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(UserResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Test public void getManagedUsersTest() { @@ -59,7 +55,7 @@ public void getManagedUsersTest() { for (int i=0; i<1000; i++) { qm.createManagedUser("managed-user-" + i, hashedPassword); } - Response response = target(V1_USER + "/managed").request() + Response response = jersey.target(V1_USER + "/managed").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -75,7 +71,7 @@ public void getLdapUsersTest() { for (int i=0; i<1000; i++) { qm.createLdapUser("ldap-user-" + i); } - Response response = target(V1_USER + "/ldap").request() + Response response = jersey.target(V1_USER + "/ldap").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -88,7 +84,7 @@ public void getLdapUsersTest() { @Test public void getSelfTest() { - Response response = target(V1_USER + "/self").request() + Response response = jersey.target(V1_USER + "/self").request() .header("Authorization", "Bearer " + jwt) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -100,7 +96,7 @@ public void getSelfTest() { @Test public void getSelfNonUserTest() { - Response response = target(V1_USER + "/self").request() + Response response = jersey.target(V1_USER + "/self").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(401, response.getStatus(), 0); @@ -112,7 +108,7 @@ public void updateSelfTest() { user.setUsername(testUser.getUsername()); user.setFullname("Captain BlackBeard"); user.setEmail("blackbeard@example.com"); - Response response = target(V1_USER + "/self").request() + Response response = jersey.target(V1_USER + "/self").request() .header("Authorization", "Bearer " + jwt) .post(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -129,7 +125,7 @@ public void updateSelfInvalidFullnameTest() { user.setUsername(testUser.getUsername()); user.setFullname(""); user.setEmail("blackbeard@example.com"); - Response response = target(V1_USER + "/self").request() + Response response = jersey.target(V1_USER + "/self").request() .header("Authorization", "Bearer " + jwt) .post(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -143,7 +139,7 @@ public void updateSelfInvalidEmailTest() { user.setUsername(testUser.getUsername()); user.setFullname("Captain BlackBeard"); user.setEmail(""); - Response response = target(V1_USER + "/self").request() + Response response = jersey.target(V1_USER + "/self").request() .header("Authorization", "Bearer " + jwt) .post(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -155,7 +151,7 @@ public void updateSelfInvalidEmailTest() { public void updateSelfUnauthorizedTest() { ManagedUser user = new ManagedUser(); user.setUsername(testUser.getUsername()); - Response response = target(V1_USER + "/self").request() + Response response = jersey.target(V1_USER + "/self").request() .header(X_API_KEY, apiKey) .post(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(401, response.getStatus(), 0); @@ -169,7 +165,7 @@ public void updateSelfPasswordsTest() { user.setEmail("blackbeard@example.com"); user.setNewPassword("newPassword"); user.setConfirmPassword("newPassword"); - Response response = target(V1_USER + "/self").request() + Response response = jersey.target(V1_USER + "/self").request() .header("Authorization", "Bearer " + jwt) .post(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -188,7 +184,7 @@ public void updateSelfPasswordMismatchTest() { user.setEmail("blackbeard@example.com"); user.setNewPassword("newPassword"); user.setConfirmPassword("blah"); - Response response = target(V1_USER + "/self").request() + Response response = jersey.target(V1_USER + "/self").request() .header("Authorization", "Bearer " + jwt) .post(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -200,7 +196,7 @@ public void updateSelfPasswordMismatchTest() { public void createLdapUserTest() { LdapUser user = new LdapUser(); user.setUsername("blackbeard"); - Response response = target(V1_USER + "/ldap").request() + Response response = jersey.target(V1_USER + "/ldap").request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus(), 0); @@ -214,7 +210,7 @@ public void createLdapUserTest() { public void createLdapUserInvalidUsernameTest() { LdapUser user = new LdapUser(); user.setUsername(""); - Response response = target(V1_USER + "/ldap").request() + Response response = jersey.target(V1_USER + "/ldap").request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -227,7 +223,7 @@ public void createLdapUserDuplicateUsernameTest() { qm.createLdapUser("blackbeard"); LdapUser user = new LdapUser(); user.setUsername("blackbeard"); - Response response = target(V1_USER + "/ldap").request() + Response response = jersey.target(V1_USER + "/ldap").request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(409, response.getStatus(), 0); @@ -240,7 +236,7 @@ public void deleteLdapUserTest() { qm.createLdapUser("blackbeard"); LdapUser user = new LdapUser(); user.setUsername("blackbeard"); - Response response = target(V1_USER + "/ldap").request() + Response response = jersey.target(V1_USER + "/ldap").request() .header(X_API_KEY, apiKey) .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) // HACK .method("DELETE", Entity.entity(user, MediaType.APPLICATION_JSON)); // HACK @@ -256,7 +252,7 @@ public void createManagedUserTest() { user.setUsername("blackbeard"); user.setNewPassword("password"); user.setConfirmPassword("password"); - Response response = target(V1_USER + "/managed").request() + Response response = jersey.target(V1_USER + "/managed").request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus(), 0); @@ -276,7 +272,7 @@ public void createManagedUserInvalidUsernameTest() { user.setUsername(""); user.setNewPassword("password"); user.setConfirmPassword("password"); - Response response = target(V1_USER + "/managed").request() + Response response = jersey.target(V1_USER + "/managed").request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -293,7 +289,7 @@ public void createManagedUserInvalidFullnameTest() { user.setUsername("blackbeard"); user.setNewPassword("password"); user.setConfirmPassword("password"); - Response response = target(V1_USER + "/managed").request() + Response response = jersey.target(V1_USER + "/managed").request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -310,7 +306,7 @@ public void createManagedUserInvalidEmailTest() { user.setUsername("blackbeard"); user.setNewPassword("password"); user.setConfirmPassword("password"); - Response response = target(V1_USER + "/managed").request() + Response response = jersey.target(V1_USER + "/managed").request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -327,7 +323,7 @@ public void createManagedUserInvalidPasswordTest() { user.setUsername("blackbeard"); user.setNewPassword(""); user.setConfirmPassword("password"); - Response response = target(V1_USER + "/managed").request() + Response response = jersey.target(V1_USER + "/managed").request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -344,7 +340,7 @@ public void createManagedUserPasswordMismatchTest() { user.setUsername("blackbeard"); user.setNewPassword("password"); user.setConfirmPassword("blah"); - Response response = target(V1_USER + "/managed").request() + Response response = jersey.target(V1_USER + "/managed").request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -362,7 +358,7 @@ public void createManagedUserDuplicateUsernameTest() { user.setUsername("blackbeard"); user.setNewPassword("password"); user.setConfirmPassword("password"); - Response response = target(V1_USER + "/managed").request() + Response response = jersey.target(V1_USER + "/managed").request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(409, response.getStatus(), 0); @@ -382,7 +378,7 @@ public void updateManagedUserTest() { user.setForcePasswordChange(true); user.setNonExpiryPassword(true); user.setSuspended(true); - Response response = target(V1_USER + "/managed").request() + Response response = jersey.target(V1_USER + "/managed").request() .header("Authorization", "Bearer " + jwt) .post(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -407,7 +403,7 @@ public void updateManagedUserInvalidFullnameTest() { user.setForcePasswordChange(true); user.setNonExpiryPassword(true); user.setSuspended(true); - Response response = target(V1_USER + "/managed").request() + Response response = jersey.target(V1_USER + "/managed").request() .header("Authorization", "Bearer " + jwt) .post(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -427,7 +423,7 @@ public void updateManagedUserInvalidEmailTest() { user.setForcePasswordChange(true); user.setNonExpiryPassword(true); user.setSuspended(true); - Response response = target(V1_USER + "/managed").request() + Response response = jersey.target(V1_USER + "/managed").request() .header("Authorization", "Bearer " + jwt) .post(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(400, response.getStatus(), 0); @@ -447,7 +443,7 @@ public void updateManagedUserInvalidUsernameTest() { user.setForcePasswordChange(true); user.setNonExpiryPassword(true); user.setSuspended(true); - Response response = target(V1_USER + "/managed").request() + Response response = jersey.target(V1_USER + "/managed").request() .header("Authorization", "Bearer " + jwt) .post(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -462,7 +458,7 @@ public void deleteManagedUserTest() { qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", hashedPassword, false, false, false); ManagedUser user = new ManagedUser(); user.setUsername("blackbeard"); - Response response = target(V1_USER + "/managed").request() + Response response = jersey.target(V1_USER + "/managed").request() .header(X_API_KEY, apiKey) .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) // HACK .method("DELETE", Entity.entity(user, MediaType.APPLICATION_JSON)); // HACK @@ -474,7 +470,7 @@ public void deleteManagedUserTest() { public void createOidcUserTest() { final OidcUser user = new OidcUser(); user.setUsername("blackbeard"); - Response response = target(V1_USER + "/oidc").request() + Response response = jersey.target(V1_USER + "/oidc").request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus(), 0); @@ -489,7 +485,7 @@ public void createOidcUserDuplicateUsernameTest() { qm.createOidcUser("blackbeard"); final OidcUser user = new OidcUser(); user.setUsername("blackbeard"); - Response response = target(V1_USER + "/oidc").request() + Response response = jersey.target(V1_USER + "/oidc").request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(user, MediaType.APPLICATION_JSON)); Assert.assertEquals(409, response.getStatus(), 0); @@ -506,7 +502,7 @@ public void addTeamToUserTest() { ido.setUuid(team.getUuid().toString()); ManagedUser user = new ManagedUser(); user.setUsername("blackbeard"); - Response response = target(V1_USER + "/blackbeard/membership").request() + Response response = jersey.target(V1_USER + "/blackbeard/membership").request() .header(X_API_KEY, apiKey) .post(Entity.entity(ido, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -528,7 +524,7 @@ public void addTeamToUserInvalidTeamTest() { ido.setUuid(UUID.randomUUID().toString()); ManagedUser user = new ManagedUser(); user.setUsername("blackbeard"); - Response response = target(V1_USER + "/blackbeard/membership").request() + Response response = jersey.target(V1_USER + "/blackbeard/membership").request() .header(X_API_KEY, apiKey) .post(Entity.entity(ido, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -544,7 +540,7 @@ public void addTeamToUserInvalidUserTest() { ido.setUuid(team.getUuid().toString()); ManagedUser user = new ManagedUser(); user.setUsername("blah"); - Response response = target(V1_USER + "/blackbeard/membership").request() + Response response = jersey.target(V1_USER + "/blackbeard/membership").request() .header(X_API_KEY, apiKey) .post(Entity.entity(ido, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -561,7 +557,7 @@ public void addTeamToUserDuplicateMembershipTest() { qm.addUserToTeam(user, team); IdentifiableObject ido = new IdentifiableObject(); ido.setUuid(team.getUuid().toString()); - Response response = target(V1_USER + "/blackbeard/membership").request() + Response response = jersey.target(V1_USER + "/blackbeard/membership").request() .header(X_API_KEY, apiKey) .post(Entity.entity(ido, MediaType.APPLICATION_JSON)); Assert.assertEquals(304, response.getStatus(), 0); @@ -579,7 +575,7 @@ public void removeTeamFromUserTest() { qm.addUserToTeam(user, team); IdentifiableObject ido = new IdentifiableObject(); ido.setUuid(team.getUuid().toString()); - Response response = target(V1_USER + "/blackbeard/membership").request() + Response response = jersey.target(V1_USER + "/blackbeard/membership").request() .header(X_API_KEY, apiKey) .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) // HACK .method("DELETE", Entity.entity(ido, MediaType.APPLICATION_JSON)); // HACK diff --git a/src/test/java/org/dependencytrack/resources/v1/UserResourceUnauthenticatedTest.java b/src/test/java/org/dependencytrack/resources/v1/UserResourceUnauthenticatedTest.java index ded3ad9601..ce1370f0a4 100644 --- a/src/test/java/org/dependencytrack/resources/v1/UserResourceUnauthenticatedTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/UserResourceUnauthenticatedTest.java @@ -18,15 +18,14 @@ */ package org.dependencytrack.resources.v1; -import alpine.server.filters.ApiFilter; import alpine.model.ManagedUser; import alpine.server.auth.PasswordService; +import alpine.server.filters.ApiFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import javax.ws.rs.client.Entity; @@ -36,20 +35,17 @@ public class UserResourceUnauthenticatedTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(UserResource.class) - .register(ApiFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(UserResource.class) + .register(ApiFilter.class)); @Test public void validateCredentialsTest() { Form form = new Form(); form.param("username", "testuser"); form.param("password", "testuser"); - Response response = target(V1_USER + "/login").request() + Response response = jersey.target(V1_USER + "/login").request() .accept(MediaType.TEXT_PLAIN) .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); Assert.assertEquals(200, response.getStatus(), 0); @@ -66,7 +62,7 @@ public void validateCredentialsSuspendedTest() { Form form = new Form(); form.param("username", "testuser"); form.param("password", "testuser"); - Response response = target(V1_USER + "/login").request() + Response response = jersey.target(V1_USER + "/login").request() .accept(MediaType.TEXT_PLAIN) .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); Assert.assertEquals(403, response.getStatus(), 0); @@ -77,7 +73,7 @@ public void validateCredentialsUnauthorizedTest() { Form form = new Form(); form.param("username", "testuser"); form.param("password", "wrong"); - Response response = target(V1_USER + "/login").request() + Response response = jersey.target(V1_USER + "/login").request() .accept(MediaType.TEXT_PLAIN) .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); Assert.assertEquals(401, response.getStatus(), 0); @@ -88,7 +84,7 @@ public void validateOidcAccessTokenOidcNotAvailableTest() { final Form form = new Form(); form.param("accessToken", "accessToken"); - final Response response = target(V1_USER + "/oidc/login").request() + final Response response = jersey.target(V1_USER + "/oidc/login").request() .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); // OIDC is disabled by default @@ -103,7 +99,7 @@ public void forceChangePasswordTest() { form.param("newPassword", "Password1!"); form.param("confirmPassword", "Password1!"); Assert.assertTrue(PasswordService.matches("testuser".toCharArray(), testUser)); - Response response = target(V1_USER + "/forceChangePassword").request() + Response response = jersey.target(V1_USER + "/forceChangePassword").request() .accept(MediaType.TEXT_PLAIN) .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); Assert.assertEquals(200, response.getStatus(), 0); @@ -123,7 +119,7 @@ public void forceChangePasswordFlagResetTest() { form.param("newPassword", "Password1!"); form.param("confirmPassword", "Password1!"); Assert.assertTrue(PasswordService.matches("testuser".toCharArray(), testUser)); - Response response = target(V1_USER + "/forceChangePassword").request() + Response response = jersey.target(V1_USER + "/forceChangePassword").request() .accept(MediaType.TEXT_PLAIN) .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); Assert.assertEquals(200, response.getStatus(), 0); @@ -139,7 +135,7 @@ public void forceChangePasswordMismatchTest() { form.param("password", "testuser"); form.param("newPassword", "Password1!"); form.param("confirmPassword", "blah"); - Response response = target(V1_USER + "/forceChangePassword").request() + Response response = jersey.target(V1_USER + "/forceChangePassword").request() .accept(MediaType.TEXT_PLAIN) .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); Assert.assertEquals(406, response.getStatus(), 0); @@ -155,7 +151,7 @@ public void forceChangePasswordUnchangedTest() { form.param("password", "testuser"); form.param("newPassword", "testuser"); form.param("confirmPassword", "testuser"); - Response response = target(V1_USER + "/forceChangePassword").request() + Response response = jersey.target(V1_USER + "/forceChangePassword").request() .accept(MediaType.TEXT_PLAIN) .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); Assert.assertEquals(406, response.getStatus(), 0); @@ -173,7 +169,7 @@ public void forceChangePasswordSuspendedTest() { form.param("password", "testuser"); form.param("newPassword", "Password1!"); form.param("confirmPassword", "Password1!"); - Response response = target(V1_USER + "/forceChangePassword").request() + Response response = jersey.target(V1_USER + "/forceChangePassword").request() .accept(MediaType.TEXT_PLAIN) .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); Assert.assertEquals(403, response.getStatus(), 0); @@ -189,7 +185,7 @@ public void forceChangePasswordInvalidCredsTest() { form.param("password", "blah"); form.param("newPassword", "Password1!"); form.param("confirmPassword", "Password1!"); - Response response = target(V1_USER + "/forceChangePassword").request() + Response response = jersey.target(V1_USER + "/forceChangePassword").request() .accept(MediaType.TEXT_PLAIN) .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); Assert.assertEquals(401, response.getStatus(), 0); diff --git a/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java index f061d07ca0..6b82ee739e 100644 --- a/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java @@ -21,6 +21,7 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import com.fasterxml.jackson.core.StreamReadConstraints; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.AnalysisResponse; @@ -35,9 +36,7 @@ import org.dependencytrack.tasks.scanners.AnalyzerIdentity; import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; +import org.junit.ClassRule; import org.junit.Test; import javax.ws.rs.client.Entity; @@ -48,21 +47,18 @@ import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.hamcrest.CoreMatchers.equalTo; import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_ENABLED; +import static org.hamcrest.CoreMatchers.equalTo; public class VexResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(VexResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class) - .register(MultiPartFeature.class) - .register(JsonMappingExceptionMapper.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(VexResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class) + .register(MultiPartFeature.class) + .register(JsonMappingExceptionMapper.class)); @Test public void exportProjectAsCycloneDxTest() { @@ -129,7 +125,7 @@ public void exportProjectAsCycloneDxTest() { )); qm.persist(project); - final Response response = target("%s/cyclonedx/project/%s".formatted(V1_VEX, project.getUuid())) + final Response response = jersey.target("%s/cyclonedx/project/%s".formatted(V1_VEX, project.getUuid())) .request() .header(X_API_KEY, apiKey) .get(Response.class); @@ -249,7 +245,7 @@ public void uploadVexInvalidJsonTest() { } """.getBytes()); - final Response response = target(V1_VEX).request() + final Response response = jersey.target(V1_VEX).request() .header(X_API_KEY, apiKey) .put(Entity.entity(""" { @@ -301,7 +297,7 @@ public void uploadVexInvalidXmlTest() { """.getBytes()); - final Response response = target(V1_VEX).request() + final Response response = jersey.target(V1_VEX).request() .header(X_API_KEY, apiKey) .put(Entity.entity(""" { @@ -334,7 +330,7 @@ public void uploadVexTooLargeViaPutTest() { final String vex = "a".repeat(StreamReadConstraints.DEFAULT_MAX_STRING_LEN + 1); - final Response response = target(V1_VEX).request() + final Response response = jersey.target(V1_VEX).request() .header(X_API_KEY, apiKey) .put(Entity.entity(""" { diff --git a/src/test/java/org/dependencytrack/resources/v1/ViolationAnalysisResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ViolationAnalysisResourceTest.java index 894c7b4056..a7b3670112 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ViolationAnalysisResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ViolationAnalysisResourceTest.java @@ -27,6 +27,7 @@ import alpine.server.filters.AuthenticationFilter; import alpine.server.filters.AuthorizationFilter; import net.jcip.annotations.NotThreadSafe; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Component; @@ -45,11 +46,10 @@ import org.dependencytrack.resources.v1.vo.ViolationAnalysisRequest; import org.dependencytrack.util.NotificationUtil; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; +import org.junit.After; import org.junit.AfterClass; import org.junit.BeforeClass; +import org.junit.ClassRule; import org.junit.Test; import javax.json.Json; @@ -69,15 +69,12 @@ @NotThreadSafe public class ViolationAnalysisResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(ViolationAnalysisResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class) - .register(AuthorizationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(ViolationAnalysisResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class) + .register(AuthorizationFilter.class)); public static class NotificationSubscriber implements Subscriber { @@ -100,10 +97,11 @@ public static void tearDownClass() { NotificationService.getInstance().unsubscribe(new Subscription(NotificationSubscriber.class)); } + @After @Override - public void tearDown() throws Exception { + public void after() throws Exception { NOTIFICATIONS.clear(); - super.tearDown(); + super.after(); } @Test @@ -135,7 +133,7 @@ public void retrieveAnalysisTest() { violationAnalysis = qm.persist(violationAnalysis); qm.makeViolationAnalysisComment(violationAnalysis, "Analysis comment here", "Jane Doe"); - final Response response = target(V1_VIOLATION_ANALYSIS) + final Response response = jersey.target(V1_VIOLATION_ANALYSIS) .queryParam("component", component.getUuid()) .queryParam("policyViolation", violation.getUuid()) .request() @@ -156,7 +154,7 @@ public void retrieveAnalysisTest() { @Test public void retrieveAnalysisUnauthorizedTest() { - final Response response = target(V1_VIOLATION_ANALYSIS) + final Response response = jersey.target(V1_VIOLATION_ANALYSIS) .queryParam("component", UUID.randomUUID()) .queryParam("policyViolation", UUID.randomUUID()) .request() @@ -170,7 +168,7 @@ public void retrieveAnalysisUnauthorizedTest() { public void retrieveAnalysisComponentNotFoundTest() { initializeWithPermissions(Permissions.VIEW_POLICY_VIOLATION); - final Response response = target(V1_VIOLATION_ANALYSIS) + final Response response = jersey.target(V1_VIOLATION_ANALYSIS) .queryParam("component", UUID.randomUUID()) .queryParam("policyViolation", UUID.randomUUID()) .request() @@ -193,7 +191,7 @@ public void retrieveAnalysisViolationNotFoundTest() { component.setVersion("1.0"); component = qm.createComponent(component, false); - final Response response = target(V1_VIOLATION_ANALYSIS) + final Response response = jersey.target(V1_VIOLATION_ANALYSIS) .queryParam("component", component.getUuid()) .queryParam("policyViolation", UUID.randomUUID()) .request() @@ -229,7 +227,7 @@ public void updateAnalysisCreateNewTest() throws Exception { final var request = new ViolationAnalysisRequest(component.getUuid().toString(), violation.getUuid().toString(), ViolationAnalysisState.APPROVED, "Some comment", false); - final Response response = target(V1_VIOLATION_ANALYSIS) + final Response response = jersey.target(V1_VIOLATION_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -286,7 +284,7 @@ public void updateAnalysisCreateNewWithEmptyRequestTest() throws Exception { final var request = new ViolationAnalysisRequest(component.getUuid().toString(), violation.getUuid().toString(), null, null, null); - final Response response = target(V1_VIOLATION_ANALYSIS) + final Response response = jersey.target(V1_VIOLATION_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -343,7 +341,7 @@ public void updateAnalysisUpdateExistingTest() throws Exception { final var request = new ViolationAnalysisRequest(component.getUuid().toString(), violation.getUuid().toString(), ViolationAnalysisState.REJECTED, "Some comment", false); - final Response response = target(V1_VIOLATION_ANALYSIS) + final Response response = jersey.target(V1_VIOLATION_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -411,7 +409,7 @@ public void updateAnalysisUpdateExistingNoChangesTest() throws Exception{ final var request = new ViolationAnalysisRequest(component.getUuid().toString(), violation.getUuid().toString(), ViolationAnalysisState.APPROVED, null, true); - final Response response = target(V1_VIOLATION_ANALYSIS) + final Response response = jersey.target(V1_VIOLATION_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -459,7 +457,7 @@ public void updateAnalysisUpdateExistingWithEmptyRequestTest() throws Exception final var request = new ViolationAnalysisRequest(component.getUuid().toString(), violation.getUuid().toString(), null, null, null); - final Response response = target(V1_VIOLATION_ANALYSIS) + final Response response = jersey.target(V1_VIOLATION_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -492,7 +490,7 @@ public void updateAnalysisUnauthorizedTest() { final var request = new ViolationAnalysisRequest(UUID.randomUUID().toString(), UUID.randomUUID().toString(), ViolationAnalysisState.REJECTED, "Some comment", false); - final Response response = target(V1_VIOLATION_ANALYSIS) + final Response response = jersey.target(V1_VIOLATION_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -507,7 +505,7 @@ public void updateAnalysisComponentNotFoundTest() { final var request = new ViolationAnalysisRequest(UUID.randomUUID().toString(), UUID.randomUUID().toString(), ViolationAnalysisState.REJECTED, "Some comment", false); - final Response response = target(V1_VIOLATION_ANALYSIS) + final Response response = jersey.target(V1_VIOLATION_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -531,7 +529,7 @@ public void updateAnalysisViolationNotFoundTest() { final var request = new ViolationAnalysisRequest(component.getUuid().toString(), UUID.randomUUID().toString(), ViolationAnalysisState.REJECTED, "Some comment", false); - final Response response = target(V1_VIOLATION_ANALYSIS) + final Response response = jersey.target(V1_VIOLATION_ANALYSIS) .request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); diff --git a/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java index 812b8722ff..a96d97cd2d 100644 --- a/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java @@ -21,6 +21,7 @@ import alpine.common.util.UuidUtil; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.AffectedVersionAttribution; import org.dependencytrack.model.AnalysisJustification; @@ -33,10 +34,8 @@ import org.dependencytrack.model.VulnerableSoftware; import org.dependencytrack.tasks.scanners.AnalyzerIdentity; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import javax.json.Json; @@ -50,19 +49,16 @@ public class VulnerabilityResourceTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(VulnerabilityResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(VulnerabilityResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); @Test public void getVulnerabilitiesByComponentUuidTest() { SampleData sampleData = new SampleData(); - Response response = target(V1_VULNERABILITY + "/component/" + sampleData.c1.getUuid().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/component/" + sampleData.c1.getUuid().toString()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -89,7 +85,7 @@ public void getVulnerabilitiesByComponentUuidTest() { @Test public void getVulnerabilitiesByComponentInvalidTest() { new SampleData(); - Response response = target(V1_VULNERABILITY + "/component/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/component/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); @@ -101,7 +97,7 @@ public void getVulnerabilitiesByComponentInvalidTest() { @Test public void getVulnerabilitiesByComponentUuidIncludeSuppressedTest() { SampleData sampleData = new SampleData(); - Response response = target(V1_VULNERABILITY + "/component/" + sampleData.c1.getUuid().toString()) + Response response = jersey.target(V1_VULNERABILITY + "/component/" + sampleData.c1.getUuid().toString()) .queryParam("suppressed", "true") .request() .header(X_API_KEY, apiKey) @@ -131,7 +127,7 @@ public void getVulnerabilitiesByComponentUuidIncludeSuppressedTest() { @Test public void getVulnerabilitiesByProjectTest() { SampleData sampleData = new SampleData(); - Response response = target(V1_VULNERABILITY + "/project/" + sampleData.p1.getUuid().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/project/" + sampleData.p1.getUuid().toString()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -164,7 +160,7 @@ public void getVulnerabilitiesByProjectTest() { @Test public void getVulnerabilitiesByProjectIncludeProjectSuppressedTest() { SampleData sampleData = new SampleData(); - Response response = target(V1_VULNERABILITY + "/project/" + sampleData.p2.getUuid().toString()) + Response response = jersey.target(V1_VULNERABILITY + "/project/" + sampleData.p2.getUuid().toString()) .queryParam("suppressed", "true") .request() .header(X_API_KEY, apiKey) @@ -181,7 +177,7 @@ public void getVulnerabilitiesByProjectIncludeProjectSuppressedTest() { @Test public void getVulnerabilitiesByProjectInvalidTest() { new SampleData(); - Response response = target(V1_VULNERABILITY + "/project/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/project/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); @@ -193,7 +189,7 @@ public void getVulnerabilitiesByProjectInvalidTest() { @Test public void getVulnerabilityByUuidTest() { SampleData sampleData = new SampleData(); - Response response = target(V1_VULNERABILITY + "/" + sampleData.v1.getUuid().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/" + sampleData.v1.getUuid().toString()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -206,7 +202,7 @@ public void getVulnerabilityByUuidTest() { @Test public void getVulnerabilityByUuidInvalidTest() { new SampleData(); - Response response = target(V1_VULNERABILITY + "/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); @@ -218,7 +214,7 @@ public void getVulnerabilityByUuidInvalidTest() { @Test public void getVulnerabilityByVulnIdTest() { SampleData sampleData = new SampleData(); - Response response = target(V1_VULNERABILITY + "/source/" + sampleData.v1.getSource() + "/vuln/" + sampleData.v1.getVulnId()).request() + Response response = jersey.target(V1_VULNERABILITY + "/source/" + sampleData.v1.getSource() + "/vuln/" + sampleData.v1.getVulnId()).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -236,7 +232,7 @@ public void getVulnerabilityByVulnIdTest() { @Test public void getVulnerabilityByVulnIdInvalidTest() { new SampleData(); - Response response = target(V1_VULNERABILITY + "/source/INTERNAL/vuln/blah").request() + Response response = jersey.target(V1_VULNERABILITY + "/source/INTERNAL/vuln/blah").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); @@ -248,7 +244,7 @@ public void getVulnerabilityByVulnIdInvalidTest() { @Test public void getAffectedProjectTest() { SampleData sampleData = new SampleData(); - Response response = target(V1_VULNERABILITY + "/source/" + sampleData.v1.getSource() + "/vuln/" + sampleData.v1.getVulnId() + "/projects").request() + Response response = jersey.target(V1_VULNERABILITY + "/source/" + sampleData.v1.getSource() + "/vuln/" + sampleData.v1.getVulnId() + "/projects").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -263,7 +259,7 @@ public void getAffectedProjectTest() { @Test public void getAffectedProjectInvalidTest() { new SampleData(); - Response response = target(V1_VULNERABILITY + "/source/INTERNAL/vuln/blah/projects").request() + Response response = jersey.target(V1_VULNERABILITY + "/source/INTERNAL/vuln/blah/projects").request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); @@ -275,7 +271,7 @@ public void getAffectedProjectInvalidTest() { @Test public void getAllVulnerabilitiesTest() { new SampleData(); - Response response = target(V1_VULNERABILITY).request() + Response response = jersey.target(V1_VULNERABILITY).request() .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); @@ -306,7 +302,7 @@ public void createVulnerabilityTest() { .add("versionType", "RANGE") .add("versionEndIncluding", "1.2.3"))) .build(); - Response response = target(V1_VULNERABILITY).request() + Response response = jersey.target(V1_VULNERABILITY).request() .header(X_API_KEY, apiKey) .put(Entity.json(payload.toString())); Assert.assertEquals(201, response.getStatus(), 0); @@ -360,7 +356,7 @@ public void createVulnerabilityWithBadOwaspVectorTest() { .add("versionType", "RANGE") .add("versionEndIncluding", "1.2.3"))) .build(); - Response response = target(V1_VULNERABILITY).request() + Response response = jersey.target(V1_VULNERABILITY).request() .header(X_API_KEY, apiKey) .put(Entity.json(payload.toString())); Assert.assertEquals(400, response.getStatus(), 0); @@ -379,7 +375,7 @@ public void createVulnerabilityCwePreV450CompatTest() { .add("vulnId", "ACME-1") .add("cwe", Json.createObjectBuilder().add("cweId", 80)) .build(); - Response response = target(V1_VULNERABILITY).request() + Response response = jersey.target(V1_VULNERABILITY).request() .header(X_API_KEY, apiKey) .put(Entity.json(payload.toString())); Assert.assertEquals(201, response.getStatus(), 0); @@ -410,7 +406,7 @@ public void createVulnerabilityDuplicateTest() { .add("cvssV3Vector", "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L") .add("owaspRRVector", "SL:1/M:1/O:0/S:2/ED:1/EE:1/A:1/ID:1/LC:2/LI:1/LAV:1/LAC:1/FD:1/RD:1/NC:2/PV:3") .build(); - Response response = target(V1_VULNERABILITY).request() + Response response = jersey.target(V1_VULNERABILITY).request() .header(X_API_KEY, apiKey) .put(Entity.json(payload.toString())); Assert.assertEquals(409, response.getStatus(), 0); @@ -435,7 +431,7 @@ public void updateVulnerabilityTest() { .add("owaspRRVector", "SL:1/M:1/O:0/S:2/ED:1/EE:1/A:1/ID:1/LC:2/LI:1/LAV:1/LAC:1/FD:1/RD:1/NC:2/PV:3") .add("uuid", vuln.getUuid().toString()) .build(); - Response response = target(V1_VULNERABILITY).request() + Response response = jersey.target(V1_VULNERABILITY).request() .header(X_API_KEY, apiKey) .post(Entity.json(payload.toString())); Assert.assertEquals(200, response.getStatus(), 0); @@ -474,7 +470,7 @@ public void updateVulnerabilityInvalidTest() { .add("owaspRRVector", "SL:1/M:1/O:0/S:2/ED:1/EE:1/A:1/ID:1/LC:2/LI:1/LAV:1/LAC:1/FD:1/RD:1/NC:2/PV:3") .add("uuid", UUID.randomUUID().toString()) .build(); - Response response = target(V1_VULNERABILITY).request() + Response response = jersey.target(V1_VULNERABILITY).request() .header(X_API_KEY, apiKey) .post(Entity.json(payload.toString())); Assert.assertEquals(404, response.getStatus(), 0); @@ -498,7 +494,7 @@ public void updateVulnerabilityUnchangableTest() { .add("owaspRRVector", "SL:1/M:1/O:0/S:2/ED:1/EE:1/A:1/ID:1/LC:2/LI:1/LAV:1/LAC:1/FD:1/RD:1/NC:2/PV:3") .add("uuid", vuln.getUuid().toString()) .build(); - Response response = target(V1_VULNERABILITY).request() + Response response = jersey.target(V1_VULNERABILITY).request() .header(X_API_KEY, apiKey) .post(Entity.json(payload.toString())); Assert.assertEquals(406, response.getStatus(), 0); @@ -517,7 +513,7 @@ public void deleteVulnerabilityTest() { vuln = qm.createVulnerability(vuln, false); final AffectedVersionAttribution attribution = qm.persist(new AffectedVersionAttribution(Vulnerability.Source.INTERNAL, vuln, vs)); - final Response response = target(V1_VULNERABILITY + "/" + vuln.getUuid()).request() + final Response response = jersey.target(V1_VULNERABILITY + "/" + vuln.getUuid()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(204, response.getStatus()); @@ -539,7 +535,7 @@ public void assignVulnerabilityTest() { comp.setProject(project); comp.setName("Test Component"); comp = qm.createComponent(comp, false); - Response response = target(V1_VULNERABILITY + "/source/INTERNAL/vuln/ACME-1/component/" + comp.getUuid().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/source/INTERNAL/vuln/ACME-1/component/" + comp.getUuid().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -553,7 +549,7 @@ public void assignVulnerabilityInvalidVulnerabilityTest() { comp.setProject(project); comp.setName("Test Component"); comp = qm.createComponent(comp, false); - Response response = target(V1_VULNERABILITY + "/source/INTERNAL/vuln/BLAH/component/" + comp.getUuid().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/source/INTERNAL/vuln/BLAH/component/" + comp.getUuid().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -568,7 +564,7 @@ public void assignVulnerabilityInvalidComponentTest() { vuln.setVulnId("ACME-1"); vuln.setSource(Vulnerability.Source.INTERNAL); qm.createVulnerability(vuln, false); - Response response = target(V1_VULNERABILITY + "/source/INTERNAL/vuln/ACME-1/component/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/source/INTERNAL/vuln/ACME-1/component/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -588,7 +584,7 @@ public void assignVulnerabilityByUuidTest() { comp.setProject(project); comp.setName("Test Component"); comp = qm.createComponent(comp, false); - Response response = target(V1_VULNERABILITY + "/" + vuln.getUuid().toString() + "/component/" + comp.getUuid().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/" + vuln.getUuid().toString() + "/component/" + comp.getUuid().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus(), 0); @@ -602,7 +598,7 @@ public void assignVulnerabilityByUuidInvalidVulnerabilityTest() { comp.setProject(project); comp.setName("Test Component"); comp = qm.createComponent(comp, false); - Response response = target(V1_VULNERABILITY + "/" + UUID.randomUUID().toString() + "/component/" + comp.getUuid().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/" + UUID.randomUUID().toString() + "/component/" + comp.getUuid().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -617,7 +613,7 @@ public void assignVulnerabilityByUuidInvalidComponentTest() { vuln.setVulnId("ACME-1"); vuln.setSource(Vulnerability.Source.INTERNAL); vuln = qm.createVulnerability(vuln, false); - Response response = target(V1_VULNERABILITY + "/" + vuln.getUuid().toString() + "/component/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/" + vuln.getUuid().toString() + "/component/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .post(Entity.entity(null, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); @@ -637,7 +633,7 @@ public void unassignVulnerabilityTest() { comp.setProject(project); comp.setName("Test Component"); comp = qm.createComponent(comp, false); - Response response = target(V1_VULNERABILITY + "/source/INTERNAL/vuln/ACME-1/component/" + comp.getUuid().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/source/INTERNAL/vuln/ACME-1/component/" + comp.getUuid().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(200, response.getStatus(), 0); @@ -651,7 +647,7 @@ public void unassignVulnerabilityInvalidVulnerabilityTest() { comp.setProject(project); comp.setName("Test Component"); comp = qm.createComponent(comp, false); - Response response = target(V1_VULNERABILITY + "/source/INTERNAL/vuln/BLAH/component/" + comp.getUuid().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/source/INTERNAL/vuln/BLAH/component/" + comp.getUuid().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(404, response.getStatus(), 0); @@ -666,7 +662,7 @@ public void unassignVulnerabilityInvalidComponentTest() { vuln.setVulnId("ACME-1"); vuln.setSource(Vulnerability.Source.INTERNAL); qm.createVulnerability(vuln, false); - Response response = target(V1_VULNERABILITY + "/source/INTERNAL/vuln/ACME-1/component/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/source/INTERNAL/vuln/ACME-1/component/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(404, response.getStatus(), 0); @@ -686,7 +682,7 @@ public void unassignVulnerabilityByUuidTest() { comp.setProject(project); comp.setName("Test Component"); comp = qm.createComponent(comp, false); - Response response = target(V1_VULNERABILITY + "/" + vuln.getUuid().toString() + "/component/" + comp.getUuid().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/" + vuln.getUuid().toString() + "/component/" + comp.getUuid().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(200, response.getStatus(), 0); @@ -700,7 +696,7 @@ public void unassignVulnerabilityByUuidInvalidVulnerabilityTest() { comp.setProject(project); comp.setName("Test Component"); comp = qm.createComponent(comp, false); - Response response = target(V1_VULNERABILITY + "/" + UUID.randomUUID().toString() + "/component/" + comp.getUuid().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/" + UUID.randomUUID().toString() + "/component/" + comp.getUuid().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(404, response.getStatus(), 0); @@ -715,7 +711,7 @@ public void unassignVulnerabilityByUuidInvalidComponentTest() { vuln.setVulnId("ACME-1"); vuln.setSource(Vulnerability.Source.INTERNAL); vuln = qm.createVulnerability(vuln, false); - Response response = target(V1_VULNERABILITY + "/" + vuln.getUuid().toString() + "/component/" + UUID.randomUUID().toString()).request() + Response response = jersey.target(V1_VULNERABILITY + "/" + vuln.getUuid().toString() + "/component/" + UUID.randomUUID().toString()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(404, response.getStatus(), 0); diff --git a/src/test/java/org/dependencytrack/resources/v1/exception/ClientErrorExceptionMapperTest.java b/src/test/java/org/dependencytrack/resources/v1/exception/ClientErrorExceptionMapperTest.java index bcc1964c73..038e5b73ac 100644 --- a/src/test/java/org/dependencytrack/resources/v1/exception/ClientErrorExceptionMapperTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/exception/ClientErrorExceptionMapperTest.java @@ -18,11 +18,10 @@ */ package org.dependencytrack.resources.v1.exception; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; +import org.junit.ClassRule; import org.junit.Test; import javax.ws.rs.GET; @@ -33,17 +32,14 @@ public class ClientErrorExceptionMapperTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(TestResource.class) - .register(ClientErrorExceptionMapper.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(TestResource.class) + .register(ClientErrorExceptionMapper.class)); @Test public void testNotFound() { - final Response response = target("/does/not/exist") + final Response response = jersey.target("/does/not/exist") .request() .get(); @@ -52,7 +48,7 @@ public void testNotFound() { @Test public void testMethodNotAllowed() { - final Response response = target("/test/foo") + final Response response = jersey.target("/test/foo") .request() .delete(); diff --git a/src/test/java/org/dependencytrack/resources/v1/exception/ConstraintViolationExceptionMapperTest.java b/src/test/java/org/dependencytrack/resources/v1/exception/ConstraintViolationExceptionMapperTest.java index 6af6b3330b..459187625b 100644 --- a/src/test/java/org/dependencytrack/resources/v1/exception/ConstraintViolationExceptionMapperTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/exception/ConstraintViolationExceptionMapperTest.java @@ -19,12 +19,11 @@ package org.dependencytrack.resources.v1.exception; import net.javacrumbs.jsonunit.core.Option; +import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.validation.ValidUuid; import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.servlet.ServletContainer; -import org.glassfish.jersey.test.DeploymentContext; -import org.glassfish.jersey.test.ServletDeploymentContext; +import org.junit.ClassRule; import org.junit.Test; import javax.validation.constraints.Pattern; @@ -41,17 +40,14 @@ public class ConstraintViolationExceptionMapperTest extends ResourceTest { - @Override - protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(TestResource.class) - .register(ConstraintViolationExceptionMapper.class))) - .build(); - } + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(TestResource.class) + .register(ConstraintViolationExceptionMapper.class)); @Test public void test() { - final Response response = target("/not-a-uuid") + final Response response = jersey.target("/not-a-uuid") .queryParam("foo", "666") .request() .get(); From e57beda4f505ea99fa95943e053aaca73b12362f Mon Sep 17 00:00:00 2001 From: nscuro Date: Fri, 3 May 2024 00:24:59 +0200 Subject: [PATCH 118/412] Reduce preemptive expensive calls in `ResourceTest` Turns out that hashing the password for the `testUser` that is created in `ResourceTest` takes >=1sec, making it the biggest contributing factor to test duration. Instead of re-hashing the same string over and over again, hash it once and re-use where appropriate. The loading of default objects was another factory. Some tests only need parts of the default objects, not all. Some are more expensive to load than others (loading >700 licenses takes longer than ~10 permissions). Tests that previously took over a second to run now complete in ~50ms. Signed-off-by: nscuro --- .../persistence/DefaultObjectGenerator.java | 8 ++-- .../org/dependencytrack/ResourceTest.java | 13 ++---- .../resources/v1/LicenseResourceTest.java | 8 ++-- .../v1/NotificationPublisherResourceTest.java | 15 +++--- .../v1/NotificationRuleResourceTest.java | 4 +- .../resources/v1/PermissionResourceTest.java | 17 ++++--- .../resources/v1/RepositoryResourceTest.java | 4 +- .../resources/v1/TeamResourceTest.java | 4 ++ .../v1/UserResourceAuthenticatedTest.java | 46 ++++++++++--------- .../v1/UserResourceUnauthenticatedTest.java | 10 ++++ 10 files changed, 69 insertions(+), 60 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index 330034cae7..39877bfff7 100644 --- a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -79,7 +79,7 @@ public void contextDestroyed(final ServletContextEvent event) { /** * Loads the default licenses into the database if no license data exists. */ - private void loadDefaultLicenses() { + public void loadDefaultLicenses() { try (QueryManager qm = new QueryManager()) { LOGGER.info("Synchronizing SPDX license definitions to datastore"); @@ -120,7 +120,7 @@ private void loadDefaultLicenseGroups() { /** * Loads the default permissions */ - private void loadDefaultPermissions() { + public void loadDefaultPermissions() { try (QueryManager qm = new QueryManager()) { LOGGER.info("Synchronizing permissions to datastore"); for (final Permissions permission : Permissions.values()) { @@ -197,7 +197,7 @@ private List getAutomationPermissions(final List fullLis /** * Loads the default repositories */ - private void loadDefaultRepositories() { + public void loadDefaultRepositories() { try (QueryManager qm = new QueryManager()) { LOGGER.info("Synchronizing default repositories to datastore"); qm.createRepository(RepositoryType.CPAN, "cpan-public-registry", "https://fastapi.metacpan.org/v1/", true, false, false, null, null); @@ -236,7 +236,7 @@ private void loadDefaultConfigProperties() { /** * Loads the default notification publishers */ - private void loadDefaultNotificationPublishers() { + public void loadDefaultNotificationPublishers() { try (QueryManager qm = new QueryManager()) { LOGGER.info("Synchronizing notification publishers to datastore"); for (final DefaultNotificationPublishers publisher : DefaultNotificationPublishers.values()) { diff --git a/src/test/java/org/dependencytrack/ResourceTest.java b/src/test/java/org/dependencytrack/ResourceTest.java index 05e7995128..22ddd0900e 100644 --- a/src/test/java/org/dependencytrack/ResourceTest.java +++ b/src/test/java/org/dependencytrack/ResourceTest.java @@ -19,10 +19,8 @@ package org.dependencytrack; import alpine.Config; -import alpine.model.ManagedUser; import alpine.model.Permission; import alpine.model.Team; -import alpine.server.auth.JsonWebToken; import alpine.server.auth.PasswordService; import alpine.server.persistence.PersistenceManagerFactory; import org.dependencytrack.auth.Permissions; @@ -80,12 +78,12 @@ public abstract class ResourceTest { protected final String SIZE = "size"; protected final String TOTAL_COUNT_HEADER = "X-Total-Count"; protected final String X_API_KEY = "X-Api-Key"; - protected final String V1_TAG = "/v1/tag"; + // Hashing is expensive. Do it once and re-use across tests as much as possible. + protected static final String TEST_USER_PASSWORD_HASH = new String(PasswordService.createHash("testuser".toCharArray())); + protected QueryManager qm; - protected ManagedUser testUser; - protected String jwt; protected Team team; protected String apiKey; @@ -98,10 +96,7 @@ public static void init() { public void before() throws Exception { // Add a test user and team with API key. Optional if this is used, but its available to all tests. this.qm = new QueryManager(); - testUser = qm.createManagedUser("testuser", String.valueOf(PasswordService.createHash("testuser".toCharArray()))); - this.jwt = new JsonWebToken().createToken(testUser); team = qm.createTeam("Test Users", true); - qm.addUserToTeam(testUser, team); this.apiKey = team.getApiKeys().get(0).getKey(); } @@ -124,10 +119,8 @@ public void initializeWithPermissions(Permissions... permissions) { for (Permissions permission: permissions) { permissionList.add(qm.createPermission(permission.name(), null)); } - testUser.setPermissions(permissionList); team.setPermissions(permissionList); qm.persist(team); - testUser = qm.persist(testUser); } protected String getPlainTextBody(Response response) { diff --git a/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java index 3b184ee16b..ad558ad876 100644 --- a/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java @@ -46,9 +46,11 @@ public class LicenseResourceTest extends ResourceTest { .register(AuthenticationFilter.class)); @Before - public void loadDefaultLicenses() { - DefaultObjectGenerator dog = new DefaultObjectGenerator(); - dog.contextInitialized(null); + @Override + public void before() throws Exception { + super.before(); + final var generator = new DefaultObjectGenerator(); + generator.loadDefaultLicenses(); } @Test diff --git a/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java index b734044880..064060ed54 100644 --- a/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java @@ -19,7 +19,6 @@ package org.dependencytrack.resources.v1; import alpine.common.util.UuidUtil; -import alpine.model.ConfigProperty; import alpine.notification.NotificationLevel; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; @@ -58,8 +57,8 @@ public class NotificationPublisherResourceTest extends ResourceTest { @Before public void before() throws Exception { super.before(); - DefaultObjectGenerator generator = new DefaultObjectGenerator(); - generator.contextInitialized(null); + final var generator = new DefaultObjectGenerator(); + generator.loadDefaultNotificationPublishers(); } @Test @@ -346,13 +345,13 @@ public void restoreDefaultTemplatesTest() { slackPublisher.setName(slackPublisher.getName()+" Updated"); qm.persist(slackPublisher); qm.detach(NotificationPublisher.class, slackPublisher.getId()); - ConfigProperty property = qm.getConfigProperty( + qm.createConfigProperty( ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED.getGroupName(), - ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED.getPropertyName() + ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED.getPropertyName(), + "true", + ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED.getPropertyType(), + ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED.getDescription() ); - property.setPropertyValue("true"); - qm.persist(property); - qm.detach(ConfigProperty.class, property.getId()); Response response = jersey.target(V1_NOTIFICATION_PUBLISHER + "/restoreDefaultTemplates").request() .header(X_API_KEY, apiKey) .post(Entity.json("")); diff --git a/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java index ed9884b17f..0967eed04c 100644 --- a/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java @@ -65,8 +65,8 @@ public class NotificationRuleResourceTest extends ResourceTest { @Before public void before() throws Exception { super.before(); - DefaultObjectGenerator generator = new DefaultObjectGenerator(); - generator.contextInitialized(null); + final var generator = new DefaultObjectGenerator(); + generator.loadDefaultNotificationPublishers(); } @Test diff --git a/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java index 9a0ad96db3..87d4a04a12 100644 --- a/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java @@ -21,7 +21,6 @@ import alpine.model.ManagedUser; import alpine.model.Permission; import alpine.model.Team; -import alpine.server.auth.PasswordService; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import org.dependencytrack.JerseyTestRule; @@ -52,8 +51,8 @@ public class PermissionResourceTest extends ResourceTest { @Before public void before() throws Exception { super.before(); - DefaultObjectGenerator generator = new DefaultObjectGenerator(); - generator.contextInitialized(null); + final var generator = new DefaultObjectGenerator(); + generator.loadDefaultPermissions(); } @Test @@ -72,7 +71,7 @@ public void getAllPermissionsTest() { @Test public void addPermissionToUserTest() { - ManagedUser user = qm.createManagedUser("user1", new String(PasswordService.createHash("password".toCharArray()))); + ManagedUser user = qm.createManagedUser("user1", TEST_USER_PASSWORD_HASH); String username = user.getUsername(); qm.close(); Response response = jersey.target(V1_PERMISSION + "/PORTFOLIO_MANAGEMENT/user/" + username).request() @@ -99,7 +98,7 @@ public void addPermissionToUserInvalidUserTest() { @Test public void addPermissionToUserInvalidPermissionTest() { - ManagedUser user = qm.createManagedUser("user1", new String(PasswordService.createHash("password".toCharArray()))); + ManagedUser user = qm.createManagedUser("user1", TEST_USER_PASSWORD_HASH); String username = user.getUsername(); qm.close(); Response response = jersey.target(V1_PERMISSION + "/BLAH/user/" + username).request() @@ -113,7 +112,7 @@ public void addPermissionToUserInvalidPermissionTest() { @Test public void addPermissionToUserDuplicateTest() { - ManagedUser user = qm.createManagedUser("user1", new String(PasswordService.createHash("password".toCharArray()))); + ManagedUser user = qm.createManagedUser("user1", TEST_USER_PASSWORD_HASH); String username = user.getUsername(); Permission permission = qm.getPermission(Permissions.PORTFOLIO_MANAGEMENT.name()); user.getPermissions().add(permission); @@ -128,7 +127,7 @@ public void addPermissionToUserDuplicateTest() { @Test public void removePermissionFromUserTest() { - ManagedUser user = qm.createManagedUser("user1", new String(PasswordService.createHash("password".toCharArray()))); + ManagedUser user = qm.createManagedUser("user1", TEST_USER_PASSWORD_HASH); String username = user.getUsername(); Permission permission = qm.getPermission(Permissions.PORTFOLIO_MANAGEMENT.name()); user.getPermissions().add(permission); @@ -157,7 +156,7 @@ public void removePermissionFromUserInvalidUserTest() { @Test public void removePermissionFromUserInvalidPermissionTest() { - ManagedUser user = qm.createManagedUser("user1", new String(PasswordService.createHash("password".toCharArray()))); + ManagedUser user = qm.createManagedUser("user1", TEST_USER_PASSWORD_HASH); String username = user.getUsername(); qm.close(); Response response = jersey.target(V1_PERMISSION + "/BLAH/user/" + username).request() @@ -171,7 +170,7 @@ public void removePermissionFromUserInvalidPermissionTest() { @Test public void removePermissionFromUserNoChangesTest() { - ManagedUser user = qm.createManagedUser("user1", new String(PasswordService.createHash("password".toCharArray()))); + ManagedUser user = qm.createManagedUser("user1", TEST_USER_PASSWORD_HASH); String username = user.getUsername(); Response response = jersey.target(V1_PERMISSION + "/BOM_UPLOAD/user/" + username).request() .header(X_API_KEY, apiKey) diff --git a/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java index 51b943193d..a45b559c60 100644 --- a/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java @@ -52,8 +52,8 @@ public class RepositoryResourceTest extends ResourceTest { @Before public void before() throws Exception { super.before(); - DefaultObjectGenerator generator = new DefaultObjectGenerator(); - generator.contextInitialized(null); + final var generator = new DefaultObjectGenerator(); + generator.loadDefaultRepositories(); } @Test diff --git a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java index 076e848337..784797cb4f 100644 --- a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java @@ -21,7 +21,9 @@ import alpine.common.util.UuidUtil; import alpine.model.ApiKey; import alpine.model.ConfigProperty; +import alpine.model.ManagedUser; import alpine.model.Team; +import alpine.server.auth.JsonWebToken; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import org.dependencytrack.JerseyTestRule; @@ -114,6 +116,8 @@ public void getTeamSelfTest() { Assert.assertEquals(401, response.getStatus()); // not an api-key + final ManagedUser testUser = qm.createManagedUser("testuser", TEST_USER_PASSWORD_HASH); + final String jwt = new JsonWebToken().createToken(testUser); response = jersey.target(V1_TEAM + "/self").request().header("Authorization", "Bearer " + jwt).get(Response.class); Assert.assertEquals(400, response.getStatus()); } diff --git a/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java b/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java index 322a1e7fc1..dfdd39df10 100644 --- a/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java @@ -22,7 +22,7 @@ import alpine.model.ManagedUser; import alpine.model.OidcUser; import alpine.model.Team; -import alpine.server.auth.PasswordService; +import alpine.server.auth.JsonWebToken; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import org.dependencytrack.JerseyTestRule; @@ -31,6 +31,7 @@ import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; +import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; @@ -49,11 +50,21 @@ public class UserResourceAuthenticatedTest extends ResourceTest { .register(ApiFilter.class) .register(AuthenticationFilter.class)); + private ManagedUser testUser; + private String jwt; + + @Before + public void before() throws Exception { + super.before(); + testUser = qm.createManagedUser("testuser", TEST_USER_PASSWORD_HASH); + this.jwt = new JsonWebToken().createToken(testUser); + qm.addUserToTeam(testUser, team); + } + @Test public void getManagedUsersTest() { - String hashedPassword = String.valueOf(PasswordService.createHash("password".toCharArray())); for (int i=0; i<1000; i++) { - qm.createManagedUser("managed-user-" + i, hashedPassword); + qm.createManagedUser("managed-user-" + i, TEST_USER_PASSWORD_HASH); } Response response = jersey.target(V1_USER + "/managed").request() .header(X_API_KEY, apiKey) @@ -351,7 +362,7 @@ public void createManagedUserPasswordMismatchTest() { @Test public void createManagedUserDuplicateUsernameTest() { - qm.createManagedUser("blackbeard", String.valueOf(PasswordService.createHash("password".toCharArray()))); + qm.createManagedUser("blackbeard", TEST_USER_PASSWORD_HASH); ManagedUser user = new ManagedUser(); user.setFullname("Captain BlackBeard"); user.setEmail("blackbeard@example.com"); @@ -369,8 +380,7 @@ public void createManagedUserDuplicateUsernameTest() { @Test public void updateManagedUserTest() { - String hashedPassword = String.valueOf(PasswordService.createHash("password".toCharArray())); - qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", hashedPassword, false, false, false); + qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", TEST_USER_PASSWORD_HASH, false, false, false); ManagedUser user = new ManagedUser(); user.setUsername("blackbeard"); user.setFullname("Dr BlackBeard, Ph.D."); @@ -394,8 +404,7 @@ public void updateManagedUserTest() { @Test public void updateManagedUserInvalidFullnameTest() { - String hashedPassword = String.valueOf(PasswordService.createHash("password".toCharArray())); - qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", hashedPassword, false, false, false); + qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", TEST_USER_PASSWORD_HASH, false, false, false); ManagedUser user = new ManagedUser(); user.setUsername("blackbeard"); user.setFullname(""); @@ -414,8 +423,7 @@ public void updateManagedUserInvalidFullnameTest() { @Test public void updateManagedUserInvalidEmailTest() { - String hashedPassword = String.valueOf(PasswordService.createHash("password".toCharArray())); - qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", hashedPassword, false, false, false); + qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", TEST_USER_PASSWORD_HASH, false, false, false); ManagedUser user = new ManagedUser(); user.setUsername("blackbeard"); user.setFullname("Captain BlackBeard"); @@ -434,8 +442,7 @@ public void updateManagedUserInvalidEmailTest() { @Test public void updateManagedUserInvalidUsernameTest() { - String hashedPassword = String.valueOf(PasswordService.createHash("password".toCharArray())); - qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", hashedPassword, false, false, false); + qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", TEST_USER_PASSWORD_HASH, false, false, false); ManagedUser user = new ManagedUser(); user.setUsername(""); user.setFullname("Captain BlackBeard"); @@ -454,8 +461,7 @@ public void updateManagedUserInvalidUsernameTest() { @Test public void deleteManagedUserTest() { - String hashedPassword = String.valueOf(PasswordService.createHash("password".toCharArray())); - qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", hashedPassword, false, false, false); + qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", TEST_USER_PASSWORD_HASH, false, false, false); ManagedUser user = new ManagedUser(); user.setUsername("blackbeard"); Response response = jersey.target(V1_USER + "/managed").request() @@ -495,8 +501,7 @@ public void createOidcUserDuplicateUsernameTest() { @Test public void addTeamToUserTest() { - String hashedPassword = String.valueOf(PasswordService.createHash("password".toCharArray())); - qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", hashedPassword, false, false, false); + qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", TEST_USER_PASSWORD_HASH, false, false, false); Team team = qm.createTeam("Pirates", false); IdentifiableObject ido = new IdentifiableObject(); ido.setUuid(team.getUuid().toString()); @@ -518,8 +523,7 @@ public void addTeamToUserTest() { @Test public void addTeamToUserInvalidTeamTest() { - String hashedPassword = String.valueOf(PasswordService.createHash("password".toCharArray())); - qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", hashedPassword, false, false, false); + qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", TEST_USER_PASSWORD_HASH, false, false, false); IdentifiableObject ido = new IdentifiableObject(); ido.setUuid(UUID.randomUUID().toString()); ManagedUser user = new ManagedUser(); @@ -551,9 +555,8 @@ public void addTeamToUserInvalidUserTest() { @Test public void addTeamToUserDuplicateMembershipTest() { - String hashedPassword = String.valueOf(PasswordService.createHash("password".toCharArray())); Team team = qm.createTeam("Pirates", false); - ManagedUser user = qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", hashedPassword, false, false, false); + ManagedUser user = qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", TEST_USER_PASSWORD_HASH, false, false, false); qm.addUserToTeam(user, team); IdentifiableObject ido = new IdentifiableObject(); ido.setUuid(team.getUuid().toString()); @@ -569,9 +572,8 @@ public void addTeamToUserDuplicateMembershipTest() { @Test public void removeTeamFromUserTest() { - String hashedPassword = String.valueOf(PasswordService.createHash("password".toCharArray())); Team team = qm.createTeam("Pirates", false); - ManagedUser user = qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", hashedPassword, false, false, false); + ManagedUser user = qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", TEST_USER_PASSWORD_HASH, false, false, false); qm.addUserToTeam(user, team); IdentifiableObject ido = new IdentifiableObject(); ido.setUuid(team.getUuid().toString()); diff --git a/src/test/java/org/dependencytrack/resources/v1/UserResourceUnauthenticatedTest.java b/src/test/java/org/dependencytrack/resources/v1/UserResourceUnauthenticatedTest.java index ce1370f0a4..18c6bd5f4e 100644 --- a/src/test/java/org/dependencytrack/resources/v1/UserResourceUnauthenticatedTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/UserResourceUnauthenticatedTest.java @@ -25,6 +25,7 @@ import org.dependencytrack.ResourceTest; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; +import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; @@ -40,6 +41,15 @@ public class UserResourceUnauthenticatedTest extends ResourceTest { new ResourceConfig(UserResource.class) .register(ApiFilter.class)); + private ManagedUser testUser; + + @Before + public void before() throws Exception { + super.before(); + testUser = qm.createManagedUser("testuser", TEST_USER_PASSWORD_HASH); + qm.addUserToTeam(testUser, team); + } + @Test public void validateCredentialsTest() { Form form = new Form(); From 80e5372d493b86977d9ddbb6f1899693019a6746 Mon Sep 17 00:00:00 2001 From: Ross Murphy Date: Fri, 3 May 2024 12:11:55 +0100 Subject: [PATCH 119/412] Revert "Add the project name and project URL to bom processing notifications" This reverts commit 063c2b385ce50c753b8a8b2c325d2c9775a680aa. Signed-off-by: Ross Murphy --- .../tasks/BomUploadProcessingTask.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java index 7841209f50..c8d72843da 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java @@ -19,10 +19,8 @@ package org.dependencytrack.tasks; import alpine.common.logging.Logger; -import alpine.common.util.UrlUtil; import alpine.event.framework.Event; import alpine.event.framework.Subscriber; -import alpine.model.ConfigProperty; import alpine.notification.Notification; import alpine.notification.NotificationLevel; import org.cyclonedx.BomParserFactory; @@ -222,7 +220,7 @@ public void inform(final Event e) { Notification.dispatch(new Notification() .scope(NotificationScope.PORTFOLIO) .group(NotificationGroup.BOM_PROCESSED) - .title(NotificationConstants.Title.BOM_PROCESSED + " on Project: [" + event.getProject().getName() + "]") + .title(NotificationConstants.Title.BOM_PROCESSED) .level(NotificationLevel.INFORMATIONAL) .content("A " + bomFormat.getFormatShortName() + " BOM was processed") .subject(new BomConsumedOrProcessed(detachedProject, Base64.getEncoder().encodeToString(bomBytes), bomFormat, bomSpecVersion))); @@ -231,16 +229,12 @@ public void inform(final Event e) { if (bomProcessingFailedProject != null) { bomProcessingFailedProject = qm.detach(Project.class, bomProcessingFailedProject.getId()); } - final ConfigProperty baseUrlProperty = qm.getConfigProperty( - ConfigPropertyConstants.GENERAL_BASE_URL.getGroupName(), - ConfigPropertyConstants.GENERAL_BASE_URL.getPropertyName() - ); Notification.dispatch(new Notification() .scope(NotificationScope.PORTFOLIO) .group(NotificationGroup.BOM_PROCESSING_FAILED) - .title(NotificationConstants.Title.BOM_PROCESSING_FAILED + " on Project: [" + event.getProject().getName() + "]") + .title(NotificationConstants.Title.BOM_PROCESSING_FAILED) .level(NotificationLevel.ERROR) - .content("An error occurred while processing a BOM: " + UrlUtil.normalize(baseUrlProperty.getPropertyValue()) + "/projects/" + event.getProject().getUuid()) + .content("An error occurred while processing a BOM") .subject(new BomProcessingFailed(bomProcessingFailedProject, Base64.getEncoder().encodeToString(bomBytes), ex.getMessage(), bomProcessingFailedBomFormat, bomProcessingFailedBomVersion))); } finally { qm.commitSearchIndex(true, Component.class); From 95c45ea85b2ecd326e0e9ea6a19b91755a95abdd Mon Sep 17 00:00:00 2001 From: Ross Murphy Date: Fri, 3 May 2024 13:59:32 +0100 Subject: [PATCH 120/412] Create a new entry in the msteams pebble format for BOM_PROCESS_FAILED to include the project URL and Name and add to test Previously there was no information as to what project was affected by the failed upload. Signed-off-by: Ross Murphy --- .../notification/publisher/msteams.peb | 23 +++++++++++++++++++ .../publisher/MsTeamsPublisherTest.java | 16 +++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/main/resources/templates/notification/publisher/msteams.peb b/src/main/resources/templates/notification/publisher/msteams.peb index 08c585e08a..2e7e65e322 100644 --- a/src/main/resources/templates/notification/publisher/msteams.peb +++ b/src/main/resources/templates/notification/publisher/msteams.peb @@ -96,6 +96,29 @@ "value": "{{ subject.project.toString | escape(strategy="json") }}" } ], + {% elseif notification.group == "BOM_PROCESSING_FAILED" %} + "facts": [ + { + "name": "Level", + "value": "{{ notification.level | escape(strategy="json") }}" + }, + { + "name": "Scope", + "value": "{{ notification.scope | escape(strategy="json") }}" + }, + { + "name": "Group", + "value": "{{ notification.group | escape(strategy="json") }}" + }, + { + "name": "Project", + "value": "{{ subject.project.toString | escape(strategy="json") }}" + }, + { + "name": "Project URL", + "value": "{{ baseUrl }}/projects/{{ subject.project.uuid | escape(strategy='json') }}" + } + ], {% else %} "facts": [ { diff --git a/src/test/java/org/dependencytrack/notification/publisher/MsTeamsPublisherTest.java b/src/test/java/org/dependencytrack/notification/publisher/MsTeamsPublisherTest.java index 987a930600..a7f22ed354 100644 --- a/src/test/java/org/dependencytrack/notification/publisher/MsTeamsPublisherTest.java +++ b/src/test/java/org/dependencytrack/notification/publisher/MsTeamsPublisherTest.java @@ -97,6 +97,14 @@ public void testInformWithBomProcessingFailedNotification() { { "name": "Group", "value": "BOM_PROCESSING_FAILED" + }, + { + "name": "Project", + "value": "pkg:maven/org.acme/projectName@projectVersion" + }, + { + "name": "Project URL", + "value": "https://example.com/projects/c9c9539a-e381-4b36-ac52-6a7ab83b2c95" } ], "text": "An error occurred while processing a BOM" @@ -135,6 +143,14 @@ public void testInformWithBomProcessingFailedNotificationAndNoSpecVersionInSubje { "name": "Group", "value": "BOM_PROCESSING_FAILED" + }, + { + "name": "Project", + "value": "pkg:maven/org.acme/projectName@projectVersion" + }, + { + "name": "Project URL", + "value": "https://example.com/projects/c9c9539a-e381-4b36-ac52-6a7ab83b2c95" } ], "text": "An error occurred while processing a BOM" From f93a32e8ee1c9f818d770142cce5a1161537abcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 08:10:08 +0000 Subject: [PATCH 121/412] Bump actions/dependency-review-action from 4.2.5 to 4.3.2 Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.2.5 to 4.3.2. - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/5bbc3ba658137598168acb2ab73b21c432dd411b...0c155c5e8556a497adf53f2c18edabf945ed8e70) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/dependency-review.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index 06f005fa2c..fbc882240b 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -12,4 +12,4 @@ jobs: uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 - name: Dependency Review - uses: actions/dependency-review-action@5bbc3ba658137598168acb2ab73b21c432dd411b # tag=v4.2.5 + uses: actions/dependency-review-action@0c155c5e8556a497adf53f2c18edabf945ed8e70 # tag=v4.3.2 From 8d652e28a366e75e9aac792a617ce4426343ebfe Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 6 May 2024 13:36:08 +0200 Subject: [PATCH 122/412] Run builds and CI on `feature-*` branches This allows contributors to collaborate on branches that are not `master`, and still profit from CI runs and container images being published. Names of feature branches are required to be prefixed with `feature-`. Container images built from them will be published using the branch name as tag. This approach has been implemented in Hyades already: * https://github.com/DependencyTrack/hyades-apiserver/pull/651 * https://github.com/DependencyTrack/hyades/pull/1192 Signed-off-by: nscuro --- .github/workflows/_meta-build.yaml | 18 +++++++++++++--- .github/workflows/ci-build.yaml | 5 ++++- .github/workflows/ci-publish.yaml | 1 + .github/workflows/ci-test.yaml | 2 ++ DEVELOPING.md | 33 ++++++++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 877e620862..200948a4bb 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -11,6 +11,10 @@ on: required: false default: false description: "publish and scan the container image once its built" + ref-name: + type: string + required: true + description: "Short ref name of the branch or tag that triggered the workflow run" secrets: registry-0-usr: required: true @@ -102,9 +106,17 @@ jobs: - name: Set Container Tags id: tags run: |- - TAGS="docker.io/dependencytrack/${{ matrix.distribution }}:${{ inputs.app-version }}" - if [[ "${{ inputs.app-version }}" != "snapshot" ]]; then - TAGS="${TAGS},docker.io/dependencytrack/${{ matrix.distribution }}:latest" + IMAGE_NAME="docker.io/dependencytrack/${{ matrix.distribution }}" + REF_NAME="${{ inputs.ref-name }}" + TAGS="" + + if [[ $REF_NAME == feature-* ]]; then + TAGS="${IMAGE_NAME}:${REF_NAME,,}" + else + TAGS="${IMAGE_NAME}:${{ inputs.app-version }}" + if [[ "${{ inputs.app-version }}" != "snapshot" ]]; then + TAGS="${TAGS},${IMAGE_NAME}:latest" + fi fi echo "tags=${TAGS}" >> $GITHUB_OUTPUT diff --git a/.github/workflows/ci-build.yaml b/.github/workflows/ci-build.yaml index 3b4cba2fab..3c1516ea05 100644 --- a/.github/workflows/ci-build.yaml +++ b/.github/workflows/ci-build.yaml @@ -4,6 +4,7 @@ on: push: branches: - 'master' # Default branch + - 'feature-**' # Feature branches - '[0-9]+.[0-9]+.x' # Release branches paths-ignore: - '**/*.md' @@ -11,6 +12,7 @@ on: pull_request: branches: - 'master' # Default branch + - 'feature-**' # Feature branches paths-ignore: - '**/*.md' - 'docs/**' @@ -23,7 +25,8 @@ jobs: uses: ./.github/workflows/_meta-build.yaml with: app-version: "snapshot" - publish-container: ${{ github.ref == 'refs/heads/master' }} + publish-container: ${{ github.ref_name == 'master' || startsWith(github.ref_name, 'feature-') }} + ref-name: ${{ github.ref_name }} permissions: security-events: write # Required to upload trivy's SARIF output secrets: diff --git a/.github/workflows/ci-publish.yaml b/.github/workflows/ci-publish.yaml index 9c6c0a75bb..71886560c2 100644 --- a/.github/workflows/ci-publish.yaml +++ b/.github/workflows/ci-publish.yaml @@ -38,6 +38,7 @@ jobs: with: app-version: ${{ needs.read-version.outputs.version }} publish-container: true + ref-name: ${{ github.ref_name }} permissions: security-events: write # Required to upload trivy's SARIF output secrets: diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index 56543ae946..0b0aac5335 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -4,6 +4,7 @@ on: push: branches: - 'master' # Default branch + - 'feature-**' # Feature branches - '[0-9]+.[0-9]+.x' # Release branches paths-ignore: - '**/*.md' @@ -11,6 +12,7 @@ on: pull_request: branches: - 'master' # Default branch + - 'feature-**' # Feature branches - '[0-9]+.[0-9]+.x' # Release branches paths-ignore: - '**/*.md' diff --git a/DEVELOPING.md b/DEVELOPING.md index c69bc57be7..4a2b4bad8f 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -343,3 +343,36 @@ This will start a local webserver that listens on `127.0.0.1:4000` and rebuilds > ``` > docker run --rm -it --name jekyll -p "127.0.0.1:4000:4000" -v "$(pwd)/docs:/srv/jekyll:Z" jekyll/jekyll:3.8 jekyll serve > ``` + +## Feature Branches + +When working on larger changes, it can happen that pull requests remain open for a prolonged period of time. +As the `master` branch evolves, more and more merge conflicts occur, which the PR author needs to address. + +Sometimes it's desired to get user feedback on a new feature before it's being merged to `master`. +Expecting users to clone the repository and build the project on their own however is not realistic. +Instead, it would be beneficial to offer a container image including the new feature. + +When a feature is built in collaboration by multiple individuals, teams have to jump through annoying hoops +to make it work with the GitHub workflow. Usually this requires one individual to raise a PR from their fork, +and giving the rest of the team write permissions to their repository. + +To address the use cases above, contributors to the Dependency-Track project can request a *feature branch* +from a maintainer. Feature branches follow the `feature-*` naming pattern, and are subject to branch protection rules. +Just like for the `maser` branch, changes pushed to a `feature` branch trigger a container image build. +Images built from `feature` branches are tagged with the name of the branch, for example for a branch named `feature-foobar`: + +``` +docker.io/dependencytrack/apiserver:feature-foobar +``` + +This imagine can be shared with colleagues and community members for testing. Images built this way will be deleted +shortly after the respective feature has been merged into the `master` branch. + +Instead of raising PRs into `master`, contributors would now raise PRs into `feature-foobar`. + +> [!NOTE] +> PRs into `feature` branches must still be approved by maintainers, to avoid malicious code being introduced. +> However, this is only a surface-level review, unless explicitly requested otherwise by the contributor(s). + +Once the feature is considered ready, a PR can be raised from `feature-foobar` into `master`. \ No newline at end of file From bc25ae0c412889d755bc8f3fa918d8f3d9e866d0 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 6 May 2024 14:11:14 +0200 Subject: [PATCH 123/412] Update v4.11 changelog with recent changes Signed-off-by: nscuro --- docs/_posts/2024-xx-xx-v4.11.0.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index b6842470d8..479ad463a6 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -5,7 +5,7 @@ type: major **Highlights:** -* **Optimized BOM Ingestion**. The logic how uploaded BOMs are processed and ingested into Dependency-Track has been +* **Optimized BOM Ingestion**. The logic that governs how uploaded BOMs are processed and ingested into Dependency-Track has been overhauled to be more reliable and efficient. Further, BOM processing is now an atomic operation, such that errors occurring midway do not cause a partial state to be left behind. De-duplication of components and services is more predictable, and log messages emitted during processing contain additional context, making them easier to correlate. @@ -28,6 +28,10 @@ making it possible to spot the most prevalent vulnerabilities. * Refer to the [analyzer's documentation]({{ site.baseurl }}{% link _docs/datasources/trivy.md %}) for further details, in particular the [known limitations]({{ site.baseurl }}{% link _docs/datasources/trivy.md %}#known-limitations). * *This feature was demoed in our April community meeting! Watch it [here](https://www.youtube.com/watch?v=3iIeajRJK8o&t=725s).* +* **Extended Localization**. The UI now supports 12 additional languages. Users can change their language preference +in their profile settings. While the Portuguese, Brazilian Portuguese, and Spanish translations were provided by a native speaker +(thanks [@fnxpt]!), the majority of languages is currently machine-translated. Translation improvements are a great +way to contribute to the project, please find additional details [here](https://github.com/DependencyTrack/frontend?tab=readme-ov-file#internationalization-i18n). * **Official Helm Chart**. The Dependency-Track project now offers an official Helm chart for Kubernetes deployments. Community input and contributions are highly requested. The chart repository can be found at [https://github.com/DependencyTrack/helm-charts](https://github.com/DependencyTrack/helm-charts). @@ -50,7 +54,6 @@ It is also available through [Artifact Hub](https://artifacthub.io/packages/helm * Add more context to logs emitted during BOM processing - [apiserver/#3357] * BOM format, spec version, serial number, and version * Project UUID, name, and version -* Bump SPDX license list to v3.22 - [apiserver/#3368] * Store severities in database instead of computing them ad-hoc in-memory - [apiserver/#3408] * Add OIDC docs for large enterprise configuration using Azure AD - [apiserver/#3414] * Make subject prefix for email notifications configurable - [apiserver/#3422] @@ -60,7 +63,7 @@ It is also available through [Artifact Hub](https://artifacthub.io/packages/helm * Align retry configuration and behavior across analyzers - [apiserver/#3494] * Add support for component properties - [apiserver/#3499] * Add auto-generated changelog to GitHub releases - [apiserver/#3502] -* Bump SPDX license list to v3.23 - [apiserver/#3508] +* Bump SPDX license list to v3.23, bringing in 91 new licenses - [apiserver/#3508] * Validate uploaded BOMs against CycloneDX schema prior to processing them - [apiserver/#3522] * Improve observability of Lucene search indexes - [apiserver/#3535] * Add support for Hackage repositories - [apiserver/#3549] @@ -75,6 +78,7 @@ It is also available through [Artifact Hub](https://artifacthub.io/packages/helm * Log debug information upon possible secret key corruption - [apiserver/#3651] * Add support for worker pool drain timeout - [apiserver/#3657] * Fall back to no authentication when OSS Index API token decryption fails - [apiserver/#3661] +* Include project details in MS Teams notification for *BOM_PROCESSING_FAILED* - [apiserver/#3666] * Show component count in projects list - [frontend/#683] * Add current *fail*, *warn*, and *info* values to bottom of policy violation metrics - [frontend/#707] * Remove unused policy violation widget - [frontend/#710] @@ -85,7 +89,7 @@ It is also available through [Artifact Hub](https://artifacthub.io/packages/helm * Show publisher name when expanding rows in the "Alerts" table - [frontend/#728] * Improve tooltip clarity for project vulnerabilities - [frontend/#733] * Show badges on "Policy Violations" tab - [frontend/#744] -* Add ESLint and prettier - [frontend/#752] +* Add ESLint and prettier for consistent code formatting - [frontend/#752] * Display *created* and *last used* timestamps for API keys - [frontend/#768] * Display API key comments and make them editable - [frontend/#768] * Add *internal* column to component search view - [frontend/#775] @@ -94,8 +98,8 @@ It is also available through [Artifact Hub](https://artifacthub.io/packages/helm * Add *deprecated* column to license list - [frontend/#792] * Use *concise* endpoint to populate license list - [frontend/#793] * Display *comment* field of external references - [frontend/#803] -* Add support for localization based on browser language - [frontend/#805] -* Improve contrast ration on progress bars - [frontend/#816] +* Add support for 12 new languages, and localization based on browser language or custom preference - [frontend/#805] +* Improve contrast ratio on progress bars - [frontend/#816] * Add language picker to profile dropdown - [frontend/#824] * Display EPSS score and percentile on vulnerability view - [frontend/#832] @@ -128,7 +132,7 @@ It is also available through [Artifact Hub](https://artifacthub.io/packages/helm * Fix finding search routes - [frontend/#689] * Fix CI build status badge - [frontend/#699] * Fix incorrect calculation of "Audited Violations" and "Audited Vulnerabilities" percentages - [frontend/#704] -* Fix percentage calculation to round to the 10th decimal - [frontend/#708] +* Fix percentage calculation to consistently round to two decimal places - [frontend/#708] * Fix percentage calculation edge cases - [frontend/#719] * Fix "Outdated Only" button being disabled when dependency graph is not available - [frontend/#725] * Fix redundant requests to `/api/v1/component` when loading project page - [frontend/#726] @@ -173,7 +177,7 @@ For a complete list of changes, refer to the respective GitHub milestones: We thank all organizations and individuals who contributed to this release, from logging issues to taking part in discussions on GitHub & Slack to testing of fixes. Special thanks to everyone who contributed code to implement enhancements and fix defects: -[@AnthonyMastrean], [@LaVibeX], [@MangoIV], [@Robbilie], [@VithikaS], [@a5a351e7], [@acdha], [@aravindparappil46], +[@2000rosser], [@AnthonyMastrean], [@LaVibeX], [@MangoIV], [@Robbilie], [@VithikaS], [@a5a351e7], [@acdha], [@aravindparappil46], [@baburkin], [@fnxpt], [@kepten], [@leec94], [@lukas-braune], [@malice00], [@mehab], [@mge-mm] [@mikkeschiren], [@mykter], [@rbt-mm], [@rkesters], [@rkg-mm], [@sahibamittal], [@sebD], [@setchy], [@validide] @@ -254,6 +258,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [apiserver/#3659]: https://github.com/DependencyTrack/dependency-track/pull/3659 [apiserver/#3661]: https://github.com/DependencyTrack/dependency-track/pull/3661 [apiserver/#3664]: https://github.com/DependencyTrack/dependency-track/pull/3664 +[apiserver/#3666]: https://github.com/DependencyTrack/dependency-track/pull/3666 [frontend/#682]: https://github.com/DependencyTrack/frontend/pull/682 [frontend/#683]: https://github.com/DependencyTrack/frontend/pull/683 @@ -292,6 +297,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [frontend/#824]: https://github.com/DependencyTrack/frontend/pull/824 [frontend/#832]: https://github.com/DependencyTrack/frontend/pull/832 +[@2000rosser]: https://github.com/2000rosser [@AnthonyMastrean]: https://github.com/AnthonyMastrean [@LaVibeX]: https://github.com/LaVibeX [@MangoIV]: https://github.com/MangoIV From b65b2f1c1b330c12e49d69a4c4edb9fa5f2003be Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 6 May 2024 14:23:34 +0200 Subject: [PATCH 124/412] Bump dependencies to their latest version Signed-off-by: nscuro --- pom.xml | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/pom.xml b/pom.xml index 83739b9055..f3ba6f42f6 100644 --- a/pom.xml +++ b/pom.xml @@ -83,11 +83,12 @@ 4.10.0 ${project.parent.version} - 4.2.0 - 10.12.5 + 4.2.1 + 0.1.2 + 10.16.0 1.18.0 - 1.16.0 - 1.16.0 + 1.18.0 + 1.18.0 2.1.0 1.26.1 1.4.2 @@ -95,25 +96,29 @@ 8.0.3 1.6.15 2.3.9 + 20240303 3.2.7 8.11.3 + 3.9.6 5.15.0 6.0.1 1.5.0 3.2.2 2.2.0 + 1.19.0 1.19.7 - 6.6.0 + 2.35.2 + 6.6.2 1.1.1 2.1.1 4.5.14 5.3.1 2.0.13 - 1.318 + 1.321 12.6.1.jre11 8.0.33 - 42.7.2 + 42.7.3 application json @@ -201,7 +206,7 @@ org.json json - 20240303 + ${lib.json-java.version} @@ -264,14 +269,12 @@ oauth.signpost signpost-core ${lib.signpost-core.version} - compile - org.brotli dec - 0.1.2 + ${lib.brotli-decoder.version} @@ -289,7 +292,7 @@ org.apache.maven maven-artifact - 3.9.6 + ${lib.maven-artifact.version} @@ -391,6 +394,7 @@ pl.pragmatists JUnitParams ${lib.junit-params.version} + test org.glassfish.jersey.test-framework.providers @@ -414,13 +418,13 @@ com.github.tomakehurst wiremock-jre8 - 2.35.2 + ${lib.wiremock.version} test com.github.stefanbirkner system-rules - 1.19.0 + ${lib.system-rules.version} test From c946e590acd269998af259f6762081a21435f9dc Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 6 May 2024 14:57:55 +0200 Subject: [PATCH 125/412] Simplify `BomUploadProcessingTaskTest` * Remove redundant `bomUploadProcessingTaskSupplier`, since the choice which task to run is now made via `ConfigProperty` * Fix string equality checks when comparing the task name Signed-off-by: nscuro --- .../tasks/BomUploadProcessingTaskTest.java | 106 +++++++++--------- 1 file changed, 50 insertions(+), 56 deletions(-) diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index f4722cd2c6..72e972ab38 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -64,7 +64,6 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.function.Supplier; import static org.apache.commons.io.IOUtils.resourceToByteArray; import static org.assertj.core.api.Assertions.assertThat; @@ -72,8 +71,8 @@ import static org.assertj.core.api.Assertions.fail; import static org.awaitility.Awaitility.await; import static org.dependencytrack.assertion.Assertions.assertConditionWithTimeout; -import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_ENABLED; import static org.dependencytrack.model.ConfigPropertyConstants.BOM_PROCESSING_TASK_V2_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_ENABLED; @RunWith(Parameterized.class) public class BomUploadProcessingTaskTest extends PersistenceCapableTest { @@ -102,19 +101,15 @@ public void inform(final Notification notification) { @Parameterized.Parameters(name = "{index}: {0}") public static Collection data() { return Arrays.asList(new Object[][]{ - {BomUploadProcessingTask.class.getSimpleName(), (Supplier) BomUploadProcessingTaskV2::new}, - {BomUploadProcessingTaskV2.class.getSimpleName(), (Supplier) BomUploadProcessingTaskV2::new} + {BomUploadProcessingTask.class.getSimpleName()}, + {BomUploadProcessingTaskV2.class.getSimpleName()} }); } - private final String ignoredBomUploadProcessingTaskName; - private final Supplier bomUploadProcessingTaskSupplier; - - public BomUploadProcessingTaskTest(final String ignoredBomUploadProcessingTaskName, - final Supplier bomUploadProcessingTaskSupplier) { + private final String taskName; - this.ignoredBomUploadProcessingTaskName = ignoredBomUploadProcessingTaskName; - this.bomUploadProcessingTaskSupplier = bomUploadProcessingTaskSupplier; + public BomUploadProcessingTaskTest(final String taskName) { + this.taskName = taskName; } @Before @@ -125,11 +120,11 @@ public void setUp() { NotificationService.getInstance().subscribe(new Subscription(NotificationSubscriber.class)); qm.createConfigProperty( - BOM_PROCESSING_TASK_V2_ENABLED.getGroupName(), - BOM_PROCESSING_TASK_V2_ENABLED.getPropertyName(), - (this.ignoredBomUploadProcessingTaskName == BomUploadProcessingTaskV2.class.getSimpleName()) ? "true" : "false", - BOM_PROCESSING_TASK_V2_ENABLED.getPropertyType(), - null + BOM_PROCESSING_TASK_V2_ENABLED.getGroupName(), + BOM_PROCESSING_TASK_V2_ENABLED.getPropertyName(), + (BomUploadProcessingTaskV2.class.getSimpleName().equals(taskName)) ? "true" : "false", + BOM_PROCESSING_TASK_V2_ENABLED.getPropertyType(), + null ); // Enable processing of CycloneDX BOMs @@ -192,7 +187,7 @@ public void informTest() throws Exception { final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-1.xml")); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); qm.getPersistenceManager().refresh(project); @@ -307,7 +302,7 @@ public void informTest() throws Exception { @Test public void informWithEmptyBomTest() throws Exception { // Known to now work with old task implementation. - if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { + if (BomUploadProcessingTask.class.getSimpleName().equals(taskName)) { return; } @@ -315,7 +310,7 @@ public void informWithEmptyBomTest() throws Exception { final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-empty.json")); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); qm.getPersistenceManager().refresh(project); @@ -328,18 +323,17 @@ public void informWithEmptyBomTest() throws Exception { @Test public void informWithInvalidCycloneDxBomTest() throws Exception { - // Known to now work with old task implementation. - if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { - return; + if (BomUploadProcessingTask.class.getSimpleName().equals(taskName)) { + return; } qm.createConfigProperty( - BOM_VALIDATION_ENABLED.getGroupName(), - BOM_VALIDATION_ENABLED.getPropertyName(), - "true", - BOM_VALIDATION_ENABLED.getPropertyType(), - null + BOM_VALIDATION_ENABLED.getGroupName(), + BOM_VALIDATION_ENABLED.getPropertyName(), + "true", + BOM_VALIDATION_ENABLED.getPropertyType(), + null ); final Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); @@ -378,7 +372,7 @@ public void informWithInvalidCycloneDxBomTest() throws Exception { @Test public void informWithNonExistentProjectTest() throws Exception { // Known to now work with old task implementation. - if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { + if (BomUploadProcessingTask.class.getSimpleName().equals(taskName)) { return; } @@ -388,7 +382,7 @@ public void informWithNonExistentProjectTest() throws Exception { project.setName("test-project"); var bomUploadEvent = new BomUploadEvent(project, resourceToByteArray("/unit/bom-1.xml")); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); await("BOM Processing Failed Notification") .atMost(Duration.ofSeconds(5)) @@ -402,7 +396,7 @@ public void informWithNonExistentProjectTest() throws Exception { @Test public void informWithComponentsUnderMetadataBomTest() throws Exception { // Known to now work with old task implementation. - if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { + if (taskName == BomUploadProcessingTask.class.getSimpleName()) { return; } @@ -410,7 +404,7 @@ public void informWithComponentsUnderMetadataBomTest() throws Exception { final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-metadata-components.json")); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); final List boms = qm.getAllBoms(project); @@ -438,7 +432,7 @@ public void informWithComponentsUnderMetadataBomTest() throws Exception { @Test public void informWithExistingDuplicateComponentsTest() { // Known to now work with old task implementation. - if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { + if (BomUploadProcessingTask.class.getSimpleName().equals(taskName)) { return; } @@ -480,7 +474,7 @@ public void informWithExistingDuplicateComponentsTest() { """.getBytes(StandardCharsets.UTF_8); final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); qm.getPersistenceManager().evictAll(); @@ -524,7 +518,7 @@ public void informWithExistingDuplicateComponentsTest() { @Test public void informWithBloatedBomTest() throws Exception { // Known to now work with old task implementation. - if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { + if (BomUploadProcessingTask.class.getSimpleName().equals(taskName)) { return; } @@ -532,7 +526,7 @@ public void informWithBloatedBomTest() throws Exception { final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-bloated.json")); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); final List boms = qm.getAllBoms(project); @@ -580,7 +574,7 @@ public void informWithBloatedBomTest() throws Exception { @Test public void informWithCustomLicenseResolutionTest() throws Exception { // Known to now work with old task implementation. - if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { + if (BomUploadProcessingTask.class.getSimpleName().equals(taskName)) { return; } @@ -592,7 +586,7 @@ public void informWithCustomLicenseResolutionTest() throws Exception { final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-custom-license.json")); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).satisfiesExactly( @@ -645,7 +639,7 @@ public void informWithBomContainingLicenseExpressionTest() { """.getBytes(StandardCharsets.UTF_8); final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { @@ -690,7 +684,7 @@ public void informWithBomContainingLicenseExpressionWithSingleIdTest() { """.getBytes(StandardCharsets.UTF_8); final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { @@ -731,7 +725,7 @@ public void informWithBomContainingInvalidLicenseExpressionTest() { """.getBytes(StandardCharsets.UTF_8); final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { @@ -775,7 +769,7 @@ public void informIssue3433Test() { """.getBytes(StandardCharsets.UTF_8); final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { @@ -826,7 +820,7 @@ public void informUpdateExistingLicenseTest() { """.getBytes(StandardCharsets.UTF_8); final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), existingBomBytes); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { @@ -860,7 +854,7 @@ public void informUpdateExistingLicenseTest() { } """.getBytes(StandardCharsets.UTF_8); - bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), updatedBomBytes)); + new BomUploadProcessingTaskV2().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), updatedBomBytes)); awaitBomProcessedNotification(bomUploadEvent); qm.getPersistenceManager().evictAll(); @@ -908,7 +902,7 @@ public void informDeleteExistingLicenseTest() { """.getBytes(StandardCharsets.UTF_8); final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), existingBomBytes); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> { @@ -936,7 +930,7 @@ public void informDeleteExistingLicenseTest() { } """.getBytes(StandardCharsets.UTF_8); - bomUploadProcessingTaskSupplier.get().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), updatedBomBytes)); + new BomUploadProcessingTaskV2().inform(new BomUploadEvent(qm.detach(Project.class, project.getId()), updatedBomBytes)); awaitBomProcessedNotification(bomUploadEvent); qm.getPersistenceManager().evictAll(); @@ -954,7 +948,7 @@ public void informWithBomContainingServiceTest() throws Exception { final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-service.json")); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); assertThat(qm.getAllComponents(project)).isNotEmpty(); @@ -994,7 +988,7 @@ public void informWithExistingComponentPropertiesAndBomWithoutComponentPropertie ] } """.getBytes()); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); qm.getPersistenceManager().refresh(component); @@ -1040,7 +1034,7 @@ public void informWithExistingComponentPropertiesAndBomWithComponentProperties() ] } """.getBytes()); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); qm.getPersistenceManager().refresh(component); @@ -1055,7 +1049,7 @@ public void informWithExistingComponentPropertiesAndBomWithComponentProperties() @Test // https://github.com/DependencyTrack/dependency-track/issues/1905 public void informIssue1905Test() throws Exception { // Known to now work with old task implementation. - if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { + if (BomUploadProcessingTask.class.getSimpleName().equals(taskName)) { return; } @@ -1064,7 +1058,7 @@ public void informIssue1905Test() throws Exception { for (int i = 0; i < 3; i++) { var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-issue1905.json")); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); // Make sure processing did not fail. awaitBomProcessedNotification(bomUploadEvent); @@ -1094,7 +1088,7 @@ public void informIssue1905Test() throws Exception { @Test // https://github.com/DependencyTrack/dependency-track/issues/2519 public void informIssue2519Test() throws Exception { // Known to now work with old task implementation. - if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { + if (BomUploadProcessingTask.class.getSimpleName().equals(taskName)) { return; } @@ -1105,7 +1099,7 @@ public void informIssue2519Test() throws Exception { for (int i = 0; i < 3; i++) { var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-issue2519.xml")); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); // Make sure processing did not fail. awaitBomProcessedNotification(bomUploadEvent); @@ -1157,14 +1151,14 @@ public void informIssue3309Test() { """.getBytes(); var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); assertProjectAuthors.run(); NOTIFICATIONS.clear(); bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); awaitBomProcessedNotification(bomUploadEvent); assertProjectAuthors.run(); } @@ -1172,7 +1166,7 @@ public void informIssue3309Test() { @Test // https://github.com/DependencyTrack/dependency-track/issues/3371 public void informIssue3371Test() throws Exception { // Known to now work with old task implementation. - if (ignoredBomUploadProcessingTaskName == BomUploadProcessingTask.class.getSimpleName()) { + if (BomUploadProcessingTask.class.getSimpleName().equals(taskName)) { return; } @@ -1183,7 +1177,7 @@ public void informIssue3371Test() throws Exception { for (int i = 0; i < 2; i++) { var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), resourceToByteArray("/unit/bom-issue3371.json")); - bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent); + new BomUploadProcessingTaskV2().inform(bomUploadEvent); // Make sure processing did not fail. awaitBomProcessedNotification(bomUploadEvent); From 122ea00883f67674d385e9a02aebe4b2ea220923 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 6 May 2024 15:03:32 +0200 Subject: [PATCH 126/412] Disable Maven transfer progress in CI The download logs are not useful at all, and instead clutter the build logs. Disable it to make inspection of CI build logs less annoying. Signed-off-by: nscuro --- .github/workflows/_meta-build.yaml | 12 ++++++------ .github/workflows/ci-release.yaml | 4 ++-- .github/workflows/ci-test.yaml | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 200948a4bb..6c2651b6ce 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -47,12 +47,12 @@ jobs: - name: Build with Maven run: |- - mvn clean - mvn package -Dmaven.test.skip=true -P enhance -P embedded-jetty -Dservices.bom.merge.skip=false -Dlogback.configuration.file=src/main/docker/logback.xml - mvn clean -P clean-exclude-wars - mvn package -Dmaven.test.skip=true -P enhance -P embedded-jetty -P bundle-ui -Dservices.bom.merge.skip=false -Dlogback.configuration.file=src/main/docker/logback.xml - mvn clean -P clean-exclude-wars - mvn cyclonedx:makeBom -Dservices.bom.merge.skip=false org.codehaus.mojo:exec-maven-plugin:exec@merge-services-bom + mvn -B --no-transfer-progress clean + mvn -B --no-transfer-progress package -Dmaven.test.skip=true -P enhance -P embedded-jetty -Dservices.bom.merge.skip=false -Dlogback.configuration.file=src/main/docker/logback.xml + mvn -B --no-transfer-progress clean -P clean-exclude-wars + mvn -B --no-transfer-progress package -Dmaven.test.skip=true -P enhance -P embedded-jetty -P bundle-ui -Dservices.bom.merge.skip=false -Dlogback.configuration.file=src/main/docker/logback.xml + mvn -B --no-transfer-progress clean -P clean-exclude-wars + mvn -B --no-transfer-progress cyclonedx:makeBom -Dservices.bom.merge.skip=false org.codehaus.mojo:exec-maven-plugin:exec@merge-services-bom - name: Upload Artifacts uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # tag=v4.3.3 diff --git a/.github/workflows/ci-release.yaml b/.github/workflows/ci-release.yaml index b24b9adca9..fb12df5410 100644 --- a/.github/workflows/ci-release.yaml +++ b/.github/workflows/ci-release.yaml @@ -61,7 +61,7 @@ jobs: cache: 'maven' - name: Set Version - run: mvn versions:set -DnewVersion=${VERSION} + run: mvn -B --no-transfer-progress versions:set -DnewVersion=${VERSION} - name: Commit Version env: @@ -123,7 +123,7 @@ jobs: ref: ${{ needs.prepare-release.outputs.release-branch }} - name: Set SNAPSHOT Version after Release - run: mvn versions:set -DnewVersion=${NEXT_VERSION} + run: mvn -B --no-transfer-progress versions:set -DnewVersion=${NEXT_VERSION} - name: Commit SNAPSHOT Version env: diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index 0b0aac5335..8b1793ef63 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -44,8 +44,8 @@ jobs: - name: Execute unit tests run: |- - mvn clean - mvn test -P enhance + mvn -B --no-transfer-progress clean + mvn -B --no-transfer-progress test -P enhance # Publishing coverage to Codacy is only possible for builds of push events. # PRs from forks do not get access to repository secrets. From 0f3d25cafea6faa217c603c644dae3525fa7108c Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 6 May 2024 18:55:51 +0200 Subject: [PATCH 127/412] Fix changelog typo; Set release date; Bump docs version Signed-off-by: nscuro --- docs/_config.yml | 2 +- docs/_posts/{2024-xx-xx-v4.11.0.md => 2024-05-07-v4.11.0.md} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/_posts/{2024-xx-xx-v4.11.0.md => 2024-05-07-v4.11.0.md} (99%) diff --git a/docs/_config.yml b/docs/_config.yml index 2a1ad4fde5..44eecfc77f 100755 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -6,7 +6,7 @@ url: "https://docs.dependencytrack.org" baseurl: show_full_navigation: true -version: v4.10 +version: v4.11 # Values for the jekyll-seo-tag gem (https://github.com/jekyll/jekyll-seo-tag) logo: /siteicon.png diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-05-07-v4.11.0.md similarity index 99% rename from docs/_posts/2024-xx-xx-v4.11.0.md rename to docs/_posts/2024-05-07-v4.11.0.md index 479ad463a6..6a5e6d1fc2 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-05-07-v4.11.0.md @@ -30,7 +30,7 @@ making it possible to spot the most prevalent vulnerabilities. * *This feature was demoed in our April community meeting! Watch it [here](https://www.youtube.com/watch?v=3iIeajRJK8o&t=725s).* * **Extended Localization**. The UI now supports 12 additional languages. Users can change their language preference in their profile settings. While the Portuguese, Brazilian Portuguese, and Spanish translations were provided by a native speaker -(thanks [@fnxpt]!), the majority of languages is currently machine-translated. Translation improvements are a great +(thanks [@fnxpt]!), the majority of languages are currently machine-translated. Translation improvements are a great way to contribute to the project, please find additional details [here](https://github.com/DependencyTrack/frontend?tab=readme-ov-file#internationalization-i18n). * **Official Helm Chart**. The Dependency-Track project now offers an official Helm chart for Kubernetes deployments. Community input and contributions are highly requested. The chart repository can be found at From 8e49d25c1bfb98c75012927a687c56241bdeff59 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 6 May 2024 18:41:09 +0200 Subject: [PATCH 128/412] Fix missing default repos for Hackage and Nixpkgs This was missed in https://github.com/DependencyTrack/dependency-track/pull/3549 Signed-off-by: nscuro --- .../persistence/DefaultObjectGenerator.java | 2 + .../DefaultObjectGeneratorTest.java | 2 +- .../resources/v1/RepositoryResourceTest.java | 58 +++++++++---------- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index 39877bfff7..d217135cd8 100644 --- a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -203,11 +203,13 @@ public void loadDefaultRepositories() { qm.createRepository(RepositoryType.CPAN, "cpan-public-registry", "https://fastapi.metacpan.org/v1/", true, false, false, null, null); qm.createRepository(RepositoryType.GEM, "rubygems.org", "https://rubygems.org/", true, false, false, null, null); qm.createRepository(RepositoryType.HEX, "hex.pm", "https://hex.pm/", true, false, false, null, null); + qm.createRepository(RepositoryType.HACKAGE, "hackage.haskell.org", "https://hackage.haskell.org/", true, false, false, null, null); qm.createRepository(RepositoryType.MAVEN, "central", "https://repo1.maven.org/maven2/", true, false, false, null, null); qm.createRepository(RepositoryType.MAVEN, "atlassian-public", "https://packages.atlassian.com/content/repositories/atlassian-public/", true, false, false, null, null); qm.createRepository(RepositoryType.MAVEN, "jboss-releases", "https://repository.jboss.org/nexus/content/repositories/releases/", true, false, false, null, null); qm.createRepository(RepositoryType.MAVEN, "clojars", "https://repo.clojars.org/", true, false, false, null, null); qm.createRepository(RepositoryType.MAVEN, "google-android", "https://maven.google.com/", true, false, false, null, null); + qm.createRepository(RepositoryType.NIXPKGS, "nixpkgs-unstable", "https://channels.nixos.org/nixpkgs-unstable/packages.json.br", true, false, false, null, null); qm.createRepository(RepositoryType.NPM, "npm-public-registry", "https://registry.npmjs.org/", true, false, false, null, null); qm.createRepository(RepositoryType.PYPI, "pypi.org", "https://pypi.org/", true, false, false, null, null); qm.createRepository(RepositoryType.NUGET, "nuget-gallery", "https://api.nuget.org/", true, false, false, null, null); diff --git a/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java b/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java index 7b24e93cf8..0ef5da69b1 100644 --- a/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java +++ b/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java @@ -102,7 +102,7 @@ public void testLoadDefaultRepositories() throws Exception { Method method = generator.getClass().getDeclaredMethod("loadDefaultRepositories"); method.setAccessible(true); method.invoke(generator); - Assert.assertEquals(15, qm.getAllRepositories().size()); + Assert.assertEquals(17, qm.getAllRepositories().size()); } @Test diff --git a/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java index a45b559c60..5d289a57bc 100644 --- a/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java @@ -62,10 +62,10 @@ public void getRepositoriesTest() { .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); - Assert.assertEquals(String.valueOf(15), response.getHeaderString(TOTAL_COUNT_HEADER)); + Assert.assertEquals(String.valueOf(17), response.getHeaderString(TOTAL_COUNT_HEADER)); JsonArray json = parseJsonArray(response); Assert.assertNotNull(json); - Assert.assertEquals(15, json.size()); + Assert.assertEquals(17, json.size()); for (int i = 0; i < json.size(); i++) { Assert.assertNotNull(json.getJsonObject(i).getString("type")); Assert.assertNotNull(json.getJsonObject(i).getString("identifier")); @@ -190,17 +190,17 @@ public void createRepositoryTest() { response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); - Assert.assertEquals(String.valueOf(16), response.getHeaderString(TOTAL_COUNT_HEADER)); + Assert.assertEquals(String.valueOf(18), response.getHeaderString(TOTAL_COUNT_HEADER)); JsonArray json = parseJsonArray(response); Assert.assertNotNull(json); - Assert.assertEquals(16, json.size()); - Assert.assertEquals("MAVEN", json.getJsonObject(12).getString("type")); - Assert.assertEquals("test", json.getJsonObject(12).getString("identifier")); - Assert.assertEquals("www.foobar.com", json.getJsonObject(12).getString("url")); - Assert.assertTrue(json.getJsonObject(12).getInt("resolutionOrder") > 0); - Assert.assertTrue(json.getJsonObject(12).getBoolean("authenticationRequired")); - Assert.assertEquals("testuser", json.getJsonObject(12).getString("username")); - Assert.assertTrue(json.getJsonObject(12).getBoolean("enabled")); + Assert.assertEquals(18, json.size()); + Assert.assertEquals("MAVEN", json.getJsonObject(13).getString("type")); + Assert.assertEquals("test", json.getJsonObject(13).getString("identifier")); + Assert.assertEquals("www.foobar.com", json.getJsonObject(13).getString("url")); + Assert.assertTrue(json.getJsonObject(13).getInt("resolutionOrder") > 0); + Assert.assertTrue(json.getJsonObject(13).getBoolean("authenticationRequired")); + Assert.assertEquals("testuser", json.getJsonObject(13).getString("username")); + Assert.assertTrue(json.getJsonObject(13).getBoolean("enabled")); } @Test @@ -222,18 +222,18 @@ public void createNonInternalRepositoryTest() { response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); - Assert.assertEquals(String.valueOf(16), response.getHeaderString(TOTAL_COUNT_HEADER)); + Assert.assertEquals(String.valueOf(18), response.getHeaderString(TOTAL_COUNT_HEADER)); JsonArray json = parseJsonArray(response); Assert.assertNotNull(json); - Assert.assertEquals(16, json.size()); - Assert.assertEquals("MAVEN", json.getJsonObject(12).getString("type")); - Assert.assertEquals("test", json.getJsonObject(12).getString("identifier")); - Assert.assertEquals("www.foobar.com", json.getJsonObject(12).getString("url")); - Assert.assertTrue(json.getJsonObject(12).getInt("resolutionOrder") > 0); - Assert.assertTrue(json.getJsonObject(12).getBoolean("authenticationRequired")); - Assert.assertFalse(json.getJsonObject(12).getBoolean("internal")); - Assert.assertEquals("testuser", json.getJsonObject(12).getString("username")); - Assert.assertTrue(json.getJsonObject(12).getBoolean("enabled")); + Assert.assertEquals(18, json.size()); + Assert.assertEquals("MAVEN", json.getJsonObject(13).getString("type")); + Assert.assertEquals("test", json.getJsonObject(13).getString("identifier")); + Assert.assertEquals("www.foobar.com", json.getJsonObject(13).getString("url")); + Assert.assertTrue(json.getJsonObject(13).getInt("resolutionOrder") > 0); + Assert.assertTrue(json.getJsonObject(13).getBoolean("authenticationRequired")); + Assert.assertFalse(json.getJsonObject(13).getBoolean("internal")); + Assert.assertEquals("testuser", json.getJsonObject(13).getString("username")); + Assert.assertTrue(json.getJsonObject(13).getBoolean("enabled")); } @Test @@ -252,16 +252,16 @@ public void createRepositoryAuthFalseTest() { response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); - Assert.assertEquals(String.valueOf(16), response.getHeaderString(TOTAL_COUNT_HEADER)); + Assert.assertEquals(String.valueOf(18), response.getHeaderString(TOTAL_COUNT_HEADER)); JsonArray json = parseJsonArray(response); Assert.assertNotNull(json); - Assert.assertEquals(16, json.size()); - Assert.assertEquals("MAVEN", json.getJsonObject(12).getString("type")); - Assert.assertEquals("test", json.getJsonObject(12).getString("identifier")); - Assert.assertEquals("www.foobar.com", json.getJsonObject(12).getString("url")); - Assert.assertTrue(json.getJsonObject(12).getInt("resolutionOrder") > 0); - Assert.assertFalse(json.getJsonObject(12).getBoolean("authenticationRequired")); - Assert.assertTrue(json.getJsonObject(12).getBoolean("enabled")); + Assert.assertEquals(18, json.size()); + Assert.assertEquals("MAVEN", json.getJsonObject(13).getString("type")); + Assert.assertEquals("test", json.getJsonObject(13).getString("identifier")); + Assert.assertEquals("www.foobar.com", json.getJsonObject(13).getString("url")); + Assert.assertTrue(json.getJsonObject(13).getInt("resolutionOrder") > 0); + Assert.assertFalse(json.getJsonObject(13).getBoolean("authenticationRequired")); + Assert.assertTrue(json.getJsonObject(13).getBoolean("enabled")); } From 339faf626278308b99b625a7c775a3ee3ab0233c Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 6 May 2024 19:53:12 +0200 Subject: [PATCH 129/412] Reduce verbosity of `ResourceTest`s Currently, every `ResourceTest` prints log statements along the lines of: ``` May 06, 2024 5:25:03 PM org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory$GrizzlyWebTestContainer INFO: Creating GrizzlyWebTestContainer configured at the base URI http://localhost:9998/ May 06, 2024 5:25:03 PM org.glassfish.grizzly.servlet.WebappContext deploy INFO: Starting application [TestContext] ... May 06, 2024 5:25:05 PM org.glassfish.grizzly.servlet.WebappContext initServlets INFO: [TestContext] Servlet [org.glassfish.jersey.servlet.ServletContainer] registered for url pattern(s) [[/*]]. May 06, 2024 5:25:05 PM org.glassfish.grizzly.servlet.WebappContext deploy INFO: Application [TestContext] is ready to service requests. Root: []. May 06, 2024 5:25:05 PM org.glassfish.grizzly.http.server.NetworkListener start INFO: Started listener bound to [localhost:9998] May 06, 2024 5:25:05 PM org.glassfish.grizzly.http.server.HttpServer start INFO: [HttpServer] Started. May 06, 2024 5:25:12 PM org.glassfish.grizzly.http.server.NetworkListener shutdownNow INFO: Stopped listener bound to [localhost:9998] ``` Both Jersey and Grizzly use Java's `java.util.logging` so changing the log levels requires a special treatment. Because the configuration requires a system property to be set, this change will not affect tests started via IDE. Signed-off-by: nscuro --- pom.xml | 8 ++++++++ src/test/resources/logging.properties | 1 + 2 files changed, 9 insertions(+) create mode 100644 src/test/resources/logging.properties diff --git a/pom.xml b/pom.xml index f3ba6f42f6..19dc7e2fe2 100644 --- a/pom.xml +++ b/pom.xml @@ -515,6 +515,14 @@ org.apache.maven.plugins maven-surefire-plugin + + + + java.util.logging.config.file + src/test/resources/logging.properties + + + org.apache.maven.surefire diff --git a/src/test/resources/logging.properties b/src/test/resources/logging.properties new file mode 100644 index 0000000000..1759c14edb --- /dev/null +++ b/src/test/resources/logging.properties @@ -0,0 +1 @@ +org.glassfish=SEVERE \ No newline at end of file From 042024bd3f6e9cde6f5a84d614f878334f273202 Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 7 May 2024 15:47:25 +0200 Subject: [PATCH 130/412] Bump frontend to 4.11.0 Signed-off-by: nscuro --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f3ba6f42f6..5f152904ad 100644 --- a/pom.xml +++ b/pom.xml @@ -81,7 +81,7 @@ - 4.10.0 + 4.11.0 ${project.parent.version} 4.2.1 0.1.2 From 95e12690574c8d9328a9afacfd89b630ff0b30f8 Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 7 May 2024 15:47:39 +0200 Subject: [PATCH 131/412] Fix typo in `DEVELOPING.md` Signed-off-by: nscuro --- DEVELOPING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEVELOPING.md b/DEVELOPING.md index 4a2b4bad8f..03bfb0460a 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -359,7 +359,7 @@ and giving the rest of the team write permissions to their repository. To address the use cases above, contributors to the Dependency-Track project can request a *feature branch* from a maintainer. Feature branches follow the `feature-*` naming pattern, and are subject to branch protection rules. -Just like for the `maser` branch, changes pushed to a `feature` branch trigger a container image build. +Just like for the `master` branch, changes pushed to a `feature` branch trigger a container image build. Images built from `feature` branches are tagged with the name of the branch, for example for a branch named `feature-foobar`: ``` From 7cd0013ccd4cb74435f606268f4b6ecbfbe17d29 Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 7 May 2024 15:48:07 +0200 Subject: [PATCH 132/412] Update v4.11 changelog with frontend checksums Signed-off-by: nscuro --- docs/_posts/2024-05-07-v4.11.0.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/_posts/2024-05-07-v4.11.0.md b/docs/_posts/2024-05-07-v4.11.0.md index 6a5e6d1fc2..13bbaa1415 100644 --- a/docs/_posts/2024-05-07-v4.11.0.md +++ b/docs/_posts/2024-05-07-v4.11.0.md @@ -197,10 +197,10 @@ Special thanks to everyone who contributed code to implement enhancements and fi ###### frontend-dist.zip -| Algorithm | Checksum | -|:----------|:---------| -| SHA-1 | | -| SHA-256 | | +| Algorithm | Checksum | +|:----------|:------------------------------------------------------------------------------------| +| SHA-1 | 80cddddaf5c9c73676065d4ab6fe7b3eff3ec8de | +| SHA-256 | 9c51c337f4b2a7e78730c70473cd24070773a0982d1c0ee6c13f9a6f18a756d5 frontend-dist.zip | ###### Software Bill of Materials (SBOM) From d0c0e23f81436471eb7c76db88e2dad78c4ea803 Mon Sep 17 00:00:00 2001 From: Dependency-Track Bot <106437498+dependencytrack-bot@users.noreply.github.com> Date: Tue, 7 May 2024 14:15:04 +0000 Subject: [PATCH 133/412] prepare-release: set version to 4.11.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 01b0b44298..05f08fddd5 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ org.dependencytrack dependency-track war - 4.11.0-SNAPSHOT + 4.11.0 Dependency-Track https://dependencytrack.org/ From 16cfd7cbdae699a60d7a0fceec2b108dba79c714 Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 7 May 2024 16:23:08 +0200 Subject: [PATCH 134/412] Update release artifact checksums for v4.11.0 Signed-off-by: nscuro --- docs/_posts/2024-05-07-v4.11.0.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/_posts/2024-05-07-v4.11.0.md b/docs/_posts/2024-05-07-v4.11.0.md index 13bbaa1415..a519fb9ea5 100644 --- a/docs/_posts/2024-05-07-v4.11.0.md +++ b/docs/_posts/2024-05-07-v4.11.0.md @@ -183,17 +183,17 @@ Special thanks to everyone who contributed code to implement enhancements and fi ###### dependency-track-apiserver.jar -| Algorithm | Checksum | -|:----------|:---------| -| SHA-1 | | -| SHA-256 | | +| Algorithm | Checksum | +|:----------|:-----------------------------------------------------------------| +| SHA-1 | a9dae58a25c8aeeb54134ff054214505eb170db9 | +| SHA-256 | 03160957fced99c3d923bbb5c6cb352740da1970bd4775b52bb451b95c4cefaf | ###### dependency-track-bundled.jar -| Algorithm | Checksum | -|:----------|:---------| -| SHA-1 | | -| SHA-256 | | +| Algorithm | Checksum | +|:----------|:-----------------------------------------------------------------| +| SHA-1 | 59b78c3f6b1979ba29c1bd754b7dc1005101fc49 | +| SHA-256 | 1a34808cd6c7a9bf7b181e4f175c077f1ee5d5a9daf327b330db9b1c63aac2d3 | ###### frontend-dist.zip From b4c9e2052ebef8cccd2d962a419f24a6f528b65e Mon Sep 17 00:00:00 2001 From: Niklas Date: Tue, 7 May 2024 22:08:16 +0200 Subject: [PATCH 135/412] Bump version to `4.12.0-SNAPSHOT` Signed-off-by: Niklas --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 05f08fddd5..120112fbc3 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ org.dependencytrack dependency-track war - 4.11.0 + 4.12.0-SNAPSHOT Dependency-Track https://dependencytrack.org/ From 63d90210dabb5fcfc2c9262998ed4d59dc674627 Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 7 May 2024 22:04:18 +0200 Subject: [PATCH 136/412] Raise baseline Java version to 21 Signed-off-by: nscuro --- .github/workflows/_meta-build.yaml | 2 +- .github/workflows/ci-release.yaml | 2 +- .github/workflows/ci-test.yaml | 2 +- DEVELOPING.md | 2 +- docs/_posts/2024-xx-xx-v4.12.0.md | 53 +++++++++++++++++++ pom.xml | 4 ++ .../tasks/NistApiMirrorTask.java | 25 +-------- .../common/ManagedHttpClientFactoryTest.java | 2 +- 8 files changed, 63 insertions(+), 29 deletions(-) create mode 100644 docs/_posts/2024-xx-xx-v4.12.0.md diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 6c2651b6ce..534556becd 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -34,7 +34,7 @@ jobs: uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v4.2.1 with: distribution: 'temurin' - java-version: '17' + java-version: '21' cache: 'maven' - name: Setup CycloneDX CLI diff --git a/.github/workflows/ci-release.yaml b/.github/workflows/ci-release.yaml index fb12df5410..d340410adf 100644 --- a/.github/workflows/ci-release.yaml +++ b/.github/workflows/ci-release.yaml @@ -57,7 +57,7 @@ jobs: uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v4.2.1 with: distribution: 'temurin' - java-version: '17' + java-version: '21' cache: 'maven' - name: Set Version diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index 8b1793ef63..b5a4281479 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -39,7 +39,7 @@ jobs: uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v4.2.1 with: distribution: 'temurin' - java-version: '17' + java-version: '21' cache: 'maven' - name: Execute unit tests diff --git a/DEVELOPING.md b/DEVELOPING.md index 03bfb0460a..63584c7d40 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -18,7 +18,7 @@ This document primarily covers the API server. Please refer to the frontend repo There are a few things you'll need on your journey: -* JDK 17+ ([Temurin](https://adoptium.net/temurin/releases) distribution recommended) +* JDK 21+ ([Temurin](https://adoptium.net/temurin/releases) distribution recommended) * Maven (comes bundled with IntelliJ and Eclipse) * A Java IDE of your preference (we recommend IntelliJ, but any other IDE is fine as well) * Docker (optional) diff --git a/docs/_posts/2024-xx-xx-v4.12.0.md b/docs/_posts/2024-xx-xx-v4.12.0.md new file mode 100644 index 0000000000..3c2b011a56 --- /dev/null +++ b/docs/_posts/2024-xx-xx-v4.12.0.md @@ -0,0 +1,53 @@ +--- +title: v4.12.0 +type: major +--- + +**Features:** + +* Raise baseline Java version to 21 - [apiserver/#3682] + +**Fixes:** + +**Upgrade Notes:** + +* The API server now requires Java 21 or newer. Users deploying Dependency-Track via containers +don't have to do anything, since those have been shipped with Java 21 since version 4.10.0. +Users deploying Dependency-Track as JAR will need to upgrade their Java installation accordingly. + +For a complete list of changes, refer to the respective GitHub milestones: + +* [API server milestone 4.12.0](https://github.com/DependencyTrack/dependency-track/milestone/27?closed=1) +* [Frontend milestone 4.12.0](https://github.com/DependencyTrack/frontend/milestone/21?closed=1) + +We thank all organizations and individuals who contributed to this release, from logging issues to taking part in discussions on GitHub & Slack to testing of fixes. + +Special thanks to everyone who contributed code to implement enhancements and fix defects: + +###### dependency-track-apiserver.jar + +| Algorithm | Checksum | +|:----------|:---------| +| SHA-1 | | +| SHA-256 | | + +###### dependency-track-bundled.jar + +| Algorithm | Checksum | +|:----------|:---------| +| SHA-1 | | +| SHA-256 | | + +###### frontend-dist.zip + +| Algorithm | Checksum | +|:----------|:---------| +| SHA-1 | | +| SHA-256 | | + +###### Software Bill of Materials (SBOM) + +* API Server: [bom.json](https://github.com/DependencyTrack/dependency-track/releases/download/4.12.0/bom.json) +* Frontend: [bom.json](https://github.com/DependencyTrack/frontend/releases/download/4.12.0/bom.json) + +[apiserver/#3682]: https://github.com/DependencyTrack/dependency-track/pull/3682 \ No newline at end of file diff --git a/pom.xml b/pom.xml index 05f08fddd5..3707d434ee 100644 --- a/pom.xml +++ b/pom.xml @@ -80,6 +80,10 @@ + + 21 + 21 + 4.11.0 ${project.parent.version} diff --git a/src/main/java/org/dependencytrack/tasks/NistApiMirrorTask.java b/src/main/java/org/dependencytrack/tasks/NistApiMirrorTask.java index d82ac2ae8e..ebea83b0c3 100644 --- a/src/main/java/org/dependencytrack/tasks/NistApiMirrorTask.java +++ b/src/main/java/org/dependencytrack/tasks/NistApiMirrorTask.java @@ -146,13 +146,12 @@ public void inform(final Event e) { .namingPattern(getClass().getSimpleName() + "-%d") .uncaughtExceptionHandler(new LoggableUncaughtExceptionHandler()) .build(); - final var executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), factory); final long startTimeNs = System.nanoTime(); final var numMirrored = new AtomicInteger(0); ZonedDateTime lastModified; try (final NvdCveClient client = createApiClient(apiUrl, apiKey, lastModifiedEpochSeconds)) { - try { + try (final var executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), factory)) { while (client.hasNext()) { for (final DefCveItem defCveItem : client.next()) { final CveItem cveItem = defCveItem.getCve(); @@ -186,28 +185,6 @@ public void inform(final Event e) { }); } } - } finally { - // Copied from ExecutorService#close (available since JDK 19). - // This code can be replaced with try-with-resources after upgrade to Java 21. - // https://github.com/openjdk/jdk/blob/890adb6410dab4606a4f26a942aed02fb2f55387/src/java.base/share/classes/java/util/concurrent/ExecutorService.java#L410-L429 - boolean terminated = executor.isTerminated(); - if (!terminated) { - executor.shutdown(); - boolean interrupted = false; - while (!terminated) { - try { - terminated = executor.awaitTermination(1L, TimeUnit.DAYS); - } catch (InterruptedException ex) { - if (!interrupted) { - executor.shutdownNow(); - interrupted = true; - } - } - } - if (interrupted) { - Thread.currentThread().interrupt(); - } - } } lastModified = client.getLastUpdated(); diff --git a/src/test/java/org/dependencytrack/common/ManagedHttpClientFactoryTest.java b/src/test/java/org/dependencytrack/common/ManagedHttpClientFactoryTest.java index a789f36cd4..368d2b102a 100644 --- a/src/test/java/org/dependencytrack/common/ManagedHttpClientFactoryTest.java +++ b/src/test/java/org/dependencytrack/common/ManagedHttpClientFactoryTest.java @@ -36,7 +36,7 @@ public class ManagedHttpClientFactoryTest { @Before public void before() { - environmentVariables.set("http_proxy", "http://acme\\username:password@127.0.0.1:1080"); + environmentVariables.set("http_proxy", "http://acme%5Cusername:password@127.0.0.1:1080"); environmentVariables.set("no_proxy", "localhost:443,127.0.0.1:8080,example.com,www.example.net"); } From cf4f2d4d9b9341ada5da3691e0fe4e13bf0baf7d Mon Sep 17 00:00:00 2001 From: Niklas Date: Wed, 8 May 2024 13:36:09 +0200 Subject: [PATCH 137/412] Update versions in issue template for defects Signed-off-by: Niklas --- .github/ISSUE_TEMPLATE/defect-report.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/defect-report.yml b/.github/ISSUE_TEMPLATE/defect-report.yml index 8233329314..2778308379 100644 --- a/.github/ISSUE_TEMPLATE/defect-report.yml +++ b/.github/ISSUE_TEMPLATE/defect-report.yml @@ -63,9 +63,9 @@ body: - 4.7.x - 4.8.x - 4.9.x - - 4.10.0 - - 4.10.1 - - 4.11.0-SNAPSHOT + - 4.10.x + - 4.11.0 + - 4.12.0-SNAPSHOT validations: required: true - type: dropdown From f6409444b71ca175619099a240649891ea5def7e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 May 2024 08:42:50 +0000 Subject: [PATCH 138/412] Bump org.testcontainers:testcontainers from 1.19.7 to 1.19.8 Bumps [org.testcontainers:testcontainers](https://github.com/testcontainers/testcontainers-java) from 1.19.7 to 1.19.8. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.19.7...1.19.8) --- updated-dependencies: - dependency-name: org.testcontainers:testcontainers dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 26bf44e0e6..e3be3c5931 100644 --- a/pom.xml +++ b/pom.xml @@ -110,7 +110,7 @@ 3.2.2 2.2.0 1.19.0 - 1.19.7 + 1.19.8 2.35.2 6.6.2 1.1.1 From 49350358f8f98ff6a8379e061d66fe521c08920f Mon Sep 17 00:00:00 2001 From: Aravind Parappil Date: Thu, 9 May 2024 20:52:17 -0400 Subject: [PATCH 139/412] Add active Field To Project Versions In order for the UI to distinguish between active or inactive project versions, we need to include the active boolean field to ProjectVersion model Signed-off-by: Aravind Parappil --- .../org/dependencytrack/model/ProjectVersion.java | 15 +++++++++++++-- .../persistence/ProjectQueryManager.java | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/ProjectVersion.java b/src/main/java/org/dependencytrack/model/ProjectVersion.java index 84c9b4e521..14d3b10d50 100644 --- a/src/main/java/org/dependencytrack/model/ProjectVersion.java +++ b/src/main/java/org/dependencytrack/model/ProjectVersion.java @@ -34,15 +34,18 @@ public class ProjectVersion implements Serializable { private String version; + private Boolean active; + public ProjectVersion() { this.uuid = null; this.version = null; + this.active = null; } - public ProjectVersion(UUID uuid, String version) { + public ProjectVersion(UUID uuid, String version, Boolean active) { this.uuid = uuid; this.version = version; - + this.active = active; } public void setUuid(UUID uuid) { @@ -60,4 +63,12 @@ public void setVersion(String version) { public String getVersion() { return version; } + + public void setActive(Boolean active) { + this.active = active; + } + + public Boolean getActive() { + return active; + } } diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 6b1a97b7f5..269dfc83ba 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -1210,7 +1210,7 @@ private List getProjectVersions(Project project) { final Query query = pm.newQuery(Project.class); query.setFilter("name == :name"); query.setParameters(project.getName()); - query.setResult("uuid, version"); + query.setResult("uuid, version, active"); return query.executeResultList(ProjectVersion.class); } } From 7697f6e030b530bde560a6505050d713665238a9 Mon Sep 17 00:00:00 2001 From: Aravind Parappil Date: Thu, 9 May 2024 23:26:39 -0400 Subject: [PATCH 140/412] Add Test For Checking Active Field In Project Versions Unit test added which checks for the active field, given 3 versions of same project exists Signed-off-by: Aravind Parappil --- .../resources/v1/ProjectResourceTest.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index dd3229bb7a..7b39462552 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -267,6 +267,36 @@ public void getProjectByUuidTest() { Assert.assertEquals("1.0", json.getJsonArray("versions").getJsonObject(0).getJsonString("version").getString()); } + @Test + public void validateProjectVersionsActiveInactiveTest() { + Project project = qm.createProject("ABC", null, "1.0", null, null, null, true, false); + qm.createProject("ABC", null, "2.0", null, null, null, false, false); + qm.createProject("ABC", null, "3.0", null, null, null, true, false); + + Response response = jersey.target(V1_PROJECT + "/" + project.getUuid()) + .request() + .header(X_API_KEY, apiKey) + .get(Response.class); + + Assert.assertEquals(200, response.getStatus(), 0); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertEquals("ABC", json.getString("name")); + Assert.assertEquals(3, json.getJsonArray("versions").size()); + + Assert.assertNotNull(json.getJsonArray("versions").getJsonObject(0).getJsonString("uuid").getString()); + Assert.assertEquals("1.0", json.getJsonArray("versions").getJsonObject(0).getJsonString("version").getString()); + Assert.assertTrue(json.getJsonArray("versions").getJsonObject(0).getBoolean("active")); + + Assert.assertNotNull(json.getJsonArray("versions").getJsonObject(1).getJsonString("uuid").getString()); + Assert.assertEquals("2.0", json.getJsonArray("versions").getJsonObject(1).getJsonString("version").getString()); + Assert.assertFalse(json.getJsonArray("versions").getJsonObject(1).getBoolean("active")); + + Assert.assertNotNull(json.getJsonArray("versions").getJsonObject(2).getJsonString("uuid").getString()); + Assert.assertEquals("3.0", json.getJsonArray("versions").getJsonObject(2).getJsonString("version").getString()); + Assert.assertTrue(json.getJsonArray("versions").getJsonObject(2).getBoolean("active")); + } + @Test public void getProjectByInvalidUuidTest() { qm.createProject("ABC", null, "1.0", null, null, null, true, false); From 31b676f0c452d7e858d53aa3a94c90640e66bd4b Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 6 May 2024 14:38:50 +0200 Subject: [PATCH 141/412] Bump Alpine to `2.2.6-SNAPSHOT` Signed-off-by: nscuro --- pom.xml | 2 +- .../model/ProjectProperty.java | 1 + .../persistence/QueryManager.java | 56 ------------------- .../VulnerabilityQueryManager.java | 2 +- .../resources/v1/TeamResource.java | 2 +- .../tasks/AbstractNistMirrorTask.java | 9 +-- src/main/resources/logback.xml | 2 + .../DefaultObjectGeneratorTest.java | 2 +- .../resources/v1/BomResourceTest.java | 2 +- .../resources/v1/ProjectResourceTest.java | 7 ++- .../resources/v1/VexResourceTest.java | 2 +- .../v1/VulnerabilityResourceTest.java | 4 +- .../tasks/BomUploadProcessingTaskTest.java | 7 ++- .../tasks/GitHubAdvisoryMirrorTaskTest.java | 1 + .../tasks/OsvDownloadTaskTest.java | 1 + 15 files changed, 25 insertions(+), 75 deletions(-) diff --git a/pom.xml b/pom.xml index e3be3c5931..1ad80be47f 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ us.springett alpine-parent - 2.2.5 + 2.2.6-SNAPSHOT 4.0.0 diff --git a/src/main/java/org/dependencytrack/model/ProjectProperty.java b/src/main/java/org/dependencytrack/model/ProjectProperty.java index ac59306a42..eeb4da705f 100644 --- a/src/main/java/org/dependencytrack/model/ProjectProperty.java +++ b/src/main/java/org/dependencytrack/model/ProjectProperty.java @@ -56,6 +56,7 @@ public class ProjectProperty implements IConfigProperty, Serializable { @Persistent @Column(name = "PROJECT_ID", allowsNull = "false") + @JsonIgnore private Project project; @Persistent diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index e8b2f7f813..fb2240504c 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -92,9 +92,6 @@ import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; /** * This QueryManager provides a concrete extension of {@link AlpineQueryManager} by @@ -1380,59 +1377,6 @@ public Query getObjectsByUuidsQuery(final Class clazz, final List - * Eventually, this may be moved to {@link alpine.persistence.AbstractAlpineQueryManager}. - * - * @param runnable The {@link Runnable} to execute - * @since 4.6.0 - */ - public void runInTransaction(final Runnable runnable) { - runInTransaction((Function) trx -> { - runnable.run(); - return null; - }); - } - - public void runInTransaction(final Consumer consumer) { - runInTransaction((Function) trx -> { - consumer.accept(trx); - return null; - }); - } - - /** - * Convenience method to execute a given {@link Supplier} within the context of a {@link Transaction}. - *

    - * Eventually, this may be moved to {@link alpine.persistence.AbstractAlpineQueryManager}. - * - * @param supplier The {@link Supplier} to execute - * @since 4.9.0 - */ - public T runInTransaction(final Supplier supplier) { - return runInTransaction((Function) trx -> supplier.get()); - } - - public T runInTransaction(final Function function) { - final Transaction trx = pm.currentTransaction(); - final boolean isJoiningExisting = trx.isActive(); - try { - if (!isJoiningExisting) { - trx.begin(); - } - final T result = function.apply(trx); - if (!isJoiningExisting) { - trx.commit(); - } - return result; - } finally { - if (!isJoiningExisting && trx.isActive()) { - trx.rollback(); - } - } - } - /** * Convenience method to ensure that any active transaction is rolled back. *

    diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index 2576fefb36..b78a37ea9a 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -530,7 +530,7 @@ public List getAffectedProjects(Vulnerability vulnerability) { } public synchronized VulnerabilityAlias synchronizeVulnerabilityAlias(final VulnerabilityAlias alias) { - return runInTransaction(() -> { + return callInTransaction(() -> { // Query existing aliases that match AT LEAST ONE identifier of the given alias. // // For each data source, we want to know the existing aliases where the respective identifier either: diff --git a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java index 9b22b3e328..a74fbc75d4 100644 --- a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java @@ -272,7 +272,7 @@ public Response updateApiKeyComment(@PathParam("key") final String key, try (final var qm = new QueryManager()) { qm.getPersistenceManager().setProperty(PROPERTY_RETAIN_VALUES, "true"); - return qm.runInTransaction(() -> { + return qm.callInTransaction(() -> { final ApiKey apiKey = qm.getApiKey(key); if (apiKey == null) { return Response diff --git a/src/main/java/org/dependencytrack/tasks/AbstractNistMirrorTask.java b/src/main/java/org/dependencytrack/tasks/AbstractNistMirrorTask.java index 5676677fae..c5d0763fe2 100644 --- a/src/main/java/org/dependencytrack/tasks/AbstractNistMirrorTask.java +++ b/src/main/java/org/dependencytrack/tasks/AbstractNistMirrorTask.java @@ -19,6 +19,7 @@ package org.dependencytrack.tasks; import alpine.common.logging.Logger; +import alpine.persistence.Transaction; import org.dependencytrack.model.AffectedVersionAttribution; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerableSoftware; @@ -46,9 +47,7 @@ abstract class AbstractNistMirrorTask { Vulnerability synchronizeVulnerability(final QueryManager qm, final Vulnerability vuln) { PersistenceUtil.assertNonPersistent(vuln, "vuln must not be persistent"); - return qm.runInTransaction(trx -> { - trx.setSerializeRead(true); // SELECT ... FOR UPDATE - + return qm.callInTransaction(Transaction.defaultOptions().withSerializeRead(true), () -> { Vulnerability persistentVuln = getVulnerabilityByCveId(qm, vuln.getVulnId()); if (persistentVuln == null) { persistentVuln = qm.getPersistenceManager().makePersistent(vuln); @@ -70,9 +69,7 @@ void synchronizeVulnerableSoftware(final QueryManager qm, final Vulnerability pe assertPersistent(persistentVuln, "vuln must be persistent"); assertNonPersistentAll(vsList, "vsList must not be persistent"); - qm.runInTransaction(tx -> { - tx.setSerializeRead(false); - + qm.runInTransaction(() -> { // Get all VulnerableSoftware records that are currently associated with the vulnerability. // Note: For SOME ODD REASON, duplicate (as in, same database ID and all) VulnerableSoftware // records are returned, when operating on data that was originally created by the feed-based diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 7779e785d1..dc9b342db0 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,6 +1,8 @@ + + ${user.home}/.dependency-track/dependency-track.log diff --git a/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java b/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java index 0ef5da69b1..ae8d47c97f 100644 --- a/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java +++ b/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java @@ -68,7 +68,7 @@ public void testLoadDefaultLicensesUpdatesExistingLicenses() throws Exception { method.setAccessible(true); method.invoke(generator); - qm.getPersistenceManager().refresh(license); + qm.getPersistenceManager().evictAll(); assertThat(license.getLicenseId()).isEqualTo("LGPL-2.1+"); assertThat(license.getName()).isEqualTo("GNU Lesser General Public License v2.1 or later"); assertThat(license.getComment()).isNotEqualTo("comment"); diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index 1258943ad4..829489f127 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -999,7 +999,7 @@ public void uploadBomTooLargeViaPutTest() { { "status": 400, "title": "The provided JSON payload could not be mapped", - "detail": "The BOM is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/bom\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String length (20000001) exceeds the maximum length (20000000) (through reference chain: org.dependencytrack.resources.v1.vo.BomSubmitRequest[\\"bom\\"])" + "detail": "The BOM is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/bom\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String value length (20000001) exceeds the maximum allowed (20000000, from `StreamReadConstraints.getMaxStringLength()`) (through reference chain: org.dependencytrack.resources.v1.vo.BomSubmitRequest[\\"bom\\"])" } """); } diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index dd3229bb7a..74ed25506e 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -665,7 +665,8 @@ public void patchProjectSuccessfullyPatchedTest() { "name": "tag4" } ], - "active": false + "active": false, + "children": [] } """); } @@ -742,7 +743,7 @@ public void patchProjectParentTest() { """); // Ensure the parent was updated. - qm.getPersistenceManager().refresh(project); + qm.getPersistenceManager().evictAll(); assertThat(project.getParent()).isNotNull(); assertThat(project.getParent().getUuid()).isEqualTo(newParent.getUuid()); } @@ -767,7 +768,7 @@ public void patchProjectParentNotFoundTest() { assertThat(getPlainTextBody(response)).isEqualTo("The UUID of the parent project could not be found."); // Ensure the parent was not modified. - qm.getPersistenceManager().refresh(project); + qm.getPersistenceManager().evictAll(); assertThat(project.getParent()).isNotNull(); assertThat(project.getParent().getUuid()).isEqualTo(parent.getUuid()); } diff --git a/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java index 6b82ee739e..fdb389de34 100644 --- a/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java @@ -345,7 +345,7 @@ public void uploadVexTooLargeViaPutTest() { { "status": 400, "title": "The provided JSON payload could not be mapped", - "detail": "The VEX is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/vex\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String length (20000001) exceeds the maximum length (20000000) (through reference chain: org.dependencytrack.resources.v1.vo.VexSubmitRequest[\\"vex\\"])" + "detail": "The VEX is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/vex\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String value length (20000001) exceeds the maximum allowed (20000000, from `StreamReadConstraints.getMaxStringLength()`) (through reference chain: org.dependencytrack.resources.v1.vo.VexSubmitRequest[\\"vex\\"])" } """); } diff --git a/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java index a96d97cd2d..c31de1dcbd 100644 --- a/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java @@ -320,7 +320,7 @@ public void createVulnerabilityTest() { Assert.assertEquals(2.8, json.getJsonNumber("cvssV3ExploitabilitySubScore").doubleValue(), 0); Assert.assertEquals("CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L", json.getString("cvssV3Vector")); Assert.assertEquals(1.0, json.getJsonNumber("owaspRRLikelihoodScore").doubleValue(), 0); - Assert.assertEquals(1.3, json.getJsonNumber("owaspRRTechnicalImpactScore").doubleValue(), 0); + Assert.assertEquals(1.25, json.getJsonNumber("owaspRRTechnicalImpactScore").doubleValue(), 0); Assert.assertEquals(1.8, json.getJsonNumber("owaspRRBusinessImpactScore").doubleValue(), 0); Assert.assertEquals("SL:1/M:1/O:0/S:2/ED:1/EE:1/A:1/ID:1/LC:2/LI:1/LAV:1/LAC:1/FD:1/RD:1/NC:2/PV:3", json.getString("owaspRRVector")); Assert.assertEquals("MEDIUM", json.getString("severity")); @@ -448,7 +448,7 @@ public void updateVulnerabilityTest() { Assert.assertEquals(3.4, json.getJsonNumber("cvssV3ImpactSubScore").doubleValue(), 0); Assert.assertEquals(2.8, json.getJsonNumber("cvssV3ExploitabilitySubScore").doubleValue(), 0); Assert.assertEquals(1.0, json.getJsonNumber("owaspRRLikelihoodScore").doubleValue(), 0); - Assert.assertEquals(1.3, json.getJsonNumber("owaspRRTechnicalImpactScore").doubleValue(), 0); + Assert.assertEquals(1.25, json.getJsonNumber("owaspRRTechnicalImpactScore").doubleValue(), 0); Assert.assertEquals(1.8, json.getJsonNumber("owaspRRBusinessImpactScore").doubleValue(), 0); Assert.assertEquals("CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L", json.getString("cvssV3Vector")); Assert.assertEquals("SL:1/M:1/O:0/S:2/ED:1/EE:1/A:1/ID:1/LC:2/LI:1/LAV:1/LAC:1/FD:1/RD:1/NC:2/PV:3", json.getString("owaspRRVector")); diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index 72e972ab38..62a478fdc9 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -455,6 +455,9 @@ public void informWithExistingDuplicateComponentsTest() { componentB.setVersion("2.0.0"); qm.persist(componentB); + final Component transientComponentA = qm.makeTransient(componentA); + final Component transientComponentB = qm.makeTransient(componentB); + final byte[] bomBytes = """ { "bomFormat": "CycloneDX", @@ -494,7 +497,7 @@ public void informWithExistingDuplicateComponentsTest() { assertThat(indexEvent.getIndexableClass()).isEqualTo(Component.class); assertThat(indexEvent.getAction()).isEqualTo(IndexEvent.Action.UPDATE); final var searchDoc = (ComponentDocument) indexEvent.getDocument(); - assertThat(searchDoc.uuid()).isEqualTo(componentA.getUuid()); + assertThat(searchDoc.uuid()).isEqualTo(transientComponentA.getUuid()); }, event -> { assertThat(event).isInstanceOf(IndexEvent.class); @@ -502,7 +505,7 @@ public void informWithExistingDuplicateComponentsTest() { assertThat(indexEvent.getIndexableClass()).isEqualTo(Component.class); assertThat(indexEvent.getAction()).isEqualTo(IndexEvent.Action.DELETE); final var searchDoc = (ComponentDocument) indexEvent.getDocument(); - assertThat(searchDoc.uuid()).isEqualTo(componentB.getUuid()); + assertThat(searchDoc.uuid()).isEqualTo(transientComponentB.getUuid()); }, event -> { assertThat(event).isInstanceOf(IndexEvent.class); diff --git a/src/test/java/org/dependencytrack/tasks/GitHubAdvisoryMirrorTaskTest.java b/src/test/java/org/dependencytrack/tasks/GitHubAdvisoryMirrorTaskTest.java index 33eebcbeef..e1a814d173 100644 --- a/src/test/java/org/dependencytrack/tasks/GitHubAdvisoryMirrorTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/GitHubAdvisoryMirrorTaskTest.java @@ -188,6 +188,7 @@ public void testUpdateDatasourceVulnerableVersionRanges() { final var task = new GitHubAdvisoryMirrorTask(); task.updateDatasource(List.of(ghAdvisory)); + qm.getPersistenceManager().evictAll(); final Vulnerability vuln = qm.getVulnerabilityByVulnId(Source.GITHUB, "GHSA-57j2-w4cx-62h2"); assertThat(vuln).isNotNull(); diff --git a/src/test/java/org/dependencytrack/tasks/OsvDownloadTaskTest.java b/src/test/java/org/dependencytrack/tasks/OsvDownloadTaskTest.java index 573a39b933..c220a72e6d 100644 --- a/src/test/java/org/dependencytrack/tasks/OsvDownloadTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/OsvDownloadTaskTest.java @@ -285,6 +285,7 @@ public void testUpdateDatasourceVulnerableVersionRanges() { } """))); + qm.getPersistenceManager().evictAll(); final Vulnerability vuln = qm.getVulnerabilityByVulnId(Vulnerability.Source.GITHUB, "GHSA-57j2-w4cx-62h2"); assertThat(vuln).isNotNull(); From d82d32a06c265ec1709eaf47afdf048a9e0c12e6 Mon Sep 17 00:00:00 2001 From: nscuro Date: Fri, 10 May 2024 21:47:34 +0200 Subject: [PATCH 142/412] Replace manual transaction commits with `callInTransaction` This is to properly support nested transactions as introduced in https://github.com/stevespringett/Alpine/pull/552. Signed-off-by: nscuro --- .../persistence/MetricsQueryManager.java | 33 ++++++++----------- .../persistence/NotificationQueryManager.java | 22 ++++++------- .../persistence/ProjectQueryManager.java | 31 ++++++++--------- .../persistence/QueryManager.java | 21 ++++++------ .../VulnerabilityQueryManager.java | 21 ++++++------ .../InternalComponentIdentificationTask.java | 14 ++------ 6 files changed, 63 insertions(+), 79 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/MetricsQueryManager.java b/src/main/java/org/dependencytrack/persistence/MetricsQueryManager.java index 8471338bf9..909d01df94 100644 --- a/src/main/java/org/dependencytrack/persistence/MetricsQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/MetricsQueryManager.java @@ -18,12 +18,8 @@ */ package org.dependencytrack.persistence; -import java.util.Date; -import java.util.List; - -import javax.jdo.PersistenceManager; -import javax.jdo.Query; - +import alpine.persistence.PaginatedResult; +import alpine.resources.AlpineRequest; import org.dependencytrack.model.Component; import org.dependencytrack.model.DependencyMetrics; import org.dependencytrack.model.PortfolioMetrics; @@ -31,8 +27,10 @@ import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.model.VulnerabilityMetrics; -import alpine.persistence.PaginatedResult; -import alpine.resources.AlpineRequest; +import javax.jdo.PersistenceManager; +import javax.jdo.Query; +import java.util.Date; +import java.util.List; public class MetricsQueryManager extends QueryManager implements IQueryManager { @@ -164,17 +162,14 @@ public List getDependencyMetricsSince(Component component, Da } public void synchronizeVulnerabilityMetrics(List metrics) { - pm.currentTransaction().begin(); - // No need for complex updating, just replace the existing ~400 rows with new ones - // Unless we have a contract with clients that the ID of metric records cannot change? - - final Query delete = pm.newQuery("DELETE FROM org.dependencytrack.model.VulnerabilityMetrics"); - delete.execute(); - - // This still does ~400 queries, probably because not all databases can do bulk insert with autogenerated PKs - // Or because Datanucleus is trying to be smart as it wants to cache all these instances - pm.makePersistentAll(metrics); - pm.currentTransaction().commit(); + runInTransaction(() -> { + // No need for complex updating, just replace the existing ~400 rows with new ones. + pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.VulnerabilityMetrics").execute(); + + // This still does ~400 queries, probably because not all databases can do bulk insert with autogenerated PKs + // Or because Datanucleus is trying to be smart as it wants to cache all these instances + pm.makePersistentAll(metrics); + }); } /** diff --git a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java index f5c2449650..81553b86b9 100644 --- a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java @@ -159,18 +159,16 @@ private NotificationPublisher getDefaultNotificationPublisher(final String clazz public NotificationPublisher createNotificationPublisher(final String name, final String description, final Class publisherClass, final String templateContent, final String templateMimeType, final boolean defaultPublisher) { - pm.currentTransaction().begin(); - final NotificationPublisher publisher = new NotificationPublisher(); - publisher.setName(name); - publisher.setDescription(description); - publisher.setPublisherClass(publisherClass.getName()); - publisher.setTemplate(templateContent); - publisher.setTemplateMimeType(templateMimeType); - publisher.setDefaultPublisher(defaultPublisher); - pm.makePersistent(publisher); - pm.currentTransaction().commit(); - pm.getFetchPlan().addGroup(NotificationPublisher.FetchGroup.ALL.name()); - return getObjectById(NotificationPublisher.class, publisher.getId()); + return callInTransaction(() -> { + final NotificationPublisher publisher = new NotificationPublisher(); + publisher.setName(name); + publisher.setDescription(description); + publisher.setPublisherClass(publisherClass.getName()); + publisher.setTemplate(templateContent); + publisher.setTemplateMimeType(templateMimeType); + publisher.setDefaultPublisher(defaultPublisher); + return pm.makePersistent(publisher); + }); } /** diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 6b1a97b7f5..7babd70ade 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -816,25 +816,26 @@ public List getProjectProperties(final Project project) { * @param project a Project object * @param tags a List of Tag objects */ - @SuppressWarnings("unchecked") @Override public void bind(Project project, List tags) { - final Query query = pm.newQuery(Tag.class, "projects.contains(:project)"); - final List currentProjectTags = (List)query.execute(project); - pm.currentTransaction().begin(); - for (final Tag tag: currentProjectTags) { - if (!tags.contains(tag)) { - tag.getProjects().remove(project); + runInTransaction(() -> { + final Query query = pm.newQuery(Tag.class, "projects.contains(:project)"); + query.setParameters(project); + final List currentProjectTags = executeAndCloseList(query); + + for (final Tag tag : currentProjectTags) { + if (!tags.contains(tag)) { + tag.getProjects().remove(project); + } } - } - project.setTags(tags); - for (final Tag tag: tags) { - final List projects = tag.getProjects(); - if (!projects.contains(project)) { - projects.add(project); + project.setTags(tags); + for (final Tag tag : tags) { + final List projects = tag.getProjects(); + if (!projects.contains(project)) { + projects.add(project); + } } - } - pm.currentTransaction().commit(); + }); } /** diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index fb2240504c..84d3b405c3 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -1395,17 +1395,16 @@ public void ensureNoActiveTransaction() { } public void recursivelyDeleteTeam(Team team) { - pm.setProperty("datanucleus.query.sql.allowAll", true); - final Transaction trx = pm.currentTransaction(); - pm.currentTransaction().begin(); - pm.deletePersistentAll(team.getApiKeys()); - String aclDeleteQuery = """ - DELETE FROM \"PROJECT_ACCESS_TEAMS\" WHERE \"PROJECT_ACCESS_TEAMS\".\"TEAM_ID\" = ? - """; - final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, aclDeleteQuery); - query.executeWithArray(team.getId()); - pm.deletePersistent(team); - pm.currentTransaction().commit(); + runInTransaction(() -> { + pm.deletePersistentAll(team.getApiKeys()); + + final Query aclDeleteQuery = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, """ + DELETE FROM "PROJECT_ACCESS_TEAMS" WHERE "PROJECT_ACCESS_TEAMS"."TEAM_ID" = ?"""); + aclDeleteQuery.setParameters(team.getId()); + executeAndClose(aclDeleteQuery); + + pm.deletePersistent(team); + }); } /** diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index b78a37ea9a..bb441ede1c 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -42,9 +42,9 @@ import java.util.Comparator; import java.util.Date; import java.util.HashMap; -import java.util.UUID; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; final class VulnerabilityQueryManager extends QueryManager implements IQueryManager { @@ -243,15 +243,16 @@ public void addVulnerability(Vulnerability vulnerability, Component component, A * @param component the component unaffected by the vulnerabiity */ public void removeVulnerability(Vulnerability vulnerability, Component component) { - if (contains(vulnerability, component)) { - pm.currentTransaction().begin(); - component.removeVulnerability(vulnerability); - pm.currentTransaction().commit(); - } - final FindingAttribution fa = getFindingAttribution(vulnerability, component); - if (fa != null) { - delete(fa); - } + runInTransaction(() -> { + if (contains(vulnerability, component)) { + component.removeVulnerability(vulnerability); + } + + final FindingAttribution fa = getFindingAttribution(vulnerability, component); + if (fa != null) { + delete(fa); + } + }); } /** diff --git a/src/main/java/org/dependencytrack/tasks/InternalComponentIdentificationTask.java b/src/main/java/org/dependencytrack/tasks/InternalComponentIdentificationTask.java index bfb2470641..1f58670574 100644 --- a/src/main/java/org/dependencytrack/tasks/InternalComponentIdentificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/InternalComponentIdentificationTask.java @@ -31,7 +31,6 @@ import javax.jdo.PersistenceManager; import javax.jdo.Query; -import javax.jdo.Transaction; import java.time.Duration; import java.time.Instant; import java.util.List; @@ -95,20 +94,11 @@ private void analyze() throws Exception { } if (component.isInternal() != internal) { - final Transaction trx = pm.currentTransaction(); - try { - trx.begin(); - component.setInternal(internal); - trx.commit(); - } finally { - if (trx.isActive()) { - trx.rollback(); - } - } + qm.runInTransaction(() -> component.setInternal(internal)); } } - final long lastId = components.get(components.size() - 1).getId(); + final long lastId = components.getLast().getId(); components = fetchNextComponentsPage(pm, lastId); } } From 1fdea759148412ff9136885474a125e30fd21ddc Mon Sep 17 00:00:00 2001 From: nscuro Date: Fri, 10 May 2024 21:48:15 +0200 Subject: [PATCH 143/412] Fix `VulnerabilityResourceTest` Signed-off-by: nscuro --- .../resources/v1/VulnerabilityResourceTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java index c31de1dcbd..e57f16cf11 100644 --- a/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java @@ -321,7 +321,7 @@ public void createVulnerabilityTest() { Assert.assertEquals("CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L", json.getString("cvssV3Vector")); Assert.assertEquals(1.0, json.getJsonNumber("owaspRRLikelihoodScore").doubleValue(), 0); Assert.assertEquals(1.25, json.getJsonNumber("owaspRRTechnicalImpactScore").doubleValue(), 0); - Assert.assertEquals(1.8, json.getJsonNumber("owaspRRBusinessImpactScore").doubleValue(), 0); + Assert.assertEquals(1.75, json.getJsonNumber("owaspRRBusinessImpactScore").doubleValue(), 0); Assert.assertEquals("SL:1/M:1/O:0/S:2/ED:1/EE:1/A:1/ID:1/LC:2/LI:1/LAV:1/LAC:1/FD:1/RD:1/NC:2/PV:3", json.getString("owaspRRVector")); Assert.assertEquals("MEDIUM", json.getString("severity")); Assert.assertNotNull(json.getJsonObject("cwe")); @@ -449,7 +449,7 @@ public void updateVulnerabilityTest() { Assert.assertEquals(2.8, json.getJsonNumber("cvssV3ExploitabilitySubScore").doubleValue(), 0); Assert.assertEquals(1.0, json.getJsonNumber("owaspRRLikelihoodScore").doubleValue(), 0); Assert.assertEquals(1.25, json.getJsonNumber("owaspRRTechnicalImpactScore").doubleValue(), 0); - Assert.assertEquals(1.8, json.getJsonNumber("owaspRRBusinessImpactScore").doubleValue(), 0); + Assert.assertEquals(1.75, json.getJsonNumber("owaspRRBusinessImpactScore").doubleValue(), 0); Assert.assertEquals("CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L", json.getString("cvssV3Vector")); Assert.assertEquals("SL:1/M:1/O:0/S:2/ED:1/EE:1/A:1/ID:1/LC:2/LI:1/LAV:1/LAC:1/FD:1/RD:1/NC:2/PV:3", json.getString("owaspRRVector")); Assert.assertEquals("MEDIUM", json.getString("severity")); From 53c92b07a56f84cf87a36264c97faab020fe5948 Mon Sep 17 00:00:00 2001 From: nscuro Date: Fri, 10 May 2024 22:34:00 +0200 Subject: [PATCH 144/412] Fix missing `datanucleus.query.sql.allowall` Signed-off-by: nscuro --- .../persistence/QueryManager.java | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 84d3b405c3..8e12a3e3ec 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -28,6 +28,7 @@ import alpine.notification.NotificationLevel; import alpine.persistence.AlpineQueryManager; import alpine.persistence.PaginatedResult; +import alpine.persistence.ScopedCustomization; import alpine.resources.AlpineRequest; import com.github.packageurl.PackageURL; import com.google.common.collect.Lists; @@ -93,6 +94,8 @@ import java.util.Set; import java.util.UUID; +import static org.datanucleus.PropertyNames.PROPERTY_QUERY_SQL_ALLOWALL; + /** * This QueryManager provides a concrete extension of {@link AlpineQueryManager} by * providing methods that operate on the Dependency-Track specific models. @@ -387,11 +390,11 @@ public PaginatedResult getProjects(final Team team, final boolean excludeInactiv return getProjectQueryManager().getProjects(team, excludeInactive, bypass, onlyRoot); } - public PaginatedResult getProjectsWithoutDescendantsOf(final boolean excludeInactive, final Project project){ + public PaginatedResult getProjectsWithoutDescendantsOf(final boolean excludeInactive, final Project project) { return getProjectQueryManager().getProjectsWithoutDescendantsOf(excludeInactive, project); } - public PaginatedResult getProjectsWithoutDescendantsOf(final String name, final boolean excludeInactive, final Project project){ + public PaginatedResult getProjectsWithoutDescendantsOf(final String name, final boolean excludeInactive, final Project project) { return getProjectQueryManager().getProjectsWithoutDescendantsOf(name, excludeInactive, project); } @@ -407,15 +410,15 @@ public PaginatedResult getProjects(final Classifier classifier, final boolean in return getProjectQueryManager().getProjects(classifier, includeMetrics, excludeInactive, onlyRoot); } - public PaginatedResult getChildrenProjects(final UUID uuid, final boolean includeMetrics, final boolean excludeInactive){ + public PaginatedResult getChildrenProjects(final UUID uuid, final boolean includeMetrics, final boolean excludeInactive) { return getProjectQueryManager().getChildrenProjects(uuid, includeMetrics, excludeInactive); } - public PaginatedResult getChildrenProjects(final Tag tag, final UUID uuid, final boolean includeMetrics, final boolean excludeInactive){ + public PaginatedResult getChildrenProjects(final Tag tag, final UUID uuid, final boolean includeMetrics, final boolean excludeInactive) { return getProjectQueryManager().getChildrenProjects(tag, uuid, includeMetrics, excludeInactive); } - public PaginatedResult getChildrenProjects(final Classifier classifier, final UUID uuid, final boolean includeMetrics, final boolean excludeInactive){ + public PaginatedResult getChildrenProjects(final Classifier classifier, final UUID uuid, final boolean includeMetrics, final boolean excludeInactive) { return getProjectQueryManager().getChildrenProjects(classifier, uuid, includeMetrics, excludeInactive); } @@ -645,7 +648,7 @@ public synchronized PolicyViolation addPolicyViolationIfNotExist(final PolicyVio return getPolicyQueryManager().addPolicyViolationIfNotExist(pv); } - public PolicyViolation clonePolicyViolation(PolicyViolation sourcePolicyViolation, Component destinationComponent){ + public PolicyViolation clonePolicyViolation(PolicyViolation sourcePolicyViolation, Component destinationComponent) { return getPolicyQueryManager().clonePolicyViolation(sourcePolicyViolation, destinationComponent); } @@ -1246,7 +1249,7 @@ public List getComponentAnalysisCache(ComponentAnalysisC } public synchronized void updateComponentAnalysisCache(ComponentAnalysisCache.CacheType cacheType, String targetHost, String targetType, String target, Date lastOccurrence, JsonObject result) { - getCacheQueryManager().updateComponentAnalysisCache(cacheType, targetHost, targetType, target, lastOccurrence, result); + getCacheQueryManager().updateComponentAnalysisCache(cacheType, targetHost, targetType, target, lastOccurrence, result); } public void clearComponentAnalysisCache() { @@ -1264,7 +1267,7 @@ public void bind(Project project, List tags) { /** * Commits the Lucene index. * @param commitIndex specifies if the search index should be committed (an expensive operation) - * @param clazz the indexable class to commit the index of + * @param clazz the indexable class to commit the index of */ public void commitSearchIndex(boolean commitIndex, Class clazz) { if (commitIndex) { @@ -1301,11 +1304,11 @@ public PaginatedResult getTags(String policyUuid) { *

    * Eventually, this may be moved to {@link alpine.persistence.AbstractAlpineQueryManager}. * - * @param clazz Class of the object to fetch - * @param uuid {@link UUID} of the object to fetch + * @param clazz Class of the object to fetch + * @param uuid {@link UUID} of the object to fetch * @param fetchGroups Fetch groups to use for this operation * @return The object if found, otherwise {@code null} - * @param Type of the object + * @param Type of the object * @throws Exception When closing the query failed * @since 4.6.0 */ @@ -1354,7 +1357,7 @@ public T detachWithGroups(final T object, final List fetchGroups) { * @param clazz Class of the object to fetch * @param uuids {@link UUID} list of uuids to fetch * @return The list of objects found - * @param Type of the object + * @param Type of the object * @since 4.9.0 */ public List getObjectsByUuids(final Class clazz, final List uuids) { @@ -1368,7 +1371,7 @@ public List getObjectsByUuids(final Class clazz, final List uuid * @param clazz Class of the object to fetch * @param uuids {@link UUID} list of uuids to fetch * @return The query to execute - * @param Type of the object + * @param Type of the object * @since 4.9.0 */ public Query getObjectsByUuidsQuery(final Class clazz, final List uuids) { @@ -1398,10 +1401,11 @@ public void recursivelyDeleteTeam(Team team) { runInTransaction(() -> { pm.deletePersistentAll(team.getApiKeys()); - final Query aclDeleteQuery = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, """ - DELETE FROM "PROJECT_ACCESS_TEAMS" WHERE "PROJECT_ACCESS_TEAMS"."TEAM_ID" = ?"""); - aclDeleteQuery.setParameters(team.getId()); - executeAndClose(aclDeleteQuery); + try (var ignored = new ScopedCustomization(pm).withProperty(PROPERTY_QUERY_SQL_ALLOWALL, "true")) { + final Query aclDeleteQuery = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, """ + DELETE FROM "PROJECT_ACCESS_TEAMS" WHERE "PROJECT_ACCESS_TEAMS"."TEAM_ID" = ?"""); + executeAndCloseWithArray(aclDeleteQuery, team.getId()); + } pm.deletePersistent(team); }); @@ -1439,7 +1443,7 @@ public List getRepositoryMetaComponentsBatch(final List /** * Returns a list of all {@link RepositoryMetaComponent} objects by {@link RepositoryQueryManager.RepositoryMetaComponentSearch} UUID. - * @param list a list of {@link RepositoryQueryManager.RepositoryMetaComponentSearch} + * @param list a list of {@link RepositoryQueryManager.RepositoryMetaComponentSearch} * @param batchSize the batch size * @return a list of {@link RepositoryMetaComponent} objects * @since 4.9.0 From 1bbed518155eed9070824c3877b1749cf888a515 Mon Sep 17 00:00:00 2001 From: Aravind Parappil Date: Fri, 10 May 2024 22:22:42 -0400 Subject: [PATCH 145/412] Remove setActive From ProjectVersion This is to see if the Codacy coverage report succeeds Signed-off-by: Aravind Parappil --- src/main/java/org/dependencytrack/model/ProjectVersion.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/ProjectVersion.java b/src/main/java/org/dependencytrack/model/ProjectVersion.java index 14d3b10d50..f7f706a7e2 100644 --- a/src/main/java/org/dependencytrack/model/ProjectVersion.java +++ b/src/main/java/org/dependencytrack/model/ProjectVersion.java @@ -64,10 +64,6 @@ public String getVersion() { return version; } - public void setActive(Boolean active) { - this.active = active; - } - public Boolean getActive() { return active; } From 14fa2eb0d6d4cf3b12d679419f8aaddcaa54f8c1 Mon Sep 17 00:00:00 2001 From: Aravind Parappil Date: Sun, 12 May 2024 11:23:13 -0400 Subject: [PATCH 146/412] Change ProjectVersion Into A Record Signed-off-by: Aravind Parappil --- .../dependencytrack/model/ProjectVersion.java | 42 +------------------ 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/ProjectVersion.java b/src/main/java/org/dependencytrack/model/ProjectVersion.java index f7f706a7e2..73bd1c2bc3 100644 --- a/src/main/java/org/dependencytrack/model/ProjectVersion.java +++ b/src/main/java/org/dependencytrack/model/ProjectVersion.java @@ -26,45 +26,5 @@ * Value object holding UUID and version for a project */ @JsonInclude(JsonInclude.Include.NON_NULL) -public class ProjectVersion implements Serializable { - - private static final long serialVersionUID = 1L; - - private UUID uuid; - - private String version; - - private Boolean active; - - public ProjectVersion() { - this.uuid = null; - this.version = null; - this.active = null; - } - - public ProjectVersion(UUID uuid, String version, Boolean active) { - this.uuid = uuid; - this.version = version; - this.active = active; - } - - public void setUuid(UUID uuid) { - this.uuid = uuid; - } - - public UUID getUuid() { - return uuid; - } - - public void setVersion(String version) { - this.version = version; - } - - public String getVersion() { - return version; - } - - public Boolean getActive() { - return active; - } +public record ProjectVersion(UUID uuid, String version, Boolean active) implements Serializable { } From 87494bcbd160209ce9fdad63036a43d9475a9d60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 08:36:45 +0000 Subject: [PATCH 147/412] Bump actions/checkout from 4.1.4 to 4.1.5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.4 to 4.1.5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/0ad4b8fadaa221de15dcec353f45205ec38ea70b...44c2b7a8a4ea60a981eaca3cf939b5f4305c123b) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 4 ++-- .github/workflows/ci-publish.yaml | 4 ++-- .github/workflows/ci-release.yaml | 6 +++--- .github/workflows/ci-test.yaml | 2 +- .github/workflows/dependency-review.yaml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 534556becd..deb8d7fa64 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # tag=v4.1.5 - name: Set up JDK uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v4.2.1 @@ -78,7 +78,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # tag=v4.1.5 - name: Download Artifacts uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # tag=v4.1.7 diff --git a/.github/workflows/ci-publish.yaml b/.github/workflows/ci-publish.yaml index 71886560c2..2f6c02f26a 100644 --- a/.github/workflows/ci-publish.yaml +++ b/.github/workflows/ci-publish.yaml @@ -23,7 +23,7 @@ jobs: exit 1 fi - name: Checkout Repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # tag=v4.1.5 - name: Parse Version from POM id: parse @@ -52,7 +52,7 @@ jobs: - call-build steps: - name: Checkout Repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # tag=v4.1.5 - name: Download Artifacts uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # tag=v4.1.7 diff --git a/.github/workflows/ci-release.yaml b/.github/workflows/ci-release.yaml index d340410adf..a42fa535c3 100644 --- a/.github/workflows/ci-release.yaml +++ b/.github/workflows/ci-release.yaml @@ -20,7 +20,7 @@ jobs: release-branch: ${{ steps.variables.outputs.release-branch }} steps: - name: Checkout Repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # tag=v4.1.5 - name: Setup Environment id: variables @@ -51,7 +51,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # tag=v4.1.5 - name: Set up JDK uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v4.2.1 @@ -118,7 +118,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # tag=v4.1.5 with: ref: ${{ needs.prepare-release.outputs.release-branch }} diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index b5a4281479..74b0bd81cb 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # tag=v4.1.5 - name: Set up JDK uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v4.2.1 diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index fbc882240b..aaffc55eff 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # tag=v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # tag=v4.1.5 - name: Dependency Review uses: actions/dependency-review-action@0c155c5e8556a497adf53f2c18edabf945ed8e70 # tag=v4.3.2 From f5aa8f1f1848b3bd763a28b83da0fee4c0bcb685 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 08:36:52 +0000 Subject: [PATCH 148/412] Bump github/codeql-action from 3.25.3 to 3.25.4 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.25.3 to 3.25.4. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/d39d31e687223d841ef683f52467bd88e9b21c14...ccf74c947955fd1cf117aef6a0e4e66191ef6f61) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 534556becd..424b9bbc71 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -145,6 +145,6 @@ jobs: - name: Upload Trivy Scan Results to GitHub Security Tab if: ${{ inputs.publish-container }} - uses: github/codeql-action/upload-sarif@d39d31e687223d841ef683f52467bd88e9b21c14 # tag=v3.25.3 + uses: github/codeql-action/upload-sarif@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 # tag=v3.25.4 with: sarif_file: 'trivy-results.sarif' From a04bb154fc5d77098d9d2a4deb5ec384e86e2311 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 08:36:57 +0000 Subject: [PATCH 149/412] Bump aquasecurity/trivy-action from 0.19.0 to 0.20.0 Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.19.0 to 0.20.0. - [Release notes](https://github.com/aquasecurity/trivy-action/releases) - [Commits](https://github.com/aquasecurity/trivy-action/compare/d710430a6722f083d3b36b8339ff66b32f22ee55...b2933f565dbc598b29947660e66259e3c7bc8561) --- updated-dependencies: - dependency-name: aquasecurity/trivy-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 534556becd..3247bf6aa6 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -135,7 +135,7 @@ jobs: - name: Run Trivy Vulnerability Scanner if: ${{ inputs.publish-container }} - uses: aquasecurity/trivy-action@d710430a6722f083d3b36b8339ff66b32f22ee55 # tag=0.19.0 + uses: aquasecurity/trivy-action@b2933f565dbc598b29947660e66259e3c7bc8561 # tag=0.20.0 with: image-ref: docker.io/dependencytrack/${{ matrix.distribution }}:${{ inputs.app-version }} format: 'sarif' From f50463f7ff583a8c6fdb397df81e51acfd5eff46 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 13 May 2024 11:21:08 +0200 Subject: [PATCH 150/412] Fix failing JSON BOM validation when `specVersion` is not one of the first fields Problem was that the search for `specVersion` was aborted upon encountering a `}` token. It should be `EOF` (or `null` in case of `JsonParser#nextToken`) instead. Fixes #3696 Signed-off-by: nscuro --- .../parser/cyclonedx/CycloneDxValidator.java | 2 +- .../parser/cyclonedx/CycloneDxValidatorTest.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidator.java b/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidator.java index 84afedc263..1ed7baf283 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidator.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidator.java @@ -126,7 +126,7 @@ private CycloneDxSchema.Version detectSchemaVersionFromJson(final byte[] bomByte } CycloneDxSchema.Version schemaVersion = null; - while (jsonParser.nextToken() != JsonToken.END_OBJECT) { + while (jsonParser.nextToken() != null) { final String fieldName = jsonParser.getCurrentName(); if ("specVersion".equals(fieldName)) { if (jsonParser.nextToken() == JsonToken.VALUE_STRING) { diff --git a/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidatorTest.java b/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidatorTest.java index 6ec7f2b48a..4c04359b4f 100644 --- a/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidatorTest.java +++ b/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidatorTest.java @@ -22,6 +22,7 @@ import org.junit.Test; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; public class CycloneDxValidatorTest { @@ -162,4 +163,17 @@ public void testValidateXmlWithInvalidComponentType() { valid with respect to its type, 'classification'."""); } + @Test // https://github.com/DependencyTrack/dependency-track/issues/3696 + public void testValidateJsonWithSpecVersionAtTheBottom() { + assertThatNoException() + .isThrownBy(() -> validator.validate(""" + { + "metadata": {}, + "components": [], + "bomFormat": "CycloneDX", + "specVersion": "1.5" + } + """.getBytes())); + } + } \ No newline at end of file From f15524e32c7494ba8cd54a02370a9612e5320063 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 13 May 2024 22:56:11 +0200 Subject: [PATCH 151/412] Fix broken global vuln audit view for MSSQL MSSQL does not support `DISTINCT` for columns of type `TEXT`, which `DESCRIPTION` and `RECOMMENDATION` are. Because the database schema is controlled by DataNucleus, and DataNucleus doesn't allow us to customize column types for specific RDBMSes, changing the respective columns to `VARCHAR(MAX)` is not possible. `DISTINCT` was needed because finding rows are joined with the `PROJECT_ACCESS_TEAMS` table, to support portfolio ACLs. If a user is member of multiple teams, the query would yield a duplicate row for each permitted team the user is a member of. The need for `DISTINCT` is eliminated by converting the ACL check from a `LEFT JOIN` to an `EXISTS` subquery. Fixes #3692 Signed-off-by: nscuro --- .../org/dependencytrack/model/Finding.java | 5 +- .../dependencytrack/model/GroupedFinding.java | 2 - .../FindingsSearchQueryManager.java | 73 ++++++++----------- .../persistence/QueryManager.java | 36 +++++++++ 4 files changed, 68 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/Finding.java b/src/main/java/org/dependencytrack/model/Finding.java index 78910d3389..aa174b14fa 100644 --- a/src/main/java/org/dependencytrack/model/Finding.java +++ b/src/main/java/org/dependencytrack/model/Finding.java @@ -100,8 +100,7 @@ public class Finding implements Serializable { // language=SQL public static final String QUERY_ALL_FINDINGS = """ - SELECT DISTINCT - "COMPONENT"."UUID" + SELECT "COMPONENT"."UUID" , "COMPONENT"."NAME" , "COMPONENT"."GROUP" , "COMPONENT"."VERSION" @@ -147,8 +146,6 @@ public class Finding implements Serializable { AND "COMPONENT"."PROJECT_ID" = "ANALYSIS"."PROJECT_ID" INNER JOIN "PROJECT" ON "COMPONENT"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "PROJECT_ACCESS_TEAMS" - ON "PROJECT"."ID" = "PROJECT_ACCESS_TEAMS"."PROJECT_ID" """; private final UUID project; diff --git a/src/main/java/org/dependencytrack/model/GroupedFinding.java b/src/main/java/org/dependencytrack/model/GroupedFinding.java index c4a1fb4fa4..d25a821063 100644 --- a/src/main/java/org/dependencytrack/model/GroupedFinding.java +++ b/src/main/java/org/dependencytrack/model/GroupedFinding.java @@ -68,8 +68,6 @@ public class GroupedFinding implements Serializable { AND "COMPONENT"."PROJECT_ID" = "ANALYSIS"."PROJECT_ID" INNER JOIN "PROJECT" ON "COMPONENT"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "PROJECT_ACCESS_TEAMS" - ON "PROJECT"."ID" = "PROJECT_ACCESS_TEAMS"."PROJECT_ID" """; private final Map vulnerability = new LinkedHashMap<>(); diff --git a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java index 3732200cf6..06ae79c2df 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java @@ -18,9 +18,6 @@ */ package org.dependencytrack.persistence; -import alpine.model.ApiKey; -import alpine.model.Team; -import alpine.model.UserPrincipal; import alpine.persistence.OrderDirection; import alpine.persistence.PaginatedResult; import alpine.resources.AlpineRequest; @@ -39,7 +36,6 @@ import javax.jdo.PersistenceManager; import javax.jdo.Query; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -347,44 +343,37 @@ private void processInputFilter(StringBuilder queryFilter, Map p } private void preprocessACLs(StringBuilder queryFilter, final Map params) { - if (super.principal != null && isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED)) { - final List teams; - if (super.principal instanceof UserPrincipal userPrincipal) { - teams = userPrincipal.getTeams(); - if (super.hasAccessManagementPermission(userPrincipal)) { - return; - } - } else if (super.principal instanceof final ApiKey apiKey) { - teams = apiKey.getTeams(); - if (super.hasAccessManagementPermission(apiKey)) { - return; - } - } else { - teams = Collections.emptyList(); - } - if (teams != null && !teams.isEmpty()) { - final StringBuilder sb = new StringBuilder(); - for (int i = 0, teamsSize = teams.size(); i < teamsSize; i++) { - final Team team = super.getObjectById(Team.class, teams.get(i).getId()); - sb.append(" \"PROJECT_ACCESS_TEAMS\".\"TEAM_ID\" = :team").append(i); - params.put("team" + i, team.getId()); - if (i < teamsSize - 1) { - sb.append(" OR "); - } - } - if (queryFilter != null && !queryFilter.isEmpty()) { - queryFilter.append(" AND (").append(sb).append(")"); - } else if (queryFilter != null) { - queryFilter.append("WHERE (").append(sb).append(")"); - } - } else { - params.put("false", false); - if (queryFilter != null && !queryFilter.isEmpty()) { - queryFilter.append(" AND :false"); - } else if (queryFilter != null) { - queryFilter.append("WHERE :false"); - } - } + if (!isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED) + || hasAccessManagementPermission(this.principal)) { + return; + } + + if (queryFilter.isEmpty()) { + queryFilter.append(" WHERE "); + } else { + queryFilter.append(" AND "); + } + + final var teamIds = new ArrayList<>(getTeamIds(principal)); + if (teamIds.isEmpty()) { + queryFilter.append(":false"); + params.put("false", false); + return; + } + + // NB: Need to work around the fact that the RDBMSes can't agree on how to do member checks. Oh joy! :))) + final var teamIdChecks = new ArrayList(); + for (int i = 0; i < teamIds.size(); i++) { + teamIdChecks.add("\"PROJECT_ACCESS_TEAMS\".\"TEAM_ID\" = :teamId" + i); + params.put("teamId" + i, teamIds.get(i)); } + + queryFilter.append(""" + EXISTS ( + SELECT 1 + FROM "PROJECT_ACCESS_TEAMS" + WHERE "PROJECT_ACCESS_TEAMS"."PROJECT_ID" = "PROJECT"."ID" + AND (%s) + )""".formatted(String.join(" OR ", teamIdChecks))); } } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index e8b2f7f813..5a8285585f 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -31,6 +31,7 @@ import alpine.resources.AlpineRequest; import com.github.packageurl.PackageURL; import com.google.common.collect.Lists; +import org.apache.commons.lang3.ClassUtils; import org.datanucleus.PropertyNames; import org.datanucleus.api.jdo.JDOQuery; import org.dependencytrack.event.IndexEvent; @@ -88,6 +89,7 @@ import java.security.Principal; import java.util.ArrayList; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -351,6 +353,30 @@ public QueryManager withL2CacheDisabled() { return this; } + /** + * Get the IDs of the {@link Team}s a given {@link Principal} is a member of. + * + * @return A {@link Set} of {@link Team} IDs + * @since 4.11.1 + */ + protected Set getTeamIds(final Principal principal) { + final var principalTeamIds = new HashSet(); + + if (principal instanceof final UserPrincipal userPrincipal + && userPrincipal.getTeams() != null) { + for (final Team userInTeam : userPrincipal.getTeams()) { + principalTeamIds.add(userInTeam.getId()); + } + } else if (principal instanceof final ApiKey apiKey + && apiKey.getTeams() != null) { + for (final Team userInTeam : apiKey.getTeams()) { + principalTeamIds.add(userInTeam.getId()); + } + } + + return principalTeamIds; + } + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //// BEGIN WRAPPER METHODS //// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1283,6 +1309,16 @@ public void commitSearchIndex(Class clazz) { commitSearchIndex(true, clazz); } + public boolean hasAccessManagementPermission(final Object principal) { + if (principal instanceof final UserPrincipal userPrincipal) { + return hasAccessManagementPermission(userPrincipal); + } else if (principal instanceof final ApiKey apiKey) { + return hasAccessManagementPermission(apiKey); + } + + throw new IllegalArgumentException("Provided principal is of invalid type " + ClassUtils.getName(principal)); + } + public boolean hasAccessManagementPermission(final UserPrincipal userPrincipal) { return getProjectQueryManager().hasAccessManagementPermission(userPrincipal); } From 1cc69c20feb0df9287b978ce3481ea89ee7609dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 08:08:33 +0000 Subject: [PATCH 152/412] Bump com.google.cloud.sql:cloud-sql-connector-jdbc-sqlserver Bumps com.google.cloud.sql:cloud-sql-connector-jdbc-sqlserver from 1.18.0 to 1.18.1. --- updated-dependencies: - dependency-name: com.google.cloud.sql:cloud-sql-connector-jdbc-sqlserver dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e3be3c5931..93e6e9ff1b 100644 --- a/pom.xml +++ b/pom.xml @@ -90,7 +90,7 @@ 4.2.1 0.1.2 10.16.0 - 1.18.0 + 1.18.1 1.18.0 1.18.0 2.1.0 From d0c5194e9dcd5483fbbcd78ff05fce7f9b2fffe3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 08:49:13 +0000 Subject: [PATCH 153/412] Bump debian from `ff39497` to `2b2e35d` in /src/main/docker Bumps debian from `ff39497` to `2b2e35d`. --- updated-dependencies: - dependency-name: debian dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- src/main/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docker/Dockerfile b/src/main/docker/Dockerfile index b48899717b..1e9c544cc1 100644 --- a/src/main/docker/Dockerfile +++ b/src/main/docker/Dockerfile @@ -1,6 +1,6 @@ FROM eclipse-temurin:21.0.3_9-jre-jammy@sha256:a56ee1f79cf57b2b31152cd471a4c85b6deb3057e4a1fbe8e50b57e7d2a1d7c9 AS jre-build -FROM debian:stable-slim@sha256:ff394977014e94e9a7c67bb22f5014ea069d156b86e001174f4bae6f4618297a +FROM debian:stable-slim@sha256:2b2e35d67c8fda0ba853d40cd18e57b99ab12d82fd3200607015eb09784068bd # Arguments that can be passed at build time # Directory names must end with / to avoid errors when ADDing and COPYing From c97bb5433c11c0dd0325e18912656d17e0a78e51 Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 15 May 2024 17:55:35 +0200 Subject: [PATCH 154/412] Support ingestion of CycloneDX v1.6 BOMs * Updates `cyclonedx-core-java` to version `9.0.0` * Bumps Jackson to version `2.17.1` to resolve compatibility issues with `cyclonedx-core-java` * Resolve various compilation errors due to refactoring in `cyclonedx-core-java` * Add validator tests for all CycloneDX versions Note that BOM exports will continue to use v1.5 for the time being. This avoids breaking users' workflows in case their tooling doesn't yet support v1.6. Closes #3584 Signed-off-by: nscuro --- pom.xml | 4 +- .../parser/cyclonedx/CycloneDXExporter.java | 10 +- .../parser/cyclonedx/CycloneDxValidator.java | 32 +-- .../parser/cyclonedx/util/ModelConverter.java | 18 +- .../tasks/BomUploadProcessingTask.java | 2 +- .../tasks/BomUploadProcessingTaskV2.java | 2 +- .../tasks/VexUploadProcessingTask.java | 2 +- .../cyclonedx/CycloneDXVexImporterTest.java | 2 +- .../cyclonedx/CycloneDxValidatorTest.java | 42 ++++ .../resources/v1/BomResourceTest.java | 2 +- .../resources/v1/VexResourceTest.java | 2 +- .../unit/cyclonedx/valid-bom-1.0.xml | 69 ++++++ .../unit/cyclonedx/valid-bom-1.1.xml | 118 ++++++++++ .../unit/cyclonedx/valid-bom-1.2.json | 177 +++++++++++++++ .../unit/cyclonedx/valid-bom-1.2.xml | 181 ++++++++++++++++ .../unit/cyclonedx/valid-bom-1.3.json | 177 +++++++++++++++ .../unit/cyclonedx/valid-bom-1.3.xml | 181 ++++++++++++++++ .../unit/cyclonedx/valid-bom-1.4.json | 177 +++++++++++++++ .../unit/cyclonedx/valid-bom-1.4.xml | 181 ++++++++++++++++ .../unit/cyclonedx/valid-bom-1.5.json | 177 +++++++++++++++ .../unit/cyclonedx/valid-bom-1.5.xml | 181 ++++++++++++++++ .../unit/cyclonedx/valid-bom-1.6.json | 201 ++++++++++++++++++ .../unit/cyclonedx/valid-bom-1.6.xml | 198 +++++++++++++++++ 23 files changed, 2104 insertions(+), 32 deletions(-) create mode 100644 src/test/resources/unit/cyclonedx/valid-bom-1.0.xml create mode 100644 src/test/resources/unit/cyclonedx/valid-bom-1.1.xml create mode 100644 src/test/resources/unit/cyclonedx/valid-bom-1.2.json create mode 100644 src/test/resources/unit/cyclonedx/valid-bom-1.2.xml create mode 100644 src/test/resources/unit/cyclonedx/valid-bom-1.3.json create mode 100644 src/test/resources/unit/cyclonedx/valid-bom-1.3.xml create mode 100644 src/test/resources/unit/cyclonedx/valid-bom-1.4.json create mode 100644 src/test/resources/unit/cyclonedx/valid-bom-1.4.xml create mode 100644 src/test/resources/unit/cyclonedx/valid-bom-1.5.json create mode 100644 src/test/resources/unit/cyclonedx/valid-bom-1.5.xml create mode 100644 src/test/resources/unit/cyclonedx/valid-bom-1.6.json create mode 100644 src/test/resources/unit/cyclonedx/valid-bom-1.6.xml diff --git a/pom.xml b/pom.xml index e3be3c5931..59d5f8878f 100644 --- a/pom.xml +++ b/pom.xml @@ -97,8 +97,10 @@ 1.26.1 1.4.2 1.0.1 - 8.0.3 + 9.0.0 1.6.15 + 2.17.1 + 2.17.1 2.3.9 20240303 3.2.7 diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXExporter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXExporter.java index 1ef5903fe7..05b88a68ca 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXExporter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXExporter.java @@ -18,9 +18,9 @@ */ package org.dependencytrack.parser.cyclonedx; -import org.cyclonedx.BomGeneratorFactory; -import org.cyclonedx.CycloneDxSchema; +import org.cyclonedx.Version; import org.cyclonedx.exception.GeneratorException; +import org.cyclonedx.generators.BomGeneratorFactory; import org.cyclonedx.model.Bom; import org.dependencytrack.model.Component; import org.dependencytrack.model.Finding; @@ -95,10 +95,12 @@ private Bom create(List components, final List serv } public String export(final Bom bom, final Format format) throws GeneratorException { + // TODO: The output version should be user-controllable. + if (Format.JSON == format) { - return BomGeneratorFactory.createJson(CycloneDxSchema.VERSION_LATEST, bom).toJsonString(); + return BomGeneratorFactory.createJson(Version.VERSION_15, bom).toJsonString(); } else { - return BomGeneratorFactory.createXml(CycloneDxSchema.VERSION_LATEST, bom).toXmlString(); + return BomGeneratorFactory.createXml(Version.VERSION_15, bom).toXmlString(); } } diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidator.java b/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidator.java index 1ed7baf283..c98e24ce4f 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidator.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidator.java @@ -23,7 +23,7 @@ import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.json.JsonMapper; import org.codehaus.stax2.XMLInputFactory2; -import org.cyclonedx.CycloneDxSchema; +import org.cyclonedx.Version; import org.cyclonedx.exception.ParseException; import org.cyclonedx.parsers.JsonParser; import org.cyclonedx.parsers.Parser; @@ -45,12 +45,14 @@ import static org.cyclonedx.CycloneDxSchema.NS_BOM_13; import static org.cyclonedx.CycloneDxSchema.NS_BOM_14; import static org.cyclonedx.CycloneDxSchema.NS_BOM_15; -import static org.cyclonedx.CycloneDxSchema.Version.VERSION_10; -import static org.cyclonedx.CycloneDxSchema.Version.VERSION_11; -import static org.cyclonedx.CycloneDxSchema.Version.VERSION_12; -import static org.cyclonedx.CycloneDxSchema.Version.VERSION_13; -import static org.cyclonedx.CycloneDxSchema.Version.VERSION_14; -import static org.cyclonedx.CycloneDxSchema.Version.VERSION_15; +import static org.cyclonedx.CycloneDxSchema.NS_BOM_16; +import static org.cyclonedx.Version.VERSION_10; +import static org.cyclonedx.Version.VERSION_11; +import static org.cyclonedx.Version.VERSION_12; +import static org.cyclonedx.Version.VERSION_13; +import static org.cyclonedx.Version.VERSION_14; +import static org.cyclonedx.Version.VERSION_15; +import static org.cyclonedx.Version.VERSION_16; /** * @since 4.11.0 @@ -93,7 +95,7 @@ public void validate(final byte[] bomBytes) { private FormatAndVersion detectFormatAndSchemaVersion(final byte[] bomBytes) { try { - final CycloneDxSchema.Version version = detectSchemaVersionFromJson(bomBytes); + final Version version = detectSchemaVersionFromJson(bomBytes); return new FormatAndVersion(Format.JSON, version); } catch (JsonParseException e) { if (LOGGER.isDebugEnabled()) { @@ -104,7 +106,7 @@ private FormatAndVersion detectFormatAndSchemaVersion(final byte[] bomBytes) { } try { - final CycloneDxSchema.Version version = detectSchemaVersionFromXml(bomBytes); + final Version version = detectSchemaVersionFromXml(bomBytes); return new FormatAndVersion(Format.XML, version); } catch (XMLStreamException e) { if (LOGGER.isDebugEnabled()) { @@ -115,7 +117,7 @@ private FormatAndVersion detectFormatAndSchemaVersion(final byte[] bomBytes) { throw new InvalidBomException("BOM is neither valid JSON nor XML"); } - private CycloneDxSchema.Version detectSchemaVersionFromJson(final byte[] bomBytes) throws IOException { + private Version detectSchemaVersionFromJson(final byte[] bomBytes) throws IOException { try (final com.fasterxml.jackson.core.JsonParser jsonParser = jsonMapper.createParser(bomBytes)) { JsonToken currentToken = jsonParser.nextToken(); if (currentToken != JsonToken.START_OBJECT) { @@ -125,7 +127,7 @@ private CycloneDxSchema.Version detectSchemaVersionFromJson(final byte[] bomByte .formatted(JsonToken.START_OBJECT.asString(), currentTokenAsString)); } - CycloneDxSchema.Version schemaVersion = null; + Version schemaVersion = null; while (jsonParser.nextToken() != null) { final String fieldName = jsonParser.getCurrentName(); if ("specVersion".equals(fieldName)) { @@ -138,6 +140,7 @@ private CycloneDxSchema.Version detectSchemaVersionFromJson(final byte[] bomByte case "1.3" -> VERSION_13; case "1.4" -> VERSION_14; case "1.5" -> VERSION_15; + case "1.6" -> VERSION_16; default -> throw new InvalidBomException("Unrecognized specVersion %s".formatted(specVersion)); }; @@ -153,12 +156,12 @@ private CycloneDxSchema.Version detectSchemaVersionFromJson(final byte[] bomByte } } - private CycloneDxSchema.Version detectSchemaVersionFromXml(final byte[] bomBytes) throws XMLStreamException { + private Version detectSchemaVersionFromXml(final byte[] bomBytes) throws XMLStreamException { final XMLInputFactory xmlInputFactory = XMLInputFactory2.newFactory(); final var bomBytesStream = new ByteArrayInputStream(bomBytes); final XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(bomBytesStream); - CycloneDxSchema.Version schemaVersion = null; + Version schemaVersion = null; while (xmlStreamReader.hasNext()) { if (xmlStreamReader.next() == XMLEvent.START_ELEMENT) { if (!"bom".equalsIgnoreCase(xmlStreamReader.getLocalName())) { @@ -177,6 +180,7 @@ private CycloneDxSchema.Version detectSchemaVersionFromXml(final byte[] bomBytes case NS_BOM_13 -> VERSION_13; case NS_BOM_14 -> VERSION_14; case NS_BOM_15 -> VERSION_15; + case NS_BOM_16 -> VERSION_16; default -> null; }; } @@ -202,7 +206,7 @@ private enum Format { XML } - private record FormatAndVersion(Format format, CycloneDxSchema.Version version) { + private record FormatAndVersion(Format format, Version version) { } } diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java index 003d85a18f..388ee1fe13 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -33,6 +33,7 @@ import org.cyclonedx.model.Hash; import org.cyclonedx.model.LicenseChoice; import org.cyclonedx.model.Swid; +import org.cyclonedx.model.license.Expression; import org.dependencytrack.model.Analysis; import org.dependencytrack.model.AnalysisJustification; import org.dependencytrack.model.AnalysisResponse; @@ -223,12 +224,13 @@ public static Component convertComponent(final org.cyclonedx.model.Component cdx .forEach(licenseCandidates::add); } - if (isNotBlank(cdxComponent.getLicenseChoice().getExpression())) { + final Expression licenseExpression = cdxComponent.getLicenseChoice().getExpression(); + if (licenseExpression != null && isNotBlank(licenseExpression.getValue())) { // If the expression consists of just one license ID, add it as another option. final var expressionParser = new SpdxExpressionParser(); - final SpdxExpression expression = expressionParser.parse(cdxComponent.getLicenseChoice().getExpression()); + final SpdxExpression expression = expressionParser.parse(licenseExpression.getValue()); if (!SpdxExpression.INVALID.equals(expression)) { - component.setLicenseExpression(trim(cdxComponent.getLicenseChoice().getExpression())); + component.setLicenseExpression(trim(licenseExpression.getValue())); if (expression.getSpdxLicenseId() != null) { final var expressionLicense = new org.cyclonedx.model.License(); @@ -529,13 +531,13 @@ public static Component convert(final QueryManager qm, final org.cyclonedx.model final LicenseChoice licenseChoice = cycloneDxComponent.getLicenseChoice(); if (licenseChoice != null) { final List licenseOptions = new ArrayList<>(); - if (licenseChoice.getExpression() != null) { + if (licenseChoice.getExpression() != null && isNotBlank(licenseChoice.getExpression().getValue())) { final var expressionParser = new SpdxExpressionParser(); - final SpdxExpression parsedExpression = expressionParser.parse(licenseChoice.getExpression()); + final SpdxExpression parsedExpression = expressionParser.parse(licenseChoice.getExpression().getValue()); if (!Objects.equals(parsedExpression, SpdxExpression.INVALID)) { // store license expression, but don't overwrite manual changes to the field if (component.getLicenseExpression() == null) { - component.setLicenseExpression(licenseChoice.getExpression()); + component.setLicenseExpression(licenseChoice.getExpression().getValue()); } // if the expression just consists of one license id, we can add it as another license option if (parsedExpression.getSpdxLicenseId() != null) { @@ -769,7 +771,9 @@ public static org.cyclonedx.model.Component convert(final QueryManager qm, final cycloneComponent.setLicenseChoice(licenseChoice); } if (component.getLicenseExpression() != null) { - licenseChoice.setExpression(component.getLicenseExpression()); + final var licenseExpression = new Expression(); + licenseExpression.setValue(component.getLicenseExpression()); + licenseChoice.setExpression(licenseExpression); cycloneComponent.setLicenseChoice(licenseChoice); } diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java index c8d72843da..e18f04176a 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java @@ -23,7 +23,7 @@ import alpine.event.framework.Subscriber; import alpine.notification.Notification; import alpine.notification.NotificationLevel; -import org.cyclonedx.BomParserFactory; +import org.cyclonedx.parsers.BomParserFactory; import org.cyclonedx.parsers.Parser; import org.dependencytrack.event.BomUploadEvent; import org.dependencytrack.event.NewVulnerableDependencyAnalysisEvent; diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java index b6b0619e04..bf699b0e71 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTaskV2.java @@ -26,8 +26,8 @@ import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.multimap.HashSetValuedHashMap; import org.apache.commons.lang3.exception.ExceptionUtils; -import org.cyclonedx.BomParserFactory; import org.cyclonedx.exception.ParseException; +import org.cyclonedx.parsers.BomParserFactory; import org.cyclonedx.parsers.Parser; import org.datanucleus.flush.FlushMode; import org.datanucleus.store.query.QueryNotUniqueException; diff --git a/src/main/java/org/dependencytrack/tasks/VexUploadProcessingTask.java b/src/main/java/org/dependencytrack/tasks/VexUploadProcessingTask.java index 99a2567c29..fd2b8afde2 100644 --- a/src/main/java/org/dependencytrack/tasks/VexUploadProcessingTask.java +++ b/src/main/java/org/dependencytrack/tasks/VexUploadProcessingTask.java @@ -23,7 +23,7 @@ import alpine.event.framework.Subscriber; import alpine.notification.Notification; import alpine.notification.NotificationLevel; -import org.cyclonedx.BomParserFactory; +import org.cyclonedx.parsers.BomParserFactory; import org.cyclonedx.parsers.Parser; import org.dependencytrack.event.VexUploadEvent; import org.dependencytrack.model.ConfigPropertyConstants; diff --git a/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporterTest.java b/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporterTest.java index 82a956fdeb..e8cb67002d 100644 --- a/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporterTest.java +++ b/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporterTest.java @@ -1,8 +1,8 @@ package org.dependencytrack.parser.cyclonedx; import org.assertj.core.api.Assertions; -import org.cyclonedx.BomParserFactory; import org.cyclonedx.exception.ParseException; +import org.cyclonedx.parsers.BomParserFactory; import org.dependencytrack.PersistenceCapableTest; import org.dependencytrack.model.Analysis; import org.dependencytrack.model.AnalysisJustification; diff --git a/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidatorTest.java b/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidatorTest.java index 4c04359b4f..6312107a82 100644 --- a/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidatorTest.java +++ b/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidatorTest.java @@ -18,12 +18,26 @@ */ package org.dependencytrack.parser.cyclonedx; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; + +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNoException; +@RunWith(JUnitParamsRunner.class) public class CycloneDxValidatorTest { private CycloneDxValidator validator; @@ -176,4 +190,32 @@ public void testValidateJsonWithSpecVersionAtTheBottom() { """.getBytes())); } + @SuppressWarnings("unused") + private Object[] testValidateWithValidBomParameters() throws Exception { + final PathMatcher pathMatcherJson = FileSystems.getDefault().getPathMatcher("glob:**/valid-bom-*.json"); + final PathMatcher pathMatcherXml = FileSystems.getDefault().getPathMatcher("glob:**/valid-bom-*.xml"); + final var bomFilePaths = new ArrayList(); + + Files.walkFileTree(Paths.get("./src/test/resources/unit/cyclonedx"), new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) { + if (pathMatcherJson.matches(file) || pathMatcherXml.matches(file)) { + bomFilePaths.add(file); + } + + return FileVisitResult.CONTINUE; + } + }); + + return bomFilePaths.stream().sorted().toArray(); + } + + @Test + @Parameters(method = "testValidateWithValidBomParameters") + public void testValidateWithValidBom(final Path bomFilePath) throws Exception { + final byte[] bomBytes = Files.readAllBytes(bomFilePath); + + assertThatNoException().isThrownBy(() -> validator.validate(bomBytes)); + } + } \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index 1258943ad4..829489f127 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -999,7 +999,7 @@ public void uploadBomTooLargeViaPutTest() { { "status": 400, "title": "The provided JSON payload could not be mapped", - "detail": "The BOM is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/bom\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String length (20000001) exceeds the maximum length (20000000) (through reference chain: org.dependencytrack.resources.v1.vo.BomSubmitRequest[\\"bom\\"])" + "detail": "The BOM is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/bom\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String value length (20000001) exceeds the maximum allowed (20000000, from `StreamReadConstraints.getMaxStringLength()`) (through reference chain: org.dependencytrack.resources.v1.vo.BomSubmitRequest[\\"bom\\"])" } """); } diff --git a/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java index 6b82ee739e..fdb389de34 100644 --- a/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java @@ -345,7 +345,7 @@ public void uploadVexTooLargeViaPutTest() { { "status": 400, "title": "The provided JSON payload could not be mapped", - "detail": "The VEX is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/vex\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String length (20000001) exceeds the maximum length (20000000) (through reference chain: org.dependencytrack.resources.v1.vo.VexSubmitRequest[\\"vex\\"])" + "detail": "The VEX is too large to be transmitted safely via Base64 encoded JSON value. Please use the \\"POST /api/v1/vex\\" endpoint with Content-Type \\"multipart/form-data\\" instead. Original cause: String value length (20000001) exceeds the maximum allowed (20000000, from `StreamReadConstraints.getMaxStringLength()`) (through reference chain: org.dependencytrack.resources.v1.vo.VexSubmitRequest[\\"vex\\"])" } """); } diff --git a/src/test/resources/unit/cyclonedx/valid-bom-1.0.xml b/src/test/resources/unit/cyclonedx/valid-bom-1.0.xml new file mode 100644 index 0000000000..f0bfba2211 --- /dev/null +++ b/src/test/resources/unit/cyclonedx/valid-bom-1.0.xml @@ -0,0 +1,69 @@ + + + + + org.example + myapplication + 1.0.0 + An example application + + 2342c2eaf1feb9a80195dbaddf2ebaa3 + 68b78babe00a053f9e35ec6a2d9080f5b90122b0 + 708f1f53b41f11f02d12a11b1a38d2905d47b099afc71a0f1124ef8582ec7313 + 387b7ae16b9cae45f830671541539bf544202faae5aac544a93b7b0a04f5f846fa2f4e81ef3f1677e13aed7496408a441f5657ab6d54423e56bf6f38da124aef + + + + Apache-2.0 + + + Copyright Example Inc. All rights reserved. + cpe:/a:example:myapplication:1.0.0 + pkg:maven/com.example/myapplication@1.0.0?packaging=war + false + + + + + twitter + bootstrap + 3.3.7 + The most popular front-end framework for developing responsive, mobile first projects on the web. + + 3942447fac867ae5cdb3229b658f4d48 + e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a + f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b + e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282 + + + + MIT + + + pkg:npm/bootstrap@3.3.7 + false + + + com.example + myframework + 1.0.0 + Example Inc, enterprise framework + + cfcb0b64aacd2f81c1cd546543de965a + 7fbeef2346c45d565c3341f037bce4e088af8a52 + 0384db3cec55d86a6898c489fdb75a8e75fe66b26639634983d2f3c3558493d1 + 854909cdb9e3ca183056837144aab6d8069b377bd66445087cc7157bf0c3f620418705dd0b83bdc2f73a508c2bdb316ca1809d75ee6972d02023a3e7dd655c79 + + + + Apache-2.0 + + + pkg:maven/com.example/myframework@1.0.0?packaging=war + false + + + + + + diff --git a/src/test/resources/unit/cyclonedx/valid-bom-1.1.xml b/src/test/resources/unit/cyclonedx/valid-bom-1.1.xml new file mode 100644 index 0000000000..06fc5e870a --- /dev/null +++ b/src/test/resources/unit/cyclonedx/valid-bom-1.1.xml @@ -0,0 +1,118 @@ + + + + + Acme Inc + com.acme + tomcat-catalina + 9.0.14 + Modified version of Apache Catalina + required + + 3942447fac867ae5cdb3229b658f4d48 + e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a + f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b + e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282 + + + + Apache-2.0 + CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFwYWNoZSBMaWNlbnNlCiAgICAgICAgICAgICAgICAgICAgICAgICAgIFZlcnNpb24gMi4wLCBKYW51YXJ5IDIwMDQKICAgICAgICAgICAgICAgICAgICAgICAgaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzLwoKICAgVEVSTVMgQU5EIENPTkRJVElPTlMgRk9SIFVTRSwgUkVQUk9EVUNUSU9OLCBBTkQgRElTVFJJQlVUSU9OCgogICAxLiBEZWZpbml0aW9ucy4KCiAgICAgICJMaWNlbnNlIiBzaGFsbCBtZWFuIHRoZSB0ZXJtcyBhbmQgY29uZGl0aW9ucyBmb3IgdXNlLCByZXByb2R1Y3Rpb24sCiAgICAgIGFuZCBkaXN0cmlidXRpb24gYXMgZGVmaW5lZCBieSBTZWN0aW9ucyAxIHRocm91Z2ggOSBvZiB0aGlzIGRvY3VtZW50LgoKICAgICAgIkxpY2Vuc29yIiBzaGFsbCBtZWFuIHRoZSBjb3B5cmlnaHQgb3duZXIgb3IgZW50aXR5IGF1dGhvcml6ZWQgYnkKICAgICAgdGhlIGNvcHlyaWdodCBvd25lciB0aGF0IGlzIGdyYW50aW5nIHRoZSBMaWNlbnNlLgoKICAgICAgIkxlZ2FsIEVudGl0eSIgc2hhbGwgbWVhbiB0aGUgdW5pb24gb2YgdGhlIGFjdGluZyBlbnRpdHkgYW5kIGFsbAogICAgICBvdGhlciBlbnRpdGllcyB0aGF0IGNvbnRyb2wsIGFyZSBjb250cm9sbGVkIGJ5LCBvciBhcmUgdW5kZXIgY29tbW9uCiAgICAgIGNvbnRyb2wgd2l0aCB0aGF0IGVudGl0eS4gRm9yIHRoZSBwdXJwb3NlcyBvZiB0aGlzIGRlZmluaXRpb24sCiAgICAgICJjb250cm9sIiBtZWFucyAoaSkgdGhlIHBvd2VyLCBkaXJlY3Qgb3IgaW5kaXJlY3QsIHRvIGNhdXNlIHRoZQogICAgICBkaXJlY3Rpb24gb3IgbWFuYWdlbWVudCBvZiBzdWNoIGVudGl0eSwgd2hldGhlciBieSBjb250cmFjdCBvcgogICAgICBvdGhlcndpc2UsIG9yIChpaSkgb3duZXJzaGlwIG9mIGZpZnR5IHBlcmNlbnQgKDUwJSkgb3IgbW9yZSBvZiB0aGUKICAgICAgb3V0c3RhbmRpbmcgc2hhcmVzLCBvciAoaWlpKSBiZW5lZmljaWFsIG93bmVyc2hpcCBvZiBzdWNoIGVudGl0eS4KCiAgICAgICJZb3UiIChvciAiWW91ciIpIHNoYWxsIG1lYW4gYW4gaW5kaXZpZHVhbCBvciBMZWdhbCBFbnRpdHkKICAgICAgZXhlcmNpc2luZyBwZXJtaXNzaW9ucyBncmFudGVkIGJ5IHRoaXMgTGljZW5zZS4KCiAgICAgICJTb3VyY2UiIGZvcm0gc2hhbGwgbWVhbiB0aGUgcHJlZmVycmVkIGZvcm0gZm9yIG1ha2luZyBtb2RpZmljYXRpb25zLAogICAgICBpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIHNvZnR3YXJlIHNvdXJjZSBjb2RlLCBkb2N1bWVudGF0aW9uCiAgICAgIHNvdXJjZSwgYW5kIGNvbmZpZ3VyYXRpb24gZmlsZXMuCgogICAgICAiT2JqZWN0IiBmb3JtIHNoYWxsIG1lYW4gYW55IGZvcm0gcmVzdWx0aW5nIGZyb20gbWVjaGFuaWNhbAogICAgICB0cmFuc2Zvcm1hdGlvbiBvciB0cmFuc2xhdGlvbiBvZiBhIFNvdXJjZSBmb3JtLCBpbmNsdWRpbmcgYnV0CiAgICAgIG5vdCBsaW1pdGVkIHRvIGNvbXBpbGVkIG9iamVjdCBjb2RlLCBnZW5lcmF0ZWQgZG9jdW1lbnRhdGlvbiwKICAgICAgYW5kIGNvbnZlcnNpb25zIHRvIG90aGVyIG1lZGlhIHR5cGVzLgoKICAgICAgIldvcmsiIHNoYWxsIG1lYW4gdGhlIHdvcmsgb2YgYXV0aG9yc2hpcCwgd2hldGhlciBpbiBTb3VyY2Ugb3IKICAgICAgT2JqZWN0IGZvcm0sIG1hZGUgYXZhaWxhYmxlIHVuZGVyIHRoZSBMaWNlbnNlLCBhcyBpbmRpY2F0ZWQgYnkgYQogICAgICBjb3B5cmlnaHQgbm90aWNlIHRoYXQgaXMgaW5jbHVkZWQgaW4gb3IgYXR0YWNoZWQgdG8gdGhlIHdvcmsKICAgICAgKGFuIGV4YW1wbGUgaXMgcHJvdmlkZWQgaW4gdGhlIEFwcGVuZGl4IGJlbG93KS4KCiAgICAgICJEZXJpdmF0aXZlIFdvcmtzIiBzaGFsbCBtZWFuIGFueSB3b3JrLCB3aGV0aGVyIGluIFNvdXJjZSBvciBPYmplY3QKICAgICAgZm9ybSwgdGhhdCBpcyBiYXNlZCBvbiAob3IgZGVyaXZlZCBmcm9tKSB0aGUgV29yayBhbmQgZm9yIHdoaWNoIHRoZQogICAgICBlZGl0b3JpYWwgcmV2aXNpb25zLCBhbm5vdGF0aW9ucywgZWxhYm9yYXRpb25zLCBvciBvdGhlciBtb2RpZmljYXRpb25zCiAgICAgIHJlcHJlc2VudCwgYXMgYSB3aG9sZSwgYW4gb3JpZ2luYWwgd29yayBvZiBhdXRob3JzaGlwLiBGb3IgdGhlIHB1cnBvc2VzCiAgICAgIG9mIHRoaXMgTGljZW5zZSwgRGVyaXZhdGl2ZSBXb3JrcyBzaGFsbCBub3QgaW5jbHVkZSB3b3JrcyB0aGF0IHJlbWFpbgogICAgICBzZXBhcmFibGUgZnJvbSwgb3IgbWVyZWx5IGxpbmsgKG9yIGJpbmQgYnkgbmFtZSkgdG8gdGhlIGludGVyZmFjZXMgb2YsCiAgICAgIHRoZSBXb3JrIGFuZCBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YuCgogICAgICAiQ29udHJpYnV0aW9uIiBzaGFsbCBtZWFuIGFueSB3b3JrIG9mIGF1dGhvcnNoaXAsIGluY2x1ZGluZwogICAgICB0aGUgb3JpZ2luYWwgdmVyc2lvbiBvZiB0aGUgV29yayBhbmQgYW55IG1vZGlmaWNhdGlvbnMgb3IgYWRkaXRpb25zCiAgICAgIHRvIHRoYXQgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIHRoYXQgaXMgaW50ZW50aW9uYWxseQogICAgICBzdWJtaXR0ZWQgdG8gTGljZW5zb3IgZm9yIGluY2x1c2lvbiBpbiB0aGUgV29yayBieSB0aGUgY29weXJpZ2h0IG93bmVyCiAgICAgIG9yIGJ5IGFuIGluZGl2aWR1YWwgb3IgTGVnYWwgRW50aXR5IGF1dGhvcml6ZWQgdG8gc3VibWl0IG9uIGJlaGFsZiBvZgogICAgICB0aGUgY29weXJpZ2h0IG93bmVyLiBGb3IgdGhlIHB1cnBvc2VzIG9mIHRoaXMgZGVmaW5pdGlvbiwgInN1Ym1pdHRlZCIKICAgICAgbWVhbnMgYW55IGZvcm0gb2YgZWxlY3Ryb25pYywgdmVyYmFsLCBvciB3cml0dGVuIGNvbW11bmljYXRpb24gc2VudAogICAgICB0byB0aGUgTGljZW5zb3Igb3IgaXRzIHJlcHJlc2VudGF0aXZlcywgaW5jbHVkaW5nIGJ1dCBub3QgbGltaXRlZCB0bwogICAgICBjb21tdW5pY2F0aW9uIG9uIGVsZWN0cm9uaWMgbWFpbGluZyBsaXN0cywgc291cmNlIGNvZGUgY29udHJvbCBzeXN0ZW1zLAogICAgICBhbmQgaXNzdWUgdHJhY2tpbmcgc3lzdGVtcyB0aGF0IGFyZSBtYW5hZ2VkIGJ5LCBvciBvbiBiZWhhbGYgb2YsIHRoZQogICAgICBMaWNlbnNvciBmb3IgdGhlIHB1cnBvc2Ugb2YgZGlzY3Vzc2luZyBhbmQgaW1wcm92aW5nIHRoZSBXb3JrLCBidXQKICAgICAgZXhjbHVkaW5nIGNvbW11bmljYXRpb24gdGhhdCBpcyBjb25zcGljdW91c2x5IG1hcmtlZCBvciBvdGhlcndpc2UKICAgICAgZGVzaWduYXRlZCBpbiB3cml0aW5nIGJ5IHRoZSBjb3B5cmlnaHQgb3duZXIgYXMgIk5vdCBhIENvbnRyaWJ1dGlvbi4iCgogICAgICAiQ29udHJpYnV0b3IiIHNoYWxsIG1lYW4gTGljZW5zb3IgYW5kIGFueSBpbmRpdmlkdWFsIG9yIExlZ2FsIEVudGl0eQogICAgICBvbiBiZWhhbGYgb2Ygd2hvbSBhIENvbnRyaWJ1dGlvbiBoYXMgYmVlbiByZWNlaXZlZCBieSBMaWNlbnNvciBhbmQKICAgICAgc3Vic2VxdWVudGx5IGluY29ycG9yYXRlZCB3aXRoaW4gdGhlIFdvcmsuCgogICAyLiBHcmFudCBvZiBDb3B5cmlnaHQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICBjb3B5cmlnaHQgbGljZW5zZSB0byByZXByb2R1Y2UsIHByZXBhcmUgRGVyaXZhdGl2ZSBXb3JrcyBvZiwKICAgICAgcHVibGljbHkgZGlzcGxheSwgcHVibGljbHkgcGVyZm9ybSwgc3VibGljZW5zZSwgYW5kIGRpc3RyaWJ1dGUgdGhlCiAgICAgIFdvcmsgYW5kIHN1Y2ggRGVyaXZhdGl2ZSBXb3JrcyBpbiBTb3VyY2Ugb3IgT2JqZWN0IGZvcm0uCgogICAzLiBHcmFudCBvZiBQYXRlbnQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICAoZXhjZXB0IGFzIHN0YXRlZCBpbiB0aGlzIHNlY3Rpb24pIHBhdGVudCBsaWNlbnNlIHRvIG1ha2UsIGhhdmUgbWFkZSwKICAgICAgdXNlLCBvZmZlciB0byBzZWxsLCBzZWxsLCBpbXBvcnQsIGFuZCBvdGhlcndpc2UgdHJhbnNmZXIgdGhlIFdvcmssCiAgICAgIHdoZXJlIHN1Y2ggbGljZW5zZSBhcHBsaWVzIG9ubHkgdG8gdGhvc2UgcGF0ZW50IGNsYWltcyBsaWNlbnNhYmxlCiAgICAgIGJ5IHN1Y2ggQ29udHJpYnV0b3IgdGhhdCBhcmUgbmVjZXNzYXJpbHkgaW5mcmluZ2VkIGJ5IHRoZWlyCiAgICAgIENvbnRyaWJ1dGlvbihzKSBhbG9uZSBvciBieSBjb21iaW5hdGlvbiBvZiB0aGVpciBDb250cmlidXRpb24ocykKICAgICAgd2l0aCB0aGUgV29yayB0byB3aGljaCBzdWNoIENvbnRyaWJ1dGlvbihzKSB3YXMgc3VibWl0dGVkLiBJZiBZb3UKICAgICAgaW5zdGl0dXRlIHBhdGVudCBsaXRpZ2F0aW9uIGFnYWluc3QgYW55IGVudGl0eSAoaW5jbHVkaW5nIGEKICAgICAgY3Jvc3MtY2xhaW0gb3IgY291bnRlcmNsYWltIGluIGEgbGF3c3VpdCkgYWxsZWdpbmcgdGhhdCB0aGUgV29yawogICAgICBvciBhIENvbnRyaWJ1dGlvbiBpbmNvcnBvcmF0ZWQgd2l0aGluIHRoZSBXb3JrIGNvbnN0aXR1dGVzIGRpcmVjdAogICAgICBvciBjb250cmlidXRvcnkgcGF0ZW50IGluZnJpbmdlbWVudCwgdGhlbiBhbnkgcGF0ZW50IGxpY2Vuc2VzCiAgICAgIGdyYW50ZWQgdG8gWW91IHVuZGVyIHRoaXMgTGljZW5zZSBmb3IgdGhhdCBXb3JrIHNoYWxsIHRlcm1pbmF0ZQogICAgICBhcyBvZiB0aGUgZGF0ZSBzdWNoIGxpdGlnYXRpb24gaXMgZmlsZWQuCgogICA0LiBSZWRpc3RyaWJ1dGlvbi4gWW91IG1heSByZXByb2R1Y2UgYW5kIGRpc3RyaWJ1dGUgY29waWVzIG9mIHRoZQogICAgICBXb3JrIG9yIERlcml2YXRpdmUgV29ya3MgdGhlcmVvZiBpbiBhbnkgbWVkaXVtLCB3aXRoIG9yIHdpdGhvdXQKICAgICAgbW9kaWZpY2F0aW9ucywgYW5kIGluIFNvdXJjZSBvciBPYmplY3QgZm9ybSwgcHJvdmlkZWQgdGhhdCBZb3UKICAgICAgbWVldCB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgogICAgICAoYSkgWW91IG11c3QgZ2l2ZSBhbnkgb3RoZXIgcmVjaXBpZW50cyBvZiB0aGUgV29yayBvcgogICAgICAgICAgRGVyaXZhdGl2ZSBXb3JrcyBhIGNvcHkgb2YgdGhpcyBMaWNlbnNlOyBhbmQKCiAgICAgIChiKSBZb3UgbXVzdCBjYXVzZSBhbnkgbW9kaWZpZWQgZmlsZXMgdG8gY2FycnkgcHJvbWluZW50IG5vdGljZXMKICAgICAgICAgIHN0YXRpbmcgdGhhdCBZb3UgY2hhbmdlZCB0aGUgZmlsZXM7IGFuZAoKICAgICAgKGMpIFlvdSBtdXN0IHJldGFpbiwgaW4gdGhlIFNvdXJjZSBmb3JtIG9mIGFueSBEZXJpdmF0aXZlIFdvcmtzCiAgICAgICAgICB0aGF0IFlvdSBkaXN0cmlidXRlLCBhbGwgY29weXJpZ2h0LCBwYXRlbnQsIHRyYWRlbWFyaywgYW5kCiAgICAgICAgICBhdHRyaWJ1dGlvbiBub3RpY2VzIGZyb20gdGhlIFNvdXJjZSBmb3JtIG9mIHRoZSBXb3JrLAogICAgICAgICAgZXhjbHVkaW5nIHRob3NlIG5vdGljZXMgdGhhdCBkbyBub3QgcGVydGFpbiB0byBhbnkgcGFydCBvZgogICAgICAgICAgdGhlIERlcml2YXRpdmUgV29ya3M7IGFuZAoKICAgICAgKGQpIElmIHRoZSBXb3JrIGluY2x1ZGVzIGEgIk5PVElDRSIgdGV4dCBmaWxlIGFzIHBhcnQgb2YgaXRzCiAgICAgICAgICBkaXN0cmlidXRpb24sIHRoZW4gYW55IERlcml2YXRpdmUgV29ya3MgdGhhdCBZb3UgZGlzdHJpYnV0ZSBtdXN0CiAgICAgICAgICBpbmNsdWRlIGEgcmVhZGFibGUgY29weSBvZiB0aGUgYXR0cmlidXRpb24gbm90aWNlcyBjb250YWluZWQKICAgICAgICAgIHdpdGhpbiBzdWNoIE5PVElDRSBmaWxlLCBleGNsdWRpbmcgdGhvc2Ugbm90aWNlcyB0aGF0IGRvIG5vdAogICAgICAgICAgcGVydGFpbiB0byBhbnkgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaW4gYXQgbGVhc3Qgb25lCiAgICAgICAgICBvZiB0aGUgZm9sbG93aW5nIHBsYWNlczogd2l0aGluIGEgTk9USUNFIHRleHQgZmlsZSBkaXN0cmlidXRlZAogICAgICAgICAgYXMgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgd2l0aGluIHRoZSBTb3VyY2UgZm9ybSBvcgogICAgICAgICAgZG9jdW1lbnRhdGlvbiwgaWYgcHJvdmlkZWQgYWxvbmcgd2l0aCB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgb3IsCiAgICAgICAgICB3aXRoaW4gYSBkaXNwbGF5IGdlbmVyYXRlZCBieSB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaWYgYW5kCiAgICAgICAgICB3aGVyZXZlciBzdWNoIHRoaXJkLXBhcnR5IG5vdGljZXMgbm9ybWFsbHkgYXBwZWFyLiBUaGUgY29udGVudHMKICAgICAgICAgIG9mIHRoZSBOT1RJQ0UgZmlsZSBhcmUgZm9yIGluZm9ybWF0aW9uYWwgcHVycG9zZXMgb25seSBhbmQKICAgICAgICAgIGRvIG5vdCBtb2RpZnkgdGhlIExpY2Vuc2UuIFlvdSBtYXkgYWRkIFlvdXIgb3duIGF0dHJpYnV0aW9uCiAgICAgICAgICBub3RpY2VzIHdpdGhpbiBEZXJpdmF0aXZlIFdvcmtzIHRoYXQgWW91IGRpc3RyaWJ1dGUsIGFsb25nc2lkZQogICAgICAgICAgb3IgYXMgYW4gYWRkZW5kdW0gdG8gdGhlIE5PVElDRSB0ZXh0IGZyb20gdGhlIFdvcmssIHByb3ZpZGVkCiAgICAgICAgICB0aGF0IHN1Y2ggYWRkaXRpb25hbCBhdHRyaWJ1dGlvbiBub3RpY2VzIGNhbm5vdCBiZSBjb25zdHJ1ZWQKICAgICAgICAgIGFzIG1vZGlmeWluZyB0aGUgTGljZW5zZS4KCiAgICAgIFlvdSBtYXkgYWRkIFlvdXIgb3duIGNvcHlyaWdodCBzdGF0ZW1lbnQgdG8gWW91ciBtb2RpZmljYXRpb25zIGFuZAogICAgICBtYXkgcHJvdmlkZSBhZGRpdGlvbmFsIG9yIGRpZmZlcmVudCBsaWNlbnNlIHRlcm1zIGFuZCBjb25kaXRpb25zCiAgICAgIGZvciB1c2UsIHJlcHJvZHVjdGlvbiwgb3IgZGlzdHJpYnV0aW9uIG9mIFlvdXIgbW9kaWZpY2F0aW9ucywgb3IKICAgICAgZm9yIGFueSBzdWNoIERlcml2YXRpdmUgV29ya3MgYXMgYSB3aG9sZSwgcHJvdmlkZWQgWW91ciB1c2UsCiAgICAgIHJlcHJvZHVjdGlvbiwgYW5kIGRpc3RyaWJ1dGlvbiBvZiB0aGUgV29yayBvdGhlcndpc2UgY29tcGxpZXMgd2l0aAogICAgICB0aGUgY29uZGl0aW9ucyBzdGF0ZWQgaW4gdGhpcyBMaWNlbnNlLgoKICAgNS4gU3VibWlzc2lvbiBvZiBDb250cmlidXRpb25zLiBVbmxlc3MgWW91IGV4cGxpY2l0bHkgc3RhdGUgb3RoZXJ3aXNlLAogICAgICBhbnkgQ29udHJpYnV0aW9uIGludGVudGlvbmFsbHkgc3VibWl0dGVkIGZvciBpbmNsdXNpb24gaW4gdGhlIFdvcmsKICAgICAgYnkgWW91IHRvIHRoZSBMaWNlbnNvciBzaGFsbCBiZSB1bmRlciB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCB3aXRob3V0IGFueSBhZGRpdGlvbmFsIHRlcm1zIG9yIGNvbmRpdGlvbnMuCiAgICAgIE5vdHdpdGhzdGFuZGluZyB0aGUgYWJvdmUsIG5vdGhpbmcgaGVyZWluIHNoYWxsIHN1cGVyc2VkZSBvciBtb2RpZnkKICAgICAgdGhlIHRlcm1zIG9mIGFueSBzZXBhcmF0ZSBsaWNlbnNlIGFncmVlbWVudCB5b3UgbWF5IGhhdmUgZXhlY3V0ZWQKICAgICAgd2l0aCBMaWNlbnNvciByZWdhcmRpbmcgc3VjaCBDb250cmlidXRpb25zLgoKICAgNi4gVHJhZGVtYXJrcy4gVGhpcyBMaWNlbnNlIGRvZXMgbm90IGdyYW50IHBlcm1pc3Npb24gdG8gdXNlIHRoZSB0cmFkZQogICAgICBuYW1lcywgdHJhZGVtYXJrcywgc2VydmljZSBtYXJrcywgb3IgcHJvZHVjdCBuYW1lcyBvZiB0aGUgTGljZW5zb3IsCiAgICAgIGV4Y2VwdCBhcyByZXF1aXJlZCBmb3IgcmVhc29uYWJsZSBhbmQgY3VzdG9tYXJ5IHVzZSBpbiBkZXNjcmliaW5nIHRoZQogICAgICBvcmlnaW4gb2YgdGhlIFdvcmsgYW5kIHJlcHJvZHVjaW5nIHRoZSBjb250ZW50IG9mIHRoZSBOT1RJQ0UgZmlsZS4KCiAgIDcuIERpc2NsYWltZXIgb2YgV2FycmFudHkuIFVubGVzcyByZXF1aXJlZCBieSBhcHBsaWNhYmxlIGxhdyBvcgogICAgICBhZ3JlZWQgdG8gaW4gd3JpdGluZywgTGljZW5zb3IgcHJvdmlkZXMgdGhlIFdvcmsgKGFuZCBlYWNoCiAgICAgIENvbnRyaWJ1dG9yIHByb3ZpZGVzIGl0cyBDb250cmlidXRpb25zKSBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IKICAgICAgaW1wbGllZCwgaW5jbHVkaW5nLCB3aXRob3V0IGxpbWl0YXRpb24sIGFueSB3YXJyYW50aWVzIG9yIGNvbmRpdGlvbnMKICAgICAgb2YgVElUTEUsIE5PTi1JTkZSSU5HRU1FTlQsIE1FUkNIQU5UQUJJTElUWSwgb3IgRklUTkVTUyBGT1IgQQogICAgICBQQVJUSUNVTEFSIFBVUlBPU0UuIFlvdSBhcmUgc29sZWx5IHJlc3BvbnNpYmxlIGZvciBkZXRlcm1pbmluZyB0aGUKICAgICAgYXBwcm9wcmlhdGVuZXNzIG9mIHVzaW5nIG9yIHJlZGlzdHJpYnV0aW5nIHRoZSBXb3JrIGFuZCBhc3N1bWUgYW55CiAgICAgIHJpc2tzIGFzc29jaWF0ZWQgd2l0aCBZb3VyIGV4ZXJjaXNlIG9mIHBlcm1pc3Npb25zIHVuZGVyIHRoaXMgTGljZW5zZS4KCiAgIDguIExpbWl0YXRpb24gb2YgTGlhYmlsaXR5LiBJbiBubyBldmVudCBhbmQgdW5kZXIgbm8gbGVnYWwgdGhlb3J5LAogICAgICB3aGV0aGVyIGluIHRvcnQgKGluY2x1ZGluZyBuZWdsaWdlbmNlKSwgY29udHJhY3QsIG9yIG90aGVyd2lzZSwKICAgICAgdW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IChzdWNoIGFzIGRlbGliZXJhdGUgYW5kIGdyb3NzbHkKICAgICAgbmVnbGlnZW50IGFjdHMpIG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzaGFsbCBhbnkgQ29udHJpYnV0b3IgYmUKICAgICAgbGlhYmxlIHRvIFlvdSBmb3IgZGFtYWdlcywgaW5jbHVkaW5nIGFueSBkaXJlY3QsIGluZGlyZWN0LCBzcGVjaWFsLAogICAgICBpbmNpZGVudGFsLCBvciBjb25zZXF1ZW50aWFsIGRhbWFnZXMgb2YgYW55IGNoYXJhY3RlciBhcmlzaW5nIGFzIGEKICAgICAgcmVzdWx0IG9mIHRoaXMgTGljZW5zZSBvciBvdXQgb2YgdGhlIHVzZSBvciBpbmFiaWxpdHkgdG8gdXNlIHRoZQogICAgICBXb3JrIChpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIGRhbWFnZXMgZm9yIGxvc3Mgb2YgZ29vZHdpbGwsCiAgICAgIHdvcmsgc3RvcHBhZ2UsIGNvbXB1dGVyIGZhaWx1cmUgb3IgbWFsZnVuY3Rpb24sIG9yIGFueSBhbmQgYWxsCiAgICAgIG90aGVyIGNvbW1lcmNpYWwgZGFtYWdlcyBvciBsb3NzZXMpLCBldmVuIGlmIHN1Y2ggQ29udHJpYnV0b3IKICAgICAgaGFzIGJlZW4gYWR2aXNlZCBvZiB0aGUgcG9zc2liaWxpdHkgb2Ygc3VjaCBkYW1hZ2VzLgoKICAgOS4gQWNjZXB0aW5nIFdhcnJhbnR5IG9yIEFkZGl0aW9uYWwgTGlhYmlsaXR5LiBXaGlsZSByZWRpc3RyaWJ1dGluZwogICAgICB0aGUgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIFlvdSBtYXkgY2hvb3NlIHRvIG9mZmVyLAogICAgICBhbmQgY2hhcmdlIGEgZmVlIGZvciwgYWNjZXB0YW5jZSBvZiBzdXBwb3J0LCB3YXJyYW50eSwgaW5kZW1uaXR5LAogICAgICBvciBvdGhlciBsaWFiaWxpdHkgb2JsaWdhdGlvbnMgYW5kL29yIHJpZ2h0cyBjb25zaXN0ZW50IHdpdGggdGhpcwogICAgICBMaWNlbnNlLiBIb3dldmVyLCBpbiBhY2NlcHRpbmcgc3VjaCBvYmxpZ2F0aW9ucywgWW91IG1heSBhY3Qgb25seQogICAgICBvbiBZb3VyIG93biBiZWhhbGYgYW5kIG9uIFlvdXIgc29sZSByZXNwb25zaWJpbGl0eSwgbm90IG9uIGJlaGFsZgogICAgICBvZiBhbnkgb3RoZXIgQ29udHJpYnV0b3IsIGFuZCBvbmx5IGlmIFlvdSBhZ3JlZSB0byBpbmRlbW5pZnksCiAgICAgIGRlZmVuZCwgYW5kIGhvbGQgZWFjaCBDb250cmlidXRvciBoYXJtbGVzcyBmb3IgYW55IGxpYWJpbGl0eQogICAgICBpbmN1cnJlZCBieSwgb3IgY2xhaW1zIGFzc2VydGVkIGFnYWluc3QsIHN1Y2ggQ29udHJpYnV0b3IgYnkgcmVhc29uCiAgICAgIG9mIHlvdXIgYWNjZXB0aW5nIGFueSBzdWNoIHdhcnJhbnR5IG9yIGFkZGl0aW9uYWwgbGlhYmlsaXR5LgoKICAgRU5EIE9GIFRFUk1TIEFORCBDT05ESVRJT05TCgogICBBUFBFTkRJWDogSG93IHRvIGFwcGx5IHRoZSBBcGFjaGUgTGljZW5zZSB0byB5b3VyIHdvcmsuCgogICAgICBUbyBhcHBseSB0aGUgQXBhY2hlIExpY2Vuc2UgdG8geW91ciB3b3JrLCBhdHRhY2ggdGhlIGZvbGxvd2luZwogICAgICBib2lsZXJwbGF0ZSBub3RpY2UsIHdpdGggdGhlIGZpZWxkcyBlbmNsb3NlZCBieSBicmFja2V0cyAiW10iCiAgICAgIHJlcGxhY2VkIHdpdGggeW91ciBvd24gaWRlbnRpZnlpbmcgaW5mb3JtYXRpb24uIChEb24ndCBpbmNsdWRlCiAgICAgIHRoZSBicmFja2V0cyEpICBUaGUgdGV4dCBzaG91bGQgYmUgZW5jbG9zZWQgaW4gdGhlIGFwcHJvcHJpYXRlCiAgICAgIGNvbW1lbnQgc3ludGF4IGZvciB0aGUgZmlsZSBmb3JtYXQuIFdlIGFsc28gcmVjb21tZW5kIHRoYXQgYQogICAgICBmaWxlIG9yIGNsYXNzIG5hbWUgYW5kIGRlc2NyaXB0aW9uIG9mIHB1cnBvc2UgYmUgaW5jbHVkZWQgb24gdGhlCiAgICAgIHNhbWUgInByaW50ZWQgcGFnZSIgYXMgdGhlIGNvcHlyaWdodCBub3RpY2UgZm9yIGVhc2llcgogICAgICBpZGVudGlmaWNhdGlvbiB3aXRoaW4gdGhpcmQtcGFydHkgYXJjaGl2ZXMuCgogICBDb3B5cmlnaHQgW3l5eXldIFtuYW1lIG9mIGNvcHlyaWdodCBvd25lcl0KCiAgIExpY2Vuc2VkIHVuZGVyIHRoZSBBcGFjaGUgTGljZW5zZSwgVmVyc2lvbiAyLjAgKHRoZSAiTGljZW5zZSIpOwogICB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCiAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgIGh0dHA6Ly93d3cuYXBhY2hlLm9yZy9saWNlbnNlcy9MSUNFTlNFLTIuMAoKICAgVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQogICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IgaW1wbGllZC4KICAgU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAogICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4= + https://www.apache.org/licenses/LICENSE-2.0.txt + + + pkg:maven/com.acme/tomcat-catalina@9.0.14?packaging=jar + + + + Apache + org.apache.tomcat + tomcat-catalina + 9.0.14 + Apache Catalina + + + Apache-2.0 + + + pkg:maven/org.apache.tomcat/tomcat-catalina@9.0.14?packaging=jar + + + + + 7638417db6d59f3c431d3e1f261cc637155684cd + https://location/to/7638417db6d59f3c431d3e1f261cc637155684cd + + 2018-11-07T22:01:45Z + John Doe + john.doe@example.com + + + 2018-11-07T22:01:45Z + Jane Doe + jane.doe@example.com + + Initial commit + + + Commentary here + + + + org.example + mylibrary + 1.0.0 + required + + 2342c2eaf1feb9a80195dbaddf2ebaa3 + 68b78babe00a053f9e35ec6a2d9080f5b90122b0 + 708f1f53b41f11f02d12a11b1a38d2905d47b099afc71a0f1124ef8582ec7313 + 387b7ae16b9cae45f830671541539bf544202faae5aac544a93b7b0a04f5f846fa2f4e81ef3f1677e13aed7496408a441f5657ab6d54423e56bf6f38da124aef + + + EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + Copyright Example Inc. All rights reserved. + cpe:/a:example:myapplication:1.0.0 + pkg:maven/com.example/myapplication@1.0.0?packaging=war + false + + + http://example.org/docs + All component versions are documented here + + + http://example.org/security + + + + + com.example + myframework + 1.0.0 + Example Inc, enterprise framework + required + + cfcb0b64aacd2f81c1cd546543de965a + 7fbeef2346c45d565c3341f037bce4e088af8a52 + 0384db3cec55d86a6898c489fdb75a8e75fe66b26639634983d2f3c3558493d1 + 854909cdb9e3ca183056837144aab6d8069b377bd66445087cc7157bf0c3f620418705dd0b83bdc2f73a508c2bdb316ca1809d75ee6972d02023a3e7dd655c79 + + + + Some random license + + + pkg:maven/com.example/myframework@1.0.0?packaging=war + false + + + http://example.com/myframework + + + http://example.com/security + + + + + \ No newline at end of file diff --git a/src/test/resources/unit/cyclonedx/valid-bom-1.2.json b/src/test/resources/unit/cyclonedx/valid-bom-1.2.json new file mode 100644 index 0000000000..629793568e --- /dev/null +++ b/src/test/resources/unit/cyclonedx/valid-bom-1.2.json @@ -0,0 +1,177 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.2", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2020-04-13T20:20:39+00:00", + "tools": [ + { + "vendor": "Awesome Vendor", + "name": "Awesome Tool", + "version": "9.1.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "25ed8e31b995bb927966616df2a42b979a2717f0" + }, + { + "alg": "SHA-256", + "content": "a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df" + } + ] + } + ], + "authors": [ + { + "name": "Samantha Wright", + "email": "samantha.wright@example.com", + "phone": "800-555-1212" + } + ], + "component": { + "type": "application", + "author": "Acme Super Heros", + "name": "Acme Application", + "version": "9.1.1", + "swid": { + "tagId": "swidgen-242eb18a-503e-ca37-393b-cf156ef09691_9.1.1", + "name": "Acme Application", + "version": "9.1.1", + "text": { + "contentType": "text/xml", + "encoding": "base64", + "content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg==" + } + } + }, + "manufacture": { + "name": "Acme, Inc.", + "url": [ + "https://example.com" + ], + "contact": [ + { + "name": "Acme Professional Services", + "email": "professional.services@example.com" + } + ] + }, + "supplier": { + "name": "Acme, Inc.", + "url": [ + "https://example.com" + ], + "contact": [ + { + "name": "Acme Distribution", + "email": "distribution@example.com" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:npm/acme/component@1.0.0", + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "tomcat-catalina", + "version": "9.0.14", + "hashes": [ + { + "alg": "MD5", + "content": "3942447fac867ae5cdb3229b658f4d48" + }, + { + "alg": "SHA-1", + "content": "e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a" + }, + { + "alg": "SHA-256", + "content": "f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b" + }, + { + "alg": "SHA-512", + "content": "e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0", + "text": { + "contentType": "text/plain", + "encoding": "base64", + "content": "CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFwYWNoZSBMaWNlbnNlCiAgICAgICAgICAgICAgICAgICAgICAgICAgIFZlcnNpb24gMi4wLCBKYW51YXJ5IDIwMDQKICAgICAgICAgICAgICAgICAgICAgICAgaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzLwoKICAgVEVSTVMgQU5EIENPTkRJVElPTlMgRk9SIFVTRSwgUkVQUk9EVUNUSU9OLCBBTkQgRElTVFJJQlVUSU9OCgogICAxLiBEZWZpbml0aW9ucy4KCiAgICAgICJMaWNlbnNlIiBzaGFsbCBtZWFuIHRoZSB0ZXJtcyBhbmQgY29uZGl0aW9ucyBmb3IgdXNlLCByZXByb2R1Y3Rpb24sCiAgICAgIGFuZCBkaXN0cmlidXRpb24gYXMgZGVmaW5lZCBieSBTZWN0aW9ucyAxIHRocm91Z2ggOSBvZiB0aGlzIGRvY3VtZW50LgoKICAgICAgIkxpY2Vuc29yIiBzaGFsbCBtZWFuIHRoZSBjb3B5cmlnaHQgb3duZXIgb3IgZW50aXR5IGF1dGhvcml6ZWQgYnkKICAgICAgdGhlIGNvcHlyaWdodCBvd25lciB0aGF0IGlzIGdyYW50aW5nIHRoZSBMaWNlbnNlLgoKICAgICAgIkxlZ2FsIEVudGl0eSIgc2hhbGwgbWVhbiB0aGUgdW5pb24gb2YgdGhlIGFjdGluZyBlbnRpdHkgYW5kIGFsbAogICAgICBvdGhlciBlbnRpdGllcyB0aGF0IGNvbnRyb2wsIGFyZSBjb250cm9sbGVkIGJ5LCBvciBhcmUgdW5kZXIgY29tbW9uCiAgICAgIGNvbnRyb2wgd2l0aCB0aGF0IGVudGl0eS4gRm9yIHRoZSBwdXJwb3NlcyBvZiB0aGlzIGRlZmluaXRpb24sCiAgICAgICJjb250cm9sIiBtZWFucyAoaSkgdGhlIHBvd2VyLCBkaXJlY3Qgb3IgaW5kaXJlY3QsIHRvIGNhdXNlIHRoZQogICAgICBkaXJlY3Rpb24gb3IgbWFuYWdlbWVudCBvZiBzdWNoIGVudGl0eSwgd2hldGhlciBieSBjb250cmFjdCBvcgogICAgICBvdGhlcndpc2UsIG9yIChpaSkgb3duZXJzaGlwIG9mIGZpZnR5IHBlcmNlbnQgKDUwJSkgb3IgbW9yZSBvZiB0aGUKICAgICAgb3V0c3RhbmRpbmcgc2hhcmVzLCBvciAoaWlpKSBiZW5lZmljaWFsIG93bmVyc2hpcCBvZiBzdWNoIGVudGl0eS4KCiAgICAgICJZb3UiIChvciAiWW91ciIpIHNoYWxsIG1lYW4gYW4gaW5kaXZpZHVhbCBvciBMZWdhbCBFbnRpdHkKICAgICAgZXhlcmNpc2luZyBwZXJtaXNzaW9ucyBncmFudGVkIGJ5IHRoaXMgTGljZW5zZS4KCiAgICAgICJTb3VyY2UiIGZvcm0gc2hhbGwgbWVhbiB0aGUgcHJlZmVycmVkIGZvcm0gZm9yIG1ha2luZyBtb2RpZmljYXRpb25zLAogICAgICBpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIHNvZnR3YXJlIHNvdXJjZSBjb2RlLCBkb2N1bWVudGF0aW9uCiAgICAgIHNvdXJjZSwgYW5kIGNvbmZpZ3VyYXRpb24gZmlsZXMuCgogICAgICAiT2JqZWN0IiBmb3JtIHNoYWxsIG1lYW4gYW55IGZvcm0gcmVzdWx0aW5nIGZyb20gbWVjaGFuaWNhbAogICAgICB0cmFuc2Zvcm1hdGlvbiBvciB0cmFuc2xhdGlvbiBvZiBhIFNvdXJjZSBmb3JtLCBpbmNsdWRpbmcgYnV0CiAgICAgIG5vdCBsaW1pdGVkIHRvIGNvbXBpbGVkIG9iamVjdCBjb2RlLCBnZW5lcmF0ZWQgZG9jdW1lbnRhdGlvbiwKICAgICAgYW5kIGNvbnZlcnNpb25zIHRvIG90aGVyIG1lZGlhIHR5cGVzLgoKICAgICAgIldvcmsiIHNoYWxsIG1lYW4gdGhlIHdvcmsgb2YgYXV0aG9yc2hpcCwgd2hldGhlciBpbiBTb3VyY2Ugb3IKICAgICAgT2JqZWN0IGZvcm0sIG1hZGUgYXZhaWxhYmxlIHVuZGVyIHRoZSBMaWNlbnNlLCBhcyBpbmRpY2F0ZWQgYnkgYQogICAgICBjb3B5cmlnaHQgbm90aWNlIHRoYXQgaXMgaW5jbHVkZWQgaW4gb3IgYXR0YWNoZWQgdG8gdGhlIHdvcmsKICAgICAgKGFuIGV4YW1wbGUgaXMgcHJvdmlkZWQgaW4gdGhlIEFwcGVuZGl4IGJlbG93KS4KCiAgICAgICJEZXJpdmF0aXZlIFdvcmtzIiBzaGFsbCBtZWFuIGFueSB3b3JrLCB3aGV0aGVyIGluIFNvdXJjZSBvciBPYmplY3QKICAgICAgZm9ybSwgdGhhdCBpcyBiYXNlZCBvbiAob3IgZGVyaXZlZCBmcm9tKSB0aGUgV29yayBhbmQgZm9yIHdoaWNoIHRoZQogICAgICBlZGl0b3JpYWwgcmV2aXNpb25zLCBhbm5vdGF0aW9ucywgZWxhYm9yYXRpb25zLCBvciBvdGhlciBtb2RpZmljYXRpb25zCiAgICAgIHJlcHJlc2VudCwgYXMgYSB3aG9sZSwgYW4gb3JpZ2luYWwgd29yayBvZiBhdXRob3JzaGlwLiBGb3IgdGhlIHB1cnBvc2VzCiAgICAgIG9mIHRoaXMgTGljZW5zZSwgRGVyaXZhdGl2ZSBXb3JrcyBzaGFsbCBub3QgaW5jbHVkZSB3b3JrcyB0aGF0IHJlbWFpbgogICAgICBzZXBhcmFibGUgZnJvbSwgb3IgbWVyZWx5IGxpbmsgKG9yIGJpbmQgYnkgbmFtZSkgdG8gdGhlIGludGVyZmFjZXMgb2YsCiAgICAgIHRoZSBXb3JrIGFuZCBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YuCgogICAgICAiQ29udHJpYnV0aW9uIiBzaGFsbCBtZWFuIGFueSB3b3JrIG9mIGF1dGhvcnNoaXAsIGluY2x1ZGluZwogICAgICB0aGUgb3JpZ2luYWwgdmVyc2lvbiBvZiB0aGUgV29yayBhbmQgYW55IG1vZGlmaWNhdGlvbnMgb3IgYWRkaXRpb25zCiAgICAgIHRvIHRoYXQgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIHRoYXQgaXMgaW50ZW50aW9uYWxseQogICAgICBzdWJtaXR0ZWQgdG8gTGljZW5zb3IgZm9yIGluY2x1c2lvbiBpbiB0aGUgV29yayBieSB0aGUgY29weXJpZ2h0IG93bmVyCiAgICAgIG9yIGJ5IGFuIGluZGl2aWR1YWwgb3IgTGVnYWwgRW50aXR5IGF1dGhvcml6ZWQgdG8gc3VibWl0IG9uIGJlaGFsZiBvZgogICAgICB0aGUgY29weXJpZ2h0IG93bmVyLiBGb3IgdGhlIHB1cnBvc2VzIG9mIHRoaXMgZGVmaW5pdGlvbiwgInN1Ym1pdHRlZCIKICAgICAgbWVhbnMgYW55IGZvcm0gb2YgZWxlY3Ryb25pYywgdmVyYmFsLCBvciB3cml0dGVuIGNvbW11bmljYXRpb24gc2VudAogICAgICB0byB0aGUgTGljZW5zb3Igb3IgaXRzIHJlcHJlc2VudGF0aXZlcywgaW5jbHVkaW5nIGJ1dCBub3QgbGltaXRlZCB0bwogICAgICBjb21tdW5pY2F0aW9uIG9uIGVsZWN0cm9uaWMgbWFpbGluZyBsaXN0cywgc291cmNlIGNvZGUgY29udHJvbCBzeXN0ZW1zLAogICAgICBhbmQgaXNzdWUgdHJhY2tpbmcgc3lzdGVtcyB0aGF0IGFyZSBtYW5hZ2VkIGJ5LCBvciBvbiBiZWhhbGYgb2YsIHRoZQogICAgICBMaWNlbnNvciBmb3IgdGhlIHB1cnBvc2Ugb2YgZGlzY3Vzc2luZyBhbmQgaW1wcm92aW5nIHRoZSBXb3JrLCBidXQKICAgICAgZXhjbHVkaW5nIGNvbW11bmljYXRpb24gdGhhdCBpcyBjb25zcGljdW91c2x5IG1hcmtlZCBvciBvdGhlcndpc2UKICAgICAgZGVzaWduYXRlZCBpbiB3cml0aW5nIGJ5IHRoZSBjb3B5cmlnaHQgb3duZXIgYXMgIk5vdCBhIENvbnRyaWJ1dGlvbi4iCgogICAgICAiQ29udHJpYnV0b3IiIHNoYWxsIG1lYW4gTGljZW5zb3IgYW5kIGFueSBpbmRpdmlkdWFsIG9yIExlZ2FsIEVudGl0eQogICAgICBvbiBiZWhhbGYgb2Ygd2hvbSBhIENvbnRyaWJ1dGlvbiBoYXMgYmVlbiByZWNlaXZlZCBieSBMaWNlbnNvciBhbmQKICAgICAgc3Vic2VxdWVudGx5IGluY29ycG9yYXRlZCB3aXRoaW4gdGhlIFdvcmsuCgogICAyLiBHcmFudCBvZiBDb3B5cmlnaHQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICBjb3B5cmlnaHQgbGljZW5zZSB0byByZXByb2R1Y2UsIHByZXBhcmUgRGVyaXZhdGl2ZSBXb3JrcyBvZiwKICAgICAgcHVibGljbHkgZGlzcGxheSwgcHVibGljbHkgcGVyZm9ybSwgc3VibGljZW5zZSwgYW5kIGRpc3RyaWJ1dGUgdGhlCiAgICAgIFdvcmsgYW5kIHN1Y2ggRGVyaXZhdGl2ZSBXb3JrcyBpbiBTb3VyY2Ugb3IgT2JqZWN0IGZvcm0uCgogICAzLiBHcmFudCBvZiBQYXRlbnQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICAoZXhjZXB0IGFzIHN0YXRlZCBpbiB0aGlzIHNlY3Rpb24pIHBhdGVudCBsaWNlbnNlIHRvIG1ha2UsIGhhdmUgbWFkZSwKICAgICAgdXNlLCBvZmZlciB0byBzZWxsLCBzZWxsLCBpbXBvcnQsIGFuZCBvdGhlcndpc2UgdHJhbnNmZXIgdGhlIFdvcmssCiAgICAgIHdoZXJlIHN1Y2ggbGljZW5zZSBhcHBsaWVzIG9ubHkgdG8gdGhvc2UgcGF0ZW50IGNsYWltcyBsaWNlbnNhYmxlCiAgICAgIGJ5IHN1Y2ggQ29udHJpYnV0b3IgdGhhdCBhcmUgbmVjZXNzYXJpbHkgaW5mcmluZ2VkIGJ5IHRoZWlyCiAgICAgIENvbnRyaWJ1dGlvbihzKSBhbG9uZSBvciBieSBjb21iaW5hdGlvbiBvZiB0aGVpciBDb250cmlidXRpb24ocykKICAgICAgd2l0aCB0aGUgV29yayB0byB3aGljaCBzdWNoIENvbnRyaWJ1dGlvbihzKSB3YXMgc3VibWl0dGVkLiBJZiBZb3UKICAgICAgaW5zdGl0dXRlIHBhdGVudCBsaXRpZ2F0aW9uIGFnYWluc3QgYW55IGVudGl0eSAoaW5jbHVkaW5nIGEKICAgICAgY3Jvc3MtY2xhaW0gb3IgY291bnRlcmNsYWltIGluIGEgbGF3c3VpdCkgYWxsZWdpbmcgdGhhdCB0aGUgV29yawogICAgICBvciBhIENvbnRyaWJ1dGlvbiBpbmNvcnBvcmF0ZWQgd2l0aGluIHRoZSBXb3JrIGNvbnN0aXR1dGVzIGRpcmVjdAogICAgICBvciBjb250cmlidXRvcnkgcGF0ZW50IGluZnJpbmdlbWVudCwgdGhlbiBhbnkgcGF0ZW50IGxpY2Vuc2VzCiAgICAgIGdyYW50ZWQgdG8gWW91IHVuZGVyIHRoaXMgTGljZW5zZSBmb3IgdGhhdCBXb3JrIHNoYWxsIHRlcm1pbmF0ZQogICAgICBhcyBvZiB0aGUgZGF0ZSBzdWNoIGxpdGlnYXRpb24gaXMgZmlsZWQuCgogICA0LiBSZWRpc3RyaWJ1dGlvbi4gWW91IG1heSByZXByb2R1Y2UgYW5kIGRpc3RyaWJ1dGUgY29waWVzIG9mIHRoZQogICAgICBXb3JrIG9yIERlcml2YXRpdmUgV29ya3MgdGhlcmVvZiBpbiBhbnkgbWVkaXVtLCB3aXRoIG9yIHdpdGhvdXQKICAgICAgbW9kaWZpY2F0aW9ucywgYW5kIGluIFNvdXJjZSBvciBPYmplY3QgZm9ybSwgcHJvdmlkZWQgdGhhdCBZb3UKICAgICAgbWVldCB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgogICAgICAoYSkgWW91IG11c3QgZ2l2ZSBhbnkgb3RoZXIgcmVjaXBpZW50cyBvZiB0aGUgV29yayBvcgogICAgICAgICAgRGVyaXZhdGl2ZSBXb3JrcyBhIGNvcHkgb2YgdGhpcyBMaWNlbnNlOyBhbmQKCiAgICAgIChiKSBZb3UgbXVzdCBjYXVzZSBhbnkgbW9kaWZpZWQgZmlsZXMgdG8gY2FycnkgcHJvbWluZW50IG5vdGljZXMKICAgICAgICAgIHN0YXRpbmcgdGhhdCBZb3UgY2hhbmdlZCB0aGUgZmlsZXM7IGFuZAoKICAgICAgKGMpIFlvdSBtdXN0IHJldGFpbiwgaW4gdGhlIFNvdXJjZSBmb3JtIG9mIGFueSBEZXJpdmF0aXZlIFdvcmtzCiAgICAgICAgICB0aGF0IFlvdSBkaXN0cmlidXRlLCBhbGwgY29weXJpZ2h0LCBwYXRlbnQsIHRyYWRlbWFyaywgYW5kCiAgICAgICAgICBhdHRyaWJ1dGlvbiBub3RpY2VzIGZyb20gdGhlIFNvdXJjZSBmb3JtIG9mIHRoZSBXb3JrLAogICAgICAgICAgZXhjbHVkaW5nIHRob3NlIG5vdGljZXMgdGhhdCBkbyBub3QgcGVydGFpbiB0byBhbnkgcGFydCBvZgogICAgICAgICAgdGhlIERlcml2YXRpdmUgV29ya3M7IGFuZAoKICAgICAgKGQpIElmIHRoZSBXb3JrIGluY2x1ZGVzIGEgIk5PVElDRSIgdGV4dCBmaWxlIGFzIHBhcnQgb2YgaXRzCiAgICAgICAgICBkaXN0cmlidXRpb24sIHRoZW4gYW55IERlcml2YXRpdmUgV29ya3MgdGhhdCBZb3UgZGlzdHJpYnV0ZSBtdXN0CiAgICAgICAgICBpbmNsdWRlIGEgcmVhZGFibGUgY29weSBvZiB0aGUgYXR0cmlidXRpb24gbm90aWNlcyBjb250YWluZWQKICAgICAgICAgIHdpdGhpbiBzdWNoIE5PVElDRSBmaWxlLCBleGNsdWRpbmcgdGhvc2Ugbm90aWNlcyB0aGF0IGRvIG5vdAogICAgICAgICAgcGVydGFpbiB0byBhbnkgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaW4gYXQgbGVhc3Qgb25lCiAgICAgICAgICBvZiB0aGUgZm9sbG93aW5nIHBsYWNlczogd2l0aGluIGEgTk9USUNFIHRleHQgZmlsZSBkaXN0cmlidXRlZAogICAgICAgICAgYXMgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgd2l0aGluIHRoZSBTb3VyY2UgZm9ybSBvcgogICAgICAgICAgZG9jdW1lbnRhdGlvbiwgaWYgcHJvdmlkZWQgYWxvbmcgd2l0aCB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgb3IsCiAgICAgICAgICB3aXRoaW4gYSBkaXNwbGF5IGdlbmVyYXRlZCBieSB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaWYgYW5kCiAgICAgICAgICB3aGVyZXZlciBzdWNoIHRoaXJkLXBhcnR5IG5vdGljZXMgbm9ybWFsbHkgYXBwZWFyLiBUaGUgY29udGVudHMKICAgICAgICAgIG9mIHRoZSBOT1RJQ0UgZmlsZSBhcmUgZm9yIGluZm9ybWF0aW9uYWwgcHVycG9zZXMgb25seSBhbmQKICAgICAgICAgIGRvIG5vdCBtb2RpZnkgdGhlIExpY2Vuc2UuIFlvdSBtYXkgYWRkIFlvdXIgb3duIGF0dHJpYnV0aW9uCiAgICAgICAgICBub3RpY2VzIHdpdGhpbiBEZXJpdmF0aXZlIFdvcmtzIHRoYXQgWW91IGRpc3RyaWJ1dGUsIGFsb25nc2lkZQogICAgICAgICAgb3IgYXMgYW4gYWRkZW5kdW0gdG8gdGhlIE5PVElDRSB0ZXh0IGZyb20gdGhlIFdvcmssIHByb3ZpZGVkCiAgICAgICAgICB0aGF0IHN1Y2ggYWRkaXRpb25hbCBhdHRyaWJ1dGlvbiBub3RpY2VzIGNhbm5vdCBiZSBjb25zdHJ1ZWQKICAgICAgICAgIGFzIG1vZGlmeWluZyB0aGUgTGljZW5zZS4KCiAgICAgIFlvdSBtYXkgYWRkIFlvdXIgb3duIGNvcHlyaWdodCBzdGF0ZW1lbnQgdG8gWW91ciBtb2RpZmljYXRpb25zIGFuZAogICAgICBtYXkgcHJvdmlkZSBhZGRpdGlvbmFsIG9yIGRpZmZlcmVudCBsaWNlbnNlIHRlcm1zIGFuZCBjb25kaXRpb25zCiAgICAgIGZvciB1c2UsIHJlcHJvZHVjdGlvbiwgb3IgZGlzdHJpYnV0aW9uIG9mIFlvdXIgbW9kaWZpY2F0aW9ucywgb3IKICAgICAgZm9yIGFueSBzdWNoIERlcml2YXRpdmUgV29ya3MgYXMgYSB3aG9sZSwgcHJvdmlkZWQgWW91ciB1c2UsCiAgICAgIHJlcHJvZHVjdGlvbiwgYW5kIGRpc3RyaWJ1dGlvbiBvZiB0aGUgV29yayBvdGhlcndpc2UgY29tcGxpZXMgd2l0aAogICAgICB0aGUgY29uZGl0aW9ucyBzdGF0ZWQgaW4gdGhpcyBMaWNlbnNlLgoKICAgNS4gU3VibWlzc2lvbiBvZiBDb250cmlidXRpb25zLiBVbmxlc3MgWW91IGV4cGxpY2l0bHkgc3RhdGUgb3RoZXJ3aXNlLAogICAgICBhbnkgQ29udHJpYnV0aW9uIGludGVudGlvbmFsbHkgc3VibWl0dGVkIGZvciBpbmNsdXNpb24gaW4gdGhlIFdvcmsKICAgICAgYnkgWW91IHRvIHRoZSBMaWNlbnNvciBzaGFsbCBiZSB1bmRlciB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCB3aXRob3V0IGFueSBhZGRpdGlvbmFsIHRlcm1zIG9yIGNvbmRpdGlvbnMuCiAgICAgIE5vdHdpdGhzdGFuZGluZyB0aGUgYWJvdmUsIG5vdGhpbmcgaGVyZWluIHNoYWxsIHN1cGVyc2VkZSBvciBtb2RpZnkKICAgICAgdGhlIHRlcm1zIG9mIGFueSBzZXBhcmF0ZSBsaWNlbnNlIGFncmVlbWVudCB5b3UgbWF5IGhhdmUgZXhlY3V0ZWQKICAgICAgd2l0aCBMaWNlbnNvciByZWdhcmRpbmcgc3VjaCBDb250cmlidXRpb25zLgoKICAgNi4gVHJhZGVtYXJrcy4gVGhpcyBMaWNlbnNlIGRvZXMgbm90IGdyYW50IHBlcm1pc3Npb24gdG8gdXNlIHRoZSB0cmFkZQogICAgICBuYW1lcywgdHJhZGVtYXJrcywgc2VydmljZSBtYXJrcywgb3IgcHJvZHVjdCBuYW1lcyBvZiB0aGUgTGljZW5zb3IsCiAgICAgIGV4Y2VwdCBhcyByZXF1aXJlZCBmb3IgcmVhc29uYWJsZSBhbmQgY3VzdG9tYXJ5IHVzZSBpbiBkZXNjcmliaW5nIHRoZQogICAgICBvcmlnaW4gb2YgdGhlIFdvcmsgYW5kIHJlcHJvZHVjaW5nIHRoZSBjb250ZW50IG9mIHRoZSBOT1RJQ0UgZmlsZS4KCiAgIDcuIERpc2NsYWltZXIgb2YgV2FycmFudHkuIFVubGVzcyByZXF1aXJlZCBieSBhcHBsaWNhYmxlIGxhdyBvcgogICAgICBhZ3JlZWQgdG8gaW4gd3JpdGluZywgTGljZW5zb3IgcHJvdmlkZXMgdGhlIFdvcmsgKGFuZCBlYWNoCiAgICAgIENvbnRyaWJ1dG9yIHByb3ZpZGVzIGl0cyBDb250cmlidXRpb25zKSBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IKICAgICAgaW1wbGllZCwgaW5jbHVkaW5nLCB3aXRob3V0IGxpbWl0YXRpb24sIGFueSB3YXJyYW50aWVzIG9yIGNvbmRpdGlvbnMKICAgICAgb2YgVElUTEUsIE5PTi1JTkZSSU5HRU1FTlQsIE1FUkNIQU5UQUJJTElUWSwgb3IgRklUTkVTUyBGT1IgQQogICAgICBQQVJUSUNVTEFSIFBVUlBPU0UuIFlvdSBhcmUgc29sZWx5IHJlc3BvbnNpYmxlIGZvciBkZXRlcm1pbmluZyB0aGUKICAgICAgYXBwcm9wcmlhdGVuZXNzIG9mIHVzaW5nIG9yIHJlZGlzdHJpYnV0aW5nIHRoZSBXb3JrIGFuZCBhc3N1bWUgYW55CiAgICAgIHJpc2tzIGFzc29jaWF0ZWQgd2l0aCBZb3VyIGV4ZXJjaXNlIG9mIHBlcm1pc3Npb25zIHVuZGVyIHRoaXMgTGljZW5zZS4KCiAgIDguIExpbWl0YXRpb24gb2YgTGlhYmlsaXR5LiBJbiBubyBldmVudCBhbmQgdW5kZXIgbm8gbGVnYWwgdGhlb3J5LAogICAgICB3aGV0aGVyIGluIHRvcnQgKGluY2x1ZGluZyBuZWdsaWdlbmNlKSwgY29udHJhY3QsIG9yIG90aGVyd2lzZSwKICAgICAgdW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IChzdWNoIGFzIGRlbGliZXJhdGUgYW5kIGdyb3NzbHkKICAgICAgbmVnbGlnZW50IGFjdHMpIG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzaGFsbCBhbnkgQ29udHJpYnV0b3IgYmUKICAgICAgbGlhYmxlIHRvIFlvdSBmb3IgZGFtYWdlcywgaW5jbHVkaW5nIGFueSBkaXJlY3QsIGluZGlyZWN0LCBzcGVjaWFsLAogICAgICBpbmNpZGVudGFsLCBvciBjb25zZXF1ZW50aWFsIGRhbWFnZXMgb2YgYW55IGNoYXJhY3RlciBhcmlzaW5nIGFzIGEKICAgICAgcmVzdWx0IG9mIHRoaXMgTGljZW5zZSBvciBvdXQgb2YgdGhlIHVzZSBvciBpbmFiaWxpdHkgdG8gdXNlIHRoZQogICAgICBXb3JrIChpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIGRhbWFnZXMgZm9yIGxvc3Mgb2YgZ29vZHdpbGwsCiAgICAgIHdvcmsgc3RvcHBhZ2UsIGNvbXB1dGVyIGZhaWx1cmUgb3IgbWFsZnVuY3Rpb24sIG9yIGFueSBhbmQgYWxsCiAgICAgIG90aGVyIGNvbW1lcmNpYWwgZGFtYWdlcyBvciBsb3NzZXMpLCBldmVuIGlmIHN1Y2ggQ29udHJpYnV0b3IKICAgICAgaGFzIGJlZW4gYWR2aXNlZCBvZiB0aGUgcG9zc2liaWxpdHkgb2Ygc3VjaCBkYW1hZ2VzLgoKICAgOS4gQWNjZXB0aW5nIFdhcnJhbnR5IG9yIEFkZGl0aW9uYWwgTGlhYmlsaXR5LiBXaGlsZSByZWRpc3RyaWJ1dGluZwogICAgICB0aGUgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIFlvdSBtYXkgY2hvb3NlIHRvIG9mZmVyLAogICAgICBhbmQgY2hhcmdlIGEgZmVlIGZvciwgYWNjZXB0YW5jZSBvZiBzdXBwb3J0LCB3YXJyYW50eSwgaW5kZW1uaXR5LAogICAgICBvciBvdGhlciBsaWFiaWxpdHkgb2JsaWdhdGlvbnMgYW5kL29yIHJpZ2h0cyBjb25zaXN0ZW50IHdpdGggdGhpcwogICAgICBMaWNlbnNlLiBIb3dldmVyLCBpbiBhY2NlcHRpbmcgc3VjaCBvYmxpZ2F0aW9ucywgWW91IG1heSBhY3Qgb25seQogICAgICBvbiBZb3VyIG93biBiZWhhbGYgYW5kIG9uIFlvdXIgc29sZSByZXNwb25zaWJpbGl0eSwgbm90IG9uIGJlaGFsZgogICAgICBvZiBhbnkgb3RoZXIgQ29udHJpYnV0b3IsIGFuZCBvbmx5IGlmIFlvdSBhZ3JlZSB0byBpbmRlbW5pZnksCiAgICAgIGRlZmVuZCwgYW5kIGhvbGQgZWFjaCBDb250cmlidXRvciBoYXJtbGVzcyBmb3IgYW55IGxpYWJpbGl0eQogICAgICBpbmN1cnJlZCBieSwgb3IgY2xhaW1zIGFzc2VydGVkIGFnYWluc3QsIHN1Y2ggQ29udHJpYnV0b3IgYnkgcmVhc29uCiAgICAgIG9mIHlvdXIgYWNjZXB0aW5nIGFueSBzdWNoIHdhcnJhbnR5IG9yIGFkZGl0aW9uYWwgbGlhYmlsaXR5LgoKICAgRU5EIE9GIFRFUk1TIEFORCBDT05ESVRJT05TCgogICBBUFBFTkRJWDogSG93IHRvIGFwcGx5IHRoZSBBcGFjaGUgTGljZW5zZSB0byB5b3VyIHdvcmsuCgogICAgICBUbyBhcHBseSB0aGUgQXBhY2hlIExpY2Vuc2UgdG8geW91ciB3b3JrLCBhdHRhY2ggdGhlIGZvbGxvd2luZwogICAgICBib2lsZXJwbGF0ZSBub3RpY2UsIHdpdGggdGhlIGZpZWxkcyBlbmNsb3NlZCBieSBicmFja2V0cyAiW10iCiAgICAgIHJlcGxhY2VkIHdpdGggeW91ciBvd24gaWRlbnRpZnlpbmcgaW5mb3JtYXRpb24uIChEb24ndCBpbmNsdWRlCiAgICAgIHRoZSBicmFja2V0cyEpICBUaGUgdGV4dCBzaG91bGQgYmUgZW5jbG9zZWQgaW4gdGhlIGFwcHJvcHJpYXRlCiAgICAgIGNvbW1lbnQgc3ludGF4IGZvciB0aGUgZmlsZSBmb3JtYXQuIFdlIGFsc28gcmVjb21tZW5kIHRoYXQgYQogICAgICBmaWxlIG9yIGNsYXNzIG5hbWUgYW5kIGRlc2NyaXB0aW9uIG9mIHB1cnBvc2UgYmUgaW5jbHVkZWQgb24gdGhlCiAgICAgIHNhbWUgInByaW50ZWQgcGFnZSIgYXMgdGhlIGNvcHlyaWdodCBub3RpY2UgZm9yIGVhc2llcgogICAgICBpZGVudGlmaWNhdGlvbiB3aXRoaW4gdGhpcmQtcGFydHkgYXJjaGl2ZXMuCgogICBDb3B5cmlnaHQgW3l5eXldIFtuYW1lIG9mIGNvcHlyaWdodCBvd25lcl0KCiAgIExpY2Vuc2VkIHVuZGVyIHRoZSBBcGFjaGUgTGljZW5zZSwgVmVyc2lvbiAyLjAgKHRoZSAiTGljZW5zZSIpOwogICB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCiAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgIGh0dHA6Ly93d3cuYXBhY2hlLm9yZy9saWNlbnNlcy9MSUNFTlNFLTIuMAoKICAgVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQogICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IgaW1wbGllZC4KICAgU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAogICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4=" + }, + "url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + ], + "purl": "pkg:npm/acme/component@1.0.0", + "pedigree": { + "ancestors": [ + { + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "tomcat-catalina", + "version": "9.0.14" + }, + { + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "tomcat-catalina", + "version": "9.0.14" + } + ], + "commits": [ + { + "uid": "7638417db6d59f3c431d3e1f261cc637155684cd", + "url": "https://location/to/7638417db6d59f3c431d3e1f261cc637155684cd", + "author": { + "timestamp": "2018-11-13T20:20:39+00:00", + "name": "me", + "email": "me@acme.org" + } + } + ] + } + }, + { + "type": "library", + "supplier": { + "name": "Example, Inc.", + "url": [ + "https://example.com", + "https://example.net" + ], + "contact": [ + { + "name": "Example Support AMER Distribution", + "email": "support@example.com", + "phone": "800-555-1212" + }, + { + "name": "Example Support APAC", + "email": "support@apac.example.com" + } + ] + }, + "author": "Example Super Heros", + "group": "org.example", + "name": "mylibrary", + "version": "1.0.0" + } + ], + "dependencies": [ + { + "ref": "pkg:npm/acme/component@1.0.0", + "dependsOn": [ + "pkg:npm/acme/component@1.0.0" + ] + } + ] +} diff --git a/src/test/resources/unit/cyclonedx/valid-bom-1.2.xml b/src/test/resources/unit/cyclonedx/valid-bom-1.2.xml new file mode 100644 index 0000000000..6be1b4f062 --- /dev/null +++ b/src/test/resources/unit/cyclonedx/valid-bom-1.2.xml @@ -0,0 +1,181 @@ + + + + 2020-04-07T07:01:00Z + + + Awesome Vendor + Awesome Tool + 9.1.2 + + 25ed8e31b995bb927966616df2a42b979a2717f0 + a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df + + + + + + Samantha Wright + samantha.wright@example.com + 800-555-1212 + + + + Acme Super Heros + Acme Application + 9.1.1 + + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg== + + + + Acme, Inc. + https://example.com + + Acme Professional Services + professional.services@example.com + + + + Acme, Inc. + https://example.com + + Acme Distribution + distribution@example.com + + + + + + Acme Super Heros + Acme Inc + com.acme + tomcat-catalina + 9.0.14 + Modified version of Apache Catalina + required + + 3942447fac867ae5cdb3229b658f4d48 + e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a + f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b + e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282 + + + + Apache-2.0 + CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFwYWNoZSBMaWNlbnNlCiAgICAgICAgICAgICAgICAgICAgICAgICAgIFZlcnNpb24gMi4wLCBKYW51YXJ5IDIwMDQKICAgICAgICAgICAgICAgICAgICAgICAgaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzLwoKICAgVEVSTVMgQU5EIENPTkRJVElPTlMgRk9SIFVTRSwgUkVQUk9EVUNUSU9OLCBBTkQgRElTVFJJQlVUSU9OCgogICAxLiBEZWZpbml0aW9ucy4KCiAgICAgICJMaWNlbnNlIiBzaGFsbCBtZWFuIHRoZSB0ZXJtcyBhbmQgY29uZGl0aW9ucyBmb3IgdXNlLCByZXByb2R1Y3Rpb24sCiAgICAgIGFuZCBkaXN0cmlidXRpb24gYXMgZGVmaW5lZCBieSBTZWN0aW9ucyAxIHRocm91Z2ggOSBvZiB0aGlzIGRvY3VtZW50LgoKICAgICAgIkxpY2Vuc29yIiBzaGFsbCBtZWFuIHRoZSBjb3B5cmlnaHQgb3duZXIgb3IgZW50aXR5IGF1dGhvcml6ZWQgYnkKICAgICAgdGhlIGNvcHlyaWdodCBvd25lciB0aGF0IGlzIGdyYW50aW5nIHRoZSBMaWNlbnNlLgoKICAgICAgIkxlZ2FsIEVudGl0eSIgc2hhbGwgbWVhbiB0aGUgdW5pb24gb2YgdGhlIGFjdGluZyBlbnRpdHkgYW5kIGFsbAogICAgICBvdGhlciBlbnRpdGllcyB0aGF0IGNvbnRyb2wsIGFyZSBjb250cm9sbGVkIGJ5LCBvciBhcmUgdW5kZXIgY29tbW9uCiAgICAgIGNvbnRyb2wgd2l0aCB0aGF0IGVudGl0eS4gRm9yIHRoZSBwdXJwb3NlcyBvZiB0aGlzIGRlZmluaXRpb24sCiAgICAgICJjb250cm9sIiBtZWFucyAoaSkgdGhlIHBvd2VyLCBkaXJlY3Qgb3IgaW5kaXJlY3QsIHRvIGNhdXNlIHRoZQogICAgICBkaXJlY3Rpb24gb3IgbWFuYWdlbWVudCBvZiBzdWNoIGVudGl0eSwgd2hldGhlciBieSBjb250cmFjdCBvcgogICAgICBvdGhlcndpc2UsIG9yIChpaSkgb3duZXJzaGlwIG9mIGZpZnR5IHBlcmNlbnQgKDUwJSkgb3IgbW9yZSBvZiB0aGUKICAgICAgb3V0c3RhbmRpbmcgc2hhcmVzLCBvciAoaWlpKSBiZW5lZmljaWFsIG93bmVyc2hpcCBvZiBzdWNoIGVudGl0eS4KCiAgICAgICJZb3UiIChvciAiWW91ciIpIHNoYWxsIG1lYW4gYW4gaW5kaXZpZHVhbCBvciBMZWdhbCBFbnRpdHkKICAgICAgZXhlcmNpc2luZyBwZXJtaXNzaW9ucyBncmFudGVkIGJ5IHRoaXMgTGljZW5zZS4KCiAgICAgICJTb3VyY2UiIGZvcm0gc2hhbGwgbWVhbiB0aGUgcHJlZmVycmVkIGZvcm0gZm9yIG1ha2luZyBtb2RpZmljYXRpb25zLAogICAgICBpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIHNvZnR3YXJlIHNvdXJjZSBjb2RlLCBkb2N1bWVudGF0aW9uCiAgICAgIHNvdXJjZSwgYW5kIGNvbmZpZ3VyYXRpb24gZmlsZXMuCgogICAgICAiT2JqZWN0IiBmb3JtIHNoYWxsIG1lYW4gYW55IGZvcm0gcmVzdWx0aW5nIGZyb20gbWVjaGFuaWNhbAogICAgICB0cmFuc2Zvcm1hdGlvbiBvciB0cmFuc2xhdGlvbiBvZiBhIFNvdXJjZSBmb3JtLCBpbmNsdWRpbmcgYnV0CiAgICAgIG5vdCBsaW1pdGVkIHRvIGNvbXBpbGVkIG9iamVjdCBjb2RlLCBnZW5lcmF0ZWQgZG9jdW1lbnRhdGlvbiwKICAgICAgYW5kIGNvbnZlcnNpb25zIHRvIG90aGVyIG1lZGlhIHR5cGVzLgoKICAgICAgIldvcmsiIHNoYWxsIG1lYW4gdGhlIHdvcmsgb2YgYXV0aG9yc2hpcCwgd2hldGhlciBpbiBTb3VyY2Ugb3IKICAgICAgT2JqZWN0IGZvcm0sIG1hZGUgYXZhaWxhYmxlIHVuZGVyIHRoZSBMaWNlbnNlLCBhcyBpbmRpY2F0ZWQgYnkgYQogICAgICBjb3B5cmlnaHQgbm90aWNlIHRoYXQgaXMgaW5jbHVkZWQgaW4gb3IgYXR0YWNoZWQgdG8gdGhlIHdvcmsKICAgICAgKGFuIGV4YW1wbGUgaXMgcHJvdmlkZWQgaW4gdGhlIEFwcGVuZGl4IGJlbG93KS4KCiAgICAgICJEZXJpdmF0aXZlIFdvcmtzIiBzaGFsbCBtZWFuIGFueSB3b3JrLCB3aGV0aGVyIGluIFNvdXJjZSBvciBPYmplY3QKICAgICAgZm9ybSwgdGhhdCBpcyBiYXNlZCBvbiAob3IgZGVyaXZlZCBmcm9tKSB0aGUgV29yayBhbmQgZm9yIHdoaWNoIHRoZQogICAgICBlZGl0b3JpYWwgcmV2aXNpb25zLCBhbm5vdGF0aW9ucywgZWxhYm9yYXRpb25zLCBvciBvdGhlciBtb2RpZmljYXRpb25zCiAgICAgIHJlcHJlc2VudCwgYXMgYSB3aG9sZSwgYW4gb3JpZ2luYWwgd29yayBvZiBhdXRob3JzaGlwLiBGb3IgdGhlIHB1cnBvc2VzCiAgICAgIG9mIHRoaXMgTGljZW5zZSwgRGVyaXZhdGl2ZSBXb3JrcyBzaGFsbCBub3QgaW5jbHVkZSB3b3JrcyB0aGF0IHJlbWFpbgogICAgICBzZXBhcmFibGUgZnJvbSwgb3IgbWVyZWx5IGxpbmsgKG9yIGJpbmQgYnkgbmFtZSkgdG8gdGhlIGludGVyZmFjZXMgb2YsCiAgICAgIHRoZSBXb3JrIGFuZCBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YuCgogICAgICAiQ29udHJpYnV0aW9uIiBzaGFsbCBtZWFuIGFueSB3b3JrIG9mIGF1dGhvcnNoaXAsIGluY2x1ZGluZwogICAgICB0aGUgb3JpZ2luYWwgdmVyc2lvbiBvZiB0aGUgV29yayBhbmQgYW55IG1vZGlmaWNhdGlvbnMgb3IgYWRkaXRpb25zCiAgICAgIHRvIHRoYXQgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIHRoYXQgaXMgaW50ZW50aW9uYWxseQogICAgICBzdWJtaXR0ZWQgdG8gTGljZW5zb3IgZm9yIGluY2x1c2lvbiBpbiB0aGUgV29yayBieSB0aGUgY29weXJpZ2h0IG93bmVyCiAgICAgIG9yIGJ5IGFuIGluZGl2aWR1YWwgb3IgTGVnYWwgRW50aXR5IGF1dGhvcml6ZWQgdG8gc3VibWl0IG9uIGJlaGFsZiBvZgogICAgICB0aGUgY29weXJpZ2h0IG93bmVyLiBGb3IgdGhlIHB1cnBvc2VzIG9mIHRoaXMgZGVmaW5pdGlvbiwgInN1Ym1pdHRlZCIKICAgICAgbWVhbnMgYW55IGZvcm0gb2YgZWxlY3Ryb25pYywgdmVyYmFsLCBvciB3cml0dGVuIGNvbW11bmljYXRpb24gc2VudAogICAgICB0byB0aGUgTGljZW5zb3Igb3IgaXRzIHJlcHJlc2VudGF0aXZlcywgaW5jbHVkaW5nIGJ1dCBub3QgbGltaXRlZCB0bwogICAgICBjb21tdW5pY2F0aW9uIG9uIGVsZWN0cm9uaWMgbWFpbGluZyBsaXN0cywgc291cmNlIGNvZGUgY29udHJvbCBzeXN0ZW1zLAogICAgICBhbmQgaXNzdWUgdHJhY2tpbmcgc3lzdGVtcyB0aGF0IGFyZSBtYW5hZ2VkIGJ5LCBvciBvbiBiZWhhbGYgb2YsIHRoZQogICAgICBMaWNlbnNvciBmb3IgdGhlIHB1cnBvc2Ugb2YgZGlzY3Vzc2luZyBhbmQgaW1wcm92aW5nIHRoZSBXb3JrLCBidXQKICAgICAgZXhjbHVkaW5nIGNvbW11bmljYXRpb24gdGhhdCBpcyBjb25zcGljdW91c2x5IG1hcmtlZCBvciBvdGhlcndpc2UKICAgICAgZGVzaWduYXRlZCBpbiB3cml0aW5nIGJ5IHRoZSBjb3B5cmlnaHQgb3duZXIgYXMgIk5vdCBhIENvbnRyaWJ1dGlvbi4iCgogICAgICAiQ29udHJpYnV0b3IiIHNoYWxsIG1lYW4gTGljZW5zb3IgYW5kIGFueSBpbmRpdmlkdWFsIG9yIExlZ2FsIEVudGl0eQogICAgICBvbiBiZWhhbGYgb2Ygd2hvbSBhIENvbnRyaWJ1dGlvbiBoYXMgYmVlbiByZWNlaXZlZCBieSBMaWNlbnNvciBhbmQKICAgICAgc3Vic2VxdWVudGx5IGluY29ycG9yYXRlZCB3aXRoaW4gdGhlIFdvcmsuCgogICAyLiBHcmFudCBvZiBDb3B5cmlnaHQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICBjb3B5cmlnaHQgbGljZW5zZSB0byByZXByb2R1Y2UsIHByZXBhcmUgRGVyaXZhdGl2ZSBXb3JrcyBvZiwKICAgICAgcHVibGljbHkgZGlzcGxheSwgcHVibGljbHkgcGVyZm9ybSwgc3VibGljZW5zZSwgYW5kIGRpc3RyaWJ1dGUgdGhlCiAgICAgIFdvcmsgYW5kIHN1Y2ggRGVyaXZhdGl2ZSBXb3JrcyBpbiBTb3VyY2Ugb3IgT2JqZWN0IGZvcm0uCgogICAzLiBHcmFudCBvZiBQYXRlbnQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICAoZXhjZXB0IGFzIHN0YXRlZCBpbiB0aGlzIHNlY3Rpb24pIHBhdGVudCBsaWNlbnNlIHRvIG1ha2UsIGhhdmUgbWFkZSwKICAgICAgdXNlLCBvZmZlciB0byBzZWxsLCBzZWxsLCBpbXBvcnQsIGFuZCBvdGhlcndpc2UgdHJhbnNmZXIgdGhlIFdvcmssCiAgICAgIHdoZXJlIHN1Y2ggbGljZW5zZSBhcHBsaWVzIG9ubHkgdG8gdGhvc2UgcGF0ZW50IGNsYWltcyBsaWNlbnNhYmxlCiAgICAgIGJ5IHN1Y2ggQ29udHJpYnV0b3IgdGhhdCBhcmUgbmVjZXNzYXJpbHkgaW5mcmluZ2VkIGJ5IHRoZWlyCiAgICAgIENvbnRyaWJ1dGlvbihzKSBhbG9uZSBvciBieSBjb21iaW5hdGlvbiBvZiB0aGVpciBDb250cmlidXRpb24ocykKICAgICAgd2l0aCB0aGUgV29yayB0byB3aGljaCBzdWNoIENvbnRyaWJ1dGlvbihzKSB3YXMgc3VibWl0dGVkLiBJZiBZb3UKICAgICAgaW5zdGl0dXRlIHBhdGVudCBsaXRpZ2F0aW9uIGFnYWluc3QgYW55IGVudGl0eSAoaW5jbHVkaW5nIGEKICAgICAgY3Jvc3MtY2xhaW0gb3IgY291bnRlcmNsYWltIGluIGEgbGF3c3VpdCkgYWxsZWdpbmcgdGhhdCB0aGUgV29yawogICAgICBvciBhIENvbnRyaWJ1dGlvbiBpbmNvcnBvcmF0ZWQgd2l0aGluIHRoZSBXb3JrIGNvbnN0aXR1dGVzIGRpcmVjdAogICAgICBvciBjb250cmlidXRvcnkgcGF0ZW50IGluZnJpbmdlbWVudCwgdGhlbiBhbnkgcGF0ZW50IGxpY2Vuc2VzCiAgICAgIGdyYW50ZWQgdG8gWW91IHVuZGVyIHRoaXMgTGljZW5zZSBmb3IgdGhhdCBXb3JrIHNoYWxsIHRlcm1pbmF0ZQogICAgICBhcyBvZiB0aGUgZGF0ZSBzdWNoIGxpdGlnYXRpb24gaXMgZmlsZWQuCgogICA0LiBSZWRpc3RyaWJ1dGlvbi4gWW91IG1heSByZXByb2R1Y2UgYW5kIGRpc3RyaWJ1dGUgY29waWVzIG9mIHRoZQogICAgICBXb3JrIG9yIERlcml2YXRpdmUgV29ya3MgdGhlcmVvZiBpbiBhbnkgbWVkaXVtLCB3aXRoIG9yIHdpdGhvdXQKICAgICAgbW9kaWZpY2F0aW9ucywgYW5kIGluIFNvdXJjZSBvciBPYmplY3QgZm9ybSwgcHJvdmlkZWQgdGhhdCBZb3UKICAgICAgbWVldCB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgogICAgICAoYSkgWW91IG11c3QgZ2l2ZSBhbnkgb3RoZXIgcmVjaXBpZW50cyBvZiB0aGUgV29yayBvcgogICAgICAgICAgRGVyaXZhdGl2ZSBXb3JrcyBhIGNvcHkgb2YgdGhpcyBMaWNlbnNlOyBhbmQKCiAgICAgIChiKSBZb3UgbXVzdCBjYXVzZSBhbnkgbW9kaWZpZWQgZmlsZXMgdG8gY2FycnkgcHJvbWluZW50IG5vdGljZXMKICAgICAgICAgIHN0YXRpbmcgdGhhdCBZb3UgY2hhbmdlZCB0aGUgZmlsZXM7IGFuZAoKICAgICAgKGMpIFlvdSBtdXN0IHJldGFpbiwgaW4gdGhlIFNvdXJjZSBmb3JtIG9mIGFueSBEZXJpdmF0aXZlIFdvcmtzCiAgICAgICAgICB0aGF0IFlvdSBkaXN0cmlidXRlLCBhbGwgY29weXJpZ2h0LCBwYXRlbnQsIHRyYWRlbWFyaywgYW5kCiAgICAgICAgICBhdHRyaWJ1dGlvbiBub3RpY2VzIGZyb20gdGhlIFNvdXJjZSBmb3JtIG9mIHRoZSBXb3JrLAogICAgICAgICAgZXhjbHVkaW5nIHRob3NlIG5vdGljZXMgdGhhdCBkbyBub3QgcGVydGFpbiB0byBhbnkgcGFydCBvZgogICAgICAgICAgdGhlIERlcml2YXRpdmUgV29ya3M7IGFuZAoKICAgICAgKGQpIElmIHRoZSBXb3JrIGluY2x1ZGVzIGEgIk5PVElDRSIgdGV4dCBmaWxlIGFzIHBhcnQgb2YgaXRzCiAgICAgICAgICBkaXN0cmlidXRpb24sIHRoZW4gYW55IERlcml2YXRpdmUgV29ya3MgdGhhdCBZb3UgZGlzdHJpYnV0ZSBtdXN0CiAgICAgICAgICBpbmNsdWRlIGEgcmVhZGFibGUgY29weSBvZiB0aGUgYXR0cmlidXRpb24gbm90aWNlcyBjb250YWluZWQKICAgICAgICAgIHdpdGhpbiBzdWNoIE5PVElDRSBmaWxlLCBleGNsdWRpbmcgdGhvc2Ugbm90aWNlcyB0aGF0IGRvIG5vdAogICAgICAgICAgcGVydGFpbiB0byBhbnkgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaW4gYXQgbGVhc3Qgb25lCiAgICAgICAgICBvZiB0aGUgZm9sbG93aW5nIHBsYWNlczogd2l0aGluIGEgTk9USUNFIHRleHQgZmlsZSBkaXN0cmlidXRlZAogICAgICAgICAgYXMgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgd2l0aGluIHRoZSBTb3VyY2UgZm9ybSBvcgogICAgICAgICAgZG9jdW1lbnRhdGlvbiwgaWYgcHJvdmlkZWQgYWxvbmcgd2l0aCB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgb3IsCiAgICAgICAgICB3aXRoaW4gYSBkaXNwbGF5IGdlbmVyYXRlZCBieSB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaWYgYW5kCiAgICAgICAgICB3aGVyZXZlciBzdWNoIHRoaXJkLXBhcnR5IG5vdGljZXMgbm9ybWFsbHkgYXBwZWFyLiBUaGUgY29udGVudHMKICAgICAgICAgIG9mIHRoZSBOT1RJQ0UgZmlsZSBhcmUgZm9yIGluZm9ybWF0aW9uYWwgcHVycG9zZXMgb25seSBhbmQKICAgICAgICAgIGRvIG5vdCBtb2RpZnkgdGhlIExpY2Vuc2UuIFlvdSBtYXkgYWRkIFlvdXIgb3duIGF0dHJpYnV0aW9uCiAgICAgICAgICBub3RpY2VzIHdpdGhpbiBEZXJpdmF0aXZlIFdvcmtzIHRoYXQgWW91IGRpc3RyaWJ1dGUsIGFsb25nc2lkZQogICAgICAgICAgb3IgYXMgYW4gYWRkZW5kdW0gdG8gdGhlIE5PVElDRSB0ZXh0IGZyb20gdGhlIFdvcmssIHByb3ZpZGVkCiAgICAgICAgICB0aGF0IHN1Y2ggYWRkaXRpb25hbCBhdHRyaWJ1dGlvbiBub3RpY2VzIGNhbm5vdCBiZSBjb25zdHJ1ZWQKICAgICAgICAgIGFzIG1vZGlmeWluZyB0aGUgTGljZW5zZS4KCiAgICAgIFlvdSBtYXkgYWRkIFlvdXIgb3duIGNvcHlyaWdodCBzdGF0ZW1lbnQgdG8gWW91ciBtb2RpZmljYXRpb25zIGFuZAogICAgICBtYXkgcHJvdmlkZSBhZGRpdGlvbmFsIG9yIGRpZmZlcmVudCBsaWNlbnNlIHRlcm1zIGFuZCBjb25kaXRpb25zCiAgICAgIGZvciB1c2UsIHJlcHJvZHVjdGlvbiwgb3IgZGlzdHJpYnV0aW9uIG9mIFlvdXIgbW9kaWZpY2F0aW9ucywgb3IKICAgICAgZm9yIGFueSBzdWNoIERlcml2YXRpdmUgV29ya3MgYXMgYSB3aG9sZSwgcHJvdmlkZWQgWW91ciB1c2UsCiAgICAgIHJlcHJvZHVjdGlvbiwgYW5kIGRpc3RyaWJ1dGlvbiBvZiB0aGUgV29yayBvdGhlcndpc2UgY29tcGxpZXMgd2l0aAogICAgICB0aGUgY29uZGl0aW9ucyBzdGF0ZWQgaW4gdGhpcyBMaWNlbnNlLgoKICAgNS4gU3VibWlzc2lvbiBvZiBDb250cmlidXRpb25zLiBVbmxlc3MgWW91IGV4cGxpY2l0bHkgc3RhdGUgb3RoZXJ3aXNlLAogICAgICBhbnkgQ29udHJpYnV0aW9uIGludGVudGlvbmFsbHkgc3VibWl0dGVkIGZvciBpbmNsdXNpb24gaW4gdGhlIFdvcmsKICAgICAgYnkgWW91IHRvIHRoZSBMaWNlbnNvciBzaGFsbCBiZSB1bmRlciB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCB3aXRob3V0IGFueSBhZGRpdGlvbmFsIHRlcm1zIG9yIGNvbmRpdGlvbnMuCiAgICAgIE5vdHdpdGhzdGFuZGluZyB0aGUgYWJvdmUsIG5vdGhpbmcgaGVyZWluIHNoYWxsIHN1cGVyc2VkZSBvciBtb2RpZnkKICAgICAgdGhlIHRlcm1zIG9mIGFueSBzZXBhcmF0ZSBsaWNlbnNlIGFncmVlbWVudCB5b3UgbWF5IGhhdmUgZXhlY3V0ZWQKICAgICAgd2l0aCBMaWNlbnNvciByZWdhcmRpbmcgc3VjaCBDb250cmlidXRpb25zLgoKICAgNi4gVHJhZGVtYXJrcy4gVGhpcyBMaWNlbnNlIGRvZXMgbm90IGdyYW50IHBlcm1pc3Npb24gdG8gdXNlIHRoZSB0cmFkZQogICAgICBuYW1lcywgdHJhZGVtYXJrcywgc2VydmljZSBtYXJrcywgb3IgcHJvZHVjdCBuYW1lcyBvZiB0aGUgTGljZW5zb3IsCiAgICAgIGV4Y2VwdCBhcyByZXF1aXJlZCBmb3IgcmVhc29uYWJsZSBhbmQgY3VzdG9tYXJ5IHVzZSBpbiBkZXNjcmliaW5nIHRoZQogICAgICBvcmlnaW4gb2YgdGhlIFdvcmsgYW5kIHJlcHJvZHVjaW5nIHRoZSBjb250ZW50IG9mIHRoZSBOT1RJQ0UgZmlsZS4KCiAgIDcuIERpc2NsYWltZXIgb2YgV2FycmFudHkuIFVubGVzcyByZXF1aXJlZCBieSBhcHBsaWNhYmxlIGxhdyBvcgogICAgICBhZ3JlZWQgdG8gaW4gd3JpdGluZywgTGljZW5zb3IgcHJvdmlkZXMgdGhlIFdvcmsgKGFuZCBlYWNoCiAgICAgIENvbnRyaWJ1dG9yIHByb3ZpZGVzIGl0cyBDb250cmlidXRpb25zKSBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IKICAgICAgaW1wbGllZCwgaW5jbHVkaW5nLCB3aXRob3V0IGxpbWl0YXRpb24sIGFueSB3YXJyYW50aWVzIG9yIGNvbmRpdGlvbnMKICAgICAgb2YgVElUTEUsIE5PTi1JTkZSSU5HRU1FTlQsIE1FUkNIQU5UQUJJTElUWSwgb3IgRklUTkVTUyBGT1IgQQogICAgICBQQVJUSUNVTEFSIFBVUlBPU0UuIFlvdSBhcmUgc29sZWx5IHJlc3BvbnNpYmxlIGZvciBkZXRlcm1pbmluZyB0aGUKICAgICAgYXBwcm9wcmlhdGVuZXNzIG9mIHVzaW5nIG9yIHJlZGlzdHJpYnV0aW5nIHRoZSBXb3JrIGFuZCBhc3N1bWUgYW55CiAgICAgIHJpc2tzIGFzc29jaWF0ZWQgd2l0aCBZb3VyIGV4ZXJjaXNlIG9mIHBlcm1pc3Npb25zIHVuZGVyIHRoaXMgTGljZW5zZS4KCiAgIDguIExpbWl0YXRpb24gb2YgTGlhYmlsaXR5LiBJbiBubyBldmVudCBhbmQgdW5kZXIgbm8gbGVnYWwgdGhlb3J5LAogICAgICB3aGV0aGVyIGluIHRvcnQgKGluY2x1ZGluZyBuZWdsaWdlbmNlKSwgY29udHJhY3QsIG9yIG90aGVyd2lzZSwKICAgICAgdW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IChzdWNoIGFzIGRlbGliZXJhdGUgYW5kIGdyb3NzbHkKICAgICAgbmVnbGlnZW50IGFjdHMpIG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzaGFsbCBhbnkgQ29udHJpYnV0b3IgYmUKICAgICAgbGlhYmxlIHRvIFlvdSBmb3IgZGFtYWdlcywgaW5jbHVkaW5nIGFueSBkaXJlY3QsIGluZGlyZWN0LCBzcGVjaWFsLAogICAgICBpbmNpZGVudGFsLCBvciBjb25zZXF1ZW50aWFsIGRhbWFnZXMgb2YgYW55IGNoYXJhY3RlciBhcmlzaW5nIGFzIGEKICAgICAgcmVzdWx0IG9mIHRoaXMgTGljZW5zZSBvciBvdXQgb2YgdGhlIHVzZSBvciBpbmFiaWxpdHkgdG8gdXNlIHRoZQogICAgICBXb3JrIChpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIGRhbWFnZXMgZm9yIGxvc3Mgb2YgZ29vZHdpbGwsCiAgICAgIHdvcmsgc3RvcHBhZ2UsIGNvbXB1dGVyIGZhaWx1cmUgb3IgbWFsZnVuY3Rpb24sIG9yIGFueSBhbmQgYWxsCiAgICAgIG90aGVyIGNvbW1lcmNpYWwgZGFtYWdlcyBvciBsb3NzZXMpLCBldmVuIGlmIHN1Y2ggQ29udHJpYnV0b3IKICAgICAgaGFzIGJlZW4gYWR2aXNlZCBvZiB0aGUgcG9zc2liaWxpdHkgb2Ygc3VjaCBkYW1hZ2VzLgoKICAgOS4gQWNjZXB0aW5nIFdhcnJhbnR5IG9yIEFkZGl0aW9uYWwgTGlhYmlsaXR5LiBXaGlsZSByZWRpc3RyaWJ1dGluZwogICAgICB0aGUgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIFlvdSBtYXkgY2hvb3NlIHRvIG9mZmVyLAogICAgICBhbmQgY2hhcmdlIGEgZmVlIGZvciwgYWNjZXB0YW5jZSBvZiBzdXBwb3J0LCB3YXJyYW50eSwgaW5kZW1uaXR5LAogICAgICBvciBvdGhlciBsaWFiaWxpdHkgb2JsaWdhdGlvbnMgYW5kL29yIHJpZ2h0cyBjb25zaXN0ZW50IHdpdGggdGhpcwogICAgICBMaWNlbnNlLiBIb3dldmVyLCBpbiBhY2NlcHRpbmcgc3VjaCBvYmxpZ2F0aW9ucywgWW91IG1heSBhY3Qgb25seQogICAgICBvbiBZb3VyIG93biBiZWhhbGYgYW5kIG9uIFlvdXIgc29sZSByZXNwb25zaWJpbGl0eSwgbm90IG9uIGJlaGFsZgogICAgICBvZiBhbnkgb3RoZXIgQ29udHJpYnV0b3IsIGFuZCBvbmx5IGlmIFlvdSBhZ3JlZSB0byBpbmRlbW5pZnksCiAgICAgIGRlZmVuZCwgYW5kIGhvbGQgZWFjaCBDb250cmlidXRvciBoYXJtbGVzcyBmb3IgYW55IGxpYWJpbGl0eQogICAgICBpbmN1cnJlZCBieSwgb3IgY2xhaW1zIGFzc2VydGVkIGFnYWluc3QsIHN1Y2ggQ29udHJpYnV0b3IgYnkgcmVhc29uCiAgICAgIG9mIHlvdXIgYWNjZXB0aW5nIGFueSBzdWNoIHdhcnJhbnR5IG9yIGFkZGl0aW9uYWwgbGlhYmlsaXR5LgoKICAgRU5EIE9GIFRFUk1TIEFORCBDT05ESVRJT05TCgogICBBUFBFTkRJWDogSG93IHRvIGFwcGx5IHRoZSBBcGFjaGUgTGljZW5zZSB0byB5b3VyIHdvcmsuCgogICAgICBUbyBhcHBseSB0aGUgQXBhY2hlIExpY2Vuc2UgdG8geW91ciB3b3JrLCBhdHRhY2ggdGhlIGZvbGxvd2luZwogICAgICBib2lsZXJwbGF0ZSBub3RpY2UsIHdpdGggdGhlIGZpZWxkcyBlbmNsb3NlZCBieSBicmFja2V0cyAiW10iCiAgICAgIHJlcGxhY2VkIHdpdGggeW91ciBvd24gaWRlbnRpZnlpbmcgaW5mb3JtYXRpb24uIChEb24ndCBpbmNsdWRlCiAgICAgIHRoZSBicmFja2V0cyEpICBUaGUgdGV4dCBzaG91bGQgYmUgZW5jbG9zZWQgaW4gdGhlIGFwcHJvcHJpYXRlCiAgICAgIGNvbW1lbnQgc3ludGF4IGZvciB0aGUgZmlsZSBmb3JtYXQuIFdlIGFsc28gcmVjb21tZW5kIHRoYXQgYQogICAgICBmaWxlIG9yIGNsYXNzIG5hbWUgYW5kIGRlc2NyaXB0aW9uIG9mIHB1cnBvc2UgYmUgaW5jbHVkZWQgb24gdGhlCiAgICAgIHNhbWUgInByaW50ZWQgcGFnZSIgYXMgdGhlIGNvcHlyaWdodCBub3RpY2UgZm9yIGVhc2llcgogICAgICBpZGVudGlmaWNhdGlvbiB3aXRoaW4gdGhpcmQtcGFydHkgYXJjaGl2ZXMuCgogICBDb3B5cmlnaHQgW3l5eXldIFtuYW1lIG9mIGNvcHlyaWdodCBvd25lcl0KCiAgIExpY2Vuc2VkIHVuZGVyIHRoZSBBcGFjaGUgTGljZW5zZSwgVmVyc2lvbiAyLjAgKHRoZSAiTGljZW5zZSIpOwogICB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCiAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgIGh0dHA6Ly93d3cuYXBhY2hlLm9yZy9saWNlbnNlcy9MSUNFTlNFLTIuMAoKICAgVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQogICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IgaW1wbGllZC4KICAgU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAogICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4= + https://www.apache.org/licenses/LICENSE-2.0.txt + + + pkg:maven/com.acme/tomcat-catalina@9.0.14?packaging=jar + + + + Apache Super Heros + Apache + org.apache.tomcat + tomcat-catalina + 9.0.14 + Apache Catalina + + + Apache-2.0 + + + pkg:maven/org.apache.tomcat/tomcat-catalina@9.0.14?packaging=jar + + + + + 7638417db6d59f3c431d3e1f261cc637155684cd + https://location/to/7638417db6d59f3c431d3e1f261cc637155684cd + + 2018-11-07T22:01:45Z + John Doe + john.doe@example.com + + + 2018-11-07T22:01:45Z + Jane Doe + jane.doe@example.com + + Initial commit + + + Commentary here + + + + + Example Inc. + https://example.com + https://example.net + + Example Support AMER + support@example.com + 800-555-1212 + + + Example Support APAC + support@apac.example.com + + + Example Super Heros + org.example + mylibrary + 1.0.0 + required + + 2342c2eaf1feb9a80195dbaddf2ebaa3 + 68b78babe00a053f9e35ec6a2d9080f5b90122b0 + 708f1f53b41f11f02d12a11b1a38d2905d47b099afc71a0f1124ef8582ec7313 + 387b7ae16b9cae45f830671541539bf544202faae5aac544a93b7b0a04f5f846fa2f4e81ef3f1677e13aed7496408a441f5657ab6d54423e56bf6f38da124aef + + + EPL-2.0 OR GPL-2.0-with-classpath-exception + + Copyright Example Inc. All rights reserved. + cpe:/a:example:myapplication:1.0.0 + pkg:maven/com.example/myapplication@1.0.0?packaging=war + false + + + http://example.org/docs + All component versions are documented here + + + http://example.org/security + + + + + Example Super Heros + com.example + myframework + 1.0.0 + Example Inc, enterprise framework + required + + cfcb0b64aacd2f81c1cd546543de965a + 7fbeef2346c45d565c3341f037bce4e088af8a52 + 0384db3cec55d86a6898c489fdb75a8e75fe66b26639634983d2f3c3558493d1 + 854909cdb9e3ca183056837144aab6d8069b377bd66445087cc7157bf0c3f620418705dd0b83bdc2f73a508c2bdb316ca1809d75ee6972d02023a3e7dd655c79 + + + + Some random license + + + pkg:maven/com.example/myframework@1.0.0?packaging=war + false + + + http://example.com/myframework + + + http://example.com/security + + + + + diff --git a/src/test/resources/unit/cyclonedx/valid-bom-1.3.json b/src/test/resources/unit/cyclonedx/valid-bom-1.3.json new file mode 100644 index 0000000000..75550db716 --- /dev/null +++ b/src/test/resources/unit/cyclonedx/valid-bom-1.3.json @@ -0,0 +1,177 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.3", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2020-04-13T20:20:39+00:00", + "tools": [ + { + "vendor": "Awesome Vendor", + "name": "Awesome Tool", + "version": "9.1.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "25ed8e31b995bb927966616df2a42b979a2717f0" + }, + { + "alg": "SHA-256", + "content": "a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df" + } + ] + } + ], + "authors": [ + { + "name": "Samantha Wright", + "email": "samantha.wright@example.com", + "phone": "800-555-1212" + } + ], + "component": { + "type": "application", + "author": "Acme Super Heros", + "name": "Acme Application", + "version": "9.1.1", + "swid": { + "tagId": "swidgen-242eb18a-503e-ca37-393b-cf156ef09691_9.1.1", + "name": "Acme Application", + "version": "9.1.1", + "text": { + "contentType": "text/xml", + "encoding": "base64", + "content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg==" + } + } + }, + "manufacture": { + "name": "Acme, Inc.", + "url": [ + "https://example.com" + ], + "contact": [ + { + "name": "Acme Professional Services", + "email": "professional.services@example.com" + } + ] + }, + "supplier": { + "name": "Acme, Inc.", + "url": [ + "https://example.com" + ], + "contact": [ + { + "name": "Acme Distribution", + "email": "distribution@example.com" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:npm/acme/component@1.0.0", + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "tomcat-catalina", + "version": "9.0.14", + "hashes": [ + { + "alg": "MD5", + "content": "3942447fac867ae5cdb3229b658f4d48" + }, + { + "alg": "SHA-1", + "content": "e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a" + }, + { + "alg": "SHA-256", + "content": "f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b" + }, + { + "alg": "SHA-512", + "content": "e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0", + "text": { + "contentType": "text/plain", + "encoding": "base64", + "content": "CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFwYWNoZSBMaWNlbnNlCiAgICAgICAgICAgICAgICAgICAgICAgICAgIFZlcnNpb24gMi4wLCBKYW51YXJ5IDIwMDQKICAgICAgICAgICAgICAgICAgICAgICAgaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzLwoKICAgVEVSTVMgQU5EIENPTkRJVElPTlMgRk9SIFVTRSwgUkVQUk9EVUNUSU9OLCBBTkQgRElTVFJJQlVUSU9OCgogICAxLiBEZWZpbml0aW9ucy4KCiAgICAgICJMaWNlbnNlIiBzaGFsbCBtZWFuIHRoZSB0ZXJtcyBhbmQgY29uZGl0aW9ucyBmb3IgdXNlLCByZXByb2R1Y3Rpb24sCiAgICAgIGFuZCBkaXN0cmlidXRpb24gYXMgZGVmaW5lZCBieSBTZWN0aW9ucyAxIHRocm91Z2ggOSBvZiB0aGlzIGRvY3VtZW50LgoKICAgICAgIkxpY2Vuc29yIiBzaGFsbCBtZWFuIHRoZSBjb3B5cmlnaHQgb3duZXIgb3IgZW50aXR5IGF1dGhvcml6ZWQgYnkKICAgICAgdGhlIGNvcHlyaWdodCBvd25lciB0aGF0IGlzIGdyYW50aW5nIHRoZSBMaWNlbnNlLgoKICAgICAgIkxlZ2FsIEVudGl0eSIgc2hhbGwgbWVhbiB0aGUgdW5pb24gb2YgdGhlIGFjdGluZyBlbnRpdHkgYW5kIGFsbAogICAgICBvdGhlciBlbnRpdGllcyB0aGF0IGNvbnRyb2wsIGFyZSBjb250cm9sbGVkIGJ5LCBvciBhcmUgdW5kZXIgY29tbW9uCiAgICAgIGNvbnRyb2wgd2l0aCB0aGF0IGVudGl0eS4gRm9yIHRoZSBwdXJwb3NlcyBvZiB0aGlzIGRlZmluaXRpb24sCiAgICAgICJjb250cm9sIiBtZWFucyAoaSkgdGhlIHBvd2VyLCBkaXJlY3Qgb3IgaW5kaXJlY3QsIHRvIGNhdXNlIHRoZQogICAgICBkaXJlY3Rpb24gb3IgbWFuYWdlbWVudCBvZiBzdWNoIGVudGl0eSwgd2hldGhlciBieSBjb250cmFjdCBvcgogICAgICBvdGhlcndpc2UsIG9yIChpaSkgb3duZXJzaGlwIG9mIGZpZnR5IHBlcmNlbnQgKDUwJSkgb3IgbW9yZSBvZiB0aGUKICAgICAgb3V0c3RhbmRpbmcgc2hhcmVzLCBvciAoaWlpKSBiZW5lZmljaWFsIG93bmVyc2hpcCBvZiBzdWNoIGVudGl0eS4KCiAgICAgICJZb3UiIChvciAiWW91ciIpIHNoYWxsIG1lYW4gYW4gaW5kaXZpZHVhbCBvciBMZWdhbCBFbnRpdHkKICAgICAgZXhlcmNpc2luZyBwZXJtaXNzaW9ucyBncmFudGVkIGJ5IHRoaXMgTGljZW5zZS4KCiAgICAgICJTb3VyY2UiIGZvcm0gc2hhbGwgbWVhbiB0aGUgcHJlZmVycmVkIGZvcm0gZm9yIG1ha2luZyBtb2RpZmljYXRpb25zLAogICAgICBpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIHNvZnR3YXJlIHNvdXJjZSBjb2RlLCBkb2N1bWVudGF0aW9uCiAgICAgIHNvdXJjZSwgYW5kIGNvbmZpZ3VyYXRpb24gZmlsZXMuCgogICAgICAiT2JqZWN0IiBmb3JtIHNoYWxsIG1lYW4gYW55IGZvcm0gcmVzdWx0aW5nIGZyb20gbWVjaGFuaWNhbAogICAgICB0cmFuc2Zvcm1hdGlvbiBvciB0cmFuc2xhdGlvbiBvZiBhIFNvdXJjZSBmb3JtLCBpbmNsdWRpbmcgYnV0CiAgICAgIG5vdCBsaW1pdGVkIHRvIGNvbXBpbGVkIG9iamVjdCBjb2RlLCBnZW5lcmF0ZWQgZG9jdW1lbnRhdGlvbiwKICAgICAgYW5kIGNvbnZlcnNpb25zIHRvIG90aGVyIG1lZGlhIHR5cGVzLgoKICAgICAgIldvcmsiIHNoYWxsIG1lYW4gdGhlIHdvcmsgb2YgYXV0aG9yc2hpcCwgd2hldGhlciBpbiBTb3VyY2Ugb3IKICAgICAgT2JqZWN0IGZvcm0sIG1hZGUgYXZhaWxhYmxlIHVuZGVyIHRoZSBMaWNlbnNlLCBhcyBpbmRpY2F0ZWQgYnkgYQogICAgICBjb3B5cmlnaHQgbm90aWNlIHRoYXQgaXMgaW5jbHVkZWQgaW4gb3IgYXR0YWNoZWQgdG8gdGhlIHdvcmsKICAgICAgKGFuIGV4YW1wbGUgaXMgcHJvdmlkZWQgaW4gdGhlIEFwcGVuZGl4IGJlbG93KS4KCiAgICAgICJEZXJpdmF0aXZlIFdvcmtzIiBzaGFsbCBtZWFuIGFueSB3b3JrLCB3aGV0aGVyIGluIFNvdXJjZSBvciBPYmplY3QKICAgICAgZm9ybSwgdGhhdCBpcyBiYXNlZCBvbiAob3IgZGVyaXZlZCBmcm9tKSB0aGUgV29yayBhbmQgZm9yIHdoaWNoIHRoZQogICAgICBlZGl0b3JpYWwgcmV2aXNpb25zLCBhbm5vdGF0aW9ucywgZWxhYm9yYXRpb25zLCBvciBvdGhlciBtb2RpZmljYXRpb25zCiAgICAgIHJlcHJlc2VudCwgYXMgYSB3aG9sZSwgYW4gb3JpZ2luYWwgd29yayBvZiBhdXRob3JzaGlwLiBGb3IgdGhlIHB1cnBvc2VzCiAgICAgIG9mIHRoaXMgTGljZW5zZSwgRGVyaXZhdGl2ZSBXb3JrcyBzaGFsbCBub3QgaW5jbHVkZSB3b3JrcyB0aGF0IHJlbWFpbgogICAgICBzZXBhcmFibGUgZnJvbSwgb3IgbWVyZWx5IGxpbmsgKG9yIGJpbmQgYnkgbmFtZSkgdG8gdGhlIGludGVyZmFjZXMgb2YsCiAgICAgIHRoZSBXb3JrIGFuZCBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YuCgogICAgICAiQ29udHJpYnV0aW9uIiBzaGFsbCBtZWFuIGFueSB3b3JrIG9mIGF1dGhvcnNoaXAsIGluY2x1ZGluZwogICAgICB0aGUgb3JpZ2luYWwgdmVyc2lvbiBvZiB0aGUgV29yayBhbmQgYW55IG1vZGlmaWNhdGlvbnMgb3IgYWRkaXRpb25zCiAgICAgIHRvIHRoYXQgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIHRoYXQgaXMgaW50ZW50aW9uYWxseQogICAgICBzdWJtaXR0ZWQgdG8gTGljZW5zb3IgZm9yIGluY2x1c2lvbiBpbiB0aGUgV29yayBieSB0aGUgY29weXJpZ2h0IG93bmVyCiAgICAgIG9yIGJ5IGFuIGluZGl2aWR1YWwgb3IgTGVnYWwgRW50aXR5IGF1dGhvcml6ZWQgdG8gc3VibWl0IG9uIGJlaGFsZiBvZgogICAgICB0aGUgY29weXJpZ2h0IG93bmVyLiBGb3IgdGhlIHB1cnBvc2VzIG9mIHRoaXMgZGVmaW5pdGlvbiwgInN1Ym1pdHRlZCIKICAgICAgbWVhbnMgYW55IGZvcm0gb2YgZWxlY3Ryb25pYywgdmVyYmFsLCBvciB3cml0dGVuIGNvbW11bmljYXRpb24gc2VudAogICAgICB0byB0aGUgTGljZW5zb3Igb3IgaXRzIHJlcHJlc2VudGF0aXZlcywgaW5jbHVkaW5nIGJ1dCBub3QgbGltaXRlZCB0bwogICAgICBjb21tdW5pY2F0aW9uIG9uIGVsZWN0cm9uaWMgbWFpbGluZyBsaXN0cywgc291cmNlIGNvZGUgY29udHJvbCBzeXN0ZW1zLAogICAgICBhbmQgaXNzdWUgdHJhY2tpbmcgc3lzdGVtcyB0aGF0IGFyZSBtYW5hZ2VkIGJ5LCBvciBvbiBiZWhhbGYgb2YsIHRoZQogICAgICBMaWNlbnNvciBmb3IgdGhlIHB1cnBvc2Ugb2YgZGlzY3Vzc2luZyBhbmQgaW1wcm92aW5nIHRoZSBXb3JrLCBidXQKICAgICAgZXhjbHVkaW5nIGNvbW11bmljYXRpb24gdGhhdCBpcyBjb25zcGljdW91c2x5IG1hcmtlZCBvciBvdGhlcndpc2UKICAgICAgZGVzaWduYXRlZCBpbiB3cml0aW5nIGJ5IHRoZSBjb3B5cmlnaHQgb3duZXIgYXMgIk5vdCBhIENvbnRyaWJ1dGlvbi4iCgogICAgICAiQ29udHJpYnV0b3IiIHNoYWxsIG1lYW4gTGljZW5zb3IgYW5kIGFueSBpbmRpdmlkdWFsIG9yIExlZ2FsIEVudGl0eQogICAgICBvbiBiZWhhbGYgb2Ygd2hvbSBhIENvbnRyaWJ1dGlvbiBoYXMgYmVlbiByZWNlaXZlZCBieSBMaWNlbnNvciBhbmQKICAgICAgc3Vic2VxdWVudGx5IGluY29ycG9yYXRlZCB3aXRoaW4gdGhlIFdvcmsuCgogICAyLiBHcmFudCBvZiBDb3B5cmlnaHQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICBjb3B5cmlnaHQgbGljZW5zZSB0byByZXByb2R1Y2UsIHByZXBhcmUgRGVyaXZhdGl2ZSBXb3JrcyBvZiwKICAgICAgcHVibGljbHkgZGlzcGxheSwgcHVibGljbHkgcGVyZm9ybSwgc3VibGljZW5zZSwgYW5kIGRpc3RyaWJ1dGUgdGhlCiAgICAgIFdvcmsgYW5kIHN1Y2ggRGVyaXZhdGl2ZSBXb3JrcyBpbiBTb3VyY2Ugb3IgT2JqZWN0IGZvcm0uCgogICAzLiBHcmFudCBvZiBQYXRlbnQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICAoZXhjZXB0IGFzIHN0YXRlZCBpbiB0aGlzIHNlY3Rpb24pIHBhdGVudCBsaWNlbnNlIHRvIG1ha2UsIGhhdmUgbWFkZSwKICAgICAgdXNlLCBvZmZlciB0byBzZWxsLCBzZWxsLCBpbXBvcnQsIGFuZCBvdGhlcndpc2UgdHJhbnNmZXIgdGhlIFdvcmssCiAgICAgIHdoZXJlIHN1Y2ggbGljZW5zZSBhcHBsaWVzIG9ubHkgdG8gdGhvc2UgcGF0ZW50IGNsYWltcyBsaWNlbnNhYmxlCiAgICAgIGJ5IHN1Y2ggQ29udHJpYnV0b3IgdGhhdCBhcmUgbmVjZXNzYXJpbHkgaW5mcmluZ2VkIGJ5IHRoZWlyCiAgICAgIENvbnRyaWJ1dGlvbihzKSBhbG9uZSBvciBieSBjb21iaW5hdGlvbiBvZiB0aGVpciBDb250cmlidXRpb24ocykKICAgICAgd2l0aCB0aGUgV29yayB0byB3aGljaCBzdWNoIENvbnRyaWJ1dGlvbihzKSB3YXMgc3VibWl0dGVkLiBJZiBZb3UKICAgICAgaW5zdGl0dXRlIHBhdGVudCBsaXRpZ2F0aW9uIGFnYWluc3QgYW55IGVudGl0eSAoaW5jbHVkaW5nIGEKICAgICAgY3Jvc3MtY2xhaW0gb3IgY291bnRlcmNsYWltIGluIGEgbGF3c3VpdCkgYWxsZWdpbmcgdGhhdCB0aGUgV29yawogICAgICBvciBhIENvbnRyaWJ1dGlvbiBpbmNvcnBvcmF0ZWQgd2l0aGluIHRoZSBXb3JrIGNvbnN0aXR1dGVzIGRpcmVjdAogICAgICBvciBjb250cmlidXRvcnkgcGF0ZW50IGluZnJpbmdlbWVudCwgdGhlbiBhbnkgcGF0ZW50IGxpY2Vuc2VzCiAgICAgIGdyYW50ZWQgdG8gWW91IHVuZGVyIHRoaXMgTGljZW5zZSBmb3IgdGhhdCBXb3JrIHNoYWxsIHRlcm1pbmF0ZQogICAgICBhcyBvZiB0aGUgZGF0ZSBzdWNoIGxpdGlnYXRpb24gaXMgZmlsZWQuCgogICA0LiBSZWRpc3RyaWJ1dGlvbi4gWW91IG1heSByZXByb2R1Y2UgYW5kIGRpc3RyaWJ1dGUgY29waWVzIG9mIHRoZQogICAgICBXb3JrIG9yIERlcml2YXRpdmUgV29ya3MgdGhlcmVvZiBpbiBhbnkgbWVkaXVtLCB3aXRoIG9yIHdpdGhvdXQKICAgICAgbW9kaWZpY2F0aW9ucywgYW5kIGluIFNvdXJjZSBvciBPYmplY3QgZm9ybSwgcHJvdmlkZWQgdGhhdCBZb3UKICAgICAgbWVldCB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgogICAgICAoYSkgWW91IG11c3QgZ2l2ZSBhbnkgb3RoZXIgcmVjaXBpZW50cyBvZiB0aGUgV29yayBvcgogICAgICAgICAgRGVyaXZhdGl2ZSBXb3JrcyBhIGNvcHkgb2YgdGhpcyBMaWNlbnNlOyBhbmQKCiAgICAgIChiKSBZb3UgbXVzdCBjYXVzZSBhbnkgbW9kaWZpZWQgZmlsZXMgdG8gY2FycnkgcHJvbWluZW50IG5vdGljZXMKICAgICAgICAgIHN0YXRpbmcgdGhhdCBZb3UgY2hhbmdlZCB0aGUgZmlsZXM7IGFuZAoKICAgICAgKGMpIFlvdSBtdXN0IHJldGFpbiwgaW4gdGhlIFNvdXJjZSBmb3JtIG9mIGFueSBEZXJpdmF0aXZlIFdvcmtzCiAgICAgICAgICB0aGF0IFlvdSBkaXN0cmlidXRlLCBhbGwgY29weXJpZ2h0LCBwYXRlbnQsIHRyYWRlbWFyaywgYW5kCiAgICAgICAgICBhdHRyaWJ1dGlvbiBub3RpY2VzIGZyb20gdGhlIFNvdXJjZSBmb3JtIG9mIHRoZSBXb3JrLAogICAgICAgICAgZXhjbHVkaW5nIHRob3NlIG5vdGljZXMgdGhhdCBkbyBub3QgcGVydGFpbiB0byBhbnkgcGFydCBvZgogICAgICAgICAgdGhlIERlcml2YXRpdmUgV29ya3M7IGFuZAoKICAgICAgKGQpIElmIHRoZSBXb3JrIGluY2x1ZGVzIGEgIk5PVElDRSIgdGV4dCBmaWxlIGFzIHBhcnQgb2YgaXRzCiAgICAgICAgICBkaXN0cmlidXRpb24sIHRoZW4gYW55IERlcml2YXRpdmUgV29ya3MgdGhhdCBZb3UgZGlzdHJpYnV0ZSBtdXN0CiAgICAgICAgICBpbmNsdWRlIGEgcmVhZGFibGUgY29weSBvZiB0aGUgYXR0cmlidXRpb24gbm90aWNlcyBjb250YWluZWQKICAgICAgICAgIHdpdGhpbiBzdWNoIE5PVElDRSBmaWxlLCBleGNsdWRpbmcgdGhvc2Ugbm90aWNlcyB0aGF0IGRvIG5vdAogICAgICAgICAgcGVydGFpbiB0byBhbnkgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaW4gYXQgbGVhc3Qgb25lCiAgICAgICAgICBvZiB0aGUgZm9sbG93aW5nIHBsYWNlczogd2l0aGluIGEgTk9USUNFIHRleHQgZmlsZSBkaXN0cmlidXRlZAogICAgICAgICAgYXMgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgd2l0aGluIHRoZSBTb3VyY2UgZm9ybSBvcgogICAgICAgICAgZG9jdW1lbnRhdGlvbiwgaWYgcHJvdmlkZWQgYWxvbmcgd2l0aCB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgb3IsCiAgICAgICAgICB3aXRoaW4gYSBkaXNwbGF5IGdlbmVyYXRlZCBieSB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaWYgYW5kCiAgICAgICAgICB3aGVyZXZlciBzdWNoIHRoaXJkLXBhcnR5IG5vdGljZXMgbm9ybWFsbHkgYXBwZWFyLiBUaGUgY29udGVudHMKICAgICAgICAgIG9mIHRoZSBOT1RJQ0UgZmlsZSBhcmUgZm9yIGluZm9ybWF0aW9uYWwgcHVycG9zZXMgb25seSBhbmQKICAgICAgICAgIGRvIG5vdCBtb2RpZnkgdGhlIExpY2Vuc2UuIFlvdSBtYXkgYWRkIFlvdXIgb3duIGF0dHJpYnV0aW9uCiAgICAgICAgICBub3RpY2VzIHdpdGhpbiBEZXJpdmF0aXZlIFdvcmtzIHRoYXQgWW91IGRpc3RyaWJ1dGUsIGFsb25nc2lkZQogICAgICAgICAgb3IgYXMgYW4gYWRkZW5kdW0gdG8gdGhlIE5PVElDRSB0ZXh0IGZyb20gdGhlIFdvcmssIHByb3ZpZGVkCiAgICAgICAgICB0aGF0IHN1Y2ggYWRkaXRpb25hbCBhdHRyaWJ1dGlvbiBub3RpY2VzIGNhbm5vdCBiZSBjb25zdHJ1ZWQKICAgICAgICAgIGFzIG1vZGlmeWluZyB0aGUgTGljZW5zZS4KCiAgICAgIFlvdSBtYXkgYWRkIFlvdXIgb3duIGNvcHlyaWdodCBzdGF0ZW1lbnQgdG8gWW91ciBtb2RpZmljYXRpb25zIGFuZAogICAgICBtYXkgcHJvdmlkZSBhZGRpdGlvbmFsIG9yIGRpZmZlcmVudCBsaWNlbnNlIHRlcm1zIGFuZCBjb25kaXRpb25zCiAgICAgIGZvciB1c2UsIHJlcHJvZHVjdGlvbiwgb3IgZGlzdHJpYnV0aW9uIG9mIFlvdXIgbW9kaWZpY2F0aW9ucywgb3IKICAgICAgZm9yIGFueSBzdWNoIERlcml2YXRpdmUgV29ya3MgYXMgYSB3aG9sZSwgcHJvdmlkZWQgWW91ciB1c2UsCiAgICAgIHJlcHJvZHVjdGlvbiwgYW5kIGRpc3RyaWJ1dGlvbiBvZiB0aGUgV29yayBvdGhlcndpc2UgY29tcGxpZXMgd2l0aAogICAgICB0aGUgY29uZGl0aW9ucyBzdGF0ZWQgaW4gdGhpcyBMaWNlbnNlLgoKICAgNS4gU3VibWlzc2lvbiBvZiBDb250cmlidXRpb25zLiBVbmxlc3MgWW91IGV4cGxpY2l0bHkgc3RhdGUgb3RoZXJ3aXNlLAogICAgICBhbnkgQ29udHJpYnV0aW9uIGludGVudGlvbmFsbHkgc3VibWl0dGVkIGZvciBpbmNsdXNpb24gaW4gdGhlIFdvcmsKICAgICAgYnkgWW91IHRvIHRoZSBMaWNlbnNvciBzaGFsbCBiZSB1bmRlciB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCB3aXRob3V0IGFueSBhZGRpdGlvbmFsIHRlcm1zIG9yIGNvbmRpdGlvbnMuCiAgICAgIE5vdHdpdGhzdGFuZGluZyB0aGUgYWJvdmUsIG5vdGhpbmcgaGVyZWluIHNoYWxsIHN1cGVyc2VkZSBvciBtb2RpZnkKICAgICAgdGhlIHRlcm1zIG9mIGFueSBzZXBhcmF0ZSBsaWNlbnNlIGFncmVlbWVudCB5b3UgbWF5IGhhdmUgZXhlY3V0ZWQKICAgICAgd2l0aCBMaWNlbnNvciByZWdhcmRpbmcgc3VjaCBDb250cmlidXRpb25zLgoKICAgNi4gVHJhZGVtYXJrcy4gVGhpcyBMaWNlbnNlIGRvZXMgbm90IGdyYW50IHBlcm1pc3Npb24gdG8gdXNlIHRoZSB0cmFkZQogICAgICBuYW1lcywgdHJhZGVtYXJrcywgc2VydmljZSBtYXJrcywgb3IgcHJvZHVjdCBuYW1lcyBvZiB0aGUgTGljZW5zb3IsCiAgICAgIGV4Y2VwdCBhcyByZXF1aXJlZCBmb3IgcmVhc29uYWJsZSBhbmQgY3VzdG9tYXJ5IHVzZSBpbiBkZXNjcmliaW5nIHRoZQogICAgICBvcmlnaW4gb2YgdGhlIFdvcmsgYW5kIHJlcHJvZHVjaW5nIHRoZSBjb250ZW50IG9mIHRoZSBOT1RJQ0UgZmlsZS4KCiAgIDcuIERpc2NsYWltZXIgb2YgV2FycmFudHkuIFVubGVzcyByZXF1aXJlZCBieSBhcHBsaWNhYmxlIGxhdyBvcgogICAgICBhZ3JlZWQgdG8gaW4gd3JpdGluZywgTGljZW5zb3IgcHJvdmlkZXMgdGhlIFdvcmsgKGFuZCBlYWNoCiAgICAgIENvbnRyaWJ1dG9yIHByb3ZpZGVzIGl0cyBDb250cmlidXRpb25zKSBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IKICAgICAgaW1wbGllZCwgaW5jbHVkaW5nLCB3aXRob3V0IGxpbWl0YXRpb24sIGFueSB3YXJyYW50aWVzIG9yIGNvbmRpdGlvbnMKICAgICAgb2YgVElUTEUsIE5PTi1JTkZSSU5HRU1FTlQsIE1FUkNIQU5UQUJJTElUWSwgb3IgRklUTkVTUyBGT1IgQQogICAgICBQQVJUSUNVTEFSIFBVUlBPU0UuIFlvdSBhcmUgc29sZWx5IHJlc3BvbnNpYmxlIGZvciBkZXRlcm1pbmluZyB0aGUKICAgICAgYXBwcm9wcmlhdGVuZXNzIG9mIHVzaW5nIG9yIHJlZGlzdHJpYnV0aW5nIHRoZSBXb3JrIGFuZCBhc3N1bWUgYW55CiAgICAgIHJpc2tzIGFzc29jaWF0ZWQgd2l0aCBZb3VyIGV4ZXJjaXNlIG9mIHBlcm1pc3Npb25zIHVuZGVyIHRoaXMgTGljZW5zZS4KCiAgIDguIExpbWl0YXRpb24gb2YgTGlhYmlsaXR5LiBJbiBubyBldmVudCBhbmQgdW5kZXIgbm8gbGVnYWwgdGhlb3J5LAogICAgICB3aGV0aGVyIGluIHRvcnQgKGluY2x1ZGluZyBuZWdsaWdlbmNlKSwgY29udHJhY3QsIG9yIG90aGVyd2lzZSwKICAgICAgdW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IChzdWNoIGFzIGRlbGliZXJhdGUgYW5kIGdyb3NzbHkKICAgICAgbmVnbGlnZW50IGFjdHMpIG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzaGFsbCBhbnkgQ29udHJpYnV0b3IgYmUKICAgICAgbGlhYmxlIHRvIFlvdSBmb3IgZGFtYWdlcywgaW5jbHVkaW5nIGFueSBkaXJlY3QsIGluZGlyZWN0LCBzcGVjaWFsLAogICAgICBpbmNpZGVudGFsLCBvciBjb25zZXF1ZW50aWFsIGRhbWFnZXMgb2YgYW55IGNoYXJhY3RlciBhcmlzaW5nIGFzIGEKICAgICAgcmVzdWx0IG9mIHRoaXMgTGljZW5zZSBvciBvdXQgb2YgdGhlIHVzZSBvciBpbmFiaWxpdHkgdG8gdXNlIHRoZQogICAgICBXb3JrIChpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIGRhbWFnZXMgZm9yIGxvc3Mgb2YgZ29vZHdpbGwsCiAgICAgIHdvcmsgc3RvcHBhZ2UsIGNvbXB1dGVyIGZhaWx1cmUgb3IgbWFsZnVuY3Rpb24sIG9yIGFueSBhbmQgYWxsCiAgICAgIG90aGVyIGNvbW1lcmNpYWwgZGFtYWdlcyBvciBsb3NzZXMpLCBldmVuIGlmIHN1Y2ggQ29udHJpYnV0b3IKICAgICAgaGFzIGJlZW4gYWR2aXNlZCBvZiB0aGUgcG9zc2liaWxpdHkgb2Ygc3VjaCBkYW1hZ2VzLgoKICAgOS4gQWNjZXB0aW5nIFdhcnJhbnR5IG9yIEFkZGl0aW9uYWwgTGlhYmlsaXR5LiBXaGlsZSByZWRpc3RyaWJ1dGluZwogICAgICB0aGUgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIFlvdSBtYXkgY2hvb3NlIHRvIG9mZmVyLAogICAgICBhbmQgY2hhcmdlIGEgZmVlIGZvciwgYWNjZXB0YW5jZSBvZiBzdXBwb3J0LCB3YXJyYW50eSwgaW5kZW1uaXR5LAogICAgICBvciBvdGhlciBsaWFiaWxpdHkgb2JsaWdhdGlvbnMgYW5kL29yIHJpZ2h0cyBjb25zaXN0ZW50IHdpdGggdGhpcwogICAgICBMaWNlbnNlLiBIb3dldmVyLCBpbiBhY2NlcHRpbmcgc3VjaCBvYmxpZ2F0aW9ucywgWW91IG1heSBhY3Qgb25seQogICAgICBvbiBZb3VyIG93biBiZWhhbGYgYW5kIG9uIFlvdXIgc29sZSByZXNwb25zaWJpbGl0eSwgbm90IG9uIGJlaGFsZgogICAgICBvZiBhbnkgb3RoZXIgQ29udHJpYnV0b3IsIGFuZCBvbmx5IGlmIFlvdSBhZ3JlZSB0byBpbmRlbW5pZnksCiAgICAgIGRlZmVuZCwgYW5kIGhvbGQgZWFjaCBDb250cmlidXRvciBoYXJtbGVzcyBmb3IgYW55IGxpYWJpbGl0eQogICAgICBpbmN1cnJlZCBieSwgb3IgY2xhaW1zIGFzc2VydGVkIGFnYWluc3QsIHN1Y2ggQ29udHJpYnV0b3IgYnkgcmVhc29uCiAgICAgIG9mIHlvdXIgYWNjZXB0aW5nIGFueSBzdWNoIHdhcnJhbnR5IG9yIGFkZGl0aW9uYWwgbGlhYmlsaXR5LgoKICAgRU5EIE9GIFRFUk1TIEFORCBDT05ESVRJT05TCgogICBBUFBFTkRJWDogSG93IHRvIGFwcGx5IHRoZSBBcGFjaGUgTGljZW5zZSB0byB5b3VyIHdvcmsuCgogICAgICBUbyBhcHBseSB0aGUgQXBhY2hlIExpY2Vuc2UgdG8geW91ciB3b3JrLCBhdHRhY2ggdGhlIGZvbGxvd2luZwogICAgICBib2lsZXJwbGF0ZSBub3RpY2UsIHdpdGggdGhlIGZpZWxkcyBlbmNsb3NlZCBieSBicmFja2V0cyAiW10iCiAgICAgIHJlcGxhY2VkIHdpdGggeW91ciBvd24gaWRlbnRpZnlpbmcgaW5mb3JtYXRpb24uIChEb24ndCBpbmNsdWRlCiAgICAgIHRoZSBicmFja2V0cyEpICBUaGUgdGV4dCBzaG91bGQgYmUgZW5jbG9zZWQgaW4gdGhlIGFwcHJvcHJpYXRlCiAgICAgIGNvbW1lbnQgc3ludGF4IGZvciB0aGUgZmlsZSBmb3JtYXQuIFdlIGFsc28gcmVjb21tZW5kIHRoYXQgYQogICAgICBmaWxlIG9yIGNsYXNzIG5hbWUgYW5kIGRlc2NyaXB0aW9uIG9mIHB1cnBvc2UgYmUgaW5jbHVkZWQgb24gdGhlCiAgICAgIHNhbWUgInByaW50ZWQgcGFnZSIgYXMgdGhlIGNvcHlyaWdodCBub3RpY2UgZm9yIGVhc2llcgogICAgICBpZGVudGlmaWNhdGlvbiB3aXRoaW4gdGhpcmQtcGFydHkgYXJjaGl2ZXMuCgogICBDb3B5cmlnaHQgW3l5eXldIFtuYW1lIG9mIGNvcHlyaWdodCBvd25lcl0KCiAgIExpY2Vuc2VkIHVuZGVyIHRoZSBBcGFjaGUgTGljZW5zZSwgVmVyc2lvbiAyLjAgKHRoZSAiTGljZW5zZSIpOwogICB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCiAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgIGh0dHA6Ly93d3cuYXBhY2hlLm9yZy9saWNlbnNlcy9MSUNFTlNFLTIuMAoKICAgVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQogICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IgaW1wbGllZC4KICAgU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAogICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4=" + }, + "url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + ], + "purl": "pkg:npm/acme/component@1.0.0", + "pedigree": { + "ancestors": [ + { + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "tomcat-catalina", + "version": "9.0.14" + }, + { + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "tomcat-catalina", + "version": "9.0.14" + } + ], + "commits": [ + { + "uid": "7638417db6d59f3c431d3e1f261cc637155684cd", + "url": "https://location/to/7638417db6d59f3c431d3e1f261cc637155684cd", + "author": { + "timestamp": "2018-11-13T20:20:39+00:00", + "name": "me", + "email": "me@acme.org" + } + } + ] + } + }, + { + "type": "library", + "supplier": { + "name": "Example, Inc.", + "url": [ + "https://example.com", + "https://example.net" + ], + "contact": [ + { + "name": "Example Support AMER Distribution", + "email": "support@example.com", + "phone": "800-555-1212" + }, + { + "name": "Example Support APAC", + "email": "support@apac.example.com" + } + ] + }, + "author": "Example Super Heros", + "group": "org.example", + "name": "mylibrary", + "version": "1.0.0" + } + ], + "dependencies": [ + { + "ref": "pkg:npm/acme/component@1.0.0", + "dependsOn": [ + "pkg:npm/acme/component@1.0.0" + ] + } + ] +} diff --git a/src/test/resources/unit/cyclonedx/valid-bom-1.3.xml b/src/test/resources/unit/cyclonedx/valid-bom-1.3.xml new file mode 100644 index 0000000000..c9cda0c6df --- /dev/null +++ b/src/test/resources/unit/cyclonedx/valid-bom-1.3.xml @@ -0,0 +1,181 @@ + + + + 2020-04-07T07:01:00Z + + + Awesome Vendor + Awesome Tool + 9.1.2 + + 25ed8e31b995bb927966616df2a42b979a2717f0 + a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df + + + + + + Samantha Wright + samantha.wright@example.com + 800-555-1212 + + + + Acme Super Heros + Acme Application + 9.1.1 + + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg== + + + + Acme, Inc. + https://example.com + + Acme Professional Services + professional.services@example.com + + + + Acme, Inc. + https://example.com + + Acme Distribution + distribution@example.com + + + + + + Acme Super Heros + Acme Inc + com.acme + tomcat-catalina + 9.0.14 + Modified version of Apache Catalina + required + + 3942447fac867ae5cdb3229b658f4d48 + e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a + f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b + e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282 + + + + Apache-2.0 + CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFwYWNoZSBMaWNlbnNlCiAgICAgICAgICAgICAgICAgICAgICAgICAgIFZlcnNpb24gMi4wLCBKYW51YXJ5IDIwMDQKICAgICAgICAgICAgICAgICAgICAgICAgaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzLwoKICAgVEVSTVMgQU5EIENPTkRJVElPTlMgRk9SIFVTRSwgUkVQUk9EVUNUSU9OLCBBTkQgRElTVFJJQlVUSU9OCgogICAxLiBEZWZpbml0aW9ucy4KCiAgICAgICJMaWNlbnNlIiBzaGFsbCBtZWFuIHRoZSB0ZXJtcyBhbmQgY29uZGl0aW9ucyBmb3IgdXNlLCByZXByb2R1Y3Rpb24sCiAgICAgIGFuZCBkaXN0cmlidXRpb24gYXMgZGVmaW5lZCBieSBTZWN0aW9ucyAxIHRocm91Z2ggOSBvZiB0aGlzIGRvY3VtZW50LgoKICAgICAgIkxpY2Vuc29yIiBzaGFsbCBtZWFuIHRoZSBjb3B5cmlnaHQgb3duZXIgb3IgZW50aXR5IGF1dGhvcml6ZWQgYnkKICAgICAgdGhlIGNvcHlyaWdodCBvd25lciB0aGF0IGlzIGdyYW50aW5nIHRoZSBMaWNlbnNlLgoKICAgICAgIkxlZ2FsIEVudGl0eSIgc2hhbGwgbWVhbiB0aGUgdW5pb24gb2YgdGhlIGFjdGluZyBlbnRpdHkgYW5kIGFsbAogICAgICBvdGhlciBlbnRpdGllcyB0aGF0IGNvbnRyb2wsIGFyZSBjb250cm9sbGVkIGJ5LCBvciBhcmUgdW5kZXIgY29tbW9uCiAgICAgIGNvbnRyb2wgd2l0aCB0aGF0IGVudGl0eS4gRm9yIHRoZSBwdXJwb3NlcyBvZiB0aGlzIGRlZmluaXRpb24sCiAgICAgICJjb250cm9sIiBtZWFucyAoaSkgdGhlIHBvd2VyLCBkaXJlY3Qgb3IgaW5kaXJlY3QsIHRvIGNhdXNlIHRoZQogICAgICBkaXJlY3Rpb24gb3IgbWFuYWdlbWVudCBvZiBzdWNoIGVudGl0eSwgd2hldGhlciBieSBjb250cmFjdCBvcgogICAgICBvdGhlcndpc2UsIG9yIChpaSkgb3duZXJzaGlwIG9mIGZpZnR5IHBlcmNlbnQgKDUwJSkgb3IgbW9yZSBvZiB0aGUKICAgICAgb3V0c3RhbmRpbmcgc2hhcmVzLCBvciAoaWlpKSBiZW5lZmljaWFsIG93bmVyc2hpcCBvZiBzdWNoIGVudGl0eS4KCiAgICAgICJZb3UiIChvciAiWW91ciIpIHNoYWxsIG1lYW4gYW4gaW5kaXZpZHVhbCBvciBMZWdhbCBFbnRpdHkKICAgICAgZXhlcmNpc2luZyBwZXJtaXNzaW9ucyBncmFudGVkIGJ5IHRoaXMgTGljZW5zZS4KCiAgICAgICJTb3VyY2UiIGZvcm0gc2hhbGwgbWVhbiB0aGUgcHJlZmVycmVkIGZvcm0gZm9yIG1ha2luZyBtb2RpZmljYXRpb25zLAogICAgICBpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIHNvZnR3YXJlIHNvdXJjZSBjb2RlLCBkb2N1bWVudGF0aW9uCiAgICAgIHNvdXJjZSwgYW5kIGNvbmZpZ3VyYXRpb24gZmlsZXMuCgogICAgICAiT2JqZWN0IiBmb3JtIHNoYWxsIG1lYW4gYW55IGZvcm0gcmVzdWx0aW5nIGZyb20gbWVjaGFuaWNhbAogICAgICB0cmFuc2Zvcm1hdGlvbiBvciB0cmFuc2xhdGlvbiBvZiBhIFNvdXJjZSBmb3JtLCBpbmNsdWRpbmcgYnV0CiAgICAgIG5vdCBsaW1pdGVkIHRvIGNvbXBpbGVkIG9iamVjdCBjb2RlLCBnZW5lcmF0ZWQgZG9jdW1lbnRhdGlvbiwKICAgICAgYW5kIGNvbnZlcnNpb25zIHRvIG90aGVyIG1lZGlhIHR5cGVzLgoKICAgICAgIldvcmsiIHNoYWxsIG1lYW4gdGhlIHdvcmsgb2YgYXV0aG9yc2hpcCwgd2hldGhlciBpbiBTb3VyY2Ugb3IKICAgICAgT2JqZWN0IGZvcm0sIG1hZGUgYXZhaWxhYmxlIHVuZGVyIHRoZSBMaWNlbnNlLCBhcyBpbmRpY2F0ZWQgYnkgYQogICAgICBjb3B5cmlnaHQgbm90aWNlIHRoYXQgaXMgaW5jbHVkZWQgaW4gb3IgYXR0YWNoZWQgdG8gdGhlIHdvcmsKICAgICAgKGFuIGV4YW1wbGUgaXMgcHJvdmlkZWQgaW4gdGhlIEFwcGVuZGl4IGJlbG93KS4KCiAgICAgICJEZXJpdmF0aXZlIFdvcmtzIiBzaGFsbCBtZWFuIGFueSB3b3JrLCB3aGV0aGVyIGluIFNvdXJjZSBvciBPYmplY3QKICAgICAgZm9ybSwgdGhhdCBpcyBiYXNlZCBvbiAob3IgZGVyaXZlZCBmcm9tKSB0aGUgV29yayBhbmQgZm9yIHdoaWNoIHRoZQogICAgICBlZGl0b3JpYWwgcmV2aXNpb25zLCBhbm5vdGF0aW9ucywgZWxhYm9yYXRpb25zLCBvciBvdGhlciBtb2RpZmljYXRpb25zCiAgICAgIHJlcHJlc2VudCwgYXMgYSB3aG9sZSwgYW4gb3JpZ2luYWwgd29yayBvZiBhdXRob3JzaGlwLiBGb3IgdGhlIHB1cnBvc2VzCiAgICAgIG9mIHRoaXMgTGljZW5zZSwgRGVyaXZhdGl2ZSBXb3JrcyBzaGFsbCBub3QgaW5jbHVkZSB3b3JrcyB0aGF0IHJlbWFpbgogICAgICBzZXBhcmFibGUgZnJvbSwgb3IgbWVyZWx5IGxpbmsgKG9yIGJpbmQgYnkgbmFtZSkgdG8gdGhlIGludGVyZmFjZXMgb2YsCiAgICAgIHRoZSBXb3JrIGFuZCBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YuCgogICAgICAiQ29udHJpYnV0aW9uIiBzaGFsbCBtZWFuIGFueSB3b3JrIG9mIGF1dGhvcnNoaXAsIGluY2x1ZGluZwogICAgICB0aGUgb3JpZ2luYWwgdmVyc2lvbiBvZiB0aGUgV29yayBhbmQgYW55IG1vZGlmaWNhdGlvbnMgb3IgYWRkaXRpb25zCiAgICAgIHRvIHRoYXQgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIHRoYXQgaXMgaW50ZW50aW9uYWxseQogICAgICBzdWJtaXR0ZWQgdG8gTGljZW5zb3IgZm9yIGluY2x1c2lvbiBpbiB0aGUgV29yayBieSB0aGUgY29weXJpZ2h0IG93bmVyCiAgICAgIG9yIGJ5IGFuIGluZGl2aWR1YWwgb3IgTGVnYWwgRW50aXR5IGF1dGhvcml6ZWQgdG8gc3VibWl0IG9uIGJlaGFsZiBvZgogICAgICB0aGUgY29weXJpZ2h0IG93bmVyLiBGb3IgdGhlIHB1cnBvc2VzIG9mIHRoaXMgZGVmaW5pdGlvbiwgInN1Ym1pdHRlZCIKICAgICAgbWVhbnMgYW55IGZvcm0gb2YgZWxlY3Ryb25pYywgdmVyYmFsLCBvciB3cml0dGVuIGNvbW11bmljYXRpb24gc2VudAogICAgICB0byB0aGUgTGljZW5zb3Igb3IgaXRzIHJlcHJlc2VudGF0aXZlcywgaW5jbHVkaW5nIGJ1dCBub3QgbGltaXRlZCB0bwogICAgICBjb21tdW5pY2F0aW9uIG9uIGVsZWN0cm9uaWMgbWFpbGluZyBsaXN0cywgc291cmNlIGNvZGUgY29udHJvbCBzeXN0ZW1zLAogICAgICBhbmQgaXNzdWUgdHJhY2tpbmcgc3lzdGVtcyB0aGF0IGFyZSBtYW5hZ2VkIGJ5LCBvciBvbiBiZWhhbGYgb2YsIHRoZQogICAgICBMaWNlbnNvciBmb3IgdGhlIHB1cnBvc2Ugb2YgZGlzY3Vzc2luZyBhbmQgaW1wcm92aW5nIHRoZSBXb3JrLCBidXQKICAgICAgZXhjbHVkaW5nIGNvbW11bmljYXRpb24gdGhhdCBpcyBjb25zcGljdW91c2x5IG1hcmtlZCBvciBvdGhlcndpc2UKICAgICAgZGVzaWduYXRlZCBpbiB3cml0aW5nIGJ5IHRoZSBjb3B5cmlnaHQgb3duZXIgYXMgIk5vdCBhIENvbnRyaWJ1dGlvbi4iCgogICAgICAiQ29udHJpYnV0b3IiIHNoYWxsIG1lYW4gTGljZW5zb3IgYW5kIGFueSBpbmRpdmlkdWFsIG9yIExlZ2FsIEVudGl0eQogICAgICBvbiBiZWhhbGYgb2Ygd2hvbSBhIENvbnRyaWJ1dGlvbiBoYXMgYmVlbiByZWNlaXZlZCBieSBMaWNlbnNvciBhbmQKICAgICAgc3Vic2VxdWVudGx5IGluY29ycG9yYXRlZCB3aXRoaW4gdGhlIFdvcmsuCgogICAyLiBHcmFudCBvZiBDb3B5cmlnaHQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICBjb3B5cmlnaHQgbGljZW5zZSB0byByZXByb2R1Y2UsIHByZXBhcmUgRGVyaXZhdGl2ZSBXb3JrcyBvZiwKICAgICAgcHVibGljbHkgZGlzcGxheSwgcHVibGljbHkgcGVyZm9ybSwgc3VibGljZW5zZSwgYW5kIGRpc3RyaWJ1dGUgdGhlCiAgICAgIFdvcmsgYW5kIHN1Y2ggRGVyaXZhdGl2ZSBXb3JrcyBpbiBTb3VyY2Ugb3IgT2JqZWN0IGZvcm0uCgogICAzLiBHcmFudCBvZiBQYXRlbnQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICAoZXhjZXB0IGFzIHN0YXRlZCBpbiB0aGlzIHNlY3Rpb24pIHBhdGVudCBsaWNlbnNlIHRvIG1ha2UsIGhhdmUgbWFkZSwKICAgICAgdXNlLCBvZmZlciB0byBzZWxsLCBzZWxsLCBpbXBvcnQsIGFuZCBvdGhlcndpc2UgdHJhbnNmZXIgdGhlIFdvcmssCiAgICAgIHdoZXJlIHN1Y2ggbGljZW5zZSBhcHBsaWVzIG9ubHkgdG8gdGhvc2UgcGF0ZW50IGNsYWltcyBsaWNlbnNhYmxlCiAgICAgIGJ5IHN1Y2ggQ29udHJpYnV0b3IgdGhhdCBhcmUgbmVjZXNzYXJpbHkgaW5mcmluZ2VkIGJ5IHRoZWlyCiAgICAgIENvbnRyaWJ1dGlvbihzKSBhbG9uZSBvciBieSBjb21iaW5hdGlvbiBvZiB0aGVpciBDb250cmlidXRpb24ocykKICAgICAgd2l0aCB0aGUgV29yayB0byB3aGljaCBzdWNoIENvbnRyaWJ1dGlvbihzKSB3YXMgc3VibWl0dGVkLiBJZiBZb3UKICAgICAgaW5zdGl0dXRlIHBhdGVudCBsaXRpZ2F0aW9uIGFnYWluc3QgYW55IGVudGl0eSAoaW5jbHVkaW5nIGEKICAgICAgY3Jvc3MtY2xhaW0gb3IgY291bnRlcmNsYWltIGluIGEgbGF3c3VpdCkgYWxsZWdpbmcgdGhhdCB0aGUgV29yawogICAgICBvciBhIENvbnRyaWJ1dGlvbiBpbmNvcnBvcmF0ZWQgd2l0aGluIHRoZSBXb3JrIGNvbnN0aXR1dGVzIGRpcmVjdAogICAgICBvciBjb250cmlidXRvcnkgcGF0ZW50IGluZnJpbmdlbWVudCwgdGhlbiBhbnkgcGF0ZW50IGxpY2Vuc2VzCiAgICAgIGdyYW50ZWQgdG8gWW91IHVuZGVyIHRoaXMgTGljZW5zZSBmb3IgdGhhdCBXb3JrIHNoYWxsIHRlcm1pbmF0ZQogICAgICBhcyBvZiB0aGUgZGF0ZSBzdWNoIGxpdGlnYXRpb24gaXMgZmlsZWQuCgogICA0LiBSZWRpc3RyaWJ1dGlvbi4gWW91IG1heSByZXByb2R1Y2UgYW5kIGRpc3RyaWJ1dGUgY29waWVzIG9mIHRoZQogICAgICBXb3JrIG9yIERlcml2YXRpdmUgV29ya3MgdGhlcmVvZiBpbiBhbnkgbWVkaXVtLCB3aXRoIG9yIHdpdGhvdXQKICAgICAgbW9kaWZpY2F0aW9ucywgYW5kIGluIFNvdXJjZSBvciBPYmplY3QgZm9ybSwgcHJvdmlkZWQgdGhhdCBZb3UKICAgICAgbWVldCB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgogICAgICAoYSkgWW91IG11c3QgZ2l2ZSBhbnkgb3RoZXIgcmVjaXBpZW50cyBvZiB0aGUgV29yayBvcgogICAgICAgICAgRGVyaXZhdGl2ZSBXb3JrcyBhIGNvcHkgb2YgdGhpcyBMaWNlbnNlOyBhbmQKCiAgICAgIChiKSBZb3UgbXVzdCBjYXVzZSBhbnkgbW9kaWZpZWQgZmlsZXMgdG8gY2FycnkgcHJvbWluZW50IG5vdGljZXMKICAgICAgICAgIHN0YXRpbmcgdGhhdCBZb3UgY2hhbmdlZCB0aGUgZmlsZXM7IGFuZAoKICAgICAgKGMpIFlvdSBtdXN0IHJldGFpbiwgaW4gdGhlIFNvdXJjZSBmb3JtIG9mIGFueSBEZXJpdmF0aXZlIFdvcmtzCiAgICAgICAgICB0aGF0IFlvdSBkaXN0cmlidXRlLCBhbGwgY29weXJpZ2h0LCBwYXRlbnQsIHRyYWRlbWFyaywgYW5kCiAgICAgICAgICBhdHRyaWJ1dGlvbiBub3RpY2VzIGZyb20gdGhlIFNvdXJjZSBmb3JtIG9mIHRoZSBXb3JrLAogICAgICAgICAgZXhjbHVkaW5nIHRob3NlIG5vdGljZXMgdGhhdCBkbyBub3QgcGVydGFpbiB0byBhbnkgcGFydCBvZgogICAgICAgICAgdGhlIERlcml2YXRpdmUgV29ya3M7IGFuZAoKICAgICAgKGQpIElmIHRoZSBXb3JrIGluY2x1ZGVzIGEgIk5PVElDRSIgdGV4dCBmaWxlIGFzIHBhcnQgb2YgaXRzCiAgICAgICAgICBkaXN0cmlidXRpb24sIHRoZW4gYW55IERlcml2YXRpdmUgV29ya3MgdGhhdCBZb3UgZGlzdHJpYnV0ZSBtdXN0CiAgICAgICAgICBpbmNsdWRlIGEgcmVhZGFibGUgY29weSBvZiB0aGUgYXR0cmlidXRpb24gbm90aWNlcyBjb250YWluZWQKICAgICAgICAgIHdpdGhpbiBzdWNoIE5PVElDRSBmaWxlLCBleGNsdWRpbmcgdGhvc2Ugbm90aWNlcyB0aGF0IGRvIG5vdAogICAgICAgICAgcGVydGFpbiB0byBhbnkgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaW4gYXQgbGVhc3Qgb25lCiAgICAgICAgICBvZiB0aGUgZm9sbG93aW5nIHBsYWNlczogd2l0aGluIGEgTk9USUNFIHRleHQgZmlsZSBkaXN0cmlidXRlZAogICAgICAgICAgYXMgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgd2l0aGluIHRoZSBTb3VyY2UgZm9ybSBvcgogICAgICAgICAgZG9jdW1lbnRhdGlvbiwgaWYgcHJvdmlkZWQgYWxvbmcgd2l0aCB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgb3IsCiAgICAgICAgICB3aXRoaW4gYSBkaXNwbGF5IGdlbmVyYXRlZCBieSB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaWYgYW5kCiAgICAgICAgICB3aGVyZXZlciBzdWNoIHRoaXJkLXBhcnR5IG5vdGljZXMgbm9ybWFsbHkgYXBwZWFyLiBUaGUgY29udGVudHMKICAgICAgICAgIG9mIHRoZSBOT1RJQ0UgZmlsZSBhcmUgZm9yIGluZm9ybWF0aW9uYWwgcHVycG9zZXMgb25seSBhbmQKICAgICAgICAgIGRvIG5vdCBtb2RpZnkgdGhlIExpY2Vuc2UuIFlvdSBtYXkgYWRkIFlvdXIgb3duIGF0dHJpYnV0aW9uCiAgICAgICAgICBub3RpY2VzIHdpdGhpbiBEZXJpdmF0aXZlIFdvcmtzIHRoYXQgWW91IGRpc3RyaWJ1dGUsIGFsb25nc2lkZQogICAgICAgICAgb3IgYXMgYW4gYWRkZW5kdW0gdG8gdGhlIE5PVElDRSB0ZXh0IGZyb20gdGhlIFdvcmssIHByb3ZpZGVkCiAgICAgICAgICB0aGF0IHN1Y2ggYWRkaXRpb25hbCBhdHRyaWJ1dGlvbiBub3RpY2VzIGNhbm5vdCBiZSBjb25zdHJ1ZWQKICAgICAgICAgIGFzIG1vZGlmeWluZyB0aGUgTGljZW5zZS4KCiAgICAgIFlvdSBtYXkgYWRkIFlvdXIgb3duIGNvcHlyaWdodCBzdGF0ZW1lbnQgdG8gWW91ciBtb2RpZmljYXRpb25zIGFuZAogICAgICBtYXkgcHJvdmlkZSBhZGRpdGlvbmFsIG9yIGRpZmZlcmVudCBsaWNlbnNlIHRlcm1zIGFuZCBjb25kaXRpb25zCiAgICAgIGZvciB1c2UsIHJlcHJvZHVjdGlvbiwgb3IgZGlzdHJpYnV0aW9uIG9mIFlvdXIgbW9kaWZpY2F0aW9ucywgb3IKICAgICAgZm9yIGFueSBzdWNoIERlcml2YXRpdmUgV29ya3MgYXMgYSB3aG9sZSwgcHJvdmlkZWQgWW91ciB1c2UsCiAgICAgIHJlcHJvZHVjdGlvbiwgYW5kIGRpc3RyaWJ1dGlvbiBvZiB0aGUgV29yayBvdGhlcndpc2UgY29tcGxpZXMgd2l0aAogICAgICB0aGUgY29uZGl0aW9ucyBzdGF0ZWQgaW4gdGhpcyBMaWNlbnNlLgoKICAgNS4gU3VibWlzc2lvbiBvZiBDb250cmlidXRpb25zLiBVbmxlc3MgWW91IGV4cGxpY2l0bHkgc3RhdGUgb3RoZXJ3aXNlLAogICAgICBhbnkgQ29udHJpYnV0aW9uIGludGVudGlvbmFsbHkgc3VibWl0dGVkIGZvciBpbmNsdXNpb24gaW4gdGhlIFdvcmsKICAgICAgYnkgWW91IHRvIHRoZSBMaWNlbnNvciBzaGFsbCBiZSB1bmRlciB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCB3aXRob3V0IGFueSBhZGRpdGlvbmFsIHRlcm1zIG9yIGNvbmRpdGlvbnMuCiAgICAgIE5vdHdpdGhzdGFuZGluZyB0aGUgYWJvdmUsIG5vdGhpbmcgaGVyZWluIHNoYWxsIHN1cGVyc2VkZSBvciBtb2RpZnkKICAgICAgdGhlIHRlcm1zIG9mIGFueSBzZXBhcmF0ZSBsaWNlbnNlIGFncmVlbWVudCB5b3UgbWF5IGhhdmUgZXhlY3V0ZWQKICAgICAgd2l0aCBMaWNlbnNvciByZWdhcmRpbmcgc3VjaCBDb250cmlidXRpb25zLgoKICAgNi4gVHJhZGVtYXJrcy4gVGhpcyBMaWNlbnNlIGRvZXMgbm90IGdyYW50IHBlcm1pc3Npb24gdG8gdXNlIHRoZSB0cmFkZQogICAgICBuYW1lcywgdHJhZGVtYXJrcywgc2VydmljZSBtYXJrcywgb3IgcHJvZHVjdCBuYW1lcyBvZiB0aGUgTGljZW5zb3IsCiAgICAgIGV4Y2VwdCBhcyByZXF1aXJlZCBmb3IgcmVhc29uYWJsZSBhbmQgY3VzdG9tYXJ5IHVzZSBpbiBkZXNjcmliaW5nIHRoZQogICAgICBvcmlnaW4gb2YgdGhlIFdvcmsgYW5kIHJlcHJvZHVjaW5nIHRoZSBjb250ZW50IG9mIHRoZSBOT1RJQ0UgZmlsZS4KCiAgIDcuIERpc2NsYWltZXIgb2YgV2FycmFudHkuIFVubGVzcyByZXF1aXJlZCBieSBhcHBsaWNhYmxlIGxhdyBvcgogICAgICBhZ3JlZWQgdG8gaW4gd3JpdGluZywgTGljZW5zb3IgcHJvdmlkZXMgdGhlIFdvcmsgKGFuZCBlYWNoCiAgICAgIENvbnRyaWJ1dG9yIHByb3ZpZGVzIGl0cyBDb250cmlidXRpb25zKSBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IKICAgICAgaW1wbGllZCwgaW5jbHVkaW5nLCB3aXRob3V0IGxpbWl0YXRpb24sIGFueSB3YXJyYW50aWVzIG9yIGNvbmRpdGlvbnMKICAgICAgb2YgVElUTEUsIE5PTi1JTkZSSU5HRU1FTlQsIE1FUkNIQU5UQUJJTElUWSwgb3IgRklUTkVTUyBGT1IgQQogICAgICBQQVJUSUNVTEFSIFBVUlBPU0UuIFlvdSBhcmUgc29sZWx5IHJlc3BvbnNpYmxlIGZvciBkZXRlcm1pbmluZyB0aGUKICAgICAgYXBwcm9wcmlhdGVuZXNzIG9mIHVzaW5nIG9yIHJlZGlzdHJpYnV0aW5nIHRoZSBXb3JrIGFuZCBhc3N1bWUgYW55CiAgICAgIHJpc2tzIGFzc29jaWF0ZWQgd2l0aCBZb3VyIGV4ZXJjaXNlIG9mIHBlcm1pc3Npb25zIHVuZGVyIHRoaXMgTGljZW5zZS4KCiAgIDguIExpbWl0YXRpb24gb2YgTGlhYmlsaXR5LiBJbiBubyBldmVudCBhbmQgdW5kZXIgbm8gbGVnYWwgdGhlb3J5LAogICAgICB3aGV0aGVyIGluIHRvcnQgKGluY2x1ZGluZyBuZWdsaWdlbmNlKSwgY29udHJhY3QsIG9yIG90aGVyd2lzZSwKICAgICAgdW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IChzdWNoIGFzIGRlbGliZXJhdGUgYW5kIGdyb3NzbHkKICAgICAgbmVnbGlnZW50IGFjdHMpIG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzaGFsbCBhbnkgQ29udHJpYnV0b3IgYmUKICAgICAgbGlhYmxlIHRvIFlvdSBmb3IgZGFtYWdlcywgaW5jbHVkaW5nIGFueSBkaXJlY3QsIGluZGlyZWN0LCBzcGVjaWFsLAogICAgICBpbmNpZGVudGFsLCBvciBjb25zZXF1ZW50aWFsIGRhbWFnZXMgb2YgYW55IGNoYXJhY3RlciBhcmlzaW5nIGFzIGEKICAgICAgcmVzdWx0IG9mIHRoaXMgTGljZW5zZSBvciBvdXQgb2YgdGhlIHVzZSBvciBpbmFiaWxpdHkgdG8gdXNlIHRoZQogICAgICBXb3JrIChpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIGRhbWFnZXMgZm9yIGxvc3Mgb2YgZ29vZHdpbGwsCiAgICAgIHdvcmsgc3RvcHBhZ2UsIGNvbXB1dGVyIGZhaWx1cmUgb3IgbWFsZnVuY3Rpb24sIG9yIGFueSBhbmQgYWxsCiAgICAgIG90aGVyIGNvbW1lcmNpYWwgZGFtYWdlcyBvciBsb3NzZXMpLCBldmVuIGlmIHN1Y2ggQ29udHJpYnV0b3IKICAgICAgaGFzIGJlZW4gYWR2aXNlZCBvZiB0aGUgcG9zc2liaWxpdHkgb2Ygc3VjaCBkYW1hZ2VzLgoKICAgOS4gQWNjZXB0aW5nIFdhcnJhbnR5IG9yIEFkZGl0aW9uYWwgTGlhYmlsaXR5LiBXaGlsZSByZWRpc3RyaWJ1dGluZwogICAgICB0aGUgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIFlvdSBtYXkgY2hvb3NlIHRvIG9mZmVyLAogICAgICBhbmQgY2hhcmdlIGEgZmVlIGZvciwgYWNjZXB0YW5jZSBvZiBzdXBwb3J0LCB3YXJyYW50eSwgaW5kZW1uaXR5LAogICAgICBvciBvdGhlciBsaWFiaWxpdHkgb2JsaWdhdGlvbnMgYW5kL29yIHJpZ2h0cyBjb25zaXN0ZW50IHdpdGggdGhpcwogICAgICBMaWNlbnNlLiBIb3dldmVyLCBpbiBhY2NlcHRpbmcgc3VjaCBvYmxpZ2F0aW9ucywgWW91IG1heSBhY3Qgb25seQogICAgICBvbiBZb3VyIG93biBiZWhhbGYgYW5kIG9uIFlvdXIgc29sZSByZXNwb25zaWJpbGl0eSwgbm90IG9uIGJlaGFsZgogICAgICBvZiBhbnkgb3RoZXIgQ29udHJpYnV0b3IsIGFuZCBvbmx5IGlmIFlvdSBhZ3JlZSB0byBpbmRlbW5pZnksCiAgICAgIGRlZmVuZCwgYW5kIGhvbGQgZWFjaCBDb250cmlidXRvciBoYXJtbGVzcyBmb3IgYW55IGxpYWJpbGl0eQogICAgICBpbmN1cnJlZCBieSwgb3IgY2xhaW1zIGFzc2VydGVkIGFnYWluc3QsIHN1Y2ggQ29udHJpYnV0b3IgYnkgcmVhc29uCiAgICAgIG9mIHlvdXIgYWNjZXB0aW5nIGFueSBzdWNoIHdhcnJhbnR5IG9yIGFkZGl0aW9uYWwgbGlhYmlsaXR5LgoKICAgRU5EIE9GIFRFUk1TIEFORCBDT05ESVRJT05TCgogICBBUFBFTkRJWDogSG93IHRvIGFwcGx5IHRoZSBBcGFjaGUgTGljZW5zZSB0byB5b3VyIHdvcmsuCgogICAgICBUbyBhcHBseSB0aGUgQXBhY2hlIExpY2Vuc2UgdG8geW91ciB3b3JrLCBhdHRhY2ggdGhlIGZvbGxvd2luZwogICAgICBib2lsZXJwbGF0ZSBub3RpY2UsIHdpdGggdGhlIGZpZWxkcyBlbmNsb3NlZCBieSBicmFja2V0cyAiW10iCiAgICAgIHJlcGxhY2VkIHdpdGggeW91ciBvd24gaWRlbnRpZnlpbmcgaW5mb3JtYXRpb24uIChEb24ndCBpbmNsdWRlCiAgICAgIHRoZSBicmFja2V0cyEpICBUaGUgdGV4dCBzaG91bGQgYmUgZW5jbG9zZWQgaW4gdGhlIGFwcHJvcHJpYXRlCiAgICAgIGNvbW1lbnQgc3ludGF4IGZvciB0aGUgZmlsZSBmb3JtYXQuIFdlIGFsc28gcmVjb21tZW5kIHRoYXQgYQogICAgICBmaWxlIG9yIGNsYXNzIG5hbWUgYW5kIGRlc2NyaXB0aW9uIG9mIHB1cnBvc2UgYmUgaW5jbHVkZWQgb24gdGhlCiAgICAgIHNhbWUgInByaW50ZWQgcGFnZSIgYXMgdGhlIGNvcHlyaWdodCBub3RpY2UgZm9yIGVhc2llcgogICAgICBpZGVudGlmaWNhdGlvbiB3aXRoaW4gdGhpcmQtcGFydHkgYXJjaGl2ZXMuCgogICBDb3B5cmlnaHQgW3l5eXldIFtuYW1lIG9mIGNvcHlyaWdodCBvd25lcl0KCiAgIExpY2Vuc2VkIHVuZGVyIHRoZSBBcGFjaGUgTGljZW5zZSwgVmVyc2lvbiAyLjAgKHRoZSAiTGljZW5zZSIpOwogICB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCiAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgIGh0dHA6Ly93d3cuYXBhY2hlLm9yZy9saWNlbnNlcy9MSUNFTlNFLTIuMAoKICAgVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQogICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IgaW1wbGllZC4KICAgU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAogICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4= + https://www.apache.org/licenses/LICENSE-2.0.txt + + + pkg:maven/com.acme/tomcat-catalina@9.0.14?packaging=jar + + + + Apache Super Heros + Apache + org.apache.tomcat + tomcat-catalina + 9.0.14 + Apache Catalina + + + Apache-2.0 + + + pkg:maven/org.apache.tomcat/tomcat-catalina@9.0.14?packaging=jar + + + + + 7638417db6d59f3c431d3e1f261cc637155684cd + https://location/to/7638417db6d59f3c431d3e1f261cc637155684cd + + 2018-11-07T22:01:45Z + John Doe + john.doe@example.com + + + 2018-11-07T22:01:45Z + Jane Doe + jane.doe@example.com + + Initial commit + + + Commentary here + + + + + Example Inc. + https://example.com + https://example.net + + Example Support AMER + support@example.com + 800-555-1212 + + + Example Support APAC + support@apac.example.com + + + Example Super Heros + org.example + mylibrary + 1.0.0 + required + + 2342c2eaf1feb9a80195dbaddf2ebaa3 + 68b78babe00a053f9e35ec6a2d9080f5b90122b0 + 708f1f53b41f11f02d12a11b1a38d2905d47b099afc71a0f1124ef8582ec7313 + 387b7ae16b9cae45f830671541539bf544202faae5aac544a93b7b0a04f5f846fa2f4e81ef3f1677e13aed7496408a441f5657ab6d54423e56bf6f38da124aef + + + EPL-2.0 OR GPL-2.0-with-classpath-exception + + Copyright Example Inc. All rights reserved. + cpe:/a:example:myapplication:1.0.0 + pkg:maven/com.example/myapplication@1.0.0?packaging=war + false + + + http://example.org/docs + All component versions are documented here + + + http://example.org/security + + + + + Example Super Heros + com.example + myframework + 1.0.0 + Example Inc, enterprise framework + required + + cfcb0b64aacd2f81c1cd546543de965a + 7fbeef2346c45d565c3341f037bce4e088af8a52 + 0384db3cec55d86a6898c489fdb75a8e75fe66b26639634983d2f3c3558493d1 + 854909cdb9e3ca183056837144aab6d8069b377bd66445087cc7157bf0c3f620418705dd0b83bdc2f73a508c2bdb316ca1809d75ee6972d02023a3e7dd655c79 + + + + Some random license + + + pkg:maven/com.example/myframework@1.0.0?packaging=war + false + + + http://example.com/myframework + + + http://example.com/security + + + + + diff --git a/src/test/resources/unit/cyclonedx/valid-bom-1.4.json b/src/test/resources/unit/cyclonedx/valid-bom-1.4.json new file mode 100644 index 0000000000..a6494a7b6f --- /dev/null +++ b/src/test/resources/unit/cyclonedx/valid-bom-1.4.json @@ -0,0 +1,177 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2020-04-13T20:20:39+00:00", + "tools": [ + { + "vendor": "Awesome Vendor", + "name": "Awesome Tool", + "version": "9.1.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "25ed8e31b995bb927966616df2a42b979a2717f0" + }, + { + "alg": "SHA-256", + "content": "a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df" + } + ] + } + ], + "authors": [ + { + "name": "Samantha Wright", + "email": "samantha.wright@example.com", + "phone": "800-555-1212" + } + ], + "component": { + "type": "application", + "author": "Acme Super Heros", + "name": "Acme Application", + "version": "9.1.1", + "swid": { + "tagId": "swidgen-242eb18a-503e-ca37-393b-cf156ef09691_9.1.1", + "name": "Acme Application", + "version": "9.1.1", + "text": { + "contentType": "text/xml", + "encoding": "base64", + "content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg==" + } + } + }, + "manufacture": { + "name": "Acme, Inc.", + "url": [ + "https://example.com" + ], + "contact": [ + { + "name": "Acme Professional Services", + "email": "professional.services@example.com" + } + ] + }, + "supplier": { + "name": "Acme, Inc.", + "url": [ + "https://example.com" + ], + "contact": [ + { + "name": "Acme Distribution", + "email": "distribution@example.com" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:npm/acme/component@1.0.0", + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "tomcat-catalina", + "version": "9.0.14", + "hashes": [ + { + "alg": "MD5", + "content": "3942447fac867ae5cdb3229b658f4d48" + }, + { + "alg": "SHA-1", + "content": "e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a" + }, + { + "alg": "SHA-256", + "content": "f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b" + }, + { + "alg": "SHA-512", + "content": "e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0", + "text": { + "contentType": "text/plain", + "encoding": "base64", + "content": "CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFwYWNoZSBMaWNlbnNlCiAgICAgICAgICAgICAgICAgICAgICAgICAgIFZlcnNpb24gMi4wLCBKYW51YXJ5IDIwMDQKICAgICAgICAgICAgICAgICAgICAgICAgaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzLwoKICAgVEVSTVMgQU5EIENPTkRJVElPTlMgRk9SIFVTRSwgUkVQUk9EVUNUSU9OLCBBTkQgRElTVFJJQlVUSU9OCgogICAxLiBEZWZpbml0aW9ucy4KCiAgICAgICJMaWNlbnNlIiBzaGFsbCBtZWFuIHRoZSB0ZXJtcyBhbmQgY29uZGl0aW9ucyBmb3IgdXNlLCByZXByb2R1Y3Rpb24sCiAgICAgIGFuZCBkaXN0cmlidXRpb24gYXMgZGVmaW5lZCBieSBTZWN0aW9ucyAxIHRocm91Z2ggOSBvZiB0aGlzIGRvY3VtZW50LgoKICAgICAgIkxpY2Vuc29yIiBzaGFsbCBtZWFuIHRoZSBjb3B5cmlnaHQgb3duZXIgb3IgZW50aXR5IGF1dGhvcml6ZWQgYnkKICAgICAgdGhlIGNvcHlyaWdodCBvd25lciB0aGF0IGlzIGdyYW50aW5nIHRoZSBMaWNlbnNlLgoKICAgICAgIkxlZ2FsIEVudGl0eSIgc2hhbGwgbWVhbiB0aGUgdW5pb24gb2YgdGhlIGFjdGluZyBlbnRpdHkgYW5kIGFsbAogICAgICBvdGhlciBlbnRpdGllcyB0aGF0IGNvbnRyb2wsIGFyZSBjb250cm9sbGVkIGJ5LCBvciBhcmUgdW5kZXIgY29tbW9uCiAgICAgIGNvbnRyb2wgd2l0aCB0aGF0IGVudGl0eS4gRm9yIHRoZSBwdXJwb3NlcyBvZiB0aGlzIGRlZmluaXRpb24sCiAgICAgICJjb250cm9sIiBtZWFucyAoaSkgdGhlIHBvd2VyLCBkaXJlY3Qgb3IgaW5kaXJlY3QsIHRvIGNhdXNlIHRoZQogICAgICBkaXJlY3Rpb24gb3IgbWFuYWdlbWVudCBvZiBzdWNoIGVudGl0eSwgd2hldGhlciBieSBjb250cmFjdCBvcgogICAgICBvdGhlcndpc2UsIG9yIChpaSkgb3duZXJzaGlwIG9mIGZpZnR5IHBlcmNlbnQgKDUwJSkgb3IgbW9yZSBvZiB0aGUKICAgICAgb3V0c3RhbmRpbmcgc2hhcmVzLCBvciAoaWlpKSBiZW5lZmljaWFsIG93bmVyc2hpcCBvZiBzdWNoIGVudGl0eS4KCiAgICAgICJZb3UiIChvciAiWW91ciIpIHNoYWxsIG1lYW4gYW4gaW5kaXZpZHVhbCBvciBMZWdhbCBFbnRpdHkKICAgICAgZXhlcmNpc2luZyBwZXJtaXNzaW9ucyBncmFudGVkIGJ5IHRoaXMgTGljZW5zZS4KCiAgICAgICJTb3VyY2UiIGZvcm0gc2hhbGwgbWVhbiB0aGUgcHJlZmVycmVkIGZvcm0gZm9yIG1ha2luZyBtb2RpZmljYXRpb25zLAogICAgICBpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIHNvZnR3YXJlIHNvdXJjZSBjb2RlLCBkb2N1bWVudGF0aW9uCiAgICAgIHNvdXJjZSwgYW5kIGNvbmZpZ3VyYXRpb24gZmlsZXMuCgogICAgICAiT2JqZWN0IiBmb3JtIHNoYWxsIG1lYW4gYW55IGZvcm0gcmVzdWx0aW5nIGZyb20gbWVjaGFuaWNhbAogICAgICB0cmFuc2Zvcm1hdGlvbiBvciB0cmFuc2xhdGlvbiBvZiBhIFNvdXJjZSBmb3JtLCBpbmNsdWRpbmcgYnV0CiAgICAgIG5vdCBsaW1pdGVkIHRvIGNvbXBpbGVkIG9iamVjdCBjb2RlLCBnZW5lcmF0ZWQgZG9jdW1lbnRhdGlvbiwKICAgICAgYW5kIGNvbnZlcnNpb25zIHRvIG90aGVyIG1lZGlhIHR5cGVzLgoKICAgICAgIldvcmsiIHNoYWxsIG1lYW4gdGhlIHdvcmsgb2YgYXV0aG9yc2hpcCwgd2hldGhlciBpbiBTb3VyY2Ugb3IKICAgICAgT2JqZWN0IGZvcm0sIG1hZGUgYXZhaWxhYmxlIHVuZGVyIHRoZSBMaWNlbnNlLCBhcyBpbmRpY2F0ZWQgYnkgYQogICAgICBjb3B5cmlnaHQgbm90aWNlIHRoYXQgaXMgaW5jbHVkZWQgaW4gb3IgYXR0YWNoZWQgdG8gdGhlIHdvcmsKICAgICAgKGFuIGV4YW1wbGUgaXMgcHJvdmlkZWQgaW4gdGhlIEFwcGVuZGl4IGJlbG93KS4KCiAgICAgICJEZXJpdmF0aXZlIFdvcmtzIiBzaGFsbCBtZWFuIGFueSB3b3JrLCB3aGV0aGVyIGluIFNvdXJjZSBvciBPYmplY3QKICAgICAgZm9ybSwgdGhhdCBpcyBiYXNlZCBvbiAob3IgZGVyaXZlZCBmcm9tKSB0aGUgV29yayBhbmQgZm9yIHdoaWNoIHRoZQogICAgICBlZGl0b3JpYWwgcmV2aXNpb25zLCBhbm5vdGF0aW9ucywgZWxhYm9yYXRpb25zLCBvciBvdGhlciBtb2RpZmljYXRpb25zCiAgICAgIHJlcHJlc2VudCwgYXMgYSB3aG9sZSwgYW4gb3JpZ2luYWwgd29yayBvZiBhdXRob3JzaGlwLiBGb3IgdGhlIHB1cnBvc2VzCiAgICAgIG9mIHRoaXMgTGljZW5zZSwgRGVyaXZhdGl2ZSBXb3JrcyBzaGFsbCBub3QgaW5jbHVkZSB3b3JrcyB0aGF0IHJlbWFpbgogICAgICBzZXBhcmFibGUgZnJvbSwgb3IgbWVyZWx5IGxpbmsgKG9yIGJpbmQgYnkgbmFtZSkgdG8gdGhlIGludGVyZmFjZXMgb2YsCiAgICAgIHRoZSBXb3JrIGFuZCBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YuCgogICAgICAiQ29udHJpYnV0aW9uIiBzaGFsbCBtZWFuIGFueSB3b3JrIG9mIGF1dGhvcnNoaXAsIGluY2x1ZGluZwogICAgICB0aGUgb3JpZ2luYWwgdmVyc2lvbiBvZiB0aGUgV29yayBhbmQgYW55IG1vZGlmaWNhdGlvbnMgb3IgYWRkaXRpb25zCiAgICAgIHRvIHRoYXQgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIHRoYXQgaXMgaW50ZW50aW9uYWxseQogICAgICBzdWJtaXR0ZWQgdG8gTGljZW5zb3IgZm9yIGluY2x1c2lvbiBpbiB0aGUgV29yayBieSB0aGUgY29weXJpZ2h0IG93bmVyCiAgICAgIG9yIGJ5IGFuIGluZGl2aWR1YWwgb3IgTGVnYWwgRW50aXR5IGF1dGhvcml6ZWQgdG8gc3VibWl0IG9uIGJlaGFsZiBvZgogICAgICB0aGUgY29weXJpZ2h0IG93bmVyLiBGb3IgdGhlIHB1cnBvc2VzIG9mIHRoaXMgZGVmaW5pdGlvbiwgInN1Ym1pdHRlZCIKICAgICAgbWVhbnMgYW55IGZvcm0gb2YgZWxlY3Ryb25pYywgdmVyYmFsLCBvciB3cml0dGVuIGNvbW11bmljYXRpb24gc2VudAogICAgICB0byB0aGUgTGljZW5zb3Igb3IgaXRzIHJlcHJlc2VudGF0aXZlcywgaW5jbHVkaW5nIGJ1dCBub3QgbGltaXRlZCB0bwogICAgICBjb21tdW5pY2F0aW9uIG9uIGVsZWN0cm9uaWMgbWFpbGluZyBsaXN0cywgc291cmNlIGNvZGUgY29udHJvbCBzeXN0ZW1zLAogICAgICBhbmQgaXNzdWUgdHJhY2tpbmcgc3lzdGVtcyB0aGF0IGFyZSBtYW5hZ2VkIGJ5LCBvciBvbiBiZWhhbGYgb2YsIHRoZQogICAgICBMaWNlbnNvciBmb3IgdGhlIHB1cnBvc2Ugb2YgZGlzY3Vzc2luZyBhbmQgaW1wcm92aW5nIHRoZSBXb3JrLCBidXQKICAgICAgZXhjbHVkaW5nIGNvbW11bmljYXRpb24gdGhhdCBpcyBjb25zcGljdW91c2x5IG1hcmtlZCBvciBvdGhlcndpc2UKICAgICAgZGVzaWduYXRlZCBpbiB3cml0aW5nIGJ5IHRoZSBjb3B5cmlnaHQgb3duZXIgYXMgIk5vdCBhIENvbnRyaWJ1dGlvbi4iCgogICAgICAiQ29udHJpYnV0b3IiIHNoYWxsIG1lYW4gTGljZW5zb3IgYW5kIGFueSBpbmRpdmlkdWFsIG9yIExlZ2FsIEVudGl0eQogICAgICBvbiBiZWhhbGYgb2Ygd2hvbSBhIENvbnRyaWJ1dGlvbiBoYXMgYmVlbiByZWNlaXZlZCBieSBMaWNlbnNvciBhbmQKICAgICAgc3Vic2VxdWVudGx5IGluY29ycG9yYXRlZCB3aXRoaW4gdGhlIFdvcmsuCgogICAyLiBHcmFudCBvZiBDb3B5cmlnaHQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICBjb3B5cmlnaHQgbGljZW5zZSB0byByZXByb2R1Y2UsIHByZXBhcmUgRGVyaXZhdGl2ZSBXb3JrcyBvZiwKICAgICAgcHVibGljbHkgZGlzcGxheSwgcHVibGljbHkgcGVyZm9ybSwgc3VibGljZW5zZSwgYW5kIGRpc3RyaWJ1dGUgdGhlCiAgICAgIFdvcmsgYW5kIHN1Y2ggRGVyaXZhdGl2ZSBXb3JrcyBpbiBTb3VyY2Ugb3IgT2JqZWN0IGZvcm0uCgogICAzLiBHcmFudCBvZiBQYXRlbnQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICAoZXhjZXB0IGFzIHN0YXRlZCBpbiB0aGlzIHNlY3Rpb24pIHBhdGVudCBsaWNlbnNlIHRvIG1ha2UsIGhhdmUgbWFkZSwKICAgICAgdXNlLCBvZmZlciB0byBzZWxsLCBzZWxsLCBpbXBvcnQsIGFuZCBvdGhlcndpc2UgdHJhbnNmZXIgdGhlIFdvcmssCiAgICAgIHdoZXJlIHN1Y2ggbGljZW5zZSBhcHBsaWVzIG9ubHkgdG8gdGhvc2UgcGF0ZW50IGNsYWltcyBsaWNlbnNhYmxlCiAgICAgIGJ5IHN1Y2ggQ29udHJpYnV0b3IgdGhhdCBhcmUgbmVjZXNzYXJpbHkgaW5mcmluZ2VkIGJ5IHRoZWlyCiAgICAgIENvbnRyaWJ1dGlvbihzKSBhbG9uZSBvciBieSBjb21iaW5hdGlvbiBvZiB0aGVpciBDb250cmlidXRpb24ocykKICAgICAgd2l0aCB0aGUgV29yayB0byB3aGljaCBzdWNoIENvbnRyaWJ1dGlvbihzKSB3YXMgc3VibWl0dGVkLiBJZiBZb3UKICAgICAgaW5zdGl0dXRlIHBhdGVudCBsaXRpZ2F0aW9uIGFnYWluc3QgYW55IGVudGl0eSAoaW5jbHVkaW5nIGEKICAgICAgY3Jvc3MtY2xhaW0gb3IgY291bnRlcmNsYWltIGluIGEgbGF3c3VpdCkgYWxsZWdpbmcgdGhhdCB0aGUgV29yawogICAgICBvciBhIENvbnRyaWJ1dGlvbiBpbmNvcnBvcmF0ZWQgd2l0aGluIHRoZSBXb3JrIGNvbnN0aXR1dGVzIGRpcmVjdAogICAgICBvciBjb250cmlidXRvcnkgcGF0ZW50IGluZnJpbmdlbWVudCwgdGhlbiBhbnkgcGF0ZW50IGxpY2Vuc2VzCiAgICAgIGdyYW50ZWQgdG8gWW91IHVuZGVyIHRoaXMgTGljZW5zZSBmb3IgdGhhdCBXb3JrIHNoYWxsIHRlcm1pbmF0ZQogICAgICBhcyBvZiB0aGUgZGF0ZSBzdWNoIGxpdGlnYXRpb24gaXMgZmlsZWQuCgogICA0LiBSZWRpc3RyaWJ1dGlvbi4gWW91IG1heSByZXByb2R1Y2UgYW5kIGRpc3RyaWJ1dGUgY29waWVzIG9mIHRoZQogICAgICBXb3JrIG9yIERlcml2YXRpdmUgV29ya3MgdGhlcmVvZiBpbiBhbnkgbWVkaXVtLCB3aXRoIG9yIHdpdGhvdXQKICAgICAgbW9kaWZpY2F0aW9ucywgYW5kIGluIFNvdXJjZSBvciBPYmplY3QgZm9ybSwgcHJvdmlkZWQgdGhhdCBZb3UKICAgICAgbWVldCB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgogICAgICAoYSkgWW91IG11c3QgZ2l2ZSBhbnkgb3RoZXIgcmVjaXBpZW50cyBvZiB0aGUgV29yayBvcgogICAgICAgICAgRGVyaXZhdGl2ZSBXb3JrcyBhIGNvcHkgb2YgdGhpcyBMaWNlbnNlOyBhbmQKCiAgICAgIChiKSBZb3UgbXVzdCBjYXVzZSBhbnkgbW9kaWZpZWQgZmlsZXMgdG8gY2FycnkgcHJvbWluZW50IG5vdGljZXMKICAgICAgICAgIHN0YXRpbmcgdGhhdCBZb3UgY2hhbmdlZCB0aGUgZmlsZXM7IGFuZAoKICAgICAgKGMpIFlvdSBtdXN0IHJldGFpbiwgaW4gdGhlIFNvdXJjZSBmb3JtIG9mIGFueSBEZXJpdmF0aXZlIFdvcmtzCiAgICAgICAgICB0aGF0IFlvdSBkaXN0cmlidXRlLCBhbGwgY29weXJpZ2h0LCBwYXRlbnQsIHRyYWRlbWFyaywgYW5kCiAgICAgICAgICBhdHRyaWJ1dGlvbiBub3RpY2VzIGZyb20gdGhlIFNvdXJjZSBmb3JtIG9mIHRoZSBXb3JrLAogICAgICAgICAgZXhjbHVkaW5nIHRob3NlIG5vdGljZXMgdGhhdCBkbyBub3QgcGVydGFpbiB0byBhbnkgcGFydCBvZgogICAgICAgICAgdGhlIERlcml2YXRpdmUgV29ya3M7IGFuZAoKICAgICAgKGQpIElmIHRoZSBXb3JrIGluY2x1ZGVzIGEgIk5PVElDRSIgdGV4dCBmaWxlIGFzIHBhcnQgb2YgaXRzCiAgICAgICAgICBkaXN0cmlidXRpb24sIHRoZW4gYW55IERlcml2YXRpdmUgV29ya3MgdGhhdCBZb3UgZGlzdHJpYnV0ZSBtdXN0CiAgICAgICAgICBpbmNsdWRlIGEgcmVhZGFibGUgY29weSBvZiB0aGUgYXR0cmlidXRpb24gbm90aWNlcyBjb250YWluZWQKICAgICAgICAgIHdpdGhpbiBzdWNoIE5PVElDRSBmaWxlLCBleGNsdWRpbmcgdGhvc2Ugbm90aWNlcyB0aGF0IGRvIG5vdAogICAgICAgICAgcGVydGFpbiB0byBhbnkgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaW4gYXQgbGVhc3Qgb25lCiAgICAgICAgICBvZiB0aGUgZm9sbG93aW5nIHBsYWNlczogd2l0aGluIGEgTk9USUNFIHRleHQgZmlsZSBkaXN0cmlidXRlZAogICAgICAgICAgYXMgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgd2l0aGluIHRoZSBTb3VyY2UgZm9ybSBvcgogICAgICAgICAgZG9jdW1lbnRhdGlvbiwgaWYgcHJvdmlkZWQgYWxvbmcgd2l0aCB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgb3IsCiAgICAgICAgICB3aXRoaW4gYSBkaXNwbGF5IGdlbmVyYXRlZCBieSB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaWYgYW5kCiAgICAgICAgICB3aGVyZXZlciBzdWNoIHRoaXJkLXBhcnR5IG5vdGljZXMgbm9ybWFsbHkgYXBwZWFyLiBUaGUgY29udGVudHMKICAgICAgICAgIG9mIHRoZSBOT1RJQ0UgZmlsZSBhcmUgZm9yIGluZm9ybWF0aW9uYWwgcHVycG9zZXMgb25seSBhbmQKICAgICAgICAgIGRvIG5vdCBtb2RpZnkgdGhlIExpY2Vuc2UuIFlvdSBtYXkgYWRkIFlvdXIgb3duIGF0dHJpYnV0aW9uCiAgICAgICAgICBub3RpY2VzIHdpdGhpbiBEZXJpdmF0aXZlIFdvcmtzIHRoYXQgWW91IGRpc3RyaWJ1dGUsIGFsb25nc2lkZQogICAgICAgICAgb3IgYXMgYW4gYWRkZW5kdW0gdG8gdGhlIE5PVElDRSB0ZXh0IGZyb20gdGhlIFdvcmssIHByb3ZpZGVkCiAgICAgICAgICB0aGF0IHN1Y2ggYWRkaXRpb25hbCBhdHRyaWJ1dGlvbiBub3RpY2VzIGNhbm5vdCBiZSBjb25zdHJ1ZWQKICAgICAgICAgIGFzIG1vZGlmeWluZyB0aGUgTGljZW5zZS4KCiAgICAgIFlvdSBtYXkgYWRkIFlvdXIgb3duIGNvcHlyaWdodCBzdGF0ZW1lbnQgdG8gWW91ciBtb2RpZmljYXRpb25zIGFuZAogICAgICBtYXkgcHJvdmlkZSBhZGRpdGlvbmFsIG9yIGRpZmZlcmVudCBsaWNlbnNlIHRlcm1zIGFuZCBjb25kaXRpb25zCiAgICAgIGZvciB1c2UsIHJlcHJvZHVjdGlvbiwgb3IgZGlzdHJpYnV0aW9uIG9mIFlvdXIgbW9kaWZpY2F0aW9ucywgb3IKICAgICAgZm9yIGFueSBzdWNoIERlcml2YXRpdmUgV29ya3MgYXMgYSB3aG9sZSwgcHJvdmlkZWQgWW91ciB1c2UsCiAgICAgIHJlcHJvZHVjdGlvbiwgYW5kIGRpc3RyaWJ1dGlvbiBvZiB0aGUgV29yayBvdGhlcndpc2UgY29tcGxpZXMgd2l0aAogICAgICB0aGUgY29uZGl0aW9ucyBzdGF0ZWQgaW4gdGhpcyBMaWNlbnNlLgoKICAgNS4gU3VibWlzc2lvbiBvZiBDb250cmlidXRpb25zLiBVbmxlc3MgWW91IGV4cGxpY2l0bHkgc3RhdGUgb3RoZXJ3aXNlLAogICAgICBhbnkgQ29udHJpYnV0aW9uIGludGVudGlvbmFsbHkgc3VibWl0dGVkIGZvciBpbmNsdXNpb24gaW4gdGhlIFdvcmsKICAgICAgYnkgWW91IHRvIHRoZSBMaWNlbnNvciBzaGFsbCBiZSB1bmRlciB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCB3aXRob3V0IGFueSBhZGRpdGlvbmFsIHRlcm1zIG9yIGNvbmRpdGlvbnMuCiAgICAgIE5vdHdpdGhzdGFuZGluZyB0aGUgYWJvdmUsIG5vdGhpbmcgaGVyZWluIHNoYWxsIHN1cGVyc2VkZSBvciBtb2RpZnkKICAgICAgdGhlIHRlcm1zIG9mIGFueSBzZXBhcmF0ZSBsaWNlbnNlIGFncmVlbWVudCB5b3UgbWF5IGhhdmUgZXhlY3V0ZWQKICAgICAgd2l0aCBMaWNlbnNvciByZWdhcmRpbmcgc3VjaCBDb250cmlidXRpb25zLgoKICAgNi4gVHJhZGVtYXJrcy4gVGhpcyBMaWNlbnNlIGRvZXMgbm90IGdyYW50IHBlcm1pc3Npb24gdG8gdXNlIHRoZSB0cmFkZQogICAgICBuYW1lcywgdHJhZGVtYXJrcywgc2VydmljZSBtYXJrcywgb3IgcHJvZHVjdCBuYW1lcyBvZiB0aGUgTGljZW5zb3IsCiAgICAgIGV4Y2VwdCBhcyByZXF1aXJlZCBmb3IgcmVhc29uYWJsZSBhbmQgY3VzdG9tYXJ5IHVzZSBpbiBkZXNjcmliaW5nIHRoZQogICAgICBvcmlnaW4gb2YgdGhlIFdvcmsgYW5kIHJlcHJvZHVjaW5nIHRoZSBjb250ZW50IG9mIHRoZSBOT1RJQ0UgZmlsZS4KCiAgIDcuIERpc2NsYWltZXIgb2YgV2FycmFudHkuIFVubGVzcyByZXF1aXJlZCBieSBhcHBsaWNhYmxlIGxhdyBvcgogICAgICBhZ3JlZWQgdG8gaW4gd3JpdGluZywgTGljZW5zb3IgcHJvdmlkZXMgdGhlIFdvcmsgKGFuZCBlYWNoCiAgICAgIENvbnRyaWJ1dG9yIHByb3ZpZGVzIGl0cyBDb250cmlidXRpb25zKSBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IKICAgICAgaW1wbGllZCwgaW5jbHVkaW5nLCB3aXRob3V0IGxpbWl0YXRpb24sIGFueSB3YXJyYW50aWVzIG9yIGNvbmRpdGlvbnMKICAgICAgb2YgVElUTEUsIE5PTi1JTkZSSU5HRU1FTlQsIE1FUkNIQU5UQUJJTElUWSwgb3IgRklUTkVTUyBGT1IgQQogICAgICBQQVJUSUNVTEFSIFBVUlBPU0UuIFlvdSBhcmUgc29sZWx5IHJlc3BvbnNpYmxlIGZvciBkZXRlcm1pbmluZyB0aGUKICAgICAgYXBwcm9wcmlhdGVuZXNzIG9mIHVzaW5nIG9yIHJlZGlzdHJpYnV0aW5nIHRoZSBXb3JrIGFuZCBhc3N1bWUgYW55CiAgICAgIHJpc2tzIGFzc29jaWF0ZWQgd2l0aCBZb3VyIGV4ZXJjaXNlIG9mIHBlcm1pc3Npb25zIHVuZGVyIHRoaXMgTGljZW5zZS4KCiAgIDguIExpbWl0YXRpb24gb2YgTGlhYmlsaXR5LiBJbiBubyBldmVudCBhbmQgdW5kZXIgbm8gbGVnYWwgdGhlb3J5LAogICAgICB3aGV0aGVyIGluIHRvcnQgKGluY2x1ZGluZyBuZWdsaWdlbmNlKSwgY29udHJhY3QsIG9yIG90aGVyd2lzZSwKICAgICAgdW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IChzdWNoIGFzIGRlbGliZXJhdGUgYW5kIGdyb3NzbHkKICAgICAgbmVnbGlnZW50IGFjdHMpIG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzaGFsbCBhbnkgQ29udHJpYnV0b3IgYmUKICAgICAgbGlhYmxlIHRvIFlvdSBmb3IgZGFtYWdlcywgaW5jbHVkaW5nIGFueSBkaXJlY3QsIGluZGlyZWN0LCBzcGVjaWFsLAogICAgICBpbmNpZGVudGFsLCBvciBjb25zZXF1ZW50aWFsIGRhbWFnZXMgb2YgYW55IGNoYXJhY3RlciBhcmlzaW5nIGFzIGEKICAgICAgcmVzdWx0IG9mIHRoaXMgTGljZW5zZSBvciBvdXQgb2YgdGhlIHVzZSBvciBpbmFiaWxpdHkgdG8gdXNlIHRoZQogICAgICBXb3JrIChpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIGRhbWFnZXMgZm9yIGxvc3Mgb2YgZ29vZHdpbGwsCiAgICAgIHdvcmsgc3RvcHBhZ2UsIGNvbXB1dGVyIGZhaWx1cmUgb3IgbWFsZnVuY3Rpb24sIG9yIGFueSBhbmQgYWxsCiAgICAgIG90aGVyIGNvbW1lcmNpYWwgZGFtYWdlcyBvciBsb3NzZXMpLCBldmVuIGlmIHN1Y2ggQ29udHJpYnV0b3IKICAgICAgaGFzIGJlZW4gYWR2aXNlZCBvZiB0aGUgcG9zc2liaWxpdHkgb2Ygc3VjaCBkYW1hZ2VzLgoKICAgOS4gQWNjZXB0aW5nIFdhcnJhbnR5IG9yIEFkZGl0aW9uYWwgTGlhYmlsaXR5LiBXaGlsZSByZWRpc3RyaWJ1dGluZwogICAgICB0aGUgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIFlvdSBtYXkgY2hvb3NlIHRvIG9mZmVyLAogICAgICBhbmQgY2hhcmdlIGEgZmVlIGZvciwgYWNjZXB0YW5jZSBvZiBzdXBwb3J0LCB3YXJyYW50eSwgaW5kZW1uaXR5LAogICAgICBvciBvdGhlciBsaWFiaWxpdHkgb2JsaWdhdGlvbnMgYW5kL29yIHJpZ2h0cyBjb25zaXN0ZW50IHdpdGggdGhpcwogICAgICBMaWNlbnNlLiBIb3dldmVyLCBpbiBhY2NlcHRpbmcgc3VjaCBvYmxpZ2F0aW9ucywgWW91IG1heSBhY3Qgb25seQogICAgICBvbiBZb3VyIG93biBiZWhhbGYgYW5kIG9uIFlvdXIgc29sZSByZXNwb25zaWJpbGl0eSwgbm90IG9uIGJlaGFsZgogICAgICBvZiBhbnkgb3RoZXIgQ29udHJpYnV0b3IsIGFuZCBvbmx5IGlmIFlvdSBhZ3JlZSB0byBpbmRlbW5pZnksCiAgICAgIGRlZmVuZCwgYW5kIGhvbGQgZWFjaCBDb250cmlidXRvciBoYXJtbGVzcyBmb3IgYW55IGxpYWJpbGl0eQogICAgICBpbmN1cnJlZCBieSwgb3IgY2xhaW1zIGFzc2VydGVkIGFnYWluc3QsIHN1Y2ggQ29udHJpYnV0b3IgYnkgcmVhc29uCiAgICAgIG9mIHlvdXIgYWNjZXB0aW5nIGFueSBzdWNoIHdhcnJhbnR5IG9yIGFkZGl0aW9uYWwgbGlhYmlsaXR5LgoKICAgRU5EIE9GIFRFUk1TIEFORCBDT05ESVRJT05TCgogICBBUFBFTkRJWDogSG93IHRvIGFwcGx5IHRoZSBBcGFjaGUgTGljZW5zZSB0byB5b3VyIHdvcmsuCgogICAgICBUbyBhcHBseSB0aGUgQXBhY2hlIExpY2Vuc2UgdG8geW91ciB3b3JrLCBhdHRhY2ggdGhlIGZvbGxvd2luZwogICAgICBib2lsZXJwbGF0ZSBub3RpY2UsIHdpdGggdGhlIGZpZWxkcyBlbmNsb3NlZCBieSBicmFja2V0cyAiW10iCiAgICAgIHJlcGxhY2VkIHdpdGggeW91ciBvd24gaWRlbnRpZnlpbmcgaW5mb3JtYXRpb24uIChEb24ndCBpbmNsdWRlCiAgICAgIHRoZSBicmFja2V0cyEpICBUaGUgdGV4dCBzaG91bGQgYmUgZW5jbG9zZWQgaW4gdGhlIGFwcHJvcHJpYXRlCiAgICAgIGNvbW1lbnQgc3ludGF4IGZvciB0aGUgZmlsZSBmb3JtYXQuIFdlIGFsc28gcmVjb21tZW5kIHRoYXQgYQogICAgICBmaWxlIG9yIGNsYXNzIG5hbWUgYW5kIGRlc2NyaXB0aW9uIG9mIHB1cnBvc2UgYmUgaW5jbHVkZWQgb24gdGhlCiAgICAgIHNhbWUgInByaW50ZWQgcGFnZSIgYXMgdGhlIGNvcHlyaWdodCBub3RpY2UgZm9yIGVhc2llcgogICAgICBpZGVudGlmaWNhdGlvbiB3aXRoaW4gdGhpcmQtcGFydHkgYXJjaGl2ZXMuCgogICBDb3B5cmlnaHQgW3l5eXldIFtuYW1lIG9mIGNvcHlyaWdodCBvd25lcl0KCiAgIExpY2Vuc2VkIHVuZGVyIHRoZSBBcGFjaGUgTGljZW5zZSwgVmVyc2lvbiAyLjAgKHRoZSAiTGljZW5zZSIpOwogICB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCiAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgIGh0dHA6Ly93d3cuYXBhY2hlLm9yZy9saWNlbnNlcy9MSUNFTlNFLTIuMAoKICAgVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQogICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IgaW1wbGllZC4KICAgU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAogICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4=" + }, + "url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + ], + "purl": "pkg:npm/acme/component@1.0.0", + "pedigree": { + "ancestors": [ + { + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "tomcat-catalina", + "version": "9.0.14" + }, + { + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "tomcat-catalina", + "version": "9.0.14" + } + ], + "commits": [ + { + "uid": "7638417db6d59f3c431d3e1f261cc637155684cd", + "url": "https://location/to/7638417db6d59f3c431d3e1f261cc637155684cd", + "author": { + "timestamp": "2018-11-13T20:20:39+00:00", + "name": "me", + "email": "me@acme.org" + } + } + ] + } + }, + { + "type": "library", + "supplier": { + "name": "Example, Inc.", + "url": [ + "https://example.com", + "https://example.net" + ], + "contact": [ + { + "name": "Example Support AMER Distribution", + "email": "support@example.com", + "phone": "800-555-1212" + }, + { + "name": "Example Support APAC", + "email": "support@apac.example.com" + } + ] + }, + "author": "Example Super Heros", + "group": "org.example", + "name": "mylibrary", + "version": "1.0.0" + } + ], + "dependencies": [ + { + "ref": "pkg:npm/acme/component@1.0.0", + "dependsOn": [ + "pkg:npm/acme/component@1.0.0" + ] + } + ] +} diff --git a/src/test/resources/unit/cyclonedx/valid-bom-1.4.xml b/src/test/resources/unit/cyclonedx/valid-bom-1.4.xml new file mode 100644 index 0000000000..a675a96762 --- /dev/null +++ b/src/test/resources/unit/cyclonedx/valid-bom-1.4.xml @@ -0,0 +1,181 @@ + + + + 2020-04-07T07:01:00Z + + + Awesome Vendor + Awesome Tool + 9.1.2 + + 25ed8e31b995bb927966616df2a42b979a2717f0 + a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df + + + + + + Samantha Wright + samantha.wright@example.com + 800-555-1212 + + + + Acme Super Heros + Acme Application + 9.1.1 + + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg== + + + + Acme, Inc. + https://example.com + + Acme Professional Services + professional.services@example.com + + + + Acme, Inc. + https://example.com + + Acme Distribution + distribution@example.com + + + + + + Acme Super Heros + Acme Inc + com.acme + tomcat-catalina + 9.0.14 + Modified version of Apache Catalina + required + + 3942447fac867ae5cdb3229b658f4d48 + e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a + f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b + e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282 + + + + Apache-2.0 + CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFwYWNoZSBMaWNlbnNlCiAgICAgICAgICAgICAgICAgICAgICAgICAgIFZlcnNpb24gMi4wLCBKYW51YXJ5IDIwMDQKICAgICAgICAgICAgICAgICAgICAgICAgaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzLwoKICAgVEVSTVMgQU5EIENPTkRJVElPTlMgRk9SIFVTRSwgUkVQUk9EVUNUSU9OLCBBTkQgRElTVFJJQlVUSU9OCgogICAxLiBEZWZpbml0aW9ucy4KCiAgICAgICJMaWNlbnNlIiBzaGFsbCBtZWFuIHRoZSB0ZXJtcyBhbmQgY29uZGl0aW9ucyBmb3IgdXNlLCByZXByb2R1Y3Rpb24sCiAgICAgIGFuZCBkaXN0cmlidXRpb24gYXMgZGVmaW5lZCBieSBTZWN0aW9ucyAxIHRocm91Z2ggOSBvZiB0aGlzIGRvY3VtZW50LgoKICAgICAgIkxpY2Vuc29yIiBzaGFsbCBtZWFuIHRoZSBjb3B5cmlnaHQgb3duZXIgb3IgZW50aXR5IGF1dGhvcml6ZWQgYnkKICAgICAgdGhlIGNvcHlyaWdodCBvd25lciB0aGF0IGlzIGdyYW50aW5nIHRoZSBMaWNlbnNlLgoKICAgICAgIkxlZ2FsIEVudGl0eSIgc2hhbGwgbWVhbiB0aGUgdW5pb24gb2YgdGhlIGFjdGluZyBlbnRpdHkgYW5kIGFsbAogICAgICBvdGhlciBlbnRpdGllcyB0aGF0IGNvbnRyb2wsIGFyZSBjb250cm9sbGVkIGJ5LCBvciBhcmUgdW5kZXIgY29tbW9uCiAgICAgIGNvbnRyb2wgd2l0aCB0aGF0IGVudGl0eS4gRm9yIHRoZSBwdXJwb3NlcyBvZiB0aGlzIGRlZmluaXRpb24sCiAgICAgICJjb250cm9sIiBtZWFucyAoaSkgdGhlIHBvd2VyLCBkaXJlY3Qgb3IgaW5kaXJlY3QsIHRvIGNhdXNlIHRoZQogICAgICBkaXJlY3Rpb24gb3IgbWFuYWdlbWVudCBvZiBzdWNoIGVudGl0eSwgd2hldGhlciBieSBjb250cmFjdCBvcgogICAgICBvdGhlcndpc2UsIG9yIChpaSkgb3duZXJzaGlwIG9mIGZpZnR5IHBlcmNlbnQgKDUwJSkgb3IgbW9yZSBvZiB0aGUKICAgICAgb3V0c3RhbmRpbmcgc2hhcmVzLCBvciAoaWlpKSBiZW5lZmljaWFsIG93bmVyc2hpcCBvZiBzdWNoIGVudGl0eS4KCiAgICAgICJZb3UiIChvciAiWW91ciIpIHNoYWxsIG1lYW4gYW4gaW5kaXZpZHVhbCBvciBMZWdhbCBFbnRpdHkKICAgICAgZXhlcmNpc2luZyBwZXJtaXNzaW9ucyBncmFudGVkIGJ5IHRoaXMgTGljZW5zZS4KCiAgICAgICJTb3VyY2UiIGZvcm0gc2hhbGwgbWVhbiB0aGUgcHJlZmVycmVkIGZvcm0gZm9yIG1ha2luZyBtb2RpZmljYXRpb25zLAogICAgICBpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIHNvZnR3YXJlIHNvdXJjZSBjb2RlLCBkb2N1bWVudGF0aW9uCiAgICAgIHNvdXJjZSwgYW5kIGNvbmZpZ3VyYXRpb24gZmlsZXMuCgogICAgICAiT2JqZWN0IiBmb3JtIHNoYWxsIG1lYW4gYW55IGZvcm0gcmVzdWx0aW5nIGZyb20gbWVjaGFuaWNhbAogICAgICB0cmFuc2Zvcm1hdGlvbiBvciB0cmFuc2xhdGlvbiBvZiBhIFNvdXJjZSBmb3JtLCBpbmNsdWRpbmcgYnV0CiAgICAgIG5vdCBsaW1pdGVkIHRvIGNvbXBpbGVkIG9iamVjdCBjb2RlLCBnZW5lcmF0ZWQgZG9jdW1lbnRhdGlvbiwKICAgICAgYW5kIGNvbnZlcnNpb25zIHRvIG90aGVyIG1lZGlhIHR5cGVzLgoKICAgICAgIldvcmsiIHNoYWxsIG1lYW4gdGhlIHdvcmsgb2YgYXV0aG9yc2hpcCwgd2hldGhlciBpbiBTb3VyY2Ugb3IKICAgICAgT2JqZWN0IGZvcm0sIG1hZGUgYXZhaWxhYmxlIHVuZGVyIHRoZSBMaWNlbnNlLCBhcyBpbmRpY2F0ZWQgYnkgYQogICAgICBjb3B5cmlnaHQgbm90aWNlIHRoYXQgaXMgaW5jbHVkZWQgaW4gb3IgYXR0YWNoZWQgdG8gdGhlIHdvcmsKICAgICAgKGFuIGV4YW1wbGUgaXMgcHJvdmlkZWQgaW4gdGhlIEFwcGVuZGl4IGJlbG93KS4KCiAgICAgICJEZXJpdmF0aXZlIFdvcmtzIiBzaGFsbCBtZWFuIGFueSB3b3JrLCB3aGV0aGVyIGluIFNvdXJjZSBvciBPYmplY3QKICAgICAgZm9ybSwgdGhhdCBpcyBiYXNlZCBvbiAob3IgZGVyaXZlZCBmcm9tKSB0aGUgV29yayBhbmQgZm9yIHdoaWNoIHRoZQogICAgICBlZGl0b3JpYWwgcmV2aXNpb25zLCBhbm5vdGF0aW9ucywgZWxhYm9yYXRpb25zLCBvciBvdGhlciBtb2RpZmljYXRpb25zCiAgICAgIHJlcHJlc2VudCwgYXMgYSB3aG9sZSwgYW4gb3JpZ2luYWwgd29yayBvZiBhdXRob3JzaGlwLiBGb3IgdGhlIHB1cnBvc2VzCiAgICAgIG9mIHRoaXMgTGljZW5zZSwgRGVyaXZhdGl2ZSBXb3JrcyBzaGFsbCBub3QgaW5jbHVkZSB3b3JrcyB0aGF0IHJlbWFpbgogICAgICBzZXBhcmFibGUgZnJvbSwgb3IgbWVyZWx5IGxpbmsgKG9yIGJpbmQgYnkgbmFtZSkgdG8gdGhlIGludGVyZmFjZXMgb2YsCiAgICAgIHRoZSBXb3JrIGFuZCBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YuCgogICAgICAiQ29udHJpYnV0aW9uIiBzaGFsbCBtZWFuIGFueSB3b3JrIG9mIGF1dGhvcnNoaXAsIGluY2x1ZGluZwogICAgICB0aGUgb3JpZ2luYWwgdmVyc2lvbiBvZiB0aGUgV29yayBhbmQgYW55IG1vZGlmaWNhdGlvbnMgb3IgYWRkaXRpb25zCiAgICAgIHRvIHRoYXQgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIHRoYXQgaXMgaW50ZW50aW9uYWxseQogICAgICBzdWJtaXR0ZWQgdG8gTGljZW5zb3IgZm9yIGluY2x1c2lvbiBpbiB0aGUgV29yayBieSB0aGUgY29weXJpZ2h0IG93bmVyCiAgICAgIG9yIGJ5IGFuIGluZGl2aWR1YWwgb3IgTGVnYWwgRW50aXR5IGF1dGhvcml6ZWQgdG8gc3VibWl0IG9uIGJlaGFsZiBvZgogICAgICB0aGUgY29weXJpZ2h0IG93bmVyLiBGb3IgdGhlIHB1cnBvc2VzIG9mIHRoaXMgZGVmaW5pdGlvbiwgInN1Ym1pdHRlZCIKICAgICAgbWVhbnMgYW55IGZvcm0gb2YgZWxlY3Ryb25pYywgdmVyYmFsLCBvciB3cml0dGVuIGNvbW11bmljYXRpb24gc2VudAogICAgICB0byB0aGUgTGljZW5zb3Igb3IgaXRzIHJlcHJlc2VudGF0aXZlcywgaW5jbHVkaW5nIGJ1dCBub3QgbGltaXRlZCB0bwogICAgICBjb21tdW5pY2F0aW9uIG9uIGVsZWN0cm9uaWMgbWFpbGluZyBsaXN0cywgc291cmNlIGNvZGUgY29udHJvbCBzeXN0ZW1zLAogICAgICBhbmQgaXNzdWUgdHJhY2tpbmcgc3lzdGVtcyB0aGF0IGFyZSBtYW5hZ2VkIGJ5LCBvciBvbiBiZWhhbGYgb2YsIHRoZQogICAgICBMaWNlbnNvciBmb3IgdGhlIHB1cnBvc2Ugb2YgZGlzY3Vzc2luZyBhbmQgaW1wcm92aW5nIHRoZSBXb3JrLCBidXQKICAgICAgZXhjbHVkaW5nIGNvbW11bmljYXRpb24gdGhhdCBpcyBjb25zcGljdW91c2x5IG1hcmtlZCBvciBvdGhlcndpc2UKICAgICAgZGVzaWduYXRlZCBpbiB3cml0aW5nIGJ5IHRoZSBjb3B5cmlnaHQgb3duZXIgYXMgIk5vdCBhIENvbnRyaWJ1dGlvbi4iCgogICAgICAiQ29udHJpYnV0b3IiIHNoYWxsIG1lYW4gTGljZW5zb3IgYW5kIGFueSBpbmRpdmlkdWFsIG9yIExlZ2FsIEVudGl0eQogICAgICBvbiBiZWhhbGYgb2Ygd2hvbSBhIENvbnRyaWJ1dGlvbiBoYXMgYmVlbiByZWNlaXZlZCBieSBMaWNlbnNvciBhbmQKICAgICAgc3Vic2VxdWVudGx5IGluY29ycG9yYXRlZCB3aXRoaW4gdGhlIFdvcmsuCgogICAyLiBHcmFudCBvZiBDb3B5cmlnaHQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICBjb3B5cmlnaHQgbGljZW5zZSB0byByZXByb2R1Y2UsIHByZXBhcmUgRGVyaXZhdGl2ZSBXb3JrcyBvZiwKICAgICAgcHVibGljbHkgZGlzcGxheSwgcHVibGljbHkgcGVyZm9ybSwgc3VibGljZW5zZSwgYW5kIGRpc3RyaWJ1dGUgdGhlCiAgICAgIFdvcmsgYW5kIHN1Y2ggRGVyaXZhdGl2ZSBXb3JrcyBpbiBTb3VyY2Ugb3IgT2JqZWN0IGZvcm0uCgogICAzLiBHcmFudCBvZiBQYXRlbnQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICAoZXhjZXB0IGFzIHN0YXRlZCBpbiB0aGlzIHNlY3Rpb24pIHBhdGVudCBsaWNlbnNlIHRvIG1ha2UsIGhhdmUgbWFkZSwKICAgICAgdXNlLCBvZmZlciB0byBzZWxsLCBzZWxsLCBpbXBvcnQsIGFuZCBvdGhlcndpc2UgdHJhbnNmZXIgdGhlIFdvcmssCiAgICAgIHdoZXJlIHN1Y2ggbGljZW5zZSBhcHBsaWVzIG9ubHkgdG8gdGhvc2UgcGF0ZW50IGNsYWltcyBsaWNlbnNhYmxlCiAgICAgIGJ5IHN1Y2ggQ29udHJpYnV0b3IgdGhhdCBhcmUgbmVjZXNzYXJpbHkgaW5mcmluZ2VkIGJ5IHRoZWlyCiAgICAgIENvbnRyaWJ1dGlvbihzKSBhbG9uZSBvciBieSBjb21iaW5hdGlvbiBvZiB0aGVpciBDb250cmlidXRpb24ocykKICAgICAgd2l0aCB0aGUgV29yayB0byB3aGljaCBzdWNoIENvbnRyaWJ1dGlvbihzKSB3YXMgc3VibWl0dGVkLiBJZiBZb3UKICAgICAgaW5zdGl0dXRlIHBhdGVudCBsaXRpZ2F0aW9uIGFnYWluc3QgYW55IGVudGl0eSAoaW5jbHVkaW5nIGEKICAgICAgY3Jvc3MtY2xhaW0gb3IgY291bnRlcmNsYWltIGluIGEgbGF3c3VpdCkgYWxsZWdpbmcgdGhhdCB0aGUgV29yawogICAgICBvciBhIENvbnRyaWJ1dGlvbiBpbmNvcnBvcmF0ZWQgd2l0aGluIHRoZSBXb3JrIGNvbnN0aXR1dGVzIGRpcmVjdAogICAgICBvciBjb250cmlidXRvcnkgcGF0ZW50IGluZnJpbmdlbWVudCwgdGhlbiBhbnkgcGF0ZW50IGxpY2Vuc2VzCiAgICAgIGdyYW50ZWQgdG8gWW91IHVuZGVyIHRoaXMgTGljZW5zZSBmb3IgdGhhdCBXb3JrIHNoYWxsIHRlcm1pbmF0ZQogICAgICBhcyBvZiB0aGUgZGF0ZSBzdWNoIGxpdGlnYXRpb24gaXMgZmlsZWQuCgogICA0LiBSZWRpc3RyaWJ1dGlvbi4gWW91IG1heSByZXByb2R1Y2UgYW5kIGRpc3RyaWJ1dGUgY29waWVzIG9mIHRoZQogICAgICBXb3JrIG9yIERlcml2YXRpdmUgV29ya3MgdGhlcmVvZiBpbiBhbnkgbWVkaXVtLCB3aXRoIG9yIHdpdGhvdXQKICAgICAgbW9kaWZpY2F0aW9ucywgYW5kIGluIFNvdXJjZSBvciBPYmplY3QgZm9ybSwgcHJvdmlkZWQgdGhhdCBZb3UKICAgICAgbWVldCB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgogICAgICAoYSkgWW91IG11c3QgZ2l2ZSBhbnkgb3RoZXIgcmVjaXBpZW50cyBvZiB0aGUgV29yayBvcgogICAgICAgICAgRGVyaXZhdGl2ZSBXb3JrcyBhIGNvcHkgb2YgdGhpcyBMaWNlbnNlOyBhbmQKCiAgICAgIChiKSBZb3UgbXVzdCBjYXVzZSBhbnkgbW9kaWZpZWQgZmlsZXMgdG8gY2FycnkgcHJvbWluZW50IG5vdGljZXMKICAgICAgICAgIHN0YXRpbmcgdGhhdCBZb3UgY2hhbmdlZCB0aGUgZmlsZXM7IGFuZAoKICAgICAgKGMpIFlvdSBtdXN0IHJldGFpbiwgaW4gdGhlIFNvdXJjZSBmb3JtIG9mIGFueSBEZXJpdmF0aXZlIFdvcmtzCiAgICAgICAgICB0aGF0IFlvdSBkaXN0cmlidXRlLCBhbGwgY29weXJpZ2h0LCBwYXRlbnQsIHRyYWRlbWFyaywgYW5kCiAgICAgICAgICBhdHRyaWJ1dGlvbiBub3RpY2VzIGZyb20gdGhlIFNvdXJjZSBmb3JtIG9mIHRoZSBXb3JrLAogICAgICAgICAgZXhjbHVkaW5nIHRob3NlIG5vdGljZXMgdGhhdCBkbyBub3QgcGVydGFpbiB0byBhbnkgcGFydCBvZgogICAgICAgICAgdGhlIERlcml2YXRpdmUgV29ya3M7IGFuZAoKICAgICAgKGQpIElmIHRoZSBXb3JrIGluY2x1ZGVzIGEgIk5PVElDRSIgdGV4dCBmaWxlIGFzIHBhcnQgb2YgaXRzCiAgICAgICAgICBkaXN0cmlidXRpb24sIHRoZW4gYW55IERlcml2YXRpdmUgV29ya3MgdGhhdCBZb3UgZGlzdHJpYnV0ZSBtdXN0CiAgICAgICAgICBpbmNsdWRlIGEgcmVhZGFibGUgY29weSBvZiB0aGUgYXR0cmlidXRpb24gbm90aWNlcyBjb250YWluZWQKICAgICAgICAgIHdpdGhpbiBzdWNoIE5PVElDRSBmaWxlLCBleGNsdWRpbmcgdGhvc2Ugbm90aWNlcyB0aGF0IGRvIG5vdAogICAgICAgICAgcGVydGFpbiB0byBhbnkgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaW4gYXQgbGVhc3Qgb25lCiAgICAgICAgICBvZiB0aGUgZm9sbG93aW5nIHBsYWNlczogd2l0aGluIGEgTk9USUNFIHRleHQgZmlsZSBkaXN0cmlidXRlZAogICAgICAgICAgYXMgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgd2l0aGluIHRoZSBTb3VyY2UgZm9ybSBvcgogICAgICAgICAgZG9jdW1lbnRhdGlvbiwgaWYgcHJvdmlkZWQgYWxvbmcgd2l0aCB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgb3IsCiAgICAgICAgICB3aXRoaW4gYSBkaXNwbGF5IGdlbmVyYXRlZCBieSB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaWYgYW5kCiAgICAgICAgICB3aGVyZXZlciBzdWNoIHRoaXJkLXBhcnR5IG5vdGljZXMgbm9ybWFsbHkgYXBwZWFyLiBUaGUgY29udGVudHMKICAgICAgICAgIG9mIHRoZSBOT1RJQ0UgZmlsZSBhcmUgZm9yIGluZm9ybWF0aW9uYWwgcHVycG9zZXMgb25seSBhbmQKICAgICAgICAgIGRvIG5vdCBtb2RpZnkgdGhlIExpY2Vuc2UuIFlvdSBtYXkgYWRkIFlvdXIgb3duIGF0dHJpYnV0aW9uCiAgICAgICAgICBub3RpY2VzIHdpdGhpbiBEZXJpdmF0aXZlIFdvcmtzIHRoYXQgWW91IGRpc3RyaWJ1dGUsIGFsb25nc2lkZQogICAgICAgICAgb3IgYXMgYW4gYWRkZW5kdW0gdG8gdGhlIE5PVElDRSB0ZXh0IGZyb20gdGhlIFdvcmssIHByb3ZpZGVkCiAgICAgICAgICB0aGF0IHN1Y2ggYWRkaXRpb25hbCBhdHRyaWJ1dGlvbiBub3RpY2VzIGNhbm5vdCBiZSBjb25zdHJ1ZWQKICAgICAgICAgIGFzIG1vZGlmeWluZyB0aGUgTGljZW5zZS4KCiAgICAgIFlvdSBtYXkgYWRkIFlvdXIgb3duIGNvcHlyaWdodCBzdGF0ZW1lbnQgdG8gWW91ciBtb2RpZmljYXRpb25zIGFuZAogICAgICBtYXkgcHJvdmlkZSBhZGRpdGlvbmFsIG9yIGRpZmZlcmVudCBsaWNlbnNlIHRlcm1zIGFuZCBjb25kaXRpb25zCiAgICAgIGZvciB1c2UsIHJlcHJvZHVjdGlvbiwgb3IgZGlzdHJpYnV0aW9uIG9mIFlvdXIgbW9kaWZpY2F0aW9ucywgb3IKICAgICAgZm9yIGFueSBzdWNoIERlcml2YXRpdmUgV29ya3MgYXMgYSB3aG9sZSwgcHJvdmlkZWQgWW91ciB1c2UsCiAgICAgIHJlcHJvZHVjdGlvbiwgYW5kIGRpc3RyaWJ1dGlvbiBvZiB0aGUgV29yayBvdGhlcndpc2UgY29tcGxpZXMgd2l0aAogICAgICB0aGUgY29uZGl0aW9ucyBzdGF0ZWQgaW4gdGhpcyBMaWNlbnNlLgoKICAgNS4gU3VibWlzc2lvbiBvZiBDb250cmlidXRpb25zLiBVbmxlc3MgWW91IGV4cGxpY2l0bHkgc3RhdGUgb3RoZXJ3aXNlLAogICAgICBhbnkgQ29udHJpYnV0aW9uIGludGVudGlvbmFsbHkgc3VibWl0dGVkIGZvciBpbmNsdXNpb24gaW4gdGhlIFdvcmsKICAgICAgYnkgWW91IHRvIHRoZSBMaWNlbnNvciBzaGFsbCBiZSB1bmRlciB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCB3aXRob3V0IGFueSBhZGRpdGlvbmFsIHRlcm1zIG9yIGNvbmRpdGlvbnMuCiAgICAgIE5vdHdpdGhzdGFuZGluZyB0aGUgYWJvdmUsIG5vdGhpbmcgaGVyZWluIHNoYWxsIHN1cGVyc2VkZSBvciBtb2RpZnkKICAgICAgdGhlIHRlcm1zIG9mIGFueSBzZXBhcmF0ZSBsaWNlbnNlIGFncmVlbWVudCB5b3UgbWF5IGhhdmUgZXhlY3V0ZWQKICAgICAgd2l0aCBMaWNlbnNvciByZWdhcmRpbmcgc3VjaCBDb250cmlidXRpb25zLgoKICAgNi4gVHJhZGVtYXJrcy4gVGhpcyBMaWNlbnNlIGRvZXMgbm90IGdyYW50IHBlcm1pc3Npb24gdG8gdXNlIHRoZSB0cmFkZQogICAgICBuYW1lcywgdHJhZGVtYXJrcywgc2VydmljZSBtYXJrcywgb3IgcHJvZHVjdCBuYW1lcyBvZiB0aGUgTGljZW5zb3IsCiAgICAgIGV4Y2VwdCBhcyByZXF1aXJlZCBmb3IgcmVhc29uYWJsZSBhbmQgY3VzdG9tYXJ5IHVzZSBpbiBkZXNjcmliaW5nIHRoZQogICAgICBvcmlnaW4gb2YgdGhlIFdvcmsgYW5kIHJlcHJvZHVjaW5nIHRoZSBjb250ZW50IG9mIHRoZSBOT1RJQ0UgZmlsZS4KCiAgIDcuIERpc2NsYWltZXIgb2YgV2FycmFudHkuIFVubGVzcyByZXF1aXJlZCBieSBhcHBsaWNhYmxlIGxhdyBvcgogICAgICBhZ3JlZWQgdG8gaW4gd3JpdGluZywgTGljZW5zb3IgcHJvdmlkZXMgdGhlIFdvcmsgKGFuZCBlYWNoCiAgICAgIENvbnRyaWJ1dG9yIHByb3ZpZGVzIGl0cyBDb250cmlidXRpb25zKSBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IKICAgICAgaW1wbGllZCwgaW5jbHVkaW5nLCB3aXRob3V0IGxpbWl0YXRpb24sIGFueSB3YXJyYW50aWVzIG9yIGNvbmRpdGlvbnMKICAgICAgb2YgVElUTEUsIE5PTi1JTkZSSU5HRU1FTlQsIE1FUkNIQU5UQUJJTElUWSwgb3IgRklUTkVTUyBGT1IgQQogICAgICBQQVJUSUNVTEFSIFBVUlBPU0UuIFlvdSBhcmUgc29sZWx5IHJlc3BvbnNpYmxlIGZvciBkZXRlcm1pbmluZyB0aGUKICAgICAgYXBwcm9wcmlhdGVuZXNzIG9mIHVzaW5nIG9yIHJlZGlzdHJpYnV0aW5nIHRoZSBXb3JrIGFuZCBhc3N1bWUgYW55CiAgICAgIHJpc2tzIGFzc29jaWF0ZWQgd2l0aCBZb3VyIGV4ZXJjaXNlIG9mIHBlcm1pc3Npb25zIHVuZGVyIHRoaXMgTGljZW5zZS4KCiAgIDguIExpbWl0YXRpb24gb2YgTGlhYmlsaXR5LiBJbiBubyBldmVudCBhbmQgdW5kZXIgbm8gbGVnYWwgdGhlb3J5LAogICAgICB3aGV0aGVyIGluIHRvcnQgKGluY2x1ZGluZyBuZWdsaWdlbmNlKSwgY29udHJhY3QsIG9yIG90aGVyd2lzZSwKICAgICAgdW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IChzdWNoIGFzIGRlbGliZXJhdGUgYW5kIGdyb3NzbHkKICAgICAgbmVnbGlnZW50IGFjdHMpIG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzaGFsbCBhbnkgQ29udHJpYnV0b3IgYmUKICAgICAgbGlhYmxlIHRvIFlvdSBmb3IgZGFtYWdlcywgaW5jbHVkaW5nIGFueSBkaXJlY3QsIGluZGlyZWN0LCBzcGVjaWFsLAogICAgICBpbmNpZGVudGFsLCBvciBjb25zZXF1ZW50aWFsIGRhbWFnZXMgb2YgYW55IGNoYXJhY3RlciBhcmlzaW5nIGFzIGEKICAgICAgcmVzdWx0IG9mIHRoaXMgTGljZW5zZSBvciBvdXQgb2YgdGhlIHVzZSBvciBpbmFiaWxpdHkgdG8gdXNlIHRoZQogICAgICBXb3JrIChpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIGRhbWFnZXMgZm9yIGxvc3Mgb2YgZ29vZHdpbGwsCiAgICAgIHdvcmsgc3RvcHBhZ2UsIGNvbXB1dGVyIGZhaWx1cmUgb3IgbWFsZnVuY3Rpb24sIG9yIGFueSBhbmQgYWxsCiAgICAgIG90aGVyIGNvbW1lcmNpYWwgZGFtYWdlcyBvciBsb3NzZXMpLCBldmVuIGlmIHN1Y2ggQ29udHJpYnV0b3IKICAgICAgaGFzIGJlZW4gYWR2aXNlZCBvZiB0aGUgcG9zc2liaWxpdHkgb2Ygc3VjaCBkYW1hZ2VzLgoKICAgOS4gQWNjZXB0aW5nIFdhcnJhbnR5IG9yIEFkZGl0aW9uYWwgTGlhYmlsaXR5LiBXaGlsZSByZWRpc3RyaWJ1dGluZwogICAgICB0aGUgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIFlvdSBtYXkgY2hvb3NlIHRvIG9mZmVyLAogICAgICBhbmQgY2hhcmdlIGEgZmVlIGZvciwgYWNjZXB0YW5jZSBvZiBzdXBwb3J0LCB3YXJyYW50eSwgaW5kZW1uaXR5LAogICAgICBvciBvdGhlciBsaWFiaWxpdHkgb2JsaWdhdGlvbnMgYW5kL29yIHJpZ2h0cyBjb25zaXN0ZW50IHdpdGggdGhpcwogICAgICBMaWNlbnNlLiBIb3dldmVyLCBpbiBhY2NlcHRpbmcgc3VjaCBvYmxpZ2F0aW9ucywgWW91IG1heSBhY3Qgb25seQogICAgICBvbiBZb3VyIG93biBiZWhhbGYgYW5kIG9uIFlvdXIgc29sZSByZXNwb25zaWJpbGl0eSwgbm90IG9uIGJlaGFsZgogICAgICBvZiBhbnkgb3RoZXIgQ29udHJpYnV0b3IsIGFuZCBvbmx5IGlmIFlvdSBhZ3JlZSB0byBpbmRlbW5pZnksCiAgICAgIGRlZmVuZCwgYW5kIGhvbGQgZWFjaCBDb250cmlidXRvciBoYXJtbGVzcyBmb3IgYW55IGxpYWJpbGl0eQogICAgICBpbmN1cnJlZCBieSwgb3IgY2xhaW1zIGFzc2VydGVkIGFnYWluc3QsIHN1Y2ggQ29udHJpYnV0b3IgYnkgcmVhc29uCiAgICAgIG9mIHlvdXIgYWNjZXB0aW5nIGFueSBzdWNoIHdhcnJhbnR5IG9yIGFkZGl0aW9uYWwgbGlhYmlsaXR5LgoKICAgRU5EIE9GIFRFUk1TIEFORCBDT05ESVRJT05TCgogICBBUFBFTkRJWDogSG93IHRvIGFwcGx5IHRoZSBBcGFjaGUgTGljZW5zZSB0byB5b3VyIHdvcmsuCgogICAgICBUbyBhcHBseSB0aGUgQXBhY2hlIExpY2Vuc2UgdG8geW91ciB3b3JrLCBhdHRhY2ggdGhlIGZvbGxvd2luZwogICAgICBib2lsZXJwbGF0ZSBub3RpY2UsIHdpdGggdGhlIGZpZWxkcyBlbmNsb3NlZCBieSBicmFja2V0cyAiW10iCiAgICAgIHJlcGxhY2VkIHdpdGggeW91ciBvd24gaWRlbnRpZnlpbmcgaW5mb3JtYXRpb24uIChEb24ndCBpbmNsdWRlCiAgICAgIHRoZSBicmFja2V0cyEpICBUaGUgdGV4dCBzaG91bGQgYmUgZW5jbG9zZWQgaW4gdGhlIGFwcHJvcHJpYXRlCiAgICAgIGNvbW1lbnQgc3ludGF4IGZvciB0aGUgZmlsZSBmb3JtYXQuIFdlIGFsc28gcmVjb21tZW5kIHRoYXQgYQogICAgICBmaWxlIG9yIGNsYXNzIG5hbWUgYW5kIGRlc2NyaXB0aW9uIG9mIHB1cnBvc2UgYmUgaW5jbHVkZWQgb24gdGhlCiAgICAgIHNhbWUgInByaW50ZWQgcGFnZSIgYXMgdGhlIGNvcHlyaWdodCBub3RpY2UgZm9yIGVhc2llcgogICAgICBpZGVudGlmaWNhdGlvbiB3aXRoaW4gdGhpcmQtcGFydHkgYXJjaGl2ZXMuCgogICBDb3B5cmlnaHQgW3l5eXldIFtuYW1lIG9mIGNvcHlyaWdodCBvd25lcl0KCiAgIExpY2Vuc2VkIHVuZGVyIHRoZSBBcGFjaGUgTGljZW5zZSwgVmVyc2lvbiAyLjAgKHRoZSAiTGljZW5zZSIpOwogICB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCiAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgIGh0dHA6Ly93d3cuYXBhY2hlLm9yZy9saWNlbnNlcy9MSUNFTlNFLTIuMAoKICAgVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQogICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IgaW1wbGllZC4KICAgU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAogICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4= + https://www.apache.org/licenses/LICENSE-2.0.txt + + + pkg:maven/com.acme/tomcat-catalina@9.0.14?packaging=jar + + + + Apache Super Heros + Apache + org.apache.tomcat + tomcat-catalina + 9.0.14 + Apache Catalina + + + Apache-2.0 + + + pkg:maven/org.apache.tomcat/tomcat-catalina@9.0.14?packaging=jar + + + + + 7638417db6d59f3c431d3e1f261cc637155684cd + https://location/to/7638417db6d59f3c431d3e1f261cc637155684cd + + 2018-11-07T22:01:45Z + John Doe + john.doe@example.com + + + 2018-11-07T22:01:45Z + Jane Doe + jane.doe@example.com + + Initial commit + + + Commentary here + + + + + Example Inc. + https://example.com + https://example.net + + Example Support AMER + support@example.com + 800-555-1212 + + + Example Support APAC + support@apac.example.com + + + Example Super Heros + org.example + mylibrary + 1.0.0 + required + + 2342c2eaf1feb9a80195dbaddf2ebaa3 + 68b78babe00a053f9e35ec6a2d9080f5b90122b0 + 708f1f53b41f11f02d12a11b1a38d2905d47b099afc71a0f1124ef8582ec7313 + 387b7ae16b9cae45f830671541539bf544202faae5aac544a93b7b0a04f5f846fa2f4e81ef3f1677e13aed7496408a441f5657ab6d54423e56bf6f38da124aef + + + EPL-2.0 OR GPL-2.0-with-classpath-exception + + Copyright Example Inc. All rights reserved. + cpe:/a:example:myapplication:1.0.0 + pkg:maven/com.example/myapplication@1.0.0?packaging=war + false + + + http://example.org/docs + All component versions are documented here + + + http://example.org/security + + + + + Example Super Heros + com.example + myframework + 1.0.0 + Example Inc, enterprise framework + required + + cfcb0b64aacd2f81c1cd546543de965a + 7fbeef2346c45d565c3341f037bce4e088af8a52 + 0384db3cec55d86a6898c489fdb75a8e75fe66b26639634983d2f3c3558493d1 + 854909cdb9e3ca183056837144aab6d8069b377bd66445087cc7157bf0c3f620418705dd0b83bdc2f73a508c2bdb316ca1809d75ee6972d02023a3e7dd655c79 + + + + Some random license + + + pkg:maven/com.example/myframework@1.0.0?packaging=war + false + + + http://example.com/myframework + + + http://example.com/security + + + + + diff --git a/src/test/resources/unit/cyclonedx/valid-bom-1.5.json b/src/test/resources/unit/cyclonedx/valid-bom-1.5.json new file mode 100644 index 0000000000..23197b9a03 --- /dev/null +++ b/src/test/resources/unit/cyclonedx/valid-bom-1.5.json @@ -0,0 +1,177 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2020-04-13T20:20:39+00:00", + "tools": [ + { + "vendor": "Awesome Vendor", + "name": "Awesome Tool", + "version": "9.1.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "25ed8e31b995bb927966616df2a42b979a2717f0" + }, + { + "alg": "SHA-256", + "content": "a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df" + } + ] + } + ], + "authors": [ + { + "name": "Samantha Wright", + "email": "samantha.wright@example.com", + "phone": "800-555-1212" + } + ], + "component": { + "type": "application", + "author": "Acme Super Heros", + "name": "Acme Application", + "version": "9.1.1", + "swid": { + "tagId": "swidgen-242eb18a-503e-ca37-393b-cf156ef09691_9.1.1", + "name": "Acme Application", + "version": "9.1.1", + "text": { + "contentType": "text/xml", + "encoding": "base64", + "content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg==" + } + } + }, + "manufacture": { + "name": "Acme, Inc.", + "url": [ + "https://example.com" + ], + "contact": [ + { + "name": "Acme Professional Services", + "email": "professional.services@example.com" + } + ] + }, + "supplier": { + "name": "Acme, Inc.", + "url": [ + "https://example.com" + ], + "contact": [ + { + "name": "Acme Distribution", + "email": "distribution@example.com" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:npm/acme/component@1.0.0", + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "tomcat-catalina", + "version": "9.0.14", + "hashes": [ + { + "alg": "MD5", + "content": "3942447fac867ae5cdb3229b658f4d48" + }, + { + "alg": "SHA-1", + "content": "e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a" + }, + { + "alg": "SHA-256", + "content": "f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b" + }, + { + "alg": "SHA-512", + "content": "e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0", + "text": { + "contentType": "text/plain", + "encoding": "base64", + "content": "CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFwYWNoZSBMaWNlbnNlCiAgICAgICAgICAgICAgICAgICAgICAgICAgIFZlcnNpb24gMi4wLCBKYW51YXJ5IDIwMDQKICAgICAgICAgICAgICAgICAgICAgICAgaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzLwoKICAgVEVSTVMgQU5EIENPTkRJVElPTlMgRk9SIFVTRSwgUkVQUk9EVUNUSU9OLCBBTkQgRElTVFJJQlVUSU9OCgogICAxLiBEZWZpbml0aW9ucy4KCiAgICAgICJMaWNlbnNlIiBzaGFsbCBtZWFuIHRoZSB0ZXJtcyBhbmQgY29uZGl0aW9ucyBmb3IgdXNlLCByZXByb2R1Y3Rpb24sCiAgICAgIGFuZCBkaXN0cmlidXRpb24gYXMgZGVmaW5lZCBieSBTZWN0aW9ucyAxIHRocm91Z2ggOSBvZiB0aGlzIGRvY3VtZW50LgoKICAgICAgIkxpY2Vuc29yIiBzaGFsbCBtZWFuIHRoZSBjb3B5cmlnaHQgb3duZXIgb3IgZW50aXR5IGF1dGhvcml6ZWQgYnkKICAgICAgdGhlIGNvcHlyaWdodCBvd25lciB0aGF0IGlzIGdyYW50aW5nIHRoZSBMaWNlbnNlLgoKICAgICAgIkxlZ2FsIEVudGl0eSIgc2hhbGwgbWVhbiB0aGUgdW5pb24gb2YgdGhlIGFjdGluZyBlbnRpdHkgYW5kIGFsbAogICAgICBvdGhlciBlbnRpdGllcyB0aGF0IGNvbnRyb2wsIGFyZSBjb250cm9sbGVkIGJ5LCBvciBhcmUgdW5kZXIgY29tbW9uCiAgICAgIGNvbnRyb2wgd2l0aCB0aGF0IGVudGl0eS4gRm9yIHRoZSBwdXJwb3NlcyBvZiB0aGlzIGRlZmluaXRpb24sCiAgICAgICJjb250cm9sIiBtZWFucyAoaSkgdGhlIHBvd2VyLCBkaXJlY3Qgb3IgaW5kaXJlY3QsIHRvIGNhdXNlIHRoZQogICAgICBkaXJlY3Rpb24gb3IgbWFuYWdlbWVudCBvZiBzdWNoIGVudGl0eSwgd2hldGhlciBieSBjb250cmFjdCBvcgogICAgICBvdGhlcndpc2UsIG9yIChpaSkgb3duZXJzaGlwIG9mIGZpZnR5IHBlcmNlbnQgKDUwJSkgb3IgbW9yZSBvZiB0aGUKICAgICAgb3V0c3RhbmRpbmcgc2hhcmVzLCBvciAoaWlpKSBiZW5lZmljaWFsIG93bmVyc2hpcCBvZiBzdWNoIGVudGl0eS4KCiAgICAgICJZb3UiIChvciAiWW91ciIpIHNoYWxsIG1lYW4gYW4gaW5kaXZpZHVhbCBvciBMZWdhbCBFbnRpdHkKICAgICAgZXhlcmNpc2luZyBwZXJtaXNzaW9ucyBncmFudGVkIGJ5IHRoaXMgTGljZW5zZS4KCiAgICAgICJTb3VyY2UiIGZvcm0gc2hhbGwgbWVhbiB0aGUgcHJlZmVycmVkIGZvcm0gZm9yIG1ha2luZyBtb2RpZmljYXRpb25zLAogICAgICBpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIHNvZnR3YXJlIHNvdXJjZSBjb2RlLCBkb2N1bWVudGF0aW9uCiAgICAgIHNvdXJjZSwgYW5kIGNvbmZpZ3VyYXRpb24gZmlsZXMuCgogICAgICAiT2JqZWN0IiBmb3JtIHNoYWxsIG1lYW4gYW55IGZvcm0gcmVzdWx0aW5nIGZyb20gbWVjaGFuaWNhbAogICAgICB0cmFuc2Zvcm1hdGlvbiBvciB0cmFuc2xhdGlvbiBvZiBhIFNvdXJjZSBmb3JtLCBpbmNsdWRpbmcgYnV0CiAgICAgIG5vdCBsaW1pdGVkIHRvIGNvbXBpbGVkIG9iamVjdCBjb2RlLCBnZW5lcmF0ZWQgZG9jdW1lbnRhdGlvbiwKICAgICAgYW5kIGNvbnZlcnNpb25zIHRvIG90aGVyIG1lZGlhIHR5cGVzLgoKICAgICAgIldvcmsiIHNoYWxsIG1lYW4gdGhlIHdvcmsgb2YgYXV0aG9yc2hpcCwgd2hldGhlciBpbiBTb3VyY2Ugb3IKICAgICAgT2JqZWN0IGZvcm0sIG1hZGUgYXZhaWxhYmxlIHVuZGVyIHRoZSBMaWNlbnNlLCBhcyBpbmRpY2F0ZWQgYnkgYQogICAgICBjb3B5cmlnaHQgbm90aWNlIHRoYXQgaXMgaW5jbHVkZWQgaW4gb3IgYXR0YWNoZWQgdG8gdGhlIHdvcmsKICAgICAgKGFuIGV4YW1wbGUgaXMgcHJvdmlkZWQgaW4gdGhlIEFwcGVuZGl4IGJlbG93KS4KCiAgICAgICJEZXJpdmF0aXZlIFdvcmtzIiBzaGFsbCBtZWFuIGFueSB3b3JrLCB3aGV0aGVyIGluIFNvdXJjZSBvciBPYmplY3QKICAgICAgZm9ybSwgdGhhdCBpcyBiYXNlZCBvbiAob3IgZGVyaXZlZCBmcm9tKSB0aGUgV29yayBhbmQgZm9yIHdoaWNoIHRoZQogICAgICBlZGl0b3JpYWwgcmV2aXNpb25zLCBhbm5vdGF0aW9ucywgZWxhYm9yYXRpb25zLCBvciBvdGhlciBtb2RpZmljYXRpb25zCiAgICAgIHJlcHJlc2VudCwgYXMgYSB3aG9sZSwgYW4gb3JpZ2luYWwgd29yayBvZiBhdXRob3JzaGlwLiBGb3IgdGhlIHB1cnBvc2VzCiAgICAgIG9mIHRoaXMgTGljZW5zZSwgRGVyaXZhdGl2ZSBXb3JrcyBzaGFsbCBub3QgaW5jbHVkZSB3b3JrcyB0aGF0IHJlbWFpbgogICAgICBzZXBhcmFibGUgZnJvbSwgb3IgbWVyZWx5IGxpbmsgKG9yIGJpbmQgYnkgbmFtZSkgdG8gdGhlIGludGVyZmFjZXMgb2YsCiAgICAgIHRoZSBXb3JrIGFuZCBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YuCgogICAgICAiQ29udHJpYnV0aW9uIiBzaGFsbCBtZWFuIGFueSB3b3JrIG9mIGF1dGhvcnNoaXAsIGluY2x1ZGluZwogICAgICB0aGUgb3JpZ2luYWwgdmVyc2lvbiBvZiB0aGUgV29yayBhbmQgYW55IG1vZGlmaWNhdGlvbnMgb3IgYWRkaXRpb25zCiAgICAgIHRvIHRoYXQgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIHRoYXQgaXMgaW50ZW50aW9uYWxseQogICAgICBzdWJtaXR0ZWQgdG8gTGljZW5zb3IgZm9yIGluY2x1c2lvbiBpbiB0aGUgV29yayBieSB0aGUgY29weXJpZ2h0IG93bmVyCiAgICAgIG9yIGJ5IGFuIGluZGl2aWR1YWwgb3IgTGVnYWwgRW50aXR5IGF1dGhvcml6ZWQgdG8gc3VibWl0IG9uIGJlaGFsZiBvZgogICAgICB0aGUgY29weXJpZ2h0IG93bmVyLiBGb3IgdGhlIHB1cnBvc2VzIG9mIHRoaXMgZGVmaW5pdGlvbiwgInN1Ym1pdHRlZCIKICAgICAgbWVhbnMgYW55IGZvcm0gb2YgZWxlY3Ryb25pYywgdmVyYmFsLCBvciB3cml0dGVuIGNvbW11bmljYXRpb24gc2VudAogICAgICB0byB0aGUgTGljZW5zb3Igb3IgaXRzIHJlcHJlc2VudGF0aXZlcywgaW5jbHVkaW5nIGJ1dCBub3QgbGltaXRlZCB0bwogICAgICBjb21tdW5pY2F0aW9uIG9uIGVsZWN0cm9uaWMgbWFpbGluZyBsaXN0cywgc291cmNlIGNvZGUgY29udHJvbCBzeXN0ZW1zLAogICAgICBhbmQgaXNzdWUgdHJhY2tpbmcgc3lzdGVtcyB0aGF0IGFyZSBtYW5hZ2VkIGJ5LCBvciBvbiBiZWhhbGYgb2YsIHRoZQogICAgICBMaWNlbnNvciBmb3IgdGhlIHB1cnBvc2Ugb2YgZGlzY3Vzc2luZyBhbmQgaW1wcm92aW5nIHRoZSBXb3JrLCBidXQKICAgICAgZXhjbHVkaW5nIGNvbW11bmljYXRpb24gdGhhdCBpcyBjb25zcGljdW91c2x5IG1hcmtlZCBvciBvdGhlcndpc2UKICAgICAgZGVzaWduYXRlZCBpbiB3cml0aW5nIGJ5IHRoZSBjb3B5cmlnaHQgb3duZXIgYXMgIk5vdCBhIENvbnRyaWJ1dGlvbi4iCgogICAgICAiQ29udHJpYnV0b3IiIHNoYWxsIG1lYW4gTGljZW5zb3IgYW5kIGFueSBpbmRpdmlkdWFsIG9yIExlZ2FsIEVudGl0eQogICAgICBvbiBiZWhhbGYgb2Ygd2hvbSBhIENvbnRyaWJ1dGlvbiBoYXMgYmVlbiByZWNlaXZlZCBieSBMaWNlbnNvciBhbmQKICAgICAgc3Vic2VxdWVudGx5IGluY29ycG9yYXRlZCB3aXRoaW4gdGhlIFdvcmsuCgogICAyLiBHcmFudCBvZiBDb3B5cmlnaHQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICBjb3B5cmlnaHQgbGljZW5zZSB0byByZXByb2R1Y2UsIHByZXBhcmUgRGVyaXZhdGl2ZSBXb3JrcyBvZiwKICAgICAgcHVibGljbHkgZGlzcGxheSwgcHVibGljbHkgcGVyZm9ybSwgc3VibGljZW5zZSwgYW5kIGRpc3RyaWJ1dGUgdGhlCiAgICAgIFdvcmsgYW5kIHN1Y2ggRGVyaXZhdGl2ZSBXb3JrcyBpbiBTb3VyY2Ugb3IgT2JqZWN0IGZvcm0uCgogICAzLiBHcmFudCBvZiBQYXRlbnQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICAoZXhjZXB0IGFzIHN0YXRlZCBpbiB0aGlzIHNlY3Rpb24pIHBhdGVudCBsaWNlbnNlIHRvIG1ha2UsIGhhdmUgbWFkZSwKICAgICAgdXNlLCBvZmZlciB0byBzZWxsLCBzZWxsLCBpbXBvcnQsIGFuZCBvdGhlcndpc2UgdHJhbnNmZXIgdGhlIFdvcmssCiAgICAgIHdoZXJlIHN1Y2ggbGljZW5zZSBhcHBsaWVzIG9ubHkgdG8gdGhvc2UgcGF0ZW50IGNsYWltcyBsaWNlbnNhYmxlCiAgICAgIGJ5IHN1Y2ggQ29udHJpYnV0b3IgdGhhdCBhcmUgbmVjZXNzYXJpbHkgaW5mcmluZ2VkIGJ5IHRoZWlyCiAgICAgIENvbnRyaWJ1dGlvbihzKSBhbG9uZSBvciBieSBjb21iaW5hdGlvbiBvZiB0aGVpciBDb250cmlidXRpb24ocykKICAgICAgd2l0aCB0aGUgV29yayB0byB3aGljaCBzdWNoIENvbnRyaWJ1dGlvbihzKSB3YXMgc3VibWl0dGVkLiBJZiBZb3UKICAgICAgaW5zdGl0dXRlIHBhdGVudCBsaXRpZ2F0aW9uIGFnYWluc3QgYW55IGVudGl0eSAoaW5jbHVkaW5nIGEKICAgICAgY3Jvc3MtY2xhaW0gb3IgY291bnRlcmNsYWltIGluIGEgbGF3c3VpdCkgYWxsZWdpbmcgdGhhdCB0aGUgV29yawogICAgICBvciBhIENvbnRyaWJ1dGlvbiBpbmNvcnBvcmF0ZWQgd2l0aGluIHRoZSBXb3JrIGNvbnN0aXR1dGVzIGRpcmVjdAogICAgICBvciBjb250cmlidXRvcnkgcGF0ZW50IGluZnJpbmdlbWVudCwgdGhlbiBhbnkgcGF0ZW50IGxpY2Vuc2VzCiAgICAgIGdyYW50ZWQgdG8gWW91IHVuZGVyIHRoaXMgTGljZW5zZSBmb3IgdGhhdCBXb3JrIHNoYWxsIHRlcm1pbmF0ZQogICAgICBhcyBvZiB0aGUgZGF0ZSBzdWNoIGxpdGlnYXRpb24gaXMgZmlsZWQuCgogICA0LiBSZWRpc3RyaWJ1dGlvbi4gWW91IG1heSByZXByb2R1Y2UgYW5kIGRpc3RyaWJ1dGUgY29waWVzIG9mIHRoZQogICAgICBXb3JrIG9yIERlcml2YXRpdmUgV29ya3MgdGhlcmVvZiBpbiBhbnkgbWVkaXVtLCB3aXRoIG9yIHdpdGhvdXQKICAgICAgbW9kaWZpY2F0aW9ucywgYW5kIGluIFNvdXJjZSBvciBPYmplY3QgZm9ybSwgcHJvdmlkZWQgdGhhdCBZb3UKICAgICAgbWVldCB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgogICAgICAoYSkgWW91IG11c3QgZ2l2ZSBhbnkgb3RoZXIgcmVjaXBpZW50cyBvZiB0aGUgV29yayBvcgogICAgICAgICAgRGVyaXZhdGl2ZSBXb3JrcyBhIGNvcHkgb2YgdGhpcyBMaWNlbnNlOyBhbmQKCiAgICAgIChiKSBZb3UgbXVzdCBjYXVzZSBhbnkgbW9kaWZpZWQgZmlsZXMgdG8gY2FycnkgcHJvbWluZW50IG5vdGljZXMKICAgICAgICAgIHN0YXRpbmcgdGhhdCBZb3UgY2hhbmdlZCB0aGUgZmlsZXM7IGFuZAoKICAgICAgKGMpIFlvdSBtdXN0IHJldGFpbiwgaW4gdGhlIFNvdXJjZSBmb3JtIG9mIGFueSBEZXJpdmF0aXZlIFdvcmtzCiAgICAgICAgICB0aGF0IFlvdSBkaXN0cmlidXRlLCBhbGwgY29weXJpZ2h0LCBwYXRlbnQsIHRyYWRlbWFyaywgYW5kCiAgICAgICAgICBhdHRyaWJ1dGlvbiBub3RpY2VzIGZyb20gdGhlIFNvdXJjZSBmb3JtIG9mIHRoZSBXb3JrLAogICAgICAgICAgZXhjbHVkaW5nIHRob3NlIG5vdGljZXMgdGhhdCBkbyBub3QgcGVydGFpbiB0byBhbnkgcGFydCBvZgogICAgICAgICAgdGhlIERlcml2YXRpdmUgV29ya3M7IGFuZAoKICAgICAgKGQpIElmIHRoZSBXb3JrIGluY2x1ZGVzIGEgIk5PVElDRSIgdGV4dCBmaWxlIGFzIHBhcnQgb2YgaXRzCiAgICAgICAgICBkaXN0cmlidXRpb24sIHRoZW4gYW55IERlcml2YXRpdmUgV29ya3MgdGhhdCBZb3UgZGlzdHJpYnV0ZSBtdXN0CiAgICAgICAgICBpbmNsdWRlIGEgcmVhZGFibGUgY29weSBvZiB0aGUgYXR0cmlidXRpb24gbm90aWNlcyBjb250YWluZWQKICAgICAgICAgIHdpdGhpbiBzdWNoIE5PVElDRSBmaWxlLCBleGNsdWRpbmcgdGhvc2Ugbm90aWNlcyB0aGF0IGRvIG5vdAogICAgICAgICAgcGVydGFpbiB0byBhbnkgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaW4gYXQgbGVhc3Qgb25lCiAgICAgICAgICBvZiB0aGUgZm9sbG93aW5nIHBsYWNlczogd2l0aGluIGEgTk9USUNFIHRleHQgZmlsZSBkaXN0cmlidXRlZAogICAgICAgICAgYXMgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgd2l0aGluIHRoZSBTb3VyY2UgZm9ybSBvcgogICAgICAgICAgZG9jdW1lbnRhdGlvbiwgaWYgcHJvdmlkZWQgYWxvbmcgd2l0aCB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgb3IsCiAgICAgICAgICB3aXRoaW4gYSBkaXNwbGF5IGdlbmVyYXRlZCBieSB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaWYgYW5kCiAgICAgICAgICB3aGVyZXZlciBzdWNoIHRoaXJkLXBhcnR5IG5vdGljZXMgbm9ybWFsbHkgYXBwZWFyLiBUaGUgY29udGVudHMKICAgICAgICAgIG9mIHRoZSBOT1RJQ0UgZmlsZSBhcmUgZm9yIGluZm9ybWF0aW9uYWwgcHVycG9zZXMgb25seSBhbmQKICAgICAgICAgIGRvIG5vdCBtb2RpZnkgdGhlIExpY2Vuc2UuIFlvdSBtYXkgYWRkIFlvdXIgb3duIGF0dHJpYnV0aW9uCiAgICAgICAgICBub3RpY2VzIHdpdGhpbiBEZXJpdmF0aXZlIFdvcmtzIHRoYXQgWW91IGRpc3RyaWJ1dGUsIGFsb25nc2lkZQogICAgICAgICAgb3IgYXMgYW4gYWRkZW5kdW0gdG8gdGhlIE5PVElDRSB0ZXh0IGZyb20gdGhlIFdvcmssIHByb3ZpZGVkCiAgICAgICAgICB0aGF0IHN1Y2ggYWRkaXRpb25hbCBhdHRyaWJ1dGlvbiBub3RpY2VzIGNhbm5vdCBiZSBjb25zdHJ1ZWQKICAgICAgICAgIGFzIG1vZGlmeWluZyB0aGUgTGljZW5zZS4KCiAgICAgIFlvdSBtYXkgYWRkIFlvdXIgb3duIGNvcHlyaWdodCBzdGF0ZW1lbnQgdG8gWW91ciBtb2RpZmljYXRpb25zIGFuZAogICAgICBtYXkgcHJvdmlkZSBhZGRpdGlvbmFsIG9yIGRpZmZlcmVudCBsaWNlbnNlIHRlcm1zIGFuZCBjb25kaXRpb25zCiAgICAgIGZvciB1c2UsIHJlcHJvZHVjdGlvbiwgb3IgZGlzdHJpYnV0aW9uIG9mIFlvdXIgbW9kaWZpY2F0aW9ucywgb3IKICAgICAgZm9yIGFueSBzdWNoIERlcml2YXRpdmUgV29ya3MgYXMgYSB3aG9sZSwgcHJvdmlkZWQgWW91ciB1c2UsCiAgICAgIHJlcHJvZHVjdGlvbiwgYW5kIGRpc3RyaWJ1dGlvbiBvZiB0aGUgV29yayBvdGhlcndpc2UgY29tcGxpZXMgd2l0aAogICAgICB0aGUgY29uZGl0aW9ucyBzdGF0ZWQgaW4gdGhpcyBMaWNlbnNlLgoKICAgNS4gU3VibWlzc2lvbiBvZiBDb250cmlidXRpb25zLiBVbmxlc3MgWW91IGV4cGxpY2l0bHkgc3RhdGUgb3RoZXJ3aXNlLAogICAgICBhbnkgQ29udHJpYnV0aW9uIGludGVudGlvbmFsbHkgc3VibWl0dGVkIGZvciBpbmNsdXNpb24gaW4gdGhlIFdvcmsKICAgICAgYnkgWW91IHRvIHRoZSBMaWNlbnNvciBzaGFsbCBiZSB1bmRlciB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCB3aXRob3V0IGFueSBhZGRpdGlvbmFsIHRlcm1zIG9yIGNvbmRpdGlvbnMuCiAgICAgIE5vdHdpdGhzdGFuZGluZyB0aGUgYWJvdmUsIG5vdGhpbmcgaGVyZWluIHNoYWxsIHN1cGVyc2VkZSBvciBtb2RpZnkKICAgICAgdGhlIHRlcm1zIG9mIGFueSBzZXBhcmF0ZSBsaWNlbnNlIGFncmVlbWVudCB5b3UgbWF5IGhhdmUgZXhlY3V0ZWQKICAgICAgd2l0aCBMaWNlbnNvciByZWdhcmRpbmcgc3VjaCBDb250cmlidXRpb25zLgoKICAgNi4gVHJhZGVtYXJrcy4gVGhpcyBMaWNlbnNlIGRvZXMgbm90IGdyYW50IHBlcm1pc3Npb24gdG8gdXNlIHRoZSB0cmFkZQogICAgICBuYW1lcywgdHJhZGVtYXJrcywgc2VydmljZSBtYXJrcywgb3IgcHJvZHVjdCBuYW1lcyBvZiB0aGUgTGljZW5zb3IsCiAgICAgIGV4Y2VwdCBhcyByZXF1aXJlZCBmb3IgcmVhc29uYWJsZSBhbmQgY3VzdG9tYXJ5IHVzZSBpbiBkZXNjcmliaW5nIHRoZQogICAgICBvcmlnaW4gb2YgdGhlIFdvcmsgYW5kIHJlcHJvZHVjaW5nIHRoZSBjb250ZW50IG9mIHRoZSBOT1RJQ0UgZmlsZS4KCiAgIDcuIERpc2NsYWltZXIgb2YgV2FycmFudHkuIFVubGVzcyByZXF1aXJlZCBieSBhcHBsaWNhYmxlIGxhdyBvcgogICAgICBhZ3JlZWQgdG8gaW4gd3JpdGluZywgTGljZW5zb3IgcHJvdmlkZXMgdGhlIFdvcmsgKGFuZCBlYWNoCiAgICAgIENvbnRyaWJ1dG9yIHByb3ZpZGVzIGl0cyBDb250cmlidXRpb25zKSBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IKICAgICAgaW1wbGllZCwgaW5jbHVkaW5nLCB3aXRob3V0IGxpbWl0YXRpb24sIGFueSB3YXJyYW50aWVzIG9yIGNvbmRpdGlvbnMKICAgICAgb2YgVElUTEUsIE5PTi1JTkZSSU5HRU1FTlQsIE1FUkNIQU5UQUJJTElUWSwgb3IgRklUTkVTUyBGT1IgQQogICAgICBQQVJUSUNVTEFSIFBVUlBPU0UuIFlvdSBhcmUgc29sZWx5IHJlc3BvbnNpYmxlIGZvciBkZXRlcm1pbmluZyB0aGUKICAgICAgYXBwcm9wcmlhdGVuZXNzIG9mIHVzaW5nIG9yIHJlZGlzdHJpYnV0aW5nIHRoZSBXb3JrIGFuZCBhc3N1bWUgYW55CiAgICAgIHJpc2tzIGFzc29jaWF0ZWQgd2l0aCBZb3VyIGV4ZXJjaXNlIG9mIHBlcm1pc3Npb25zIHVuZGVyIHRoaXMgTGljZW5zZS4KCiAgIDguIExpbWl0YXRpb24gb2YgTGlhYmlsaXR5LiBJbiBubyBldmVudCBhbmQgdW5kZXIgbm8gbGVnYWwgdGhlb3J5LAogICAgICB3aGV0aGVyIGluIHRvcnQgKGluY2x1ZGluZyBuZWdsaWdlbmNlKSwgY29udHJhY3QsIG9yIG90aGVyd2lzZSwKICAgICAgdW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IChzdWNoIGFzIGRlbGliZXJhdGUgYW5kIGdyb3NzbHkKICAgICAgbmVnbGlnZW50IGFjdHMpIG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzaGFsbCBhbnkgQ29udHJpYnV0b3IgYmUKICAgICAgbGlhYmxlIHRvIFlvdSBmb3IgZGFtYWdlcywgaW5jbHVkaW5nIGFueSBkaXJlY3QsIGluZGlyZWN0LCBzcGVjaWFsLAogICAgICBpbmNpZGVudGFsLCBvciBjb25zZXF1ZW50aWFsIGRhbWFnZXMgb2YgYW55IGNoYXJhY3RlciBhcmlzaW5nIGFzIGEKICAgICAgcmVzdWx0IG9mIHRoaXMgTGljZW5zZSBvciBvdXQgb2YgdGhlIHVzZSBvciBpbmFiaWxpdHkgdG8gdXNlIHRoZQogICAgICBXb3JrIChpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIGRhbWFnZXMgZm9yIGxvc3Mgb2YgZ29vZHdpbGwsCiAgICAgIHdvcmsgc3RvcHBhZ2UsIGNvbXB1dGVyIGZhaWx1cmUgb3IgbWFsZnVuY3Rpb24sIG9yIGFueSBhbmQgYWxsCiAgICAgIG90aGVyIGNvbW1lcmNpYWwgZGFtYWdlcyBvciBsb3NzZXMpLCBldmVuIGlmIHN1Y2ggQ29udHJpYnV0b3IKICAgICAgaGFzIGJlZW4gYWR2aXNlZCBvZiB0aGUgcG9zc2liaWxpdHkgb2Ygc3VjaCBkYW1hZ2VzLgoKICAgOS4gQWNjZXB0aW5nIFdhcnJhbnR5IG9yIEFkZGl0aW9uYWwgTGlhYmlsaXR5LiBXaGlsZSByZWRpc3RyaWJ1dGluZwogICAgICB0aGUgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIFlvdSBtYXkgY2hvb3NlIHRvIG9mZmVyLAogICAgICBhbmQgY2hhcmdlIGEgZmVlIGZvciwgYWNjZXB0YW5jZSBvZiBzdXBwb3J0LCB3YXJyYW50eSwgaW5kZW1uaXR5LAogICAgICBvciBvdGhlciBsaWFiaWxpdHkgb2JsaWdhdGlvbnMgYW5kL29yIHJpZ2h0cyBjb25zaXN0ZW50IHdpdGggdGhpcwogICAgICBMaWNlbnNlLiBIb3dldmVyLCBpbiBhY2NlcHRpbmcgc3VjaCBvYmxpZ2F0aW9ucywgWW91IG1heSBhY3Qgb25seQogICAgICBvbiBZb3VyIG93biBiZWhhbGYgYW5kIG9uIFlvdXIgc29sZSByZXNwb25zaWJpbGl0eSwgbm90IG9uIGJlaGFsZgogICAgICBvZiBhbnkgb3RoZXIgQ29udHJpYnV0b3IsIGFuZCBvbmx5IGlmIFlvdSBhZ3JlZSB0byBpbmRlbW5pZnksCiAgICAgIGRlZmVuZCwgYW5kIGhvbGQgZWFjaCBDb250cmlidXRvciBoYXJtbGVzcyBmb3IgYW55IGxpYWJpbGl0eQogICAgICBpbmN1cnJlZCBieSwgb3IgY2xhaW1zIGFzc2VydGVkIGFnYWluc3QsIHN1Y2ggQ29udHJpYnV0b3IgYnkgcmVhc29uCiAgICAgIG9mIHlvdXIgYWNjZXB0aW5nIGFueSBzdWNoIHdhcnJhbnR5IG9yIGFkZGl0aW9uYWwgbGlhYmlsaXR5LgoKICAgRU5EIE9GIFRFUk1TIEFORCBDT05ESVRJT05TCgogICBBUFBFTkRJWDogSG93IHRvIGFwcGx5IHRoZSBBcGFjaGUgTGljZW5zZSB0byB5b3VyIHdvcmsuCgogICAgICBUbyBhcHBseSB0aGUgQXBhY2hlIExpY2Vuc2UgdG8geW91ciB3b3JrLCBhdHRhY2ggdGhlIGZvbGxvd2luZwogICAgICBib2lsZXJwbGF0ZSBub3RpY2UsIHdpdGggdGhlIGZpZWxkcyBlbmNsb3NlZCBieSBicmFja2V0cyAiW10iCiAgICAgIHJlcGxhY2VkIHdpdGggeW91ciBvd24gaWRlbnRpZnlpbmcgaW5mb3JtYXRpb24uIChEb24ndCBpbmNsdWRlCiAgICAgIHRoZSBicmFja2V0cyEpICBUaGUgdGV4dCBzaG91bGQgYmUgZW5jbG9zZWQgaW4gdGhlIGFwcHJvcHJpYXRlCiAgICAgIGNvbW1lbnQgc3ludGF4IGZvciB0aGUgZmlsZSBmb3JtYXQuIFdlIGFsc28gcmVjb21tZW5kIHRoYXQgYQogICAgICBmaWxlIG9yIGNsYXNzIG5hbWUgYW5kIGRlc2NyaXB0aW9uIG9mIHB1cnBvc2UgYmUgaW5jbHVkZWQgb24gdGhlCiAgICAgIHNhbWUgInByaW50ZWQgcGFnZSIgYXMgdGhlIGNvcHlyaWdodCBub3RpY2UgZm9yIGVhc2llcgogICAgICBpZGVudGlmaWNhdGlvbiB3aXRoaW4gdGhpcmQtcGFydHkgYXJjaGl2ZXMuCgogICBDb3B5cmlnaHQgW3l5eXldIFtuYW1lIG9mIGNvcHlyaWdodCBvd25lcl0KCiAgIExpY2Vuc2VkIHVuZGVyIHRoZSBBcGFjaGUgTGljZW5zZSwgVmVyc2lvbiAyLjAgKHRoZSAiTGljZW5zZSIpOwogICB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCiAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgIGh0dHA6Ly93d3cuYXBhY2hlLm9yZy9saWNlbnNlcy9MSUNFTlNFLTIuMAoKICAgVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQogICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IgaW1wbGllZC4KICAgU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAogICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4=" + }, + "url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + ], + "purl": "pkg:npm/acme/component@1.0.0", + "pedigree": { + "ancestors": [ + { + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "tomcat-catalina", + "version": "9.0.14" + }, + { + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "tomcat-catalina", + "version": "9.0.14" + } + ], + "commits": [ + { + "uid": "7638417db6d59f3c431d3e1f261cc637155684cd", + "url": "https://location/to/7638417db6d59f3c431d3e1f261cc637155684cd", + "author": { + "timestamp": "2018-11-13T20:20:39+00:00", + "name": "me", + "email": "me@acme.org" + } + } + ] + } + }, + { + "type": "library", + "supplier": { + "name": "Example, Inc.", + "url": [ + "https://example.com", + "https://example.net" + ], + "contact": [ + { + "name": "Example Support AMER Distribution", + "email": "support@example.com", + "phone": "800-555-1212" + }, + { + "name": "Example Support APAC", + "email": "support@apac.example.com" + } + ] + }, + "author": "Example Super Heros", + "group": "org.example", + "name": "mylibrary", + "version": "1.0.0" + } + ], + "dependencies": [ + { + "ref": "pkg:npm/acme/component@1.0.0", + "dependsOn": [ + "pkg:npm/acme/component@1.0.0" + ] + } + ] +} diff --git a/src/test/resources/unit/cyclonedx/valid-bom-1.5.xml b/src/test/resources/unit/cyclonedx/valid-bom-1.5.xml new file mode 100644 index 0000000000..913285eac6 --- /dev/null +++ b/src/test/resources/unit/cyclonedx/valid-bom-1.5.xml @@ -0,0 +1,181 @@ + + + + 2020-04-07T07:01:00Z + + + Awesome Vendor + Awesome Tool + 9.1.2 + + 25ed8e31b995bb927966616df2a42b979a2717f0 + a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df + + + + + + Samantha Wright + samantha.wright@example.com + 800-555-1212 + + + + Acme Super Heros + Acme Application + 9.1.1 + + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg== + + + + Acme, Inc. + https://example.com + + Acme Professional Services + professional.services@example.com + + + + Acme, Inc. + https://example.com + + Acme Distribution + distribution@example.com + + + + + + Acme Super Heros + Acme Inc + com.acme + tomcat-catalina + 9.0.14 + Modified version of Apache Catalina + required + + 3942447fac867ae5cdb3229b658f4d48 + e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a + f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b + e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282 + + + + Apache-2.0 + CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFwYWNoZSBMaWNlbnNlCiAgICAgICAgICAgICAgICAgICAgICAgICAgIFZlcnNpb24gMi4wLCBKYW51YXJ5IDIwMDQKICAgICAgICAgICAgICAgICAgICAgICAgaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzLwoKICAgVEVSTVMgQU5EIENPTkRJVElPTlMgRk9SIFVTRSwgUkVQUk9EVUNUSU9OLCBBTkQgRElTVFJJQlVUSU9OCgogICAxLiBEZWZpbml0aW9ucy4KCiAgICAgICJMaWNlbnNlIiBzaGFsbCBtZWFuIHRoZSB0ZXJtcyBhbmQgY29uZGl0aW9ucyBmb3IgdXNlLCByZXByb2R1Y3Rpb24sCiAgICAgIGFuZCBkaXN0cmlidXRpb24gYXMgZGVmaW5lZCBieSBTZWN0aW9ucyAxIHRocm91Z2ggOSBvZiB0aGlzIGRvY3VtZW50LgoKICAgICAgIkxpY2Vuc29yIiBzaGFsbCBtZWFuIHRoZSBjb3B5cmlnaHQgb3duZXIgb3IgZW50aXR5IGF1dGhvcml6ZWQgYnkKICAgICAgdGhlIGNvcHlyaWdodCBvd25lciB0aGF0IGlzIGdyYW50aW5nIHRoZSBMaWNlbnNlLgoKICAgICAgIkxlZ2FsIEVudGl0eSIgc2hhbGwgbWVhbiB0aGUgdW5pb24gb2YgdGhlIGFjdGluZyBlbnRpdHkgYW5kIGFsbAogICAgICBvdGhlciBlbnRpdGllcyB0aGF0IGNvbnRyb2wsIGFyZSBjb250cm9sbGVkIGJ5LCBvciBhcmUgdW5kZXIgY29tbW9uCiAgICAgIGNvbnRyb2wgd2l0aCB0aGF0IGVudGl0eS4gRm9yIHRoZSBwdXJwb3NlcyBvZiB0aGlzIGRlZmluaXRpb24sCiAgICAgICJjb250cm9sIiBtZWFucyAoaSkgdGhlIHBvd2VyLCBkaXJlY3Qgb3IgaW5kaXJlY3QsIHRvIGNhdXNlIHRoZQogICAgICBkaXJlY3Rpb24gb3IgbWFuYWdlbWVudCBvZiBzdWNoIGVudGl0eSwgd2hldGhlciBieSBjb250cmFjdCBvcgogICAgICBvdGhlcndpc2UsIG9yIChpaSkgb3duZXJzaGlwIG9mIGZpZnR5IHBlcmNlbnQgKDUwJSkgb3IgbW9yZSBvZiB0aGUKICAgICAgb3V0c3RhbmRpbmcgc2hhcmVzLCBvciAoaWlpKSBiZW5lZmljaWFsIG93bmVyc2hpcCBvZiBzdWNoIGVudGl0eS4KCiAgICAgICJZb3UiIChvciAiWW91ciIpIHNoYWxsIG1lYW4gYW4gaW5kaXZpZHVhbCBvciBMZWdhbCBFbnRpdHkKICAgICAgZXhlcmNpc2luZyBwZXJtaXNzaW9ucyBncmFudGVkIGJ5IHRoaXMgTGljZW5zZS4KCiAgICAgICJTb3VyY2UiIGZvcm0gc2hhbGwgbWVhbiB0aGUgcHJlZmVycmVkIGZvcm0gZm9yIG1ha2luZyBtb2RpZmljYXRpb25zLAogICAgICBpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIHNvZnR3YXJlIHNvdXJjZSBjb2RlLCBkb2N1bWVudGF0aW9uCiAgICAgIHNvdXJjZSwgYW5kIGNvbmZpZ3VyYXRpb24gZmlsZXMuCgogICAgICAiT2JqZWN0IiBmb3JtIHNoYWxsIG1lYW4gYW55IGZvcm0gcmVzdWx0aW5nIGZyb20gbWVjaGFuaWNhbAogICAgICB0cmFuc2Zvcm1hdGlvbiBvciB0cmFuc2xhdGlvbiBvZiBhIFNvdXJjZSBmb3JtLCBpbmNsdWRpbmcgYnV0CiAgICAgIG5vdCBsaW1pdGVkIHRvIGNvbXBpbGVkIG9iamVjdCBjb2RlLCBnZW5lcmF0ZWQgZG9jdW1lbnRhdGlvbiwKICAgICAgYW5kIGNvbnZlcnNpb25zIHRvIG90aGVyIG1lZGlhIHR5cGVzLgoKICAgICAgIldvcmsiIHNoYWxsIG1lYW4gdGhlIHdvcmsgb2YgYXV0aG9yc2hpcCwgd2hldGhlciBpbiBTb3VyY2Ugb3IKICAgICAgT2JqZWN0IGZvcm0sIG1hZGUgYXZhaWxhYmxlIHVuZGVyIHRoZSBMaWNlbnNlLCBhcyBpbmRpY2F0ZWQgYnkgYQogICAgICBjb3B5cmlnaHQgbm90aWNlIHRoYXQgaXMgaW5jbHVkZWQgaW4gb3IgYXR0YWNoZWQgdG8gdGhlIHdvcmsKICAgICAgKGFuIGV4YW1wbGUgaXMgcHJvdmlkZWQgaW4gdGhlIEFwcGVuZGl4IGJlbG93KS4KCiAgICAgICJEZXJpdmF0aXZlIFdvcmtzIiBzaGFsbCBtZWFuIGFueSB3b3JrLCB3aGV0aGVyIGluIFNvdXJjZSBvciBPYmplY3QKICAgICAgZm9ybSwgdGhhdCBpcyBiYXNlZCBvbiAob3IgZGVyaXZlZCBmcm9tKSB0aGUgV29yayBhbmQgZm9yIHdoaWNoIHRoZQogICAgICBlZGl0b3JpYWwgcmV2aXNpb25zLCBhbm5vdGF0aW9ucywgZWxhYm9yYXRpb25zLCBvciBvdGhlciBtb2RpZmljYXRpb25zCiAgICAgIHJlcHJlc2VudCwgYXMgYSB3aG9sZSwgYW4gb3JpZ2luYWwgd29yayBvZiBhdXRob3JzaGlwLiBGb3IgdGhlIHB1cnBvc2VzCiAgICAgIG9mIHRoaXMgTGljZW5zZSwgRGVyaXZhdGl2ZSBXb3JrcyBzaGFsbCBub3QgaW5jbHVkZSB3b3JrcyB0aGF0IHJlbWFpbgogICAgICBzZXBhcmFibGUgZnJvbSwgb3IgbWVyZWx5IGxpbmsgKG9yIGJpbmQgYnkgbmFtZSkgdG8gdGhlIGludGVyZmFjZXMgb2YsCiAgICAgIHRoZSBXb3JrIGFuZCBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YuCgogICAgICAiQ29udHJpYnV0aW9uIiBzaGFsbCBtZWFuIGFueSB3b3JrIG9mIGF1dGhvcnNoaXAsIGluY2x1ZGluZwogICAgICB0aGUgb3JpZ2luYWwgdmVyc2lvbiBvZiB0aGUgV29yayBhbmQgYW55IG1vZGlmaWNhdGlvbnMgb3IgYWRkaXRpb25zCiAgICAgIHRvIHRoYXQgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIHRoYXQgaXMgaW50ZW50aW9uYWxseQogICAgICBzdWJtaXR0ZWQgdG8gTGljZW5zb3IgZm9yIGluY2x1c2lvbiBpbiB0aGUgV29yayBieSB0aGUgY29weXJpZ2h0IG93bmVyCiAgICAgIG9yIGJ5IGFuIGluZGl2aWR1YWwgb3IgTGVnYWwgRW50aXR5IGF1dGhvcml6ZWQgdG8gc3VibWl0IG9uIGJlaGFsZiBvZgogICAgICB0aGUgY29weXJpZ2h0IG93bmVyLiBGb3IgdGhlIHB1cnBvc2VzIG9mIHRoaXMgZGVmaW5pdGlvbiwgInN1Ym1pdHRlZCIKICAgICAgbWVhbnMgYW55IGZvcm0gb2YgZWxlY3Ryb25pYywgdmVyYmFsLCBvciB3cml0dGVuIGNvbW11bmljYXRpb24gc2VudAogICAgICB0byB0aGUgTGljZW5zb3Igb3IgaXRzIHJlcHJlc2VudGF0aXZlcywgaW5jbHVkaW5nIGJ1dCBub3QgbGltaXRlZCB0bwogICAgICBjb21tdW5pY2F0aW9uIG9uIGVsZWN0cm9uaWMgbWFpbGluZyBsaXN0cywgc291cmNlIGNvZGUgY29udHJvbCBzeXN0ZW1zLAogICAgICBhbmQgaXNzdWUgdHJhY2tpbmcgc3lzdGVtcyB0aGF0IGFyZSBtYW5hZ2VkIGJ5LCBvciBvbiBiZWhhbGYgb2YsIHRoZQogICAgICBMaWNlbnNvciBmb3IgdGhlIHB1cnBvc2Ugb2YgZGlzY3Vzc2luZyBhbmQgaW1wcm92aW5nIHRoZSBXb3JrLCBidXQKICAgICAgZXhjbHVkaW5nIGNvbW11bmljYXRpb24gdGhhdCBpcyBjb25zcGljdW91c2x5IG1hcmtlZCBvciBvdGhlcndpc2UKICAgICAgZGVzaWduYXRlZCBpbiB3cml0aW5nIGJ5IHRoZSBjb3B5cmlnaHQgb3duZXIgYXMgIk5vdCBhIENvbnRyaWJ1dGlvbi4iCgogICAgICAiQ29udHJpYnV0b3IiIHNoYWxsIG1lYW4gTGljZW5zb3IgYW5kIGFueSBpbmRpdmlkdWFsIG9yIExlZ2FsIEVudGl0eQogICAgICBvbiBiZWhhbGYgb2Ygd2hvbSBhIENvbnRyaWJ1dGlvbiBoYXMgYmVlbiByZWNlaXZlZCBieSBMaWNlbnNvciBhbmQKICAgICAgc3Vic2VxdWVudGx5IGluY29ycG9yYXRlZCB3aXRoaW4gdGhlIFdvcmsuCgogICAyLiBHcmFudCBvZiBDb3B5cmlnaHQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICBjb3B5cmlnaHQgbGljZW5zZSB0byByZXByb2R1Y2UsIHByZXBhcmUgRGVyaXZhdGl2ZSBXb3JrcyBvZiwKICAgICAgcHVibGljbHkgZGlzcGxheSwgcHVibGljbHkgcGVyZm9ybSwgc3VibGljZW5zZSwgYW5kIGRpc3RyaWJ1dGUgdGhlCiAgICAgIFdvcmsgYW5kIHN1Y2ggRGVyaXZhdGl2ZSBXb3JrcyBpbiBTb3VyY2Ugb3IgT2JqZWN0IGZvcm0uCgogICAzLiBHcmFudCBvZiBQYXRlbnQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICAoZXhjZXB0IGFzIHN0YXRlZCBpbiB0aGlzIHNlY3Rpb24pIHBhdGVudCBsaWNlbnNlIHRvIG1ha2UsIGhhdmUgbWFkZSwKICAgICAgdXNlLCBvZmZlciB0byBzZWxsLCBzZWxsLCBpbXBvcnQsIGFuZCBvdGhlcndpc2UgdHJhbnNmZXIgdGhlIFdvcmssCiAgICAgIHdoZXJlIHN1Y2ggbGljZW5zZSBhcHBsaWVzIG9ubHkgdG8gdGhvc2UgcGF0ZW50IGNsYWltcyBsaWNlbnNhYmxlCiAgICAgIGJ5IHN1Y2ggQ29udHJpYnV0b3IgdGhhdCBhcmUgbmVjZXNzYXJpbHkgaW5mcmluZ2VkIGJ5IHRoZWlyCiAgICAgIENvbnRyaWJ1dGlvbihzKSBhbG9uZSBvciBieSBjb21iaW5hdGlvbiBvZiB0aGVpciBDb250cmlidXRpb24ocykKICAgICAgd2l0aCB0aGUgV29yayB0byB3aGljaCBzdWNoIENvbnRyaWJ1dGlvbihzKSB3YXMgc3VibWl0dGVkLiBJZiBZb3UKICAgICAgaW5zdGl0dXRlIHBhdGVudCBsaXRpZ2F0aW9uIGFnYWluc3QgYW55IGVudGl0eSAoaW5jbHVkaW5nIGEKICAgICAgY3Jvc3MtY2xhaW0gb3IgY291bnRlcmNsYWltIGluIGEgbGF3c3VpdCkgYWxsZWdpbmcgdGhhdCB0aGUgV29yawogICAgICBvciBhIENvbnRyaWJ1dGlvbiBpbmNvcnBvcmF0ZWQgd2l0aGluIHRoZSBXb3JrIGNvbnN0aXR1dGVzIGRpcmVjdAogICAgICBvciBjb250cmlidXRvcnkgcGF0ZW50IGluZnJpbmdlbWVudCwgdGhlbiBhbnkgcGF0ZW50IGxpY2Vuc2VzCiAgICAgIGdyYW50ZWQgdG8gWW91IHVuZGVyIHRoaXMgTGljZW5zZSBmb3IgdGhhdCBXb3JrIHNoYWxsIHRlcm1pbmF0ZQogICAgICBhcyBvZiB0aGUgZGF0ZSBzdWNoIGxpdGlnYXRpb24gaXMgZmlsZWQuCgogICA0LiBSZWRpc3RyaWJ1dGlvbi4gWW91IG1heSByZXByb2R1Y2UgYW5kIGRpc3RyaWJ1dGUgY29waWVzIG9mIHRoZQogICAgICBXb3JrIG9yIERlcml2YXRpdmUgV29ya3MgdGhlcmVvZiBpbiBhbnkgbWVkaXVtLCB3aXRoIG9yIHdpdGhvdXQKICAgICAgbW9kaWZpY2F0aW9ucywgYW5kIGluIFNvdXJjZSBvciBPYmplY3QgZm9ybSwgcHJvdmlkZWQgdGhhdCBZb3UKICAgICAgbWVldCB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgogICAgICAoYSkgWW91IG11c3QgZ2l2ZSBhbnkgb3RoZXIgcmVjaXBpZW50cyBvZiB0aGUgV29yayBvcgogICAgICAgICAgRGVyaXZhdGl2ZSBXb3JrcyBhIGNvcHkgb2YgdGhpcyBMaWNlbnNlOyBhbmQKCiAgICAgIChiKSBZb3UgbXVzdCBjYXVzZSBhbnkgbW9kaWZpZWQgZmlsZXMgdG8gY2FycnkgcHJvbWluZW50IG5vdGljZXMKICAgICAgICAgIHN0YXRpbmcgdGhhdCBZb3UgY2hhbmdlZCB0aGUgZmlsZXM7IGFuZAoKICAgICAgKGMpIFlvdSBtdXN0IHJldGFpbiwgaW4gdGhlIFNvdXJjZSBmb3JtIG9mIGFueSBEZXJpdmF0aXZlIFdvcmtzCiAgICAgICAgICB0aGF0IFlvdSBkaXN0cmlidXRlLCBhbGwgY29weXJpZ2h0LCBwYXRlbnQsIHRyYWRlbWFyaywgYW5kCiAgICAgICAgICBhdHRyaWJ1dGlvbiBub3RpY2VzIGZyb20gdGhlIFNvdXJjZSBmb3JtIG9mIHRoZSBXb3JrLAogICAgICAgICAgZXhjbHVkaW5nIHRob3NlIG5vdGljZXMgdGhhdCBkbyBub3QgcGVydGFpbiB0byBhbnkgcGFydCBvZgogICAgICAgICAgdGhlIERlcml2YXRpdmUgV29ya3M7IGFuZAoKICAgICAgKGQpIElmIHRoZSBXb3JrIGluY2x1ZGVzIGEgIk5PVElDRSIgdGV4dCBmaWxlIGFzIHBhcnQgb2YgaXRzCiAgICAgICAgICBkaXN0cmlidXRpb24sIHRoZW4gYW55IERlcml2YXRpdmUgV29ya3MgdGhhdCBZb3UgZGlzdHJpYnV0ZSBtdXN0CiAgICAgICAgICBpbmNsdWRlIGEgcmVhZGFibGUgY29weSBvZiB0aGUgYXR0cmlidXRpb24gbm90aWNlcyBjb250YWluZWQKICAgICAgICAgIHdpdGhpbiBzdWNoIE5PVElDRSBmaWxlLCBleGNsdWRpbmcgdGhvc2Ugbm90aWNlcyB0aGF0IGRvIG5vdAogICAgICAgICAgcGVydGFpbiB0byBhbnkgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaW4gYXQgbGVhc3Qgb25lCiAgICAgICAgICBvZiB0aGUgZm9sbG93aW5nIHBsYWNlczogd2l0aGluIGEgTk9USUNFIHRleHQgZmlsZSBkaXN0cmlidXRlZAogICAgICAgICAgYXMgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgd2l0aGluIHRoZSBTb3VyY2UgZm9ybSBvcgogICAgICAgICAgZG9jdW1lbnRhdGlvbiwgaWYgcHJvdmlkZWQgYWxvbmcgd2l0aCB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgb3IsCiAgICAgICAgICB3aXRoaW4gYSBkaXNwbGF5IGdlbmVyYXRlZCBieSB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaWYgYW5kCiAgICAgICAgICB3aGVyZXZlciBzdWNoIHRoaXJkLXBhcnR5IG5vdGljZXMgbm9ybWFsbHkgYXBwZWFyLiBUaGUgY29udGVudHMKICAgICAgICAgIG9mIHRoZSBOT1RJQ0UgZmlsZSBhcmUgZm9yIGluZm9ybWF0aW9uYWwgcHVycG9zZXMgb25seSBhbmQKICAgICAgICAgIGRvIG5vdCBtb2RpZnkgdGhlIExpY2Vuc2UuIFlvdSBtYXkgYWRkIFlvdXIgb3duIGF0dHJpYnV0aW9uCiAgICAgICAgICBub3RpY2VzIHdpdGhpbiBEZXJpdmF0aXZlIFdvcmtzIHRoYXQgWW91IGRpc3RyaWJ1dGUsIGFsb25nc2lkZQogICAgICAgICAgb3IgYXMgYW4gYWRkZW5kdW0gdG8gdGhlIE5PVElDRSB0ZXh0IGZyb20gdGhlIFdvcmssIHByb3ZpZGVkCiAgICAgICAgICB0aGF0IHN1Y2ggYWRkaXRpb25hbCBhdHRyaWJ1dGlvbiBub3RpY2VzIGNhbm5vdCBiZSBjb25zdHJ1ZWQKICAgICAgICAgIGFzIG1vZGlmeWluZyB0aGUgTGljZW5zZS4KCiAgICAgIFlvdSBtYXkgYWRkIFlvdXIgb3duIGNvcHlyaWdodCBzdGF0ZW1lbnQgdG8gWW91ciBtb2RpZmljYXRpb25zIGFuZAogICAgICBtYXkgcHJvdmlkZSBhZGRpdGlvbmFsIG9yIGRpZmZlcmVudCBsaWNlbnNlIHRlcm1zIGFuZCBjb25kaXRpb25zCiAgICAgIGZvciB1c2UsIHJlcHJvZHVjdGlvbiwgb3IgZGlzdHJpYnV0aW9uIG9mIFlvdXIgbW9kaWZpY2F0aW9ucywgb3IKICAgICAgZm9yIGFueSBzdWNoIERlcml2YXRpdmUgV29ya3MgYXMgYSB3aG9sZSwgcHJvdmlkZWQgWW91ciB1c2UsCiAgICAgIHJlcHJvZHVjdGlvbiwgYW5kIGRpc3RyaWJ1dGlvbiBvZiB0aGUgV29yayBvdGhlcndpc2UgY29tcGxpZXMgd2l0aAogICAgICB0aGUgY29uZGl0aW9ucyBzdGF0ZWQgaW4gdGhpcyBMaWNlbnNlLgoKICAgNS4gU3VibWlzc2lvbiBvZiBDb250cmlidXRpb25zLiBVbmxlc3MgWW91IGV4cGxpY2l0bHkgc3RhdGUgb3RoZXJ3aXNlLAogICAgICBhbnkgQ29udHJpYnV0aW9uIGludGVudGlvbmFsbHkgc3VibWl0dGVkIGZvciBpbmNsdXNpb24gaW4gdGhlIFdvcmsKICAgICAgYnkgWW91IHRvIHRoZSBMaWNlbnNvciBzaGFsbCBiZSB1bmRlciB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCB3aXRob3V0IGFueSBhZGRpdGlvbmFsIHRlcm1zIG9yIGNvbmRpdGlvbnMuCiAgICAgIE5vdHdpdGhzdGFuZGluZyB0aGUgYWJvdmUsIG5vdGhpbmcgaGVyZWluIHNoYWxsIHN1cGVyc2VkZSBvciBtb2RpZnkKICAgICAgdGhlIHRlcm1zIG9mIGFueSBzZXBhcmF0ZSBsaWNlbnNlIGFncmVlbWVudCB5b3UgbWF5IGhhdmUgZXhlY3V0ZWQKICAgICAgd2l0aCBMaWNlbnNvciByZWdhcmRpbmcgc3VjaCBDb250cmlidXRpb25zLgoKICAgNi4gVHJhZGVtYXJrcy4gVGhpcyBMaWNlbnNlIGRvZXMgbm90IGdyYW50IHBlcm1pc3Npb24gdG8gdXNlIHRoZSB0cmFkZQogICAgICBuYW1lcywgdHJhZGVtYXJrcywgc2VydmljZSBtYXJrcywgb3IgcHJvZHVjdCBuYW1lcyBvZiB0aGUgTGljZW5zb3IsCiAgICAgIGV4Y2VwdCBhcyByZXF1aXJlZCBmb3IgcmVhc29uYWJsZSBhbmQgY3VzdG9tYXJ5IHVzZSBpbiBkZXNjcmliaW5nIHRoZQogICAgICBvcmlnaW4gb2YgdGhlIFdvcmsgYW5kIHJlcHJvZHVjaW5nIHRoZSBjb250ZW50IG9mIHRoZSBOT1RJQ0UgZmlsZS4KCiAgIDcuIERpc2NsYWltZXIgb2YgV2FycmFudHkuIFVubGVzcyByZXF1aXJlZCBieSBhcHBsaWNhYmxlIGxhdyBvcgogICAgICBhZ3JlZWQgdG8gaW4gd3JpdGluZywgTGljZW5zb3IgcHJvdmlkZXMgdGhlIFdvcmsgKGFuZCBlYWNoCiAgICAgIENvbnRyaWJ1dG9yIHByb3ZpZGVzIGl0cyBDb250cmlidXRpb25zKSBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IKICAgICAgaW1wbGllZCwgaW5jbHVkaW5nLCB3aXRob3V0IGxpbWl0YXRpb24sIGFueSB3YXJyYW50aWVzIG9yIGNvbmRpdGlvbnMKICAgICAgb2YgVElUTEUsIE5PTi1JTkZSSU5HRU1FTlQsIE1FUkNIQU5UQUJJTElUWSwgb3IgRklUTkVTUyBGT1IgQQogICAgICBQQVJUSUNVTEFSIFBVUlBPU0UuIFlvdSBhcmUgc29sZWx5IHJlc3BvbnNpYmxlIGZvciBkZXRlcm1pbmluZyB0aGUKICAgICAgYXBwcm9wcmlhdGVuZXNzIG9mIHVzaW5nIG9yIHJlZGlzdHJpYnV0aW5nIHRoZSBXb3JrIGFuZCBhc3N1bWUgYW55CiAgICAgIHJpc2tzIGFzc29jaWF0ZWQgd2l0aCBZb3VyIGV4ZXJjaXNlIG9mIHBlcm1pc3Npb25zIHVuZGVyIHRoaXMgTGljZW5zZS4KCiAgIDguIExpbWl0YXRpb24gb2YgTGlhYmlsaXR5LiBJbiBubyBldmVudCBhbmQgdW5kZXIgbm8gbGVnYWwgdGhlb3J5LAogICAgICB3aGV0aGVyIGluIHRvcnQgKGluY2x1ZGluZyBuZWdsaWdlbmNlKSwgY29udHJhY3QsIG9yIG90aGVyd2lzZSwKICAgICAgdW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IChzdWNoIGFzIGRlbGliZXJhdGUgYW5kIGdyb3NzbHkKICAgICAgbmVnbGlnZW50IGFjdHMpIG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzaGFsbCBhbnkgQ29udHJpYnV0b3IgYmUKICAgICAgbGlhYmxlIHRvIFlvdSBmb3IgZGFtYWdlcywgaW5jbHVkaW5nIGFueSBkaXJlY3QsIGluZGlyZWN0LCBzcGVjaWFsLAogICAgICBpbmNpZGVudGFsLCBvciBjb25zZXF1ZW50aWFsIGRhbWFnZXMgb2YgYW55IGNoYXJhY3RlciBhcmlzaW5nIGFzIGEKICAgICAgcmVzdWx0IG9mIHRoaXMgTGljZW5zZSBvciBvdXQgb2YgdGhlIHVzZSBvciBpbmFiaWxpdHkgdG8gdXNlIHRoZQogICAgICBXb3JrIChpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIGRhbWFnZXMgZm9yIGxvc3Mgb2YgZ29vZHdpbGwsCiAgICAgIHdvcmsgc3RvcHBhZ2UsIGNvbXB1dGVyIGZhaWx1cmUgb3IgbWFsZnVuY3Rpb24sIG9yIGFueSBhbmQgYWxsCiAgICAgIG90aGVyIGNvbW1lcmNpYWwgZGFtYWdlcyBvciBsb3NzZXMpLCBldmVuIGlmIHN1Y2ggQ29udHJpYnV0b3IKICAgICAgaGFzIGJlZW4gYWR2aXNlZCBvZiB0aGUgcG9zc2liaWxpdHkgb2Ygc3VjaCBkYW1hZ2VzLgoKICAgOS4gQWNjZXB0aW5nIFdhcnJhbnR5IG9yIEFkZGl0aW9uYWwgTGlhYmlsaXR5LiBXaGlsZSByZWRpc3RyaWJ1dGluZwogICAgICB0aGUgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIFlvdSBtYXkgY2hvb3NlIHRvIG9mZmVyLAogICAgICBhbmQgY2hhcmdlIGEgZmVlIGZvciwgYWNjZXB0YW5jZSBvZiBzdXBwb3J0LCB3YXJyYW50eSwgaW5kZW1uaXR5LAogICAgICBvciBvdGhlciBsaWFiaWxpdHkgb2JsaWdhdGlvbnMgYW5kL29yIHJpZ2h0cyBjb25zaXN0ZW50IHdpdGggdGhpcwogICAgICBMaWNlbnNlLiBIb3dldmVyLCBpbiBhY2NlcHRpbmcgc3VjaCBvYmxpZ2F0aW9ucywgWW91IG1heSBhY3Qgb25seQogICAgICBvbiBZb3VyIG93biBiZWhhbGYgYW5kIG9uIFlvdXIgc29sZSByZXNwb25zaWJpbGl0eSwgbm90IG9uIGJlaGFsZgogICAgICBvZiBhbnkgb3RoZXIgQ29udHJpYnV0b3IsIGFuZCBvbmx5IGlmIFlvdSBhZ3JlZSB0byBpbmRlbW5pZnksCiAgICAgIGRlZmVuZCwgYW5kIGhvbGQgZWFjaCBDb250cmlidXRvciBoYXJtbGVzcyBmb3IgYW55IGxpYWJpbGl0eQogICAgICBpbmN1cnJlZCBieSwgb3IgY2xhaW1zIGFzc2VydGVkIGFnYWluc3QsIHN1Y2ggQ29udHJpYnV0b3IgYnkgcmVhc29uCiAgICAgIG9mIHlvdXIgYWNjZXB0aW5nIGFueSBzdWNoIHdhcnJhbnR5IG9yIGFkZGl0aW9uYWwgbGlhYmlsaXR5LgoKICAgRU5EIE9GIFRFUk1TIEFORCBDT05ESVRJT05TCgogICBBUFBFTkRJWDogSG93IHRvIGFwcGx5IHRoZSBBcGFjaGUgTGljZW5zZSB0byB5b3VyIHdvcmsuCgogICAgICBUbyBhcHBseSB0aGUgQXBhY2hlIExpY2Vuc2UgdG8geW91ciB3b3JrLCBhdHRhY2ggdGhlIGZvbGxvd2luZwogICAgICBib2lsZXJwbGF0ZSBub3RpY2UsIHdpdGggdGhlIGZpZWxkcyBlbmNsb3NlZCBieSBicmFja2V0cyAiW10iCiAgICAgIHJlcGxhY2VkIHdpdGggeW91ciBvd24gaWRlbnRpZnlpbmcgaW5mb3JtYXRpb24uIChEb24ndCBpbmNsdWRlCiAgICAgIHRoZSBicmFja2V0cyEpICBUaGUgdGV4dCBzaG91bGQgYmUgZW5jbG9zZWQgaW4gdGhlIGFwcHJvcHJpYXRlCiAgICAgIGNvbW1lbnQgc3ludGF4IGZvciB0aGUgZmlsZSBmb3JtYXQuIFdlIGFsc28gcmVjb21tZW5kIHRoYXQgYQogICAgICBmaWxlIG9yIGNsYXNzIG5hbWUgYW5kIGRlc2NyaXB0aW9uIG9mIHB1cnBvc2UgYmUgaW5jbHVkZWQgb24gdGhlCiAgICAgIHNhbWUgInByaW50ZWQgcGFnZSIgYXMgdGhlIGNvcHlyaWdodCBub3RpY2UgZm9yIGVhc2llcgogICAgICBpZGVudGlmaWNhdGlvbiB3aXRoaW4gdGhpcmQtcGFydHkgYXJjaGl2ZXMuCgogICBDb3B5cmlnaHQgW3l5eXldIFtuYW1lIG9mIGNvcHlyaWdodCBvd25lcl0KCiAgIExpY2Vuc2VkIHVuZGVyIHRoZSBBcGFjaGUgTGljZW5zZSwgVmVyc2lvbiAyLjAgKHRoZSAiTGljZW5zZSIpOwogICB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCiAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgIGh0dHA6Ly93d3cuYXBhY2hlLm9yZy9saWNlbnNlcy9MSUNFTlNFLTIuMAoKICAgVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQogICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IgaW1wbGllZC4KICAgU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAogICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4= + https://www.apache.org/licenses/LICENSE-2.0.txt + + + pkg:maven/com.acme/tomcat-catalina@9.0.14?packaging=jar + + + + Apache Super Heros + Apache + org.apache.tomcat + tomcat-catalina + 9.0.14 + Apache Catalina + + + Apache-2.0 + + + pkg:maven/org.apache.tomcat/tomcat-catalina@9.0.14?packaging=jar + + + + + 7638417db6d59f3c431d3e1f261cc637155684cd + https://location/to/7638417db6d59f3c431d3e1f261cc637155684cd + + 2018-11-07T22:01:45Z + John Doe + john.doe@example.com + + + 2018-11-07T22:01:45Z + Jane Doe + jane.doe@example.com + + Initial commit + + + Commentary here + + + + + Example Inc. + https://example.com + https://example.net + + Example Support AMER + support@example.com + 800-555-1212 + + + Example Support APAC + support@apac.example.com + + + Example Super Heros + org.example + mylibrary + 1.0.0 + required + + 2342c2eaf1feb9a80195dbaddf2ebaa3 + 68b78babe00a053f9e35ec6a2d9080f5b90122b0 + 708f1f53b41f11f02d12a11b1a38d2905d47b099afc71a0f1124ef8582ec7313 + 387b7ae16b9cae45f830671541539bf544202faae5aac544a93b7b0a04f5f846fa2f4e81ef3f1677e13aed7496408a441f5657ab6d54423e56bf6f38da124aef + + + EPL-2.0 OR GPL-2.0-with-classpath-exception + + Copyright Example Inc. All rights reserved. + cpe:/a:example:myapplication:1.0.0 + pkg:maven/com.example/myapplication@1.0.0?packaging=war + false + + + http://example.org/docs + All component versions are documented here + + + http://example.org/security + + + + + Example Super Heros + com.example + myframework + 1.0.0 + Example Inc, enterprise framework + required + + cfcb0b64aacd2f81c1cd546543de965a + 7fbeef2346c45d565c3341f037bce4e088af8a52 + 0384db3cec55d86a6898c489fdb75a8e75fe66b26639634983d2f3c3558493d1 + 854909cdb9e3ca183056837144aab6d8069b377bd66445087cc7157bf0c3f620418705dd0b83bdc2f73a508c2bdb316ca1809d75ee6972d02023a3e7dd655c79 + + + + Some random license + + + pkg:maven/com.example/myframework@1.0.0?packaging=war + false + + + http://example.com/myframework + + + http://example.com/security + + + + + diff --git a/src/test/resources/unit/cyclonedx/valid-bom-1.6.json b/src/test/resources/unit/cyclonedx/valid-bom-1.6.json new file mode 100644 index 0000000000..c07ab7b0c4 --- /dev/null +++ b/src/test/resources/unit/cyclonedx/valid-bom-1.6.json @@ -0,0 +1,201 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2020-04-13T20:20:39+00:00", + "tools": [ + { + "vendor": "Awesome Vendor", + "name": "Awesome Tool", + "version": "9.1.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "25ed8e31b995bb927966616df2a42b979a2717f0" + }, + { + "alg": "SHA-256", + "content": "a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df" + } + ] + } + ], + "authors": [ + { + "name": "Samantha Wright", + "email": "samantha.wright@example.com", + "phone": "800-555-1212" + } + ], + "component": { + "type": "application", + "author": "Acme Super Heros", + "name": "Acme Application", + "version": "9.1.1", + "swid": { + "tagId": "swidgen-242eb18a-503e-ca37-393b-cf156ef09691_9.1.1", + "name": "Acme Application", + "version": "9.1.1", + "text": { + "contentType": "text/xml", + "encoding": "base64", + "content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg==" + } + } + }, + "manufacturer": { + "name": "Acme, Inc.", + "url": [ + "https://example.com" + ], + "contact": [ + { + "name": "Acme Professional Services", + "email": "professional.services@example.com" + } + ] + }, + "supplier": { + "name": "Acme, Inc.", + "url": [ + "https://example.com" + ], + "contact": [ + { + "name": "Acme Distribution", + "email": "distribution@example.com" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:npm/acme/component@1.0.0", + "type": "library", + "author": "Joane Doe et al.", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "tomcat-catalina", + "version": "9.0.14", + "hashes": [ + { + "alg": "MD5", + "content": "3942447fac867ae5cdb3229b658f4d48" + }, + { + "alg": "SHA-1", + "content": "e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a" + }, + { + "alg": "SHA-256", + "content": "f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b" + }, + { + "alg": "SHA-512", + "content": "e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0", + "text": { + "contentType": "text/plain", + "encoding": "base64", + "content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg==" + }, + "url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + ], + "purl": "pkg:npm/acme/component@1.0.0", + "pedigree": { + "ancestors": [ + { + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "tomcat-catalina", + "version": "9.0.14" + }, + { + "type": "library", + "publisher": "Acme Inc", + "group": "com.acme", + "name": "tomcat-catalina", + "version": "9.0.14" + } + ], + "commits": [ + { + "uid": "7638417db6d59f3c431d3e1f261cc637155684cd", + "url": "https://location/to/7638417db6d59f3c431d3e1f261cc637155684cd", + "author": { + "timestamp": "2018-11-13T20:20:39+00:00", + "name": "me", + "email": "me@acme.org" + } + } + ] + } + }, + { + "type": "library", + "supplier": { + "name": "Example, Inc.", + "url": [ + "https://example.com", + "https://example.net" + ], + "contact": [ + { + "name": "Example Support AMER Distribution", + "email": "support@example.com", + "phone": "800-555-1212" + }, + { + "name": "Example Support APAC", + "email": "support@apac.example.com" + } + ] + }, + "manufacturer": { + "name": "Example-2, Inc.", + "url": [ + "https://example.org" + ], + "contact": [ + { + "email": "support@example.org" + } + ] + }, + "authors": [ + { + "name": "Anthony Edward Stark", + "phone": "555-212-970-4133", + "email": "ironman@example.org" + }, + { + "name": "Peter Benjamin Parker", + "email": "spiderman@example.org" + } + ], + "group": "org.example", + "name": "mylibrary", + "version": "1.0.0", + "scope": "required" + } + ], + "dependencies": [ + { + "ref": "pkg:npm/acme/component@1.0.0", + "dependsOn": [ + "pkg:npm/acme/component@1.0.0" + ] + } + ] +} diff --git a/src/test/resources/unit/cyclonedx/valid-bom-1.6.xml b/src/test/resources/unit/cyclonedx/valid-bom-1.6.xml new file mode 100644 index 0000000000..6760b9da06 --- /dev/null +++ b/src/test/resources/unit/cyclonedx/valid-bom-1.6.xml @@ -0,0 +1,198 @@ + + + + 2020-04-07T07:01:00Z + + + Awesome Vendor + Awesome Tool + 9.1.2 + + 25ed8e31b995bb927966616df2a42b979a2717f0 + a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df + + + + + + Samantha Wright + samantha.wright@example.com + 800-555-1212 + + + + Acme Super Heros + Acme Application + 9.1.1 + + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg== + + + + Acme, Inc. + https://example.com + + Acme Professional Services + professional.services@example.com + + + + Acme, Inc. + https://example.com + + Acme Distribution + distribution@example.com + + + + + + Joane Doe et al. + Acme Inc + com.acme + tomcat-catalina + 9.0.14 + Modified version of Apache Catalina + required + + 3942447fac867ae5cdb3229b658f4d48 + e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a + f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b + e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282 + + + + Apache-2.0 + CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFwYWNoZSBMaWNlbnNlCiAgICAgICAgICAgICAgICAgICAgICAgICAgIFZlcnNpb24gMi4wLCBKYW51YXJ5IDIwMDQKICAgICAgICAgICAgICAgICAgICAgICAgaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzLwoKICAgVEVSTVMgQU5EIENPTkRJVElPTlMgRk9SIFVTRSwgUkVQUk9EVUNUSU9OLCBBTkQgRElTVFJJQlVUSU9OCgogICAxLiBEZWZpbml0aW9ucy4KCiAgICAgICJMaWNlbnNlIiBzaGFsbCBtZWFuIHRoZSB0ZXJtcyBhbmQgY29uZGl0aW9ucyBmb3IgdXNlLCByZXByb2R1Y3Rpb24sCiAgICAgIGFuZCBkaXN0cmlidXRpb24gYXMgZGVmaW5lZCBieSBTZWN0aW9ucyAxIHRocm91Z2ggOSBvZiB0aGlzIGRvY3VtZW50LgoKICAgICAgIkxpY2Vuc29yIiBzaGFsbCBtZWFuIHRoZSBjb3B5cmlnaHQgb3duZXIgb3IgZW50aXR5IGF1dGhvcml6ZWQgYnkKICAgICAgdGhlIGNvcHlyaWdodCBvd25lciB0aGF0IGlzIGdyYW50aW5nIHRoZSBMaWNlbnNlLgoKICAgICAgIkxlZ2FsIEVudGl0eSIgc2hhbGwgbWVhbiB0aGUgdW5pb24gb2YgdGhlIGFjdGluZyBlbnRpdHkgYW5kIGFsbAogICAgICBvdGhlciBlbnRpdGllcyB0aGF0IGNvbnRyb2wsIGFyZSBjb250cm9sbGVkIGJ5LCBvciBhcmUgdW5kZXIgY29tbW9uCiAgICAgIGNvbnRyb2wgd2l0aCB0aGF0IGVudGl0eS4gRm9yIHRoZSBwdXJwb3NlcyBvZiB0aGlzIGRlZmluaXRpb24sCiAgICAgICJjb250cm9sIiBtZWFucyAoaSkgdGhlIHBvd2VyLCBkaXJlY3Qgb3IgaW5kaXJlY3QsIHRvIGNhdXNlIHRoZQogICAgICBkaXJlY3Rpb24gb3IgbWFuYWdlbWVudCBvZiBzdWNoIGVudGl0eSwgd2hldGhlciBieSBjb250cmFjdCBvcgogICAgICBvdGhlcndpc2UsIG9yIChpaSkgb3duZXJzaGlwIG9mIGZpZnR5IHBlcmNlbnQgKDUwJSkgb3IgbW9yZSBvZiB0aGUKICAgICAgb3V0c3RhbmRpbmcgc2hhcmVzLCBvciAoaWlpKSBiZW5lZmljaWFsIG93bmVyc2hpcCBvZiBzdWNoIGVudGl0eS4KCiAgICAgICJZb3UiIChvciAiWW91ciIpIHNoYWxsIG1lYW4gYW4gaW5kaXZpZHVhbCBvciBMZWdhbCBFbnRpdHkKICAgICAgZXhlcmNpc2luZyBwZXJtaXNzaW9ucyBncmFudGVkIGJ5IHRoaXMgTGljZW5zZS4KCiAgICAgICJTb3VyY2UiIGZvcm0gc2hhbGwgbWVhbiB0aGUgcHJlZmVycmVkIGZvcm0gZm9yIG1ha2luZyBtb2RpZmljYXRpb25zLAogICAgICBpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIHNvZnR3YXJlIHNvdXJjZSBjb2RlLCBkb2N1bWVudGF0aW9uCiAgICAgIHNvdXJjZSwgYW5kIGNvbmZpZ3VyYXRpb24gZmlsZXMuCgogICAgICAiT2JqZWN0IiBmb3JtIHNoYWxsIG1lYW4gYW55IGZvcm0gcmVzdWx0aW5nIGZyb20gbWVjaGFuaWNhbAogICAgICB0cmFuc2Zvcm1hdGlvbiBvciB0cmFuc2xhdGlvbiBvZiBhIFNvdXJjZSBmb3JtLCBpbmNsdWRpbmcgYnV0CiAgICAgIG5vdCBsaW1pdGVkIHRvIGNvbXBpbGVkIG9iamVjdCBjb2RlLCBnZW5lcmF0ZWQgZG9jdW1lbnRhdGlvbiwKICAgICAgYW5kIGNvbnZlcnNpb25zIHRvIG90aGVyIG1lZGlhIHR5cGVzLgoKICAgICAgIldvcmsiIHNoYWxsIG1lYW4gdGhlIHdvcmsgb2YgYXV0aG9yc2hpcCwgd2hldGhlciBpbiBTb3VyY2Ugb3IKICAgICAgT2JqZWN0IGZvcm0sIG1hZGUgYXZhaWxhYmxlIHVuZGVyIHRoZSBMaWNlbnNlLCBhcyBpbmRpY2F0ZWQgYnkgYQogICAgICBjb3B5cmlnaHQgbm90aWNlIHRoYXQgaXMgaW5jbHVkZWQgaW4gb3IgYXR0YWNoZWQgdG8gdGhlIHdvcmsKICAgICAgKGFuIGV4YW1wbGUgaXMgcHJvdmlkZWQgaW4gdGhlIEFwcGVuZGl4IGJlbG93KS4KCiAgICAgICJEZXJpdmF0aXZlIFdvcmtzIiBzaGFsbCBtZWFuIGFueSB3b3JrLCB3aGV0aGVyIGluIFNvdXJjZSBvciBPYmplY3QKICAgICAgZm9ybSwgdGhhdCBpcyBiYXNlZCBvbiAob3IgZGVyaXZlZCBmcm9tKSB0aGUgV29yayBhbmQgZm9yIHdoaWNoIHRoZQogICAgICBlZGl0b3JpYWwgcmV2aXNpb25zLCBhbm5vdGF0aW9ucywgZWxhYm9yYXRpb25zLCBvciBvdGhlciBtb2RpZmljYXRpb25zCiAgICAgIHJlcHJlc2VudCwgYXMgYSB3aG9sZSwgYW4gb3JpZ2luYWwgd29yayBvZiBhdXRob3JzaGlwLiBGb3IgdGhlIHB1cnBvc2VzCiAgICAgIG9mIHRoaXMgTGljZW5zZSwgRGVyaXZhdGl2ZSBXb3JrcyBzaGFsbCBub3QgaW5jbHVkZSB3b3JrcyB0aGF0IHJlbWFpbgogICAgICBzZXBhcmFibGUgZnJvbSwgb3IgbWVyZWx5IGxpbmsgKG9yIGJpbmQgYnkgbmFtZSkgdG8gdGhlIGludGVyZmFjZXMgb2YsCiAgICAgIHRoZSBXb3JrIGFuZCBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YuCgogICAgICAiQ29udHJpYnV0aW9uIiBzaGFsbCBtZWFuIGFueSB3b3JrIG9mIGF1dGhvcnNoaXAsIGluY2x1ZGluZwogICAgICB0aGUgb3JpZ2luYWwgdmVyc2lvbiBvZiB0aGUgV29yayBhbmQgYW55IG1vZGlmaWNhdGlvbnMgb3IgYWRkaXRpb25zCiAgICAgIHRvIHRoYXQgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIHRoYXQgaXMgaW50ZW50aW9uYWxseQogICAgICBzdWJtaXR0ZWQgdG8gTGljZW5zb3IgZm9yIGluY2x1c2lvbiBpbiB0aGUgV29yayBieSB0aGUgY29weXJpZ2h0IG93bmVyCiAgICAgIG9yIGJ5IGFuIGluZGl2aWR1YWwgb3IgTGVnYWwgRW50aXR5IGF1dGhvcml6ZWQgdG8gc3VibWl0IG9uIGJlaGFsZiBvZgogICAgICB0aGUgY29weXJpZ2h0IG93bmVyLiBGb3IgdGhlIHB1cnBvc2VzIG9mIHRoaXMgZGVmaW5pdGlvbiwgInN1Ym1pdHRlZCIKICAgICAgbWVhbnMgYW55IGZvcm0gb2YgZWxlY3Ryb25pYywgdmVyYmFsLCBvciB3cml0dGVuIGNvbW11bmljYXRpb24gc2VudAogICAgICB0byB0aGUgTGljZW5zb3Igb3IgaXRzIHJlcHJlc2VudGF0aXZlcywgaW5jbHVkaW5nIGJ1dCBub3QgbGltaXRlZCB0bwogICAgICBjb21tdW5pY2F0aW9uIG9uIGVsZWN0cm9uaWMgbWFpbGluZyBsaXN0cywgc291cmNlIGNvZGUgY29udHJvbCBzeXN0ZW1zLAogICAgICBhbmQgaXNzdWUgdHJhY2tpbmcgc3lzdGVtcyB0aGF0IGFyZSBtYW5hZ2VkIGJ5LCBvciBvbiBiZWhhbGYgb2YsIHRoZQogICAgICBMaWNlbnNvciBmb3IgdGhlIHB1cnBvc2Ugb2YgZGlzY3Vzc2luZyBhbmQgaW1wcm92aW5nIHRoZSBXb3JrLCBidXQKICAgICAgZXhjbHVkaW5nIGNvbW11bmljYXRpb24gdGhhdCBpcyBjb25zcGljdW91c2x5IG1hcmtlZCBvciBvdGhlcndpc2UKICAgICAgZGVzaWduYXRlZCBpbiB3cml0aW5nIGJ5IHRoZSBjb3B5cmlnaHQgb3duZXIgYXMgIk5vdCBhIENvbnRyaWJ1dGlvbi4iCgogICAgICAiQ29udHJpYnV0b3IiIHNoYWxsIG1lYW4gTGljZW5zb3IgYW5kIGFueSBpbmRpdmlkdWFsIG9yIExlZ2FsIEVudGl0eQogICAgICBvbiBiZWhhbGYgb2Ygd2hvbSBhIENvbnRyaWJ1dGlvbiBoYXMgYmVlbiByZWNlaXZlZCBieSBMaWNlbnNvciBhbmQKICAgICAgc3Vic2VxdWVudGx5IGluY29ycG9yYXRlZCB3aXRoaW4gdGhlIFdvcmsuCgogICAyLiBHcmFudCBvZiBDb3B5cmlnaHQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICBjb3B5cmlnaHQgbGljZW5zZSB0byByZXByb2R1Y2UsIHByZXBhcmUgRGVyaXZhdGl2ZSBXb3JrcyBvZiwKICAgICAgcHVibGljbHkgZGlzcGxheSwgcHVibGljbHkgcGVyZm9ybSwgc3VibGljZW5zZSwgYW5kIGRpc3RyaWJ1dGUgdGhlCiAgICAgIFdvcmsgYW5kIHN1Y2ggRGVyaXZhdGl2ZSBXb3JrcyBpbiBTb3VyY2Ugb3IgT2JqZWN0IGZvcm0uCgogICAzLiBHcmFudCBvZiBQYXRlbnQgTGljZW5zZS4gU3ViamVjdCB0byB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCBlYWNoIENvbnRyaWJ1dG9yIGhlcmVieSBncmFudHMgdG8gWW91IGEgcGVycGV0dWFsLAogICAgICB3b3JsZHdpZGUsIG5vbi1leGNsdXNpdmUsIG5vLWNoYXJnZSwgcm95YWx0eS1mcmVlLCBpcnJldm9jYWJsZQogICAgICAoZXhjZXB0IGFzIHN0YXRlZCBpbiB0aGlzIHNlY3Rpb24pIHBhdGVudCBsaWNlbnNlIHRvIG1ha2UsIGhhdmUgbWFkZSwKICAgICAgdXNlLCBvZmZlciB0byBzZWxsLCBzZWxsLCBpbXBvcnQsIGFuZCBvdGhlcndpc2UgdHJhbnNmZXIgdGhlIFdvcmssCiAgICAgIHdoZXJlIHN1Y2ggbGljZW5zZSBhcHBsaWVzIG9ubHkgdG8gdGhvc2UgcGF0ZW50IGNsYWltcyBsaWNlbnNhYmxlCiAgICAgIGJ5IHN1Y2ggQ29udHJpYnV0b3IgdGhhdCBhcmUgbmVjZXNzYXJpbHkgaW5mcmluZ2VkIGJ5IHRoZWlyCiAgICAgIENvbnRyaWJ1dGlvbihzKSBhbG9uZSBvciBieSBjb21iaW5hdGlvbiBvZiB0aGVpciBDb250cmlidXRpb24ocykKICAgICAgd2l0aCB0aGUgV29yayB0byB3aGljaCBzdWNoIENvbnRyaWJ1dGlvbihzKSB3YXMgc3VibWl0dGVkLiBJZiBZb3UKICAgICAgaW5zdGl0dXRlIHBhdGVudCBsaXRpZ2F0aW9uIGFnYWluc3QgYW55IGVudGl0eSAoaW5jbHVkaW5nIGEKICAgICAgY3Jvc3MtY2xhaW0gb3IgY291bnRlcmNsYWltIGluIGEgbGF3c3VpdCkgYWxsZWdpbmcgdGhhdCB0aGUgV29yawogICAgICBvciBhIENvbnRyaWJ1dGlvbiBpbmNvcnBvcmF0ZWQgd2l0aGluIHRoZSBXb3JrIGNvbnN0aXR1dGVzIGRpcmVjdAogICAgICBvciBjb250cmlidXRvcnkgcGF0ZW50IGluZnJpbmdlbWVudCwgdGhlbiBhbnkgcGF0ZW50IGxpY2Vuc2VzCiAgICAgIGdyYW50ZWQgdG8gWW91IHVuZGVyIHRoaXMgTGljZW5zZSBmb3IgdGhhdCBXb3JrIHNoYWxsIHRlcm1pbmF0ZQogICAgICBhcyBvZiB0aGUgZGF0ZSBzdWNoIGxpdGlnYXRpb24gaXMgZmlsZWQuCgogICA0LiBSZWRpc3RyaWJ1dGlvbi4gWW91IG1heSByZXByb2R1Y2UgYW5kIGRpc3RyaWJ1dGUgY29waWVzIG9mIHRoZQogICAgICBXb3JrIG9yIERlcml2YXRpdmUgV29ya3MgdGhlcmVvZiBpbiBhbnkgbWVkaXVtLCB3aXRoIG9yIHdpdGhvdXQKICAgICAgbW9kaWZpY2F0aW9ucywgYW5kIGluIFNvdXJjZSBvciBPYmplY3QgZm9ybSwgcHJvdmlkZWQgdGhhdCBZb3UKICAgICAgbWVldCB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgogICAgICAoYSkgWW91IG11c3QgZ2l2ZSBhbnkgb3RoZXIgcmVjaXBpZW50cyBvZiB0aGUgV29yayBvcgogICAgICAgICAgRGVyaXZhdGl2ZSBXb3JrcyBhIGNvcHkgb2YgdGhpcyBMaWNlbnNlOyBhbmQKCiAgICAgIChiKSBZb3UgbXVzdCBjYXVzZSBhbnkgbW9kaWZpZWQgZmlsZXMgdG8gY2FycnkgcHJvbWluZW50IG5vdGljZXMKICAgICAgICAgIHN0YXRpbmcgdGhhdCBZb3UgY2hhbmdlZCB0aGUgZmlsZXM7IGFuZAoKICAgICAgKGMpIFlvdSBtdXN0IHJldGFpbiwgaW4gdGhlIFNvdXJjZSBmb3JtIG9mIGFueSBEZXJpdmF0aXZlIFdvcmtzCiAgICAgICAgICB0aGF0IFlvdSBkaXN0cmlidXRlLCBhbGwgY29weXJpZ2h0LCBwYXRlbnQsIHRyYWRlbWFyaywgYW5kCiAgICAgICAgICBhdHRyaWJ1dGlvbiBub3RpY2VzIGZyb20gdGhlIFNvdXJjZSBmb3JtIG9mIHRoZSBXb3JrLAogICAgICAgICAgZXhjbHVkaW5nIHRob3NlIG5vdGljZXMgdGhhdCBkbyBub3QgcGVydGFpbiB0byBhbnkgcGFydCBvZgogICAgICAgICAgdGhlIERlcml2YXRpdmUgV29ya3M7IGFuZAoKICAgICAgKGQpIElmIHRoZSBXb3JrIGluY2x1ZGVzIGEgIk5PVElDRSIgdGV4dCBmaWxlIGFzIHBhcnQgb2YgaXRzCiAgICAgICAgICBkaXN0cmlidXRpb24sIHRoZW4gYW55IERlcml2YXRpdmUgV29ya3MgdGhhdCBZb3UgZGlzdHJpYnV0ZSBtdXN0CiAgICAgICAgICBpbmNsdWRlIGEgcmVhZGFibGUgY29weSBvZiB0aGUgYXR0cmlidXRpb24gbm90aWNlcyBjb250YWluZWQKICAgICAgICAgIHdpdGhpbiBzdWNoIE5PVElDRSBmaWxlLCBleGNsdWRpbmcgdGhvc2Ugbm90aWNlcyB0aGF0IGRvIG5vdAogICAgICAgICAgcGVydGFpbiB0byBhbnkgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaW4gYXQgbGVhc3Qgb25lCiAgICAgICAgICBvZiB0aGUgZm9sbG93aW5nIHBsYWNlczogd2l0aGluIGEgTk9USUNFIHRleHQgZmlsZSBkaXN0cmlidXRlZAogICAgICAgICAgYXMgcGFydCBvZiB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgd2l0aGluIHRoZSBTb3VyY2UgZm9ybSBvcgogICAgICAgICAgZG9jdW1lbnRhdGlvbiwgaWYgcHJvdmlkZWQgYWxvbmcgd2l0aCB0aGUgRGVyaXZhdGl2ZSBXb3Jrczsgb3IsCiAgICAgICAgICB3aXRoaW4gYSBkaXNwbGF5IGdlbmVyYXRlZCBieSB0aGUgRGVyaXZhdGl2ZSBXb3JrcywgaWYgYW5kCiAgICAgICAgICB3aGVyZXZlciBzdWNoIHRoaXJkLXBhcnR5IG5vdGljZXMgbm9ybWFsbHkgYXBwZWFyLiBUaGUgY29udGVudHMKICAgICAgICAgIG9mIHRoZSBOT1RJQ0UgZmlsZSBhcmUgZm9yIGluZm9ybWF0aW9uYWwgcHVycG9zZXMgb25seSBhbmQKICAgICAgICAgIGRvIG5vdCBtb2RpZnkgdGhlIExpY2Vuc2UuIFlvdSBtYXkgYWRkIFlvdXIgb3duIGF0dHJpYnV0aW9uCiAgICAgICAgICBub3RpY2VzIHdpdGhpbiBEZXJpdmF0aXZlIFdvcmtzIHRoYXQgWW91IGRpc3RyaWJ1dGUsIGFsb25nc2lkZQogICAgICAgICAgb3IgYXMgYW4gYWRkZW5kdW0gdG8gdGhlIE5PVElDRSB0ZXh0IGZyb20gdGhlIFdvcmssIHByb3ZpZGVkCiAgICAgICAgICB0aGF0IHN1Y2ggYWRkaXRpb25hbCBhdHRyaWJ1dGlvbiBub3RpY2VzIGNhbm5vdCBiZSBjb25zdHJ1ZWQKICAgICAgICAgIGFzIG1vZGlmeWluZyB0aGUgTGljZW5zZS4KCiAgICAgIFlvdSBtYXkgYWRkIFlvdXIgb3duIGNvcHlyaWdodCBzdGF0ZW1lbnQgdG8gWW91ciBtb2RpZmljYXRpb25zIGFuZAogICAgICBtYXkgcHJvdmlkZSBhZGRpdGlvbmFsIG9yIGRpZmZlcmVudCBsaWNlbnNlIHRlcm1zIGFuZCBjb25kaXRpb25zCiAgICAgIGZvciB1c2UsIHJlcHJvZHVjdGlvbiwgb3IgZGlzdHJpYnV0aW9uIG9mIFlvdXIgbW9kaWZpY2F0aW9ucywgb3IKICAgICAgZm9yIGFueSBzdWNoIERlcml2YXRpdmUgV29ya3MgYXMgYSB3aG9sZSwgcHJvdmlkZWQgWW91ciB1c2UsCiAgICAgIHJlcHJvZHVjdGlvbiwgYW5kIGRpc3RyaWJ1dGlvbiBvZiB0aGUgV29yayBvdGhlcndpc2UgY29tcGxpZXMgd2l0aAogICAgICB0aGUgY29uZGl0aW9ucyBzdGF0ZWQgaW4gdGhpcyBMaWNlbnNlLgoKICAgNS4gU3VibWlzc2lvbiBvZiBDb250cmlidXRpb25zLiBVbmxlc3MgWW91IGV4cGxpY2l0bHkgc3RhdGUgb3RoZXJ3aXNlLAogICAgICBhbnkgQ29udHJpYnV0aW9uIGludGVudGlvbmFsbHkgc3VibWl0dGVkIGZvciBpbmNsdXNpb24gaW4gdGhlIFdvcmsKICAgICAgYnkgWW91IHRvIHRoZSBMaWNlbnNvciBzaGFsbCBiZSB1bmRlciB0aGUgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YKICAgICAgdGhpcyBMaWNlbnNlLCB3aXRob3V0IGFueSBhZGRpdGlvbmFsIHRlcm1zIG9yIGNvbmRpdGlvbnMuCiAgICAgIE5vdHdpdGhzdGFuZGluZyB0aGUgYWJvdmUsIG5vdGhpbmcgaGVyZWluIHNoYWxsIHN1cGVyc2VkZSBvciBtb2RpZnkKICAgICAgdGhlIHRlcm1zIG9mIGFueSBzZXBhcmF0ZSBsaWNlbnNlIGFncmVlbWVudCB5b3UgbWF5IGhhdmUgZXhlY3V0ZWQKICAgICAgd2l0aCBMaWNlbnNvciByZWdhcmRpbmcgc3VjaCBDb250cmlidXRpb25zLgoKICAgNi4gVHJhZGVtYXJrcy4gVGhpcyBMaWNlbnNlIGRvZXMgbm90IGdyYW50IHBlcm1pc3Npb24gdG8gdXNlIHRoZSB0cmFkZQogICAgICBuYW1lcywgdHJhZGVtYXJrcywgc2VydmljZSBtYXJrcywgb3IgcHJvZHVjdCBuYW1lcyBvZiB0aGUgTGljZW5zb3IsCiAgICAgIGV4Y2VwdCBhcyByZXF1aXJlZCBmb3IgcmVhc29uYWJsZSBhbmQgY3VzdG9tYXJ5IHVzZSBpbiBkZXNjcmliaW5nIHRoZQogICAgICBvcmlnaW4gb2YgdGhlIFdvcmsgYW5kIHJlcHJvZHVjaW5nIHRoZSBjb250ZW50IG9mIHRoZSBOT1RJQ0UgZmlsZS4KCiAgIDcuIERpc2NsYWltZXIgb2YgV2FycmFudHkuIFVubGVzcyByZXF1aXJlZCBieSBhcHBsaWNhYmxlIGxhdyBvcgogICAgICBhZ3JlZWQgdG8gaW4gd3JpdGluZywgTGljZW5zb3IgcHJvdmlkZXMgdGhlIFdvcmsgKGFuZCBlYWNoCiAgICAgIENvbnRyaWJ1dG9yIHByb3ZpZGVzIGl0cyBDb250cmlidXRpb25zKSBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IKICAgICAgaW1wbGllZCwgaW5jbHVkaW5nLCB3aXRob3V0IGxpbWl0YXRpb24sIGFueSB3YXJyYW50aWVzIG9yIGNvbmRpdGlvbnMKICAgICAgb2YgVElUTEUsIE5PTi1JTkZSSU5HRU1FTlQsIE1FUkNIQU5UQUJJTElUWSwgb3IgRklUTkVTUyBGT1IgQQogICAgICBQQVJUSUNVTEFSIFBVUlBPU0UuIFlvdSBhcmUgc29sZWx5IHJlc3BvbnNpYmxlIGZvciBkZXRlcm1pbmluZyB0aGUKICAgICAgYXBwcm9wcmlhdGVuZXNzIG9mIHVzaW5nIG9yIHJlZGlzdHJpYnV0aW5nIHRoZSBXb3JrIGFuZCBhc3N1bWUgYW55CiAgICAgIHJpc2tzIGFzc29jaWF0ZWQgd2l0aCBZb3VyIGV4ZXJjaXNlIG9mIHBlcm1pc3Npb25zIHVuZGVyIHRoaXMgTGljZW5zZS4KCiAgIDguIExpbWl0YXRpb24gb2YgTGlhYmlsaXR5LiBJbiBubyBldmVudCBhbmQgdW5kZXIgbm8gbGVnYWwgdGhlb3J5LAogICAgICB3aGV0aGVyIGluIHRvcnQgKGluY2x1ZGluZyBuZWdsaWdlbmNlKSwgY29udHJhY3QsIG9yIG90aGVyd2lzZSwKICAgICAgdW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IChzdWNoIGFzIGRlbGliZXJhdGUgYW5kIGdyb3NzbHkKICAgICAgbmVnbGlnZW50IGFjdHMpIG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzaGFsbCBhbnkgQ29udHJpYnV0b3IgYmUKICAgICAgbGlhYmxlIHRvIFlvdSBmb3IgZGFtYWdlcywgaW5jbHVkaW5nIGFueSBkaXJlY3QsIGluZGlyZWN0LCBzcGVjaWFsLAogICAgICBpbmNpZGVudGFsLCBvciBjb25zZXF1ZW50aWFsIGRhbWFnZXMgb2YgYW55IGNoYXJhY3RlciBhcmlzaW5nIGFzIGEKICAgICAgcmVzdWx0IG9mIHRoaXMgTGljZW5zZSBvciBvdXQgb2YgdGhlIHVzZSBvciBpbmFiaWxpdHkgdG8gdXNlIHRoZQogICAgICBXb3JrIChpbmNsdWRpbmcgYnV0IG5vdCBsaW1pdGVkIHRvIGRhbWFnZXMgZm9yIGxvc3Mgb2YgZ29vZHdpbGwsCiAgICAgIHdvcmsgc3RvcHBhZ2UsIGNvbXB1dGVyIGZhaWx1cmUgb3IgbWFsZnVuY3Rpb24sIG9yIGFueSBhbmQgYWxsCiAgICAgIG90aGVyIGNvbW1lcmNpYWwgZGFtYWdlcyBvciBsb3NzZXMpLCBldmVuIGlmIHN1Y2ggQ29udHJpYnV0b3IKICAgICAgaGFzIGJlZW4gYWR2aXNlZCBvZiB0aGUgcG9zc2liaWxpdHkgb2Ygc3VjaCBkYW1hZ2VzLgoKICAgOS4gQWNjZXB0aW5nIFdhcnJhbnR5IG9yIEFkZGl0aW9uYWwgTGlhYmlsaXR5LiBXaGlsZSByZWRpc3RyaWJ1dGluZwogICAgICB0aGUgV29yayBvciBEZXJpdmF0aXZlIFdvcmtzIHRoZXJlb2YsIFlvdSBtYXkgY2hvb3NlIHRvIG9mZmVyLAogICAgICBhbmQgY2hhcmdlIGEgZmVlIGZvciwgYWNjZXB0YW5jZSBvZiBzdXBwb3J0LCB3YXJyYW50eSwgaW5kZW1uaXR5LAogICAgICBvciBvdGhlciBsaWFiaWxpdHkgb2JsaWdhdGlvbnMgYW5kL29yIHJpZ2h0cyBjb25zaXN0ZW50IHdpdGggdGhpcwogICAgICBMaWNlbnNlLiBIb3dldmVyLCBpbiBhY2NlcHRpbmcgc3VjaCBvYmxpZ2F0aW9ucywgWW91IG1heSBhY3Qgb25seQogICAgICBvbiBZb3VyIG93biBiZWhhbGYgYW5kIG9uIFlvdXIgc29sZSByZXNwb25zaWJpbGl0eSwgbm90IG9uIGJlaGFsZgogICAgICBvZiBhbnkgb3RoZXIgQ29udHJpYnV0b3IsIGFuZCBvbmx5IGlmIFlvdSBhZ3JlZSB0byBpbmRlbW5pZnksCiAgICAgIGRlZmVuZCwgYW5kIGhvbGQgZWFjaCBDb250cmlidXRvciBoYXJtbGVzcyBmb3IgYW55IGxpYWJpbGl0eQogICAgICBpbmN1cnJlZCBieSwgb3IgY2xhaW1zIGFzc2VydGVkIGFnYWluc3QsIHN1Y2ggQ29udHJpYnV0b3IgYnkgcmVhc29uCiAgICAgIG9mIHlvdXIgYWNjZXB0aW5nIGFueSBzdWNoIHdhcnJhbnR5IG9yIGFkZGl0aW9uYWwgbGlhYmlsaXR5LgoKICAgRU5EIE9GIFRFUk1TIEFORCBDT05ESVRJT05TCgogICBBUFBFTkRJWDogSG93IHRvIGFwcGx5IHRoZSBBcGFjaGUgTGljZW5zZSB0byB5b3VyIHdvcmsuCgogICAgICBUbyBhcHBseSB0aGUgQXBhY2hlIExpY2Vuc2UgdG8geW91ciB3b3JrLCBhdHRhY2ggdGhlIGZvbGxvd2luZwogICAgICBib2lsZXJwbGF0ZSBub3RpY2UsIHdpdGggdGhlIGZpZWxkcyBlbmNsb3NlZCBieSBicmFja2V0cyAiW10iCiAgICAgIHJlcGxhY2VkIHdpdGggeW91ciBvd24gaWRlbnRpZnlpbmcgaW5mb3JtYXRpb24uIChEb24ndCBpbmNsdWRlCiAgICAgIHRoZSBicmFja2V0cyEpICBUaGUgdGV4dCBzaG91bGQgYmUgZW5jbG9zZWQgaW4gdGhlIGFwcHJvcHJpYXRlCiAgICAgIGNvbW1lbnQgc3ludGF4IGZvciB0aGUgZmlsZSBmb3JtYXQuIFdlIGFsc28gcmVjb21tZW5kIHRoYXQgYQogICAgICBmaWxlIG9yIGNsYXNzIG5hbWUgYW5kIGRlc2NyaXB0aW9uIG9mIHB1cnBvc2UgYmUgaW5jbHVkZWQgb24gdGhlCiAgICAgIHNhbWUgInByaW50ZWQgcGFnZSIgYXMgdGhlIGNvcHlyaWdodCBub3RpY2UgZm9yIGVhc2llcgogICAgICBpZGVudGlmaWNhdGlvbiB3aXRoaW4gdGhpcmQtcGFydHkgYXJjaGl2ZXMuCgogICBDb3B5cmlnaHQgW3l5eXldIFtuYW1lIG9mIGNvcHlyaWdodCBvd25lcl0KCiAgIExpY2Vuc2VkIHVuZGVyIHRoZSBBcGFjaGUgTGljZW5zZSwgVmVyc2lvbiAyLjAgKHRoZSAiTGljZW5zZSIpOwogICB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCiAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgIGh0dHA6Ly93d3cuYXBhY2hlLm9yZy9saWNlbnNlcy9MSUNFTlNFLTIuMAoKICAgVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQogICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IgaW1wbGllZC4KICAgU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAogICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4= + https://www.apache.org/licenses/LICENSE-2.0.txt + + + pkg:maven/com.acme/tomcat-catalina@9.0.14?packaging=jar + + + + Apache Super Heros + Apache + org.apache.tomcat + tomcat-catalina + 9.0.14 + Apache Catalina + + + Apache-2.0 + + + pkg:maven/org.apache.tomcat/tomcat-catalina@9.0.14?packaging=jar + + + + + 7638417db6d59f3c431d3e1f261cc637155684cd + https://location/to/7638417db6d59f3c431d3e1f261cc637155684cd + + 2018-11-07T22:01:45Z + John Doe + john.doe@example.com + + + 2018-11-07T22:01:45Z + Jane Doe + jane.doe@example.com + + Initial commit + + + Commentary here + + + + + Example Inc. + https://example.com + https://example.net + + Example Support AMER + support@example.com + 800-555-1212 + + + Example Support APAC + support@apac.example.com + + + + Example-2, Inc.Example-2, Inc. + https://example.org + + support@example.org + + + + + Anthony Edward Stark + ironman@example.org + 555-212-970-4133 + + + Peter Benjamin Parker + spiderman@example.org + + + org.example + mylibrary + 1.0.0 + required + + 2342c2eaf1feb9a80195dbaddf2ebaa3 + 68b78babe00a053f9e35ec6a2d9080f5b90122b0 + 708f1f53b41f11f02d12a11b1a38d2905d47b099afc71a0f1124ef8582ec7313 + 387b7ae16b9cae45f830671541539bf544202faae5aac544a93b7b0a04f5f846fa2f4e81ef3f1677e13aed7496408a441f5657ab6d54423e56bf6f38da124aef + + + EPL-2.0 OR GPL-2.0-with-classpath-exception + + Copyright Example Inc. All rights reserved. + cpe:/a:example:myapplication:1.0.0 + pkg:maven/com.example/myapplication@1.0.0?packaging=war + false + + + http://example.org/docs + All component versions are documented here + + + http://example.org/security + + + + + Example Super Heros + com.example + myframework + 1.0.0 + Example Inc, enterprise framework + required + + cfcb0b64aacd2f81c1cd546543de965a + 7fbeef2346c45d565c3341f037bce4e088af8a52 + 0384db3cec55d86a6898c489fdb75a8e75fe66b26639634983d2f3c3558493d1 + 854909cdb9e3ca183056837144aab6d8069b377bd66445087cc7157bf0c3f620418705dd0b83bdc2f73a508c2bdb316ca1809d75ee6972d02023a3e7dd655c79 + + + + Some random license + + + pkg:maven/com.example/myframework@1.0.0?packaging=war + false + + + http://example.com/myframework + + + http://example.com/security + + + + + From 339b1e3429109f8532190a1487314cbc48613353 Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 15 May 2024 21:44:49 +0200 Subject: [PATCH 155/412] Update database support docs * Raise supported versions for PostgreSQL and SQL Server to non-EOL versions * Add notice about deprecation of RDBMSes other than PostgreSQL in DT v5 Signed-off-by: nscuro --- docs/_docs/getting-started/configuration.md | 5 +++++ docs/_docs/getting-started/database-support.md | 8 ++++++-- src/main/resources/application.properties | 5 +++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/_docs/getting-started/configuration.md b/docs/_docs/getting-started/configuration.md index a3ac42f378..308ac78d0c 100644 --- a/docs/_docs/getting-started/configuration.md +++ b/docs/_docs/getting-started/configuration.md @@ -115,6 +115,11 @@ alpine.database.username=sa # Specifies the password to use when authenticating to the database. # alpine.database.password= +# Optional +# Specifies a path to the file holding the database password. +# To be used as alternative to alpine.database.password. +# alpine.database.password.file= + # Optional # Specifies if the database connection pool is enabled. alpine.database.pool.enabled=true diff --git a/docs/_docs/getting-started/database-support.md b/docs/_docs/getting-started/database-support.md index 0b61053404..a2c8aedbbb 100644 --- a/docs/_docs/getting-started/database-support.md +++ b/docs/_docs/getting-started/database-support.md @@ -14,8 +14,8 @@ Dependency-Track supports the following database servers: | RDBMS | Supported Versions | Recommended | |:---------------------|:-------------------|:------------| -| PostgreSQL | >= 9.0 | ✅ | -| Microsoft SQL Server | >= 2012 | ✅ | +| PostgreSQL | >= 12.0 | ✅ | +| Microsoft SQL Server | >= 2017 | ⚠️ | | MySQL | 5.6 - 5.7 | ❌ | Dependency-Track requires extensive unicode support, which is not provided per default in MySQL. @@ -23,6 +23,10 @@ Both PostgreSQL and SQL Server have been proven to work very well in production MySQL / MariaDB can require [lots of extra care](https://github.com/DependencyTrack/dependency-track/issues/271#issuecomment-1108923693). **Only use MySQL if you know what you're doing**! +> Support for H2, Microsoft SQL Server, and MySQL will be dropped in Dependency-Track v5. +> From then onwards, the project will focus on PostgreSQL. When setting up a new instance +> of Dependency-Track v4.x, consider using PostgreSQL. + Refer to the [Configuration] documentation for how database settings may be changed. ### Examples diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9b3aba9a61..a6f5fa53a8 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -81,6 +81,11 @@ alpine.database.username=sa # Specifies the password to use when authenticating to the database. # alpine.database.password= +# Optional +# Specifies a path to the file holding the database password. +# To be used as alternative to alpine.database.password. +# alpine.database.password.file= + # Optional # Specifies if the database connection pool is enabled. alpine.database.pool.enabled=true From d572c52a97572235746fa84be4f2362482d6f14d Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 15 May 2024 21:51:04 +0200 Subject: [PATCH 156/412] Remove workarounds for #2677 Signed-off-by: nscuro --- .../persistence/QueryManager.java | 18 ------------------ .../RepositoryMetaAnalyzerTask.java | 4 ---- .../scanners/BaseComponentAnalyzerTask.java | 1 - 3 files changed, 23 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 9a02d15c00..31789f646e 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -85,7 +85,6 @@ import javax.jdo.FetchPlan; import javax.jdo.PersistenceManager; import javax.jdo.Query; -import javax.jdo.Transaction; import javax.json.JsonObject; import java.security.Principal; import java.util.ArrayList; @@ -1416,23 +1415,6 @@ public Query getObjectsByUuidsQuery(final Class clazz, final List - * Calling this method may sometimes be necessary due to {@link AlpineQueryManager#persist(Object)} - * no performing a rollback in case committing the transaction fails. This can impact other persistence - * operations performed in the same session (e.g. {@code NucleusTransactionException: Invalid state. Transaction has already started}). - * - * @see Issue 2677 - * @since 4.8.0 - */ - public void ensureNoActiveTransaction() { - final Transaction trx = pm.currentTransaction(); - if (trx != null && trx.isActive()) { - trx.rollback(); - } - } - public void recursivelyDeleteTeam(Team team) { runInTransaction(() -> { pm.deletePersistentAll(team.getApiKeys()); diff --git a/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java b/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java index 2d3c8cefa9..ee62d04562 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java @@ -89,7 +89,6 @@ public void inform(final Event e) { // Refreshing the object by querying for it again is preventative LOGGER.info("Performing component repository metadata analysis against " + components.size() + " components"); for (final Component component : components) { - qm.ensureNoActiveTransaction(); // Workaround for https://github.com/DependencyTrack/dependency-track/issues/2677 analyze(qm, qm.getObjectById(Component.class, component.getId())); } LOGGER.info("Completed component repository metadata analysis against " + components.size() + " components"); @@ -102,7 +101,6 @@ public void inform(final Event e) { final List components = qm.getAllComponents(project); LOGGER.debug("Performing component repository metadata analysis against " + components.size() + " components in project: " + project.getUuid()); for (final Component component : components) { - qm.ensureNoActiveTransaction(); // Workaround for https://github.com/DependencyTrack/dependency-track/issues/2677 analyze(qm, component); } LOGGER.debug("Completed component repository metadata analysis against " + components.size() + " components in project: " + project.getUuid()); @@ -188,7 +186,6 @@ private void analyze(final QueryManager qm, final Component component, final IMe component multiple times concurrently, and is safe to ignore. \ [targetHost=%s, source=%s, target=%s]\ """.formatted(repository.getUrl(), repository.getType(), PurlUtil.silentPurlCoordinatesOnly(component.getPurl())), e); - qm.ensureNoActiveTransaction(); // Workaround for https://github.com/DependencyTrack/dependency-track/issues/2677 return; } else { throw e; @@ -215,7 +212,6 @@ private void analyze(final QueryManager qm, final Component component, final IMe component multiple times concurrently, and is safe to ignore. \ [targetHost=%s, source=%s, target=%s]\ """.formatted(repository.getUrl(), repository.getType(), PurlUtil.silentPurlCoordinatesOnly(component.getPurl())), e); - qm.ensureNoActiveTransaction(); // Workaround for https://github.com/DependencyTrack/dependency-track/issues/2677 return; } else { throw e; diff --git a/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java b/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java index 787b008c14..ee8dfa4021 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java @@ -124,7 +124,6 @@ protected synchronized void updateAnalysisCacheStats(QueryManager qm, Vulnerabil component identity multiple times concurrently, and is safe to ignore. \ [targetHost=%s, source=%s, target=%s]\ """.formatted(targetHost, source, target), e); - qm.ensureNoActiveTransaction(); // Workaround for https://github.com/DependencyTrack/dependency-track/issues/2677 } else { throw e; } From bb8c028ff02fe20e0035bfc459c693f9a6815515 Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 15 May 2024 21:54:07 +0200 Subject: [PATCH 157/412] Address relocation of `mysql:mysql-connector-java` Signed-off-by: nscuro --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index fdf52043af..dc80ef217b 100644 --- a/pom.xml +++ b/pom.xml @@ -307,8 +307,8 @@ ${lib.jdbc-driver.mssql.version} - mysql - mysql-connector-java + com.mysql + mysql-connector-j ${lib.jdbc-driver.mysql.version} - 10.0.20 + 10.0.21 src/main/webapp/** From 0c7a7ffa3ea051d81da028cb4bc92e8a3361440e Mon Sep 17 00:00:00 2001 From: nscuro Date: Thu, 16 May 2024 22:21:23 +0200 Subject: [PATCH 166/412] Migrate from Swagger v2 to OpenAPI v3 * Migrates from Swagger Core v1.x (OpenAPI 2.0) to Swagger Core v2.x (OpenAPI 3.0) * Adds declaration for multiple `securityScheme`s (API key and Bearer), where the previous solution only supported one (API key) * Removes reliance on Alpine to provide a Swagger servlet, instead use a dedicated JAX-RS resource for it BREAKING CHANGES: * Support for OpenAPI 2.0 is dropped * OpenAPI spec is now exposed via `/api/openapi.json` and `/api/openapi.yaml` - `/api/swagger.json` no longer works Signed-off-by: nscuro --- pom.xml | 11 + .../org/dependencytrack/model/Component.java | 14 +- .../model/PortfolioMetrics.java | 15 +- .../org/dependencytrack/model/Project.java | 6 +- .../dependencytrack/model/ProjectMetrics.java | 15 +- .../resources/OpenApiResource.java | 61 ++++ .../resources/v1/AccessControlResource.java | 77 +++-- .../resources/v1/AnalysisResource.java | 51 +-- .../resources/v1/BadgeResource.java | 78 +++-- .../resources/v1/BomResource.java | 130 ++++---- .../resources/v1/CalculatorResource.java | 41 +-- .../v1/ComponentPropertyResource.java | 76 +++-- .../resources/v1/ComponentResource.java | 195 +++++------ .../resources/v1/ConfigPropertyResource.java | 56 ++-- .../resources/v1/CweResource.java | 51 +-- .../resources/v1/DependencyGraphResource.java | 56 ++-- .../resources/v1/EventResource.java | 33 +- .../resources/v1/FindingResource.java | 169 +++++----- .../resources/v1/IntegrationResource.java | 48 +-- .../resources/v1/LdapResource.java | 83 ++--- .../resources/v1/LicenseGroupResource.java | 128 ++++---- .../resources/v1/LicenseResource.java | 91 +++--- .../resources/v1/MetricsResource.java | 223 ++++++------- .../v1/NotificationPublisherResource.java | 99 +++--- .../v1/NotificationRuleResource.java | 149 +++++---- .../resources/v1/OidcResource.java | 133 ++++---- .../resources/v1/PermissionResource.java | 104 +++--- .../resources/v1/PolicyConditionResource.java | 62 ++-- .../resources/v1/PolicyResource.java | 163 +++++----- .../resources/v1/PolicyViolationResource.java | 90 +++--- .../resources/v1/ProjectPropertyResource.java | 90 +++--- .../resources/v1/ProjectResource.java | 302 ++++++++++-------- .../resources/v1/RepositoryResource.java | 114 ++++--- .../resources/v1/SearchResource.java | 100 +++--- .../resources/v1/ServiceResource.java | 109 ++++--- .../resources/v1/TagResource.java | 43 ++- .../resources/v1/TeamResource.java | 157 ++++----- .../resources/v1/UserResource.java | 265 +++++++-------- .../resources/v1/VexResource.java | 87 +++-- .../v1/ViolationAnalysisResource.java | 50 +-- .../resources/v1/VulnerabilityResource.java | 261 ++++++++------- .../resources/v1/openapi/PaginatedApi.java | 56 ++-- .../v1/problems/InvalidBomProblemDetails.java | 4 +- .../resources/v1/problems/ProblemDetails.java | 25 +- .../resources/v1/vo/BomSubmitRequest.java | 18 +- .../resources/v1/vo/BomUploadResponse.java | 6 +- .../v1/vo/IsTokenBeingProcessedResponse.java | 6 +- .../webapp/WEB-INF/openapi-configuration.yaml | 17 + src/main/webapp/WEB-INF/web.xml | 6 +- 49 files changed, 2311 insertions(+), 1913 deletions(-) create mode 100644 src/main/java/org/dependencytrack/resources/OpenApiResource.java create mode 100644 src/main/webapp/WEB-INF/openapi-configuration.yaml diff --git a/pom.xml b/pom.xml index e7a5f033dc..efab40706a 100644 --- a/pom.xml +++ b/pom.xml @@ -260,6 +260,17 @@ ${lib.pebble.version} + + io.swagger.core.v3 + swagger-jaxrs2 + ${lib.swagger.version} + + + io.swagger.core.v3 + swagger-jaxrs2-servlet-initializer-v2 + ${lib.swagger.version} + + org.apache.httpcomponents httpclient diff --git a/src/main/java/org/dependencytrack/model/Component.java b/src/main/java/org/dependencytrack/model/Component.java index cd4f92327a..9bd41eba9a 100644 --- a/src/main/java/org/dependencytrack/model/Component.java +++ b/src/main/java/org/dependencytrack/model/Component.java @@ -27,7 +27,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; -import io.swagger.annotations.ApiModelProperty; +import io.swagger.v3.oas.annotations.media.Schema; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.model.validation.ValidSpdxExpression; import org.dependencytrack.persistence.converter.OrganizationalEntityJsonConverter; @@ -253,7 +253,7 @@ public enum FetchGroup { @Size(max = 786) @com.github.packageurl.validator.PackageURL @JsonDeserialize(using = TrimmedStringDeserializer.class) - @ApiModelProperty(dataType = "string") + @Schema(type = "string") private String purl; @Persistent(defaultFetchGroup = "true") @@ -587,7 +587,7 @@ public void setPurl(String purl) { } @JsonSerialize(using = CustomPackageURLSerializer.class) - @ApiModelProperty(dataType = "string", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @Schema(type = "string", accessMode = Schema.AccessMode.READ_ONLY) public PackageURL getPurlCoordinates() { if (purlCoordinates == null) { return null; @@ -785,7 +785,7 @@ public void setRepositoryMeta(RepositoryMetaComponent repositoryMeta) { } @JsonIgnore - @ApiModelProperty(hidden = true) + @Schema(hidden = true) public boolean isNew() { return isNew; } @@ -804,7 +804,7 @@ public void setLastInheritedRiskScore(Double lastInheritedRiskScore) { } @JsonIgnore - @ApiModelProperty(hidden = true) + @Schema(hidden = true) public String getBomRef() { return bomRef; } @@ -815,7 +815,7 @@ public void setBomRef(String bomRef) { } @JsonIgnore - @ApiModelProperty(hidden = true) + @Schema(hidden = true) public List getLicenseCandidates() { return licenseCandidates; } @@ -826,7 +826,7 @@ public void setLicenseCandidates(final List license } @JsonIgnore - @ApiModelProperty(hidden = true) + @Schema(hidden = true) public JsonObject getCacheResult() { return cacheResult; } diff --git a/src/main/java/org/dependencytrack/model/PortfolioMetrics.java b/src/main/java/org/dependencytrack/model/PortfolioMetrics.java index aeec76cfe8..4136e56b2c 100644 --- a/src/main/java/org/dependencytrack/model/PortfolioMetrics.java +++ b/src/main/java/org/dependencytrack/model/PortfolioMetrics.java @@ -20,8 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; - -import io.swagger.annotations.ApiModelProperty; +import io.swagger.v3.oas.annotations.media.Schema; import javax.jdo.annotations.Column; import javax.jdo.annotations.IdGeneratorStrategy; @@ -53,22 +52,22 @@ public class PortfolioMetrics implements Serializable { @Persistent @Column(name = "CRITICAL") - @ApiModelProperty(required = true) + @Schema(required = true) private int critical; @Persistent @Column(name = "HIGH") - @ApiModelProperty(required = true) + @Schema(required = true) private int high; @Persistent @Column(name = "MEDIUM") - @ApiModelProperty(required = true) + @Schema(required = true) private int medium; @Persistent @Column(name = "LOW") - @ApiModelProperty(required = true) + @Schema(required = true) private int low; @Persistent @@ -179,14 +178,14 @@ public class PortfolioMetrics implements Serializable { @Column(name = "FIRST_OCCURRENCE", allowsNull = "false") @NotNull @Index(name = "PORTFOLIOMETRICS_FIRST_OCCURRENCE_IDX") - @ApiModelProperty(required = true, dataType = "number") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, type = "number") private Date firstOccurrence; @Persistent @Column(name = "LAST_OCCURRENCE", allowsNull = "false") @NotNull @Index(name = "PORTFOLIOMETRICS_LAST_OCCURRENCE_IDX") - @ApiModelProperty(required = true, dataType = "number") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, type = "number") private Date lastOccurrence; public long getId() { diff --git a/src/main/java/org/dependencytrack/model/Project.java b/src/main/java/org/dependencytrack/model/Project.java index 6c0ad11894..f8fcae99d5 100644 --- a/src/main/java/org/dependencytrack/model/Project.java +++ b/src/main/java/org/dependencytrack/model/Project.java @@ -31,7 +31,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; -import io.swagger.annotations.ApiModelProperty; +import io.swagger.v3.oas.annotations.media.Schema; import org.dependencytrack.persistence.converter.OrganizationalEntityJsonConverter; import org.dependencytrack.resources.v1.serializers.CustomPackageURLSerializer; @@ -199,7 +199,7 @@ public enum FetchGroup { @Size(max = 786) @com.github.packageurl.validator.PackageURL @JsonDeserialize(using = TrimmedStringDeserializer.class) - @ApiModelProperty(dataType = "string") + @Schema(type = "string") private String purl; @Persistent @@ -280,7 +280,7 @@ public enum FetchGroup { private List externalReferences; @Persistent(mappedBy = "project") - @ApiModelProperty(accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @Schema(accessMode = Schema.AccessMode.READ_ONLY) private ProjectMetadata metadata; private transient String bomRef; diff --git a/src/main/java/org/dependencytrack/model/ProjectMetrics.java b/src/main/java/org/dependencytrack/model/ProjectMetrics.java index bfa68856cb..e6272824d3 100644 --- a/src/main/java/org/dependencytrack/model/ProjectMetrics.java +++ b/src/main/java/org/dependencytrack/model/ProjectMetrics.java @@ -20,8 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; - -import io.swagger.annotations.ApiModelProperty; +import io.swagger.v3.oas.annotations.media.Schema; import javax.jdo.annotations.Column; import javax.jdo.annotations.IdGeneratorStrategy; @@ -52,22 +51,22 @@ public class ProjectMetrics implements Serializable { @Persistent @Column(name = "PROJECT_ID", allowsNull = "false") - @ApiModelProperty(required = true) + @Schema(required = true) private Project project; @Persistent @Column(name = "CRITICAL") - @ApiModelProperty(required = true) + @Schema(required = true) private int critical; @Persistent @Column(name = "HIGH") - @ApiModelProperty(required = true) + @Schema(required = true) private int high; @Persistent @Column(name = "MEDIUM") - @ApiModelProperty(required = true) + @Schema(required = true) private int medium; @Persistent @@ -175,14 +174,14 @@ public class ProjectMetrics implements Serializable { @Column(name = "FIRST_OCCURRENCE", allowsNull = "false") @NotNull @Index(name = "PROJECTMETRICS_FIRST_OCCURRENCE_IDX") - @ApiModelProperty(required = true, dataType = "number") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, type = "number") private Date firstOccurrence; @Persistent @Column(name = "LAST_OCCURRENCE", allowsNull = "false") @NotNull @Index(name = "PROJECTMETRICS_LAST_OCCURRENCE_IDX") - @ApiModelProperty(required = true, dataType = "number") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, type = "number") private Date lastOccurrence; public long getId() { diff --git a/src/main/java/org/dependencytrack/resources/OpenApiResource.java b/src/main/java/org/dependencytrack/resources/OpenApiResource.java new file mode 100644 index 0000000000..84e26f8da3 --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/OpenApiResource.java @@ -0,0 +1,61 @@ +/* + * 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.resources; + +import alpine.server.auth.AuthenticationNotRequired; +import io.swagger.v3.jaxrs2.integration.resources.BaseOpenApiResource; +import io.swagger.v3.oas.annotations.Operation; + +import javax.servlet.ServletConfig; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +/** + * @since 4.12.0 + */ +@Path("/openapi.{type:json|yaml}") +public class OpenApiResource extends BaseOpenApiResource { + + @Context + ServletConfig config; + + @Context + Application app; + + @GET + @Produces({MediaType.APPLICATION_JSON, "application/yaml"}) + @Operation(hidden = true) + @AuthenticationNotRequired + public Response getOpenApi( + @Context final HttpHeaders headers, + @Context final UriInfo uriInfo, + @PathParam("type") final String type + ) throws Exception { + return super.getOpenApi(headers, config, app, uriInfo, type); + } + +} diff --git a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index 7c81e19339..a2b87a9a6f 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -23,13 +23,17 @@ import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; -import io.swagger.annotations.ResponseHeader; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Project; import org.dependencytrack.model.validation.ValidUuid; @@ -57,7 +61,11 @@ * @since 3.3.0 */ @Path("/v1/acl") -@Api(value = "acl", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "acl") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class AccessControlResource extends AlpineResource { private static final Logger LOGGER = Logger.getLogger(AccessControlResource.class); @@ -65,24 +73,26 @@ public class AccessControlResource extends AlpineResource { @GET @Path("/team/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns the projects assigned to the specified team", - response = String.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects"), - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Returns the projects assigned to the specified team", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the team could not be found"), + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the team could not be found"), }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) - public Response retrieveProjects (@ApiParam(value = "The UUID of the team to retrieve mappings for", format = "uuid", required = true) + public Response retrieveProjects (@Parameter(description = "The UUID of the team to retrieve mappings for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "Optionally excludes inactive projects from being returned", required = false) + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive, - @ApiParam(value = "Optionally excludes children projects from being returned", required = false) + @Parameter(description = "Optionally excludes children projects from being returned", required = false) @QueryParam("onlyRoot") boolean onlyRoot) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Team team = qm.getObjectByUuid(Team.class, uuid); @@ -98,15 +108,15 @@ public Response retrieveProjects (@ApiParam(value = "The UUID of the team to ret @PUT @Path("/mapping") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Adds an ACL mapping", - response = AclMappingRequest.class, - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Adds an ACL mapping", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the team or project could not be found"), - @ApiResponse(code = 409, message = "A mapping with the same team and project already exists") + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the team or project could not be found"), + @ApiResponse(responseCode = "409", description = "A mapping with the same team and project already exists") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response addMapping(AclMappingRequest request) { @@ -136,19 +146,20 @@ public Response addMapping(AclMappingRequest request) { @DELETE @Path("/mapping/team/{teamUuid}/project/{projectUuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Removes an ACL mapping", - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Removes an ACL mapping", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the team or project could not be found"), + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the team or project could not be found"), }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response deleteMapping( - @ApiParam(value = "The UUID of the team to delete the mapping for", format = "uuid", required = true) + @Parameter(description = "The UUID of the team to delete the mapping for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("teamUuid") @ValidUuid String teamUuid, - @ApiParam(value = "The UUID of the project to delete the mapping for", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to delete the mapping for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("projectUuid") @ValidUuid String projectUuid) { try (QueryManager qm = new QueryManager()) { final Team team = qm.getObjectByUuid(Team.class, teamUuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java b/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java index 19088e2b72..9deb335097 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java @@ -26,12 +26,15 @@ import alpine.model.UserPrincipal; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Analysis; @@ -61,26 +64,30 @@ * @since 3.1.0 */ @Path("/v1/analysis") -@Api(value = "analysis", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "analysis") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class AnalysisResource extends AlpineResource { @GET @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Retrieves an analysis trail", - response = Analysis.class, - notes = "

    Requires permission VIEW_VULNERABILITY

    " + @Operation( + summary = "Retrieves an analysis trail", + description = "

    Requires permission VIEW_VULNERABILITY

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The project, component, or vulnerability could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Analysis.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The project, component, or vulnerability could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) - public Response retrieveAnalysis(@ApiParam(value = "The UUID of the project", format = "uuid") + public Response retrieveAnalysis(@Parameter(description = "The UUID of the project", schema = @Schema(type = "string", format = "uuid")) @QueryParam("project") String projectUuid, - @ApiParam(value = "The UUID of the component", format = "uuid", required = true) + @Parameter(description = "The UUID of the component", schema = @Schema(type = "string", format = "uuid"), required = true) @QueryParam("component") String componentUuid, - @ApiParam(value = "The UUID of the vulnerability", format = "uuid", required = true) + @Parameter(description = "The UUID of the vulnerability", schema = @Schema(type = "string", format = "uuid"), required = true) @QueryParam("vulnerability") String vulnerabilityUuid) { failOnValidationError( new ValidationTask(RegexSequence.Pattern.UUID, projectUuid, "Project is not a valid UUID", false), // this is optional @@ -114,14 +121,14 @@ public Response retrieveAnalysis(@ApiParam(value = "The UUID of the project", fo @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Records an analysis decision", - response = Analysis.class, - notes = "

    Requires permission VULNERABILITY_ANALYSIS

    " + @Operation( + summary = "Records an analysis decision", + description = "

    Requires permission VULNERABILITY_ANALYSIS

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The project, component, or vulnerability could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Analysis.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The project, component, or vulnerability could not be found") }) @PermissionRequired(Permissions.Constants.VULNERABILITY_ANALYSIS) public Response updateAnalysis(AnalysisRequest request) { diff --git a/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java b/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java index d0f03dfed5..32fc0b10a7 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java @@ -22,11 +22,13 @@ import alpine.model.ConfigProperty; import alpine.server.auth.AuthenticationNotRequired; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.model.validation.ValidUuid; @@ -48,7 +50,7 @@ * @since 3.6.0 */ @Path("/v1/badge") -@Api(value = "badge") +@Tag(name = "badge") public class BadgeResource extends AlpineResource { private static final String SVG_MEDIA_TYPE = "image/svg+xml"; @@ -62,18 +64,17 @@ private boolean isBadgeSupportEnabled(final QueryManager qm) { @GET @Path("/vulns/project/{uuid}") @Produces(SVG_MEDIA_TYPE) - @ApiOperation( - value = "Returns current metrics for a specific project", - response = ProjectMetrics.class - ) + @Operation( + summary = "Returns current metrics for a specific project") @ApiResponses(value = { - @ApiResponse(code = 204, message = "Badge support is disabled. No content will be returned."), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "204", description = "Badge support is disabled. No content will be returned."), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @AuthenticationNotRequired public Response getProjectVulnerabilitiesBadge( - @ApiParam(value = "The UUID of the project to retrieve metrics for", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to retrieve metrics for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { if (isBadgeSupportEnabled(qm)) { @@ -94,20 +95,19 @@ public Response getProjectVulnerabilitiesBadge( @GET @Path("/vulns/project/{name}/{version}") @Produces(SVG_MEDIA_TYPE) - @ApiOperation( - value = "Returns current metrics for a specific project", - response = ProjectMetrics.class - ) + @Operation( + summary = "Returns current metrics for a specific project") @ApiResponses(value = { - @ApiResponse(code = 204, message = "Badge support is disabled. No content will be returned."), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "204", description = "Badge support is disabled. No content will be returned."), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @AuthenticationNotRequired public Response getProjectVulnerabilitiesBadge( - @ApiParam(value = "The name of the project to query on", required = true) + @Parameter(description = "The name of the project to query on", required = true) @PathParam("name") String name, - @ApiParam(value = "The version of the project to query on", required = true) + @Parameter(description = "The version of the project to query on", required = true) @PathParam("version") String version) { try (QueryManager qm = new QueryManager()) { if (isBadgeSupportEnabled(qm)) { @@ -128,18 +128,17 @@ public Response getProjectVulnerabilitiesBadge( @GET @Path("/violations/project/{uuid}") @Produces(SVG_MEDIA_TYPE) - @ApiOperation( - value = "Returns a policy violations badge for a specific project", - response = String.class - ) + @Operation( + summary = "Returns a policy violations badge for a specific project") @ApiResponses(value = { - @ApiResponse(code = 204, message = "Badge support is disabled. No content will be returned."), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "204", description = "Badge support is disabled. No content will be returned."), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @AuthenticationNotRequired public Response getProjectPolicyViolationsBadge( - @ApiParam(value = "The UUID of the project to retrieve a badge for", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to retrieve a badge for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { if (isBadgeSupportEnabled(qm)) { @@ -160,20 +159,19 @@ public Response getProjectPolicyViolationsBadge( @GET @Path("/violations/project/{name}/{version}") @Produces(SVG_MEDIA_TYPE) - @ApiOperation( - value = "Returns a policy violations badge for a specific project", - response = String.class - ) + @Operation( + summary = "Returns a policy violations badge for a specific project") @ApiResponses(value = { - @ApiResponse(code = 204, message = "Badge support is disabled. No content will be returned."), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "204", description = "Badge support is disabled. No content will be returned."), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @AuthenticationNotRequired public Response getProjectPolicyViolationsBadge( - @ApiParam(value = "The name of the project to query on", required = true) + @Parameter(description = "The name of the project to query on", required = true) @PathParam("name") String name, - @ApiParam(value = "The version of the project to query on", required = true) + @Parameter(description = "The version of the project to query on", required = true) @PathParam("version") String version) { try (QueryManager qm = new QueryManager()) { if (isBadgeSupportEnabled(qm)) { diff --git a/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/src/main/java/org/dependencytrack/resources/v1/BomResource.java index 3ca8ddf599..2b0ddaeba1 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -22,12 +22,15 @@ import alpine.event.framework.Event; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.BOMInputStream; import org.apache.commons.lang3.StringUtils; @@ -81,7 +84,11 @@ * @since 3.0.0 */ @Path("/v1/bom") -@Api(value = "bom", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "bom") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class BomResource extends AlpineResource { private static final Logger LOGGER = Logger.getLogger(BomResource.class); @@ -89,25 +96,25 @@ public class BomResource extends AlpineResource { @GET @Path("/cyclonedx/project/{uuid}") @Produces({CycloneDxMediaType.APPLICATION_CYCLONEDX_XML, CycloneDxMediaType.APPLICATION_CYCLONEDX_JSON, MediaType.APPLICATION_OCTET_STREAM}) - @ApiOperation( - value = "Returns dependency metadata for a project in CycloneDX format", - response = String.class, - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns dependency metadata for a project in CycloneDX format", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response exportProjectAsCycloneDx ( - @ApiParam(value = "The UUID of the project to export", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to export", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "The format to output (defaults to JSON)") + @Parameter(description = "The format to output (defaults to JSON)") @QueryParam("format") String format, - @ApiParam(value = "Specifies the CycloneDX variant to export. Value options are 'inventory' and 'withVulnerabilities'. (defaults to 'inventory')") + @Parameter(description = "Specifies the CycloneDX variant to export. Value options are 'inventory' and 'withVulnerabilities'. (defaults to 'inventory')") @QueryParam("variant") String variant, - @ApiParam(value = "Force the resulting BOM to be downloaded as a file (defaults to 'false')") + @Parameter(description = "Force the resulting BOM to be downloaded as a file (defaults to 'false')") @QueryParam("download") boolean download) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, uuid); @@ -159,21 +166,21 @@ public Response exportProjectAsCycloneDx ( @GET @Path("/cyclonedx/component/{uuid}") @Produces(CycloneDxMediaType.APPLICATION_CYCLONEDX_XML) - @ApiOperation( - value = "Returns dependency metadata for a specific component in CycloneDX format", - response = String.class, - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns dependency metadata for a specific component in CycloneDX format", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified component is forbidden"), - @ApiResponse(code = 404, message = "The component could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse(responseCode = "404", description = "The component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response exportComponentAsCycloneDx ( - @ApiParam(value = "The UUID of the component to export", format = "uuid", required = true) + @Parameter(description = "The UUID of the component to export", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "The format to output (defaults to JSON)") + @Parameter(description = "The format to output (defaults to JSON)") @QueryParam("format") String format) { try (QueryManager qm = new QueryManager()) { final Component component = qm.getObjectByUuid(Component.class, uuid); @@ -205,9 +212,9 @@ public Response exportComponentAsCycloneDx ( @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Upload a supported bill of material format document", - notes = """ + @Operation( + summary = "Upload a supported bill of material format document", + description = """

    Expects CycloneDX and a valid project UUID. If a UUID is not specified, then the projectName and projectVersion must be specified. @@ -227,17 +234,24 @@ public Response exportComponentAsCycloneDx ( as it does not have this limit.

    Requires permission BOM_UPLOAD

    """, - response = BomUploadResponse.class, - nickname = "UploadBomBase64Encoded" + operationId = "UploadBomBase64Encoded" ) @ApiResponses(value = { - @ApiResponse(code = 400, message = "Invalid BOM", response = InvalidBomProblemDetails.class), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = BomUploadResponse.class))), + @ApiResponse( + responseCode = "400", + description = "Invalid BOM", + content = @Content( + schema = @Schema(implementation = InvalidBomProblemDetails.class), + mediaType = ProblemDetails.MEDIA_TYPE_JSON + ) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.BOM_UPLOAD) - public Response uploadBom(@ApiParam(required = true) BomSubmitRequest request) { + public Response uploadBom(@Parameter(required = true) BomSubmitRequest request) { final Validator validator = getValidator(); if (request.getProject() != null) { // behavior in v3.0.0 failOnValidationError( @@ -295,9 +309,9 @@ public Response uploadBom(@ApiParam(required = true) BomSubmitRequest request) { @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Upload a supported bill of material format document", - notes = """ + @Operation( + summary = "Upload a supported bill of material format document", + description = """

    Expects CycloneDX and a valid project UUID. If a UUID is not specified, then the projectName and projectVersion must be specified. @@ -312,14 +326,21 @@ public Response uploadBom(@ApiParam(required = true) BomSubmitRequest request) { the response's content type will be application/problem+json.

    Requires permission BOM_UPLOAD

    """, - response = BomUploadResponse.class, - nickname = "UploadBom" + operationId = "UploadBom" ) @ApiResponses(value = { - @ApiResponse(code = 400, message = "Invalid BOM", response = InvalidBomProblemDetails.class), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = BomUploadResponse.class))), + @ApiResponse( + responseCode = "400", + description = "Invalid BOM", + content = @Content( + schema = @Schema(implementation = InvalidBomProblemDetails.class), + mediaType = ProblemDetails.MEDIA_TYPE_JSON + ) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.BOM_UPLOAD) public Response uploadBom(@FormDataParam("project") String projectUuid, @@ -329,7 +350,7 @@ public Response uploadBom(@FormDataParam("project") String projectUuid, @FormDataParam("parentName") String parentName, @FormDataParam("parentVersion") String parentVersion, @FormDataParam("parentUUID") String parentUUID, - @ApiParam(type = "string") @FormDataParam("bom") final List artifactParts) { + @Parameter(schema = @Schema(type = "string")) @FormDataParam("bom") final List artifactParts) { if (projectUuid != null) { // behavior in v3.0.0 try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, projectUuid); @@ -374,9 +395,9 @@ public Response uploadBom(@FormDataParam("project") String projectUuid, @GET @Path("/token/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Determines if there are any tasks associated with the token that are being processed, or in the queue to be processed.", - notes = """ + @Operation( + summary = "Determines if there are any tasks associated with the token that are being processed, or in the queue to be processed.", + description = """

    This endpoint is intended to be used in conjunction with uploading a supported BOM document. Upon upload, a token will be returned. The token can then be queried using this endpoint to @@ -389,16 +410,15 @@ determine if any tasks (such as vulnerability analysis) is being performed on th only that no processing is associated with the specified token.

    Requires permission BOM_UPLOAD

    -

    Deprecated. Use /v1/event/token/{uuid} instead.

    """, - response = IsTokenBeingProcessedResponse.class - ) +

    Deprecated. Use /v1/event/token/{uuid} instead.

    """) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = IsTokenBeingProcessedResponse.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.BOM_UPLOAD) @Deprecated(since = "4.11.0") public Response isTokenBeingProcessed ( - @ApiParam(value = "The UUID of the token to query", format = "uuid", required = true) + @Parameter(description = "The UUID of the token to query", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { final boolean value = Event.isEventBeingProcessed(UUID.fromString(uuid)); diff --git a/src/main/java/org/dependencytrack/resources/v1/CalculatorResource.java b/src/main/java/org/dependencytrack/resources/v1/CalculatorResource.java index 6f90d0b81b..a540d2f6eb 100644 --- a/src/main/java/org/dependencytrack/resources/v1/CalculatorResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/CalculatorResource.java @@ -19,12 +19,15 @@ package org.dependencytrack.resources.v1; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import us.springett.cvss.Cvss; import us.springett.cvss.Score; import us.springett.owasp.riskrating.MissingFactorException; @@ -44,21 +47,23 @@ * @since 3.0.0 */ @Path("/v1/calculator") -@Api(value = "calculator", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "calculator") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class CalculatorResource extends AlpineResource { @GET @Path("/cvss") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns the CVSS base score, impact sub-score and exploitability sub-score", - response = Score.class - ) + @Operation(summary = "Returns the CVSS base score, impact sub-score and exploitability sub-score") @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(type = "number"))), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) public Response getCvssScores( - @ApiParam(value = "A valid CVSSv2 or CVSSv3 vector", required = true) + @Parameter(description = "A valid CVSSv2 or CVSSv3 vector", required = true) @QueryParam("vector") String vector) { try { final Cvss cvss = Cvss.fromVector(vector); @@ -73,15 +78,13 @@ public Response getCvssScores( @GET @Path("/owasp") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns the OWASP Risk Rating likelihood score, technical impact score and business impact score", - response = us.springett.owasp.riskrating.Score.class - ) + @Operation(summary = "Returns the OWASP Risk Rating likelihood score, technical impact score and business impact score") @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(type = "number"))), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) public Response getOwaspRRScores( - @ApiParam(value = "A valid OWASP Risk Rating vector", required = true) + @Parameter(description = "A valid OWASP Risk Rating vector", required = true) @QueryParam("vector") String vector) { try { final OwaspRiskRating owaspRiskRating = OwaspRiskRating.fromVector(vector); diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java index 9a90a8c27c..bd686c358a 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentPropertyResource.java @@ -19,12 +19,16 @@ package org.dependencytrack.resources.v1; import alpine.server.auth.PermissionRequired; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Component; @@ -49,25 +53,28 @@ * @since 4.11.0 */ @Path("/v1/component/{uuid}/property") -@Api(value = "componentProperty", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "componentProperty") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class ComponentPropertyResource extends AbstractConfigPropertyResource { @GET @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all ComponentProperties for the specified component", - response = ComponentProperty.class, - responseContainer = "List", - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns a list of all ComponentProperties for the specified component", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ComponentProperty.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProperties( - @ApiParam(value = "The UUID of the component to retrieve properties for", format = "uuid", required = true) + @Parameter(description = "The UUID of the component to retrieve properties for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Component component = qm.getObjectByUuid(Component.class, uuid); @@ -97,21 +104,20 @@ public Response getProperties( @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Creates a new component property", - response = ComponentProperty.class, - code = 201, - notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " + @Operation( + summary = "Creates a new component property", + description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified component is forbidden"), - @ApiResponse(code = 404, message = "The component could not be found"), - @ApiResponse(code = 409, message = "A property with the specified component/group/name combination already exists") + @ApiResponse(responseCode = "201", content = @Content(schema = @Schema(implementation = ComponentProperty.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse(responseCode = "404", description = "The component could not be found"), + @ApiResponse(responseCode = "409", description = "A property with the specified component/group/name combination already exists") }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response createProperty( - @ApiParam(value = "The UUID of the component to create a property for", format = "uuid", required = true) + @Parameter(description = "The UUID of the component to create a property for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, ComponentProperty json) { final Validator validator = super.getValidator(); @@ -156,21 +162,21 @@ public Response createProperty( @Path("/{propertyUuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Deletes a config property", - response = ComponentProperty.class, - notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " + @Operation( + summary = "Deletes a config property", + description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified component is forbidden"), - @ApiResponse(code = 404, message = "The component or component property could not be found"), + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse(responseCode = "404", description = "The component or component property could not be found"), }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response deleteProperty( - @ApiParam(value = "The UUID of the component to delete a property from", format = "uuid", required = true) + @Parameter(description = "The UUID of the component to delete a property from", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid final String componentUuid, - @ApiParam(value = "The UUID of the component property to delete", format = "uuid", required = true) + @Parameter(description = "The UUID of the component property to delete", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("propertyUuid") @ValidUuid final String propertyUuid) { try (QueryManager qm = new QueryManager()) { final Component component = qm.getObjectByUuid(Component.class, componentUuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java index 23e90d1252..81ab367cf9 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java @@ -24,13 +24,17 @@ import alpine.server.resources.AlpineResource; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; -import io.swagger.annotations.ResponseHeader; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.InternalComponentIdentificationEvent; @@ -71,32 +75,38 @@ * @since 3.0.0 */ @Path("/v1/component") -@Api(value = "component", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "component") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class ComponentResource extends AlpineResource { @GET @Path("/project/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all components for a given project", - response = Component.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of components"), - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns a list of all components for a given project", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of components", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Component.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getAllComponents( - @ApiParam(value = "The UUID of the project to retrieve components for", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to retrieve components for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "Optionally exclude recent components so only outdated components are returned", required = false) + @Parameter(description = "Optionally exclude recent components so only outdated components are returned", required = false) @QueryParam("onlyOutdated") boolean onlyOutdated, - @ApiParam(value = "Optionally exclude transitive dependencies so only direct dependencies are returned", required = false) + @Parameter(description = "Optionally exclude transitive dependencies so only direct dependencies are returned", required = false) @QueryParam("onlyDirect") boolean onlyDirect) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); @@ -116,21 +126,21 @@ public Response getAllComponents( @GET @Path("/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a specific component", - response = Component.class, - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns a specific component", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified component is forbidden"), - @ApiResponse(code = 404, message = "The component could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Component.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse(responseCode = "404", description = "The component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getComponentByUuid( - @ApiParam(value = "The UUID of the component to retrieve", format = "uuid", required = true) + @Parameter(description = "The UUID of the component to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "Optionally includes third-party metadata about the component from external repositories", required = false) + @Parameter(description = "Optionally includes third-party metadata about the component from external repositories", required = false) @QueryParam("includeRepositoryMetaData") boolean includeRepositoryMetaData) { try (QueryManager qm = new QueryManager()) { final Component component = qm.getObjectByUuid(Component.class, uuid); @@ -158,31 +168,33 @@ public Response getComponentByUuid( @GET @Path("/identity") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of components that have the specified component identity. This resource accepts coordinates (group, name, version) or purl, cpe, or swidTagId", - responseContainer = "List", - response = Component.class, - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of components"), - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns a list of components that have the specified component identity. This resource accepts coordinates (group, name, version) or purl, cpe, or swidTagId", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of components", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Component.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response getComponentByIdentity(@ApiParam(value = "The group of the component") + public Response getComponentByIdentity(@Parameter(description = "The group of the component") @QueryParam("group") String group, - @ApiParam(value = "The name of the component") + @Parameter(description = "The name of the component") @QueryParam("name") String name, - @ApiParam(value = "The version of the component") + @Parameter(description = "The version of the component") @QueryParam("version") String version, - @ApiParam(value = "The purl of the component") + @Parameter(description = "The purl of the component") @QueryParam("purl") String purl, - @ApiParam(value = "The cpe of the component") + @Parameter(description = "The cpe of the component") @QueryParam("cpe") String cpe, - @ApiParam(value = "The swidTagId of the component") + @Parameter(description = "The swidTagId of the component") @QueryParam("swidTagId") String swidTagId, - @ApiParam(value = "The project the component belongs to", format = "uuid") + @Parameter(description = "The project the component belongs to", schema = @Schema(type = "string", format = "uuid")) @QueryParam("project") @ValidUuid String projectUuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { Project project = null; @@ -219,20 +231,22 @@ public Response getComponentByIdentity(@ApiParam(value = "The group of the compo @GET @Path("/hash/{hash}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of components that have the specified hash value", - responseContainer = "List", - response = Component.class, - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of components"), - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns a list of components that have the specified hash value", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of components", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Component.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getComponentByHash( - @ApiParam(value = "The MD5, SHA-1, SHA-256, SHA-384, SHA-512, SHA3-256, SHA3-384, SHA3-512, BLAKE2b-256, BLAKE2b-384, BLAKE2b-512, or BLAKE3 hash of the component to retrieve", required = true) + @Parameter(description = "The MD5, SHA-1, SHA-256, SHA-384, SHA-512, SHA3-256, SHA3-384, SHA3-512, BLAKE2b-256, BLAKE2b-384, BLAKE2b-512, or BLAKE3 hash of the component to retrieve", required = true) @PathParam("hash") String hash) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final PaginatedResult result = qm.getComponentByHash(hash); @@ -244,19 +258,18 @@ public Response getComponentByHash( @Path("/project/{uuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Creates a new component", - response = Component.class, - code = 201, - notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " + @Operation( + summary = "Creates a new component", + description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "201", content = @Content(schema = @Schema(implementation = Component.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) - public Response createComponent(@ApiParam(value = "The UUID of the project to create a component for", format = "uuid", required = true) + public Response createComponent(@Parameter(description = "The UUID of the project to create a component for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, Component jsonComponent) { final Validator validator = super.getValidator(); failOnValidationError( @@ -354,15 +367,15 @@ public Response createComponent(@ApiParam(value = "The UUID of the project to cr @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Updates a component", - response = Component.class, - notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " + @Operation( + summary = "Updates a component", + description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified component is forbidden"), - @ApiResponse(code = 404, message = "The UUID of the component could not be found"), + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Component.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse(responseCode = "404", description = "The UUID of the component could not be found"), }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response updateComponent(Component jsonComponent) { @@ -462,19 +475,19 @@ public Response updateComponent(Component jsonComponent) { @Path("/{uuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Deletes a component", - code = 204, - notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " + @Operation( + summary = "Deletes a component", + description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified component is forbidden"), - @ApiResponse(code = 404, message = "The UUID of the component could not be found") + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse(responseCode = "404", description = "The UUID of the component could not be found") }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response deleteComponent( - @ApiParam(value = "The UUID of the component to delete", format = "uuid", required = true) + @Parameter(description = "The UUID of the component to delete", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Component component = qm.getObjectByUuid(Component.class, uuid, Component.FetchGroup.ALL.name()); @@ -494,13 +507,13 @@ public Response deleteComponent( @GET @Path("/internal/identify") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Requests the identification of internal components in the portfolio", - code = 204, - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Requests the identification of internal components in the portfolio", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response identifyInternalComponents() { @@ -511,22 +524,24 @@ public Response identifyInternalComponents() { @GET @Path("/project/{projectUuid}/dependencyGraph/{componentUuids}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns the expanded dependency graph to every occurrence of a component", - response = Component.class, - responseContainer = "Map", - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns the expanded dependency graph to every occurrence of a component", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "- The UUID of the project could not be found\n- The UUID of the component could not be found") + @ApiResponse( + responseCode = "200", + content = @Content(schema = @Schema(type = "object")) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "- The UUID of the project could not be found\n- The UUID of the component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getDependencyGraphForComponent( - @ApiParam(value = "The UUID of the project to get the expanded dependency graph for", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to get the expanded dependency graph for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("projectUuid") @ValidUuid String projectUuid, - @ApiParam(value = "List of UUIDs of the components (separated by |) to get the expanded dependency graph for", required = true) + @Parameter(description = "List of UUIDs of the components (separated by |) to get the expanded dependency graph for", required = true) @PathParam("componentUuids") String componentUuids) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, projectUuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java index 4038b4916f..7938dd41f4 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java @@ -20,11 +20,15 @@ import alpine.model.ConfigProperty; import alpine.server.auth.PermissionRequired; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.auth.Permissions; import org.dependencytrack.persistence.QueryManager; @@ -46,19 +50,22 @@ * @since 3.2.0 */ @Path("/v1/configProperty") -@Api(value = "configProperty", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "configProperty") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class ConfigPropertyResource extends AbstractConfigPropertyResource { @GET @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all ConfigProperties for the specified groupName", - response = ConfigProperty.class, - responseContainer = "List", - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Returns a list of all ConfigProperties for the specified groupName", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ConfigProperty.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response getConfigProperties() { @@ -81,14 +88,14 @@ public Response getConfigProperties() { @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Updates a config property", - response = ConfigProperty.class, - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Updates a config property", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The config property could not be found"), + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ConfigProperty.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The config property could not be found"), }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response updateConfigProperty(ConfigProperty json) { @@ -108,15 +115,14 @@ public Response updateConfigProperty(ConfigProperty json) { @Path("aggregate") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Updates an array of config properties", - response = ConfigProperty.class, - responseContainer = "List", - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Updates an array of config properties", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "One or more config properties could not be found"), + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ConfigProperty.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "One or more config properties could not be found"), }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response updateConfigProperty(List list) { diff --git a/src/main/java/org/dependencytrack/resources/v1/CweResource.java b/src/main/java/org/dependencytrack/resources/v1/CweResource.java index 0b46568e8e..d705201f1d 100644 --- a/src/main/java/org/dependencytrack/resources/v1/CweResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/CweResource.java @@ -20,13 +20,17 @@ import alpine.persistence.PaginatedResult; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; -import io.swagger.annotations.ResponseHeader; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.model.Cwe; import org.dependencytrack.parser.common.resolver.CweResolver; import org.dependencytrack.resources.v1.openapi.PaginatedApi; @@ -45,20 +49,24 @@ * @since 3.0.0 */ @Path("/v1/cwe") -@Api(value = "cwe", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "cwe") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class CweResource extends AlpineResource { @GET @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all CWEs", - response = Cwe.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of CWEs") - ) + @Operation(summary = "Returns a list of all CWEs") @PaginatedApi @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of CWEs", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Cwe.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) public Response getCwes() { final PaginatedResult cwes = CweResolver.getInstance().all(getAlpineRequest().getPagination()); @@ -68,16 +76,15 @@ public Response getCwes() { @GET @Path("/{cweId}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a specific CWE", - response = Cwe.class - ) + @Operation( + summary = "Returns a specific CWE") @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The CWE could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Cwe.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The CWE could not be found") }) public Response getCwe( - @ApiParam(value = "The CWE ID of the CWE to retrieve", required = true) + @Parameter(description = "The CWE ID of the CWE to retrieve", required = true) @PathParam("cweId") int cweId) { final Cwe cwe = CweResolver.getInstance().lookup(cweId); if (cwe != null) { diff --git a/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java b/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java index 5c338e1368..0005d1940e 100644 --- a/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java @@ -23,12 +23,16 @@ import alpine.server.resources.AlpineResource; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Component; import org.dependencytrack.model.Project; @@ -62,26 +66,29 @@ * JAX-RS resources for processing requests related to DependencyGraph. */ @Path("/v1/dependencyGraph") -@Api(value = "dependencyGraph", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "dependencyGraph") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class DependencyGraphResource extends AlpineResource { @GET @Path("/project/{uuid}/directDependencies") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of specific components and services from project UUID", - response = DependencyGraphResponse.class, - responseContainer = "List", - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns a list of specific components and services from project UUID", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to a specified component is forbidden"), - @ApiResponse(code = 404, message = "Any component can be found"), + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = DependencyGraphResponse.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to a specified component is forbidden"), + @ApiResponse(responseCode = "404", description = "Any component can be found"), }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response getComponentsAndServicesByProjectUuid(@ApiParam(value = "The UUID of the project", format = "uuid", required = true) + public Response getComponentsAndServicesByProjectUuid(@Parameter(description = "The UUID of the project", schema = @Schema(type = "string", format = "uuid"), required = true) final @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, uuid); @@ -109,19 +116,18 @@ public Response getComponentsAndServicesByProjectUuid(@ApiParam(value = "The UUI @Path("/component/{uuid}/directDependencies") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of specific components and services from component UUID", - response = DependencyGraphResponse.class, - responseContainer = "List", - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns a list of specific components and services from component UUID", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to a specified component is forbidden"), - @ApiResponse(code = 404, message = "Any component can be found"), + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = DependencyGraphResponse.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to a specified component is forbidden"), + @ApiResponse(responseCode = "404", description = "Any component can be found"), }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response getComponentsAndServicesByComponentUuid(@ApiParam(value = "The UUID of the component", format = "uuid", required = true) + public Response getComponentsAndServicesByComponentUuid(@Parameter(description = "The UUID of the component", schema = @Schema(type = "string", format = "uuid"), required = true) final @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Component component = qm.getObjectByUuid(Component.class, uuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/EventResource.java b/src/main/java/org/dependencytrack/resources/v1/EventResource.java index 6b4b39e387..ecef631106 100644 --- a/src/main/java/org/dependencytrack/resources/v1/EventResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/EventResource.java @@ -20,12 +20,15 @@ import alpine.event.framework.Event; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.resources.v1.vo.IsTokenBeingProcessedResponse; @@ -44,16 +47,19 @@ * @since 4.11.0 */ @Path("/v1/event") -@Api(value = "event", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "event") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class EventResource extends AlpineResource { @GET @Path("/token/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Determines if there are any tasks associated with the token that are being processed, or in the queue to be processed.", - response = IsTokenBeingProcessedResponse.class, - notes = """ + @Operation( + summary = "Determines if there are any tasks associated with the token that are being processed, or in the queue to be processed.", + description = """

    This endpoint is intended to be used in conjunction with other API calls which return a token for asynchronous tasks. The token can then be queried using this endpoint to determine if the task is complete: @@ -66,10 +72,11 @@ public class EventResource extends AlpineResource {

    """ ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = IsTokenBeingProcessedResponse.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) public Response isTokenBeingProcessed ( - @ApiParam(value = "The UUID of the token to query", format = "uuid", required = true) + @Parameter(description = "The UUID of the token to query", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { final boolean value = Event.isEventBeingProcessed(UUID.fromString(uuid)); IsTokenBeingProcessedResponse response = new IsTokenBeingProcessedResponse(); diff --git a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index 7cb425c2b7..ae0e5460b9 100644 --- a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -26,13 +26,17 @@ import alpine.server.resources.AlpineResource; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.template.PebbleTemplate; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; -import io.swagger.annotations.ResponseHeader; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.commons.text.WordUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.PolicyEvaluationEvent; @@ -41,11 +45,11 @@ import org.dependencytrack.integrations.FindingPackagingFormat; import org.dependencytrack.model.Component; import org.dependencytrack.model.Finding; -import org.dependencytrack.model.GroupedFinding; import org.dependencytrack.model.Project; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.vo.BomUploadResponse; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; @@ -74,7 +78,11 @@ * @since 3.1.0 */ @Path("/v1/finding") -@Api(value = "finding", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "finding") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class FindingResource extends AlpineResource { private static final Logger LOGGER = Logger.getLogger(FindingResource.class); @@ -83,24 +91,26 @@ public class FindingResource extends AlpineResource { @GET @Path("/project/{uuid}") @Produces({MediaType.APPLICATION_JSON, MEDIA_TYPE_SARIF_JSON}) - @ApiOperation( - value = "Returns a list of all findings for a specific project or generates SARIF file if Accept: application/sarif+json header is provided", - response = Finding.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of findings"), - notes = "

    Requires permission VIEW_VULNERABILITY

    " + @Operation( + summary = "Returns a list of all findings for a specific project or generates SARIF file if Accept: application/sarif+json header is provided", + description = "

    Requires permission VIEW_VULNERABILITY

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of findings", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Finding.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) - public Response getFindingsByProject(@ApiParam(value = "The UUID of the project", format = "uuid", required = true) + public Response getFindingsByProject(@Parameter(description = "The UUID of the project", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "Optionally includes suppressed findings") + @Parameter(description = "Optionally includes suppressed findings") @QueryParam("suppressed") boolean suppressed, - @ApiParam(value = "Optionally limit findings to specific sources of vulnerability intelligence") + @Parameter(description = "Optionally limit findings to specific sources of vulnerability intelligence") @QueryParam("source") Vulnerability.Source source, @HeaderParam("accept") String acceptHeader) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { @@ -137,17 +147,18 @@ public Response getFindingsByProject(@ApiParam(value = "The UUID of the project" @GET @Path("/project/{uuid}/export") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns the findings for the specified project as FPF", - notes = "

    Requires permission VIEW_VULNERABILITY

    " + @Operation( + summary = "Returns the findings for the specified project as FPF", + description = "

    Requires permission VIEW_VULNERABILITY

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) - public Response exportFindingsByProject(@ApiParam(value = "The UUID of the project", format = "uuid", required = true) + public Response exportFindingsByProject(@Parameter(description = "The UUID of the project", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); @@ -170,19 +181,19 @@ public Response exportFindingsByProject(@ApiParam(value = "The UUID of the proje @POST @Path("/project/{uuid}/analyze") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Triggers Vulnerability Analysis on a specific project", - response = Project.class, - notes = "

    Requires permission VIEW_VULNERABILITY

    " + @Operation( + summary = "Triggers Vulnerability Analysis on a specific project", + description = "

    Requires permission VIEW_VULNERABILITY

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = BomUploadResponse.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) public Response analyzeProject( - @ApiParam(value = "The UUID of the project to analyze", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to analyze", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, uuid); @@ -211,46 +222,48 @@ public Response analyzeProject( @GET @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all findings", - response = Finding.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of findings"), - notes = "

    Requires permission VIEW_VULNERABILITY

    " + @Operation( + summary = "Returns a list of all findings", + description = "

    Requires permission VIEW_VULNERABILITY

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of findings", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Finding.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), }) @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) - public Response getAllFindings(@ApiParam(value = "Show inactive projects") + public Response getAllFindings(@Parameter(description = "Show inactive projects") @QueryParam("showInactive") boolean showInactive, - @ApiParam(value = "Show suppressed findings") + @Parameter(description = "Show suppressed findings") @QueryParam("showSuppressed") boolean showSuppressed, - @ApiParam(value = "Filter by severity") + @Parameter(description = "Filter by severity") @QueryParam("severity") String severity, - @ApiParam(value = "Filter by analysis status") + @Parameter(description = "Filter by analysis status") @QueryParam("analysisStatus") String analysisStatus, - @ApiParam(value = "Filter by vendor response") + @Parameter(description = "Filter by vendor response") @QueryParam("vendorResponse") String vendorResponse, - @ApiParam(value = "Filter published from this date") + @Parameter(description = "Filter published from this date") @QueryParam("publishDateFrom") String publishDateFrom, - @ApiParam(value = "Filter published to this date") + @Parameter(description = "Filter published to this date") @QueryParam("publishDateTo") String publishDateTo, - @ApiParam(value = "Filter attributed on from this date") + @Parameter(description = "Filter attributed on from this date") @QueryParam("attributedOnDateFrom") String attributedOnDateFrom, - @ApiParam(value = "Filter attributed on to this date") + @Parameter(description = "Filter attributed on to this date") @QueryParam("attributedOnDateTo") String attributedOnDateTo, - @ApiParam(value = "Filter the text input in these fields") + @Parameter(description = "Filter the text input in these fields") @QueryParam("textSearchField") String textSearchField, - @ApiParam(value = "Filter by this text input") + @Parameter(description = "Filter by this text input") @QueryParam("textSearchInput") String textSearchInput, - @ApiParam(value = "Filter CVSSv2 from this value") + @Parameter(description = "Filter CVSSv2 from this value") @QueryParam("cvssv2From") String cvssv2From, - @ApiParam(value = "Filter CVSSv2 from this Value") + @Parameter(description = "Filter CVSSv2 from this Value") @QueryParam("cvssv2To") String cvssv2To, - @ApiParam(value = "Filter CVSSv3 from this value") + @Parameter(description = "Filter CVSSv3 from this value") @QueryParam("cvssv3From") String cvssv3From, - @ApiParam(value = "Filter CVSSv3 from this Value") + @Parameter(description = "Filter CVSSv3 from this Value") @QueryParam("cvssv3To") String cvssv3To) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Map filters = new HashMap<>(); @@ -275,40 +288,42 @@ public Response getAllFindings(@ApiParam(value = "Show inactive projects") @GET @Path("/grouped") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all findings grouped by vulnerability", - response = GroupedFinding.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of findings"), - notes = "

    Requires permission VIEW_VULNERABILITY

    " + @Operation( + summary = "Returns a list of all findings grouped by vulnerability", + description = "

    Requires permission VIEW_VULNERABILITY

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of findings", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Finding.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), }) @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) - public Response getAllFindings(@ApiParam(value = "Show inactive projects") + public Response getAllFindings(@Parameter(description = "Show inactive projects") @QueryParam("showInactive") boolean showInactive, - @ApiParam(value = "Filter by severity") + @Parameter(description = "Filter by severity") @QueryParam("severity") String severity, - @ApiParam(value = "Filter published from this date") + @Parameter(description = "Filter published from this date") @QueryParam("publishDateFrom") String publishDateFrom, - @ApiParam(value = "Filter published to this date") + @Parameter(description = "Filter published to this date") @QueryParam("publishDateTo") String publishDateTo, - @ApiParam(value = "Filter the text input in these fields") + @Parameter(description = "Filter the text input in these fields") @QueryParam("textSearchField") String textSearchField, - @ApiParam(value = "Filter by this text input") + @Parameter(description = "Filter by this text input") @QueryParam("textSearchInput") String textSearchInput, - @ApiParam(value = "Filter CVSSv2 from this value") + @Parameter(description = "Filter CVSSv2 from this value") @QueryParam("cvssv2From") String cvssv2From, - @ApiParam(value = "Filter CVSSv2 to this value") + @Parameter(description = "Filter CVSSv2 to this value") @QueryParam("cvssv2To") String cvssv2To, - @ApiParam(value = "Filter CVSSv3 from this value") + @Parameter(description = "Filter CVSSv3 from this value") @QueryParam("cvssv3From") String cvssv3From, - @ApiParam(value = "Filter CVSSv3 to this value") + @Parameter(description = "Filter CVSSv3 to this value") @QueryParam("cvssv3To") String cvssv3To, - @ApiParam(value = "Filter occurrences in projects from this value") + @Parameter(description = "Filter occurrences in projects from this value") @QueryParam("occurrencesFrom") String occurrencesFrom, - @ApiParam(value = "Filter occurrences in projects to this value") + @Parameter(description = "Filter occurrences in projects to this value") @QueryParam("occurrencesTo") String occurrencesTo) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Map filters = new HashMap<>(); diff --git a/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java b/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java index 6f4abe28e9..c68925de89 100644 --- a/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java @@ -20,11 +20,15 @@ import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.auth.Permissions; import org.dependencytrack.tasks.OsvDownloadTask; @@ -37,20 +41,26 @@ import java.util.stream.Collectors; @Path("/v1/integration") -@Api(value = "integration", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "integration") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class IntegrationResource extends AlpineResource { @GET @Path("/osv/ecosystem") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all ecosystems in OSV", - response = String.class, - responseContainer = "List", - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Returns a list of all ecosystems in OSV", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse( + responseCode = "200", + content = @Content(array = @ArraySchema(schema = @Schema(type = "string"))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response getAllEcosystems() { @@ -62,14 +72,16 @@ public Response getAllEcosystems() { @GET @Path("/osv/ecosystem/inactive") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of available inactive ecosystems in OSV to be selected by user", - response = String.class, - responseContainer = "List", - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Returns a list of available inactive ecosystems in OSV to be selected by user", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse( + responseCode = "200", + content = @Content(array = @ArraySchema(schema = @Schema(type = "string"))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response getInactiveEcosystems() { diff --git a/src/main/java/org/dependencytrack/resources/v1/LdapResource.java b/src/main/java/org/dependencytrack/resources/v1/LdapResource.java index 47580e91a0..be78ab3aba 100644 --- a/src/main/java/org/dependencytrack/resources/v1/LdapResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/LdapResource.java @@ -25,13 +25,17 @@ import alpine.server.auth.PermissionRequired; import alpine.server.cache.CacheManager; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; -import io.swagger.annotations.ResponseHeader; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; @@ -60,7 +64,11 @@ * @since 3.3.0 */ @Path("/v1/ldap") -@Api(value = "ldap", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "ldap") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class LdapResource extends AlpineResource { private static final Logger LOGGER = Logger.getLogger(LdapResource.class); @@ -68,12 +76,9 @@ public class LdapResource extends AlpineResource { @GET @Path("/groups") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns the DNs of all accessible groups within the directory", - response = String.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of ldap groups that match the specified search criteria"), - notes = """ + @Operation( + summary = "Returns the DNs of all accessible groups within the directory", + description = """

    This API performs a pass-through query to the configured LDAP server. Search criteria results are cached using default Alpine CacheManager policy. @@ -81,7 +86,12 @@ public class LdapResource extends AlpineResource {

    Requires permission ACCESS_MANAGEMENT

    """ ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of ldap groups that match the specified search criteria", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(type = "string"))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response retrieveLdapGroups () { @@ -119,18 +129,17 @@ public Response retrieveLdapGroups () { @GET @Path("/team/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns the DNs of all groups mapped to the specified team", - response = String.class, - responseContainer = "List", - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Returns the DNs of all groups mapped to the specified team", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the team could not be found"), + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = MappedLdapGroup.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the team could not be found"), }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) - public Response retrieveLdapGroups (@ApiParam(value = "The UUID of the team to retrieve mappings for", format = "uuid", required = true) + public Response retrieveLdapGroups (@Parameter(description = "The UUID of the team to retrieve mappings for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Team team = qm.getObjectByUuid(Team.class, uuid); @@ -146,15 +155,15 @@ public Response retrieveLdapGroups (@ApiParam(value = "The UUID of the team to r @PUT @Path("/mapping") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Adds a mapping", - response = MappedLdapGroup.class, - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Adds a mapping", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the team could not be found"), - @ApiResponse(code = 409, message = "A mapping with the same team and dn already exists") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = MappedLdapGroup.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the team could not be found"), + @ApiResponse(responseCode = "409", description = "A mapping with the same team and dn already exists") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response addMapping(MappedLdapGroupRequest request) { @@ -181,18 +190,18 @@ public Response addMapping(MappedLdapGroupRequest request) { @DELETE @Path("/mapping/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Removes a mapping", - response = MappedLdapGroup.class, - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Removes a mapping", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the mapping could not be found"), + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the mapping could not be found"), }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response deleteMapping( - @ApiParam(value = "The UUID of the mapping to delete", format = "uuid", required = true) + @Parameter(description = "The UUID of the mapping to delete", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final MappedLdapGroup mapping = qm.getObjectByUuid(MappedLdapGroup.class, uuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java b/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java index 563fce13b7..c7f73f5307 100644 --- a/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/LicenseGroupResource.java @@ -21,13 +21,17 @@ import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; -import io.swagger.annotations.ResponseHeader; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.License; @@ -56,21 +60,27 @@ * @since 4.0.0 */ @Path("/v1/licenseGroup") -@Api(value = "licenseGroup", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "licenseGroup") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class LicenseGroupResource extends AlpineResource { @GET @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all license groups", - response = LicenseGroup.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of license groups"), - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Returns a list of all license groups", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of license groups", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = LicenseGroup.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response getLicenseGroups() { @@ -83,18 +93,17 @@ public Response getLicenseGroups() { @GET @Path("/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a specific license group", - response = License.class, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Returns a specific license group", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The license group could not be found") + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The license group could not be found") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response getLicenseGroup( - @ApiParam(value = "The UUID of the license group to retrieve", format = "uuid", required = true) + @Parameter(description = "The UUID of the license group to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final LicenseGroup licenseGroup = qm.getObjectByUuid(LicenseGroup.class, uuid); @@ -109,15 +118,14 @@ public Response getLicenseGroup( @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Creates a new license group", - response = LicenseGroup.class, - code = 201, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Creates a new license group", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 409, message = "A license group with the specified name already exists") + @ApiResponse(responseCode = "201", content = @Content(schema = @Schema(implementation = LicenseGroup.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "409", description = "A license group with the specified name already exists") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response createLicenseGroup(LicenseGroup jsonLicenseGroup) { @@ -140,14 +148,14 @@ public Response createLicenseGroup(LicenseGroup jsonLicenseGroup) { @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Updates a license group", - response = LicenseGroup.class, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Updates a license group", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The license group could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = LicenseGroup.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The license group could not be found") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response updateLicenseGroup(LicenseGroup jsonLicenseGroup) { @@ -171,18 +179,18 @@ public Response updateLicenseGroup(LicenseGroup jsonLicenseGroup) { @Path("/{uuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Deletes a license group", - code = 204, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Deletes a license group", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the license group could not be found") + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the license group could not be found") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response deleteLicenseGroup( - @ApiParam(value = "The UUID of the license group to delete", format = "uuid", required = true) + @Parameter(description = "The UUID of the license group to delete", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final LicenseGroup licenseGroup = qm.getObjectByUuid(LicenseGroup.class, uuid); @@ -199,21 +207,21 @@ public Response deleteLicenseGroup( @Path("/{uuid}/license/{licenseUuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Adds the license to the specified license group.", - response = LicenseGroup.class, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Adds the license to the specified license group.", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 304, message = "The license group already has the specified license assigned"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The license group or license could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = LicenseGroup.class))), + @ApiResponse(responseCode = "304", description = "The license group already has the specified license assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The license group or license could not be found") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response addLicenseToLicenseGroup( - @ApiParam(value = "A valid license group", format = "uuid", required = true) + @Parameter(description = "A valid license group", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "A valid license", format = "uuid", required = true) + @Parameter(description = "A valid license", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("licenseUuid") @ValidUuid String licenseUuid) { try (QueryManager qm = new QueryManager()) { LicenseGroup licenseGroup = qm.getObjectByUuid(LicenseGroup.class, uuid); @@ -239,21 +247,21 @@ public Response addLicenseToLicenseGroup( @Path("/{uuid}/license/{licenseUuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Removes the license from the license group.", - response = LicenseGroup.class, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Removes the license from the license group.", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 304, message = "The license is not a member with the license group"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The license group or license could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = LicenseGroup.class))), + @ApiResponse(responseCode = "304", description = "The license is not a member with the license group"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The license group or license could not be found") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response removeLicenseFromLicenseGroup( - @ApiParam(value = "A valid license group", format = "uuid", required = true) + @Parameter(description = "A valid license group", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "A valid license", format = "uuid", required = true) + @Parameter(description = "A valid license", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("licenseUuid") @ValidUuid String licenseUuid) { try (QueryManager qm = new QueryManager()) { LicenseGroup licenseGroup = qm.getObjectByUuid(LicenseGroup.class, uuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/LicenseResource.java b/src/main/java/org/dependencytrack/resources/v1/LicenseResource.java index 9a730c0a00..db9b9f6d9a 100644 --- a/src/main/java/org/dependencytrack/resources/v1/LicenseResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/LicenseResource.java @@ -22,13 +22,17 @@ import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; -import io.swagger.annotations.ResponseHeader; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.License; import org.dependencytrack.persistence.QueryManager; @@ -52,22 +56,26 @@ * @since 3.0.0 */ @Path("/v1/license") -@Api(value = "license", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "license") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class LicenseResource extends AlpineResource { private static final Logger LOGGER = Logger.getLogger(LicenseResource.class); @GET @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all licenses with complete metadata for each license", - response = License.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of licenses") - ) + @Operation(summary = "Returns a list of all licenses with complete metadata for each license") @PaginatedApi @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of licenses", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = License.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) public Response getLicenses() { try (QueryManager qm = new QueryManager(getAlpineRequest())) { @@ -79,13 +87,14 @@ public Response getLicenses() { @GET @Path("/concise") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a concise listing of all licenses", - response = License.class, - responseContainer = "List" - ) + @Operation(summary = "Returns a concise listing of all licenses") @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of licenses", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = License.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) public Response getLicenseListing() { try (QueryManager qm = new QueryManager(getAlpineRequest())) { @@ -97,16 +106,14 @@ public Response getLicenseListing() { @GET @Path("/{licenseId}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a specific license", - response = License.class - ) + @Operation(summary = "Returns a specific license") @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The license could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = License.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The license could not be found") }) public Response getLicense( - @ApiParam(value = "The SPDX License ID of the license to retrieve", required = true) + @Parameter(description = "The SPDX License ID of the license to retrieve", required = true) @PathParam("licenseId") String licenseId) { try (QueryManager qm = new QueryManager()) { final License license = qm.getLicense(licenseId); @@ -120,14 +127,14 @@ public Response getLicense( @PUT @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Creates a new custom license", - response = License.class, - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Creates a new custom license", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 409, message = "A license with the specified ID already exists.") + @ApiResponse(responseCode = "201", content = @Content(schema = @Schema(implementation = License.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "409", description = "A license with the specified ID already exists.") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response createLicense(License jsonLicense) { @@ -151,19 +158,19 @@ public Response createLicense(License jsonLicense) { @DELETE @Path("/{licenseId}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Deletes a custom license", - code = 204, - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Deletes a custom license", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The license could not be found"), - @ApiResponse(code = 409, message = "Only custom licenses can be deleted.") + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The license could not be found"), + @ApiResponse(responseCode = "409", description = "Only custom licenses can be deleted.") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response deleteLicense( - @ApiParam(value = "The SPDX License ID of the license to delete", required = true) + @Parameter(description = "The SPDX License ID of the license to delete", required = true) @PathParam("licenseId") String licenseId) { try (QueryManager qm = new QueryManager()) { final License license = qm.getLicense(licenseId); diff --git a/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java b/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java index 5d1322c1d2..92e340c34f 100644 --- a/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java @@ -21,12 +21,16 @@ import alpine.event.framework.Event; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.commons.lang3.time.DateUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.ComponentMetricsUpdateEvent; @@ -58,20 +62,23 @@ * @since 3.0.0 */ @Path("/v1/metrics") -@Api(value = "metrics", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "metrics") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class MetricsResource extends AlpineResource { @GET @Path("/vulnerability") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns the sum of all vulnerabilities in the database by year and month", - response = VulnerabilityMetrics.class, - responseContainer = "List", - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns the sum of all vulnerabilities in the database by year and month", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = VulnerabilityMetrics.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getVulnerabilityMetrics() { @@ -84,13 +91,13 @@ public Response getVulnerabilityMetrics() { @GET @Path("/portfolio/current") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns current metrics for the entire portfolio", - response = PortfolioMetrics.class, - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns current metrics for the entire portfolio", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = PortfolioMetrics.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getPortfolioCurrentMetrics() { @@ -103,20 +110,19 @@ public Response getPortfolioCurrentMetrics() { @GET @Path("/portfolio/since/{date}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns historical metrics for the entire portfolio from a specific date", - notes = """ + @Operation( + summary = "Returns historical metrics for the entire portfolio from a specific date", + description = """

    Date format must be YYYYMMDD

    -

    Requires permission VIEW_PORTFOLIO

    """, - response = PortfolioMetrics.class, - responseContainer = "List" +

    Requires permission VIEW_PORTFOLIO

    """ ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = PortfolioMetrics.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getPortfolioMetricsSince( - @ApiParam(value = "The start date to retrieve metrics for", required = true) + @Parameter(description = "The start date to retrieve metrics for", required = true) @PathParam("date") String date) { final Date since = DateUtil.parseShortDate(date); @@ -132,18 +138,17 @@ public Response getPortfolioMetricsSince( @GET @Path("/portfolio/{days}/days") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns X days of historical metrics for the entire portfolio", - response = PortfolioMetrics.class, - responseContainer = "List", - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns X days of historical metrics for the entire portfolio", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = PortfolioMetrics.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getPortfolioMetricsXDays( - @ApiParam(value = "The number of days back to retrieve metrics for", required = true) + @Parameter(description = "The number of days back to retrieve metrics for", required = true) @PathParam("days") int days) { final Date since = DateUtils.addDays(new Date(), -days); @@ -156,13 +161,13 @@ public Response getPortfolioMetricsXDays( @GET @Path("/portfolio/refresh") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Requests a refresh of the portfolio metrics", - response = PortfolioMetrics.class, - notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " + @Operation( + summary = "Requests a refresh of the portfolio metrics", + description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response RefreshPortfolioMetrics() { @@ -173,19 +178,19 @@ public Response RefreshPortfolioMetrics() { @GET @Path("/project/{uuid}/current") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns current metrics for a specific project", - response = ProjectMetrics.class, - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns current metrics for a specific project", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ProjectMetrics.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProjectCurrentMetrics( - @ApiParam(value = "The UUID of the project to retrieve metrics for", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to retrieve metrics for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); @@ -205,24 +210,23 @@ public Response getProjectCurrentMetrics( @GET @Path("/project/{uuid}/since/{date}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns historical metrics for a specific project from a specific date", - notes = """ + @Operation( + summary = "Returns historical metrics for a specific project from a specific date", + description = """

    Date format must be YYYYMMDD

    -

    Requires permission VIEW_PORTFOLIO

    """, - response = ProjectMetrics.class, - responseContainer = "List" +

    Requires permission VIEW_PORTFOLIO

    """ ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ProjectMetrics.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProjectMetricsSince( - @ApiParam(value = "The UUID of the project to retrieve metrics for", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to retrieve metrics for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "The start date to retrieve metrics for", required = true) + @Parameter(description = "The start date to retrieve metrics for", required = true) @PathParam("date") String date) { final Date since = DateUtil.parseShortDate(date); @@ -232,22 +236,21 @@ public Response getProjectMetricsSince( @GET @Path("/project/{uuid}/days/{days}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns X days of historical metrics for a specific project", - response = ProjectMetrics.class, - responseContainer = "List", - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns X days of historical metrics for a specific project", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ProjectMetrics.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProjectMetricsXDays( - @ApiParam(value = "The UUID of the project to retrieve metrics for", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to retrieve metrics for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "The number of days back to retrieve metrics for", required = true) + @Parameter(description = "The number of days back to retrieve metrics for", required = true) @PathParam("days") int days) { final Date since = DateUtils.addDays(new Date(), -days); @@ -257,18 +260,19 @@ public Response getProjectMetricsXDays( @GET @Path("/project/{uuid}/refresh") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Requests a refresh of a specific projects metrics", - notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " + @Operation( + summary = "Requests a refresh of a specific projects metrics", + description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response RefreshProjectMetrics( - @ApiParam(value = "The UUID of the project to refresh metrics on", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to refresh metrics on", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); @@ -288,19 +292,19 @@ public Response RefreshProjectMetrics( @GET @Path("/component/{uuid}/current") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns current metrics for a specific component", - response = DependencyMetrics.class, - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns current metrics for a specific component", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified component is forbidden"), - @ApiResponse(code = 404, message = "The component could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = DependencyMetrics.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse(responseCode = "404", description = "The component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getComponentCurrentMetrics( - @ApiParam(value = "The UUID of the component to retrieve metrics for", format = "uuid", required = true) + @Parameter(description = "The UUID of the component to retrieve metrics for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Component component = qm.getObjectByUuid(Component.class, uuid); @@ -320,24 +324,23 @@ public Response getComponentCurrentMetrics( @GET @Path("/component/{uuid}/since/{date}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns historical metrics for a specific component from a specific date", - notes = """ + @Operation( + summary = "Returns historical metrics for a specific component from a specific date", + description = """

    Date format must be YYYYMMDD

    -

    Requires permission VIEW_PORTFOLIO

    """, - response = DependencyMetrics.class, - responseContainer = "List" +

    Requires permission VIEW_PORTFOLIO

    """ ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified component is forbidden"), - @ApiResponse(code = 404, message = "The component could not be found") + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = DependencyMetrics.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse(responseCode = "404", description = "The component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getComponentMetricsSince( - @ApiParam(value = "The UUID of the component to retrieve metrics for", format = "uuid", required = true) + @Parameter(description = "The UUID of the component to retrieve metrics for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "The start date to retrieve metrics for", required = true) + @Parameter(description = "The start date to retrieve metrics for", required = true) @PathParam("date") String date) { final Date since = DateUtil.parseShortDate(date); @@ -350,22 +353,21 @@ public Response getComponentMetricsSince( @GET @Path("/component/{uuid}/days/{days}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns X days of historical metrics for a specific component", - response = DependencyMetrics.class, - responseContainer = "List", - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns X days of historical metrics for a specific component", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified component is forbidden"), - @ApiResponse(code = 404, message = "The component could not be found") + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = DependencyMetrics.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse(responseCode = "404", description = "The component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getComponentMetricsXDays( - @ApiParam(value = "The UUID of the component to retrieve metrics for", format = "uuid", required = true) + @Parameter(description = "The UUID of the component to retrieve metrics for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "The number of days back to retrieve metrics for", required = true) + @Parameter(description = "The number of days back to retrieve metrics for", required = true) @PathParam("days") int days) { final Date since = DateUtils.addDays(new Date(), -days); @@ -375,18 +377,19 @@ public Response getComponentMetricsXDays( @GET @Path("/component/{uuid}/refresh") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Requests a refresh of a specific components metrics", - notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " + @Operation( + summary = "Requests a refresh of a specific components metrics", + description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified component is forbidden"), - @ApiResponse(code = 404, message = "The component could not be found") + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse(responseCode = "404", description = "The component could not be found") }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response RefreshComponentMetrics( - @ApiParam(value = "The UUID of the component to refresh metrics on", format = "uuid", required = true) + @Parameter(description = "The UUID of the component to refresh metrics on", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Component component = qm.getObjectByUuid(Component.class, uuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java index 61aa5953f1..02c4316c63 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java @@ -24,16 +24,19 @@ import alpine.notification.NotificationLevel; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.NotificationPublisher; -import org.dependencytrack.model.NotificationRule; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; @@ -69,21 +72,24 @@ * @since 3.2.0 */ @Path("/v1/notification/publisher") -@Api(value = "notification", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "notification") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class NotificationPublisherResource extends AlpineResource { private static final Logger LOGGER = Logger.getLogger(NotificationPublisherResource.class); @GET @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all notification publishers", - response = NotificationPublisher.class, - responseContainer = "List", - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Returns a list of all notification publishers", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationPublisher.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response getAllNotificationPublishers() { @@ -96,16 +102,15 @@ public Response getAllNotificationPublishers() { @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Creates a new notification publisher", - response = NotificationPublisher.class, - code = 201, - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Creates a new notification publisher", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 400, message = "Invalid notification class or trying to modify a default publisher"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 409, message = "Conflict with an existing publisher's name") + @ApiResponse(responseCode = "201", content = @Content(schema = @Schema(implementation = NotificationPublisher.class))), + @ApiResponse(responseCode = "400", description = "Invalid notification class or trying to modify a default publisher"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "409", description = "Conflict with an existing publisher's name") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response createNotificationPublisher(NotificationPublisher jsonNotificationPublisher) { @@ -148,16 +153,16 @@ public Response createNotificationPublisher(NotificationPublisher jsonNotificati @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Updates a notification publisher", - response = NotificationRule.class, - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Updates a notification publisher", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 400, message = "Invalid notification class or trying to modify a default publisher"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The notification publisher could not be found"), - @ApiResponse(code = 409, message = "Conflict with an existing publisher's name") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = NotificationPublisher.class))), + @ApiResponse(responseCode = "400", description = "Invalid notification class or trying to modify a default publisher"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The notification publisher could not be found"), + @ApiResponse(responseCode = "409", description = "Conflict with an existing publisher's name") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response updateNotificationPublisher(NotificationPublisher jsonNotificationPublisher) { @@ -211,18 +216,18 @@ public Response updateNotificationPublisher(NotificationPublisher jsonNotificati @Path("/{notificationPublisherUuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Deletes a notification publisher and all related notification rules", - code = 204, - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Deletes a notification publisher and all related notification rules", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 400, message = "Deleting a default notification publisher is forbidden"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the notification publisher could not be found") + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", description = "Deleting a default notification publisher is forbidden"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the notification publisher could not be found") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) - public Response deleteNotificationPublisher(@ApiParam(value = "The UUID of the notification publisher to delete", format = "uuid", required = true) + public Response deleteNotificationPublisher(@Parameter(description = "The UUID of the notification publisher to delete", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("notificationPublisherUuid") @ValidUuid String notificationPublisherUuid) { try (QueryManager qm = new QueryManager()) { final NotificationPublisher notificationPublisher = qm.getObjectByUuid(NotificationPublisher.class, notificationPublisherUuid); @@ -243,12 +248,13 @@ public Response deleteNotificationPublisher(@ApiParam(value = "The UUID of the n @Path("/restoreDefaultTemplates") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Restore the default notification publisher templates using the ones in the solution classpath", - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Restore the default notification publisher templates using the ones in the solution classpath", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response restoreDefaultTemplates() { @@ -271,12 +277,13 @@ public Response restoreDefaultTemplates() { @Path("/test/smtp") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Dispatches a SMTP notification test", - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Dispatches a SMTP notification test", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response testSmtpPublisherConfig(@FormParam("destination") String destination) { diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java index edbaa2f12d..c1b2b82c05 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java @@ -23,13 +23,17 @@ import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; -import io.swagger.annotations.ResponseHeader; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.NotificationPublisher; @@ -61,23 +65,29 @@ * @since 3.2.0 */ @Path("/v1/notification/rule") -@Api(value = "notification", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "notification") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class NotificationRuleResource extends AlpineResource { private static final Logger LOGGER = Logger.getLogger(NotificationRuleResource.class); @GET @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all notification rules", - response = NotificationRule.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of notification rules"), - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Returns a list of all notification rules", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of notification rules", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationRule.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response getAllNotificationRules() { @@ -90,15 +100,14 @@ public Response getAllNotificationRules() { @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Creates a new notification rule", - response = NotificationRule.class, - code = 201, - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Creates a new notification rule", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the notification publisher could not be found") + @ApiResponse(responseCode = "201", content = @Content(schema = @Schema(implementation = NotificationRule.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the notification publisher could not be found") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response createNotificationRule(NotificationRule jsonRule) { @@ -128,14 +137,14 @@ public Response createNotificationRule(NotificationRule jsonRule) { @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Updates a notification rule", - response = NotificationRule.class, - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Updates a notification rule", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the notification rule could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = NotificationRule.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the notification rule could not be found") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response updateNotificationRule(NotificationRule jsonRule) { @@ -160,14 +169,14 @@ public Response updateNotificationRule(NotificationRule jsonRule) { @DELETE @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Deletes a notification rule", - code = 204, - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Deletes a notification rule", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the notification rule could not be found") + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the notification rule could not be found") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response deleteNotificationRule(NotificationRule jsonRule) { @@ -186,21 +195,21 @@ public Response deleteNotificationRule(NotificationRule jsonRule) { @Path("/{ruleUuid}/project/{projectUuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Adds a project to a notification rule", - response = NotificationRule.class, - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Adds a project to a notification rule", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 304, message = "The rule already has the specified project assigned"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The notification rule or project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = NotificationRule.class))), + @ApiResponse(responseCode = "304", description = "The rule already has the specified project assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The notification rule or project could not be found") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response addProjectToRule( - @ApiParam(value = "The UUID of the rule to add a project to", format = "uuid", required = true) + @Parameter(description = "The UUID of the rule to add a project to", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("ruleUuid") @ValidUuid String ruleUuid, - @ApiParam(value = "The UUID of the project to add to the rule", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to add to the rule", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("projectUuid") @ValidUuid String projectUuid) { try (QueryManager qm = new QueryManager()) { final NotificationRule rule = qm.getObjectByUuid(NotificationRule.class, ruleUuid); @@ -228,21 +237,21 @@ public Response addProjectToRule( @Path("/{ruleUuid}/project/{projectUuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Removes a project from a notification rule", - response = NotificationRule.class, - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Removes a project from a notification rule", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 304, message = "The rule does not have the specified project assigned"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The notification rule or project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = NotificationRule.class))), + @ApiResponse(responseCode = "304", description = "The rule does not have the specified project assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The notification rule or project could not be found") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response removeProjectFromRule( - @ApiParam(value = "The UUID of the rule to remove the project from", format = "uuid", required = true) + @Parameter(description = "The UUID of the rule to remove the project from", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("ruleUuid") @ValidUuid String ruleUuid, - @ApiParam(value = "The UUID of the project to remove from the rule", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to remove from the rule", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("projectUuid") @ValidUuid String projectUuid) { try (QueryManager qm = new QueryManager()) { final NotificationRule rule = qm.getObjectByUuid(NotificationRule.class, ruleUuid); @@ -270,21 +279,21 @@ public Response removeProjectFromRule( @Path("/{ruleUuid}/team/{teamUuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Adds a team to a notification rule", - response = NotificationRule.class, - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Adds a team to a notification rule", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 304, message = "The rule already has the specified team assigned"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The notification rule or team could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = NotificationRule.class))), + @ApiResponse(responseCode = "304", description = "The rule already has the specified team assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The notification rule or team could not be found") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response addTeamToRule( - @ApiParam(value = "The UUID of the rule to add a team to", format = "uuid", required = true) + @Parameter(description = "The UUID of the rule to add a team to", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("ruleUuid") @ValidUuid String ruleUuid, - @ApiParam(value = "The UUID of the team to add to the rule", format = "uuid", required = true) + @Parameter(description = "The UUID of the team to add to the rule", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("teamUuid") @ValidUuid String teamUuid) { try (QueryManager qm = new QueryManager()) { final NotificationRule rule = qm.getObjectByUuid(NotificationRule.class, ruleUuid); @@ -312,21 +321,21 @@ public Response addTeamToRule( @Path("/{ruleUuid}/team/{teamUuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Removes a team from a notification rule", - response = NotificationRule.class, - notes = "

    Requires permission SYSTEM_CONFIGURATION

    " + @Operation( + summary = "Removes a team from a notification rule", + description = "

    Requires permission SYSTEM_CONFIGURATION

    " ) @ApiResponses(value = { - @ApiResponse(code = 304, message = "The rule does not have the specified team assigned"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The notification rule or team could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = NotificationRule.class))), + @ApiResponse(responseCode = "304", description = "The rule does not have the specified team assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The notification rule or team could not be found") }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response removeTeamFromRule( - @ApiParam(value = "The UUID of the rule to remove the project from", format = "uuid", required = true) + @Parameter(description = "The UUID of the rule to remove the project from", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("ruleUuid") @ValidUuid String ruleUuid, - @ApiParam(value = "The UUID of the project to remove from the rule", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to remove from the rule", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("teamUuid") @ValidUuid String teamUuid) { try (QueryManager qm = new QueryManager()) { final NotificationRule rule = qm.getObjectByUuid(NotificationRule.class, ruleUuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/OidcResource.java b/src/main/java/org/dependencytrack/resources/v1/OidcResource.java index 9a6f9170f9..6ff1231d4a 100644 --- a/src/main/java/org/dependencytrack/resources/v1/OidcResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/OidcResource.java @@ -26,12 +26,16 @@ import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; import alpine.server.util.OidcUtil; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; @@ -58,7 +62,11 @@ * @since 4.0.0 */ @Path("/v1/oidc") -@Api(value = "oidc", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "oidc") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class OidcResource extends AlpineResource { private static final Logger LOGGER = Logger.getLogger(OidcResource.class); @@ -66,10 +74,8 @@ public class OidcResource extends AlpineResource { @GET @Path("/available") @Produces(MediaType.TEXT_PLAIN) - @ApiOperation( - value = "Indicates if OpenID Connect is available for this application", - response = Boolean.class - ) + @Operation( + summary = "Indicates if OpenID Connect is available for this application") @AuthenticationNotRequired public Response isAvailable() { return Response.ok(OidcUtil.isOidcAvailable()).build(); @@ -78,14 +84,13 @@ public Response isAvailable() { @GET @Path("/group") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all groups", - response = OidcGroup.class, - responseContainer = "List", - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Returns a list of all groups", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = OidcGroup.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response retrieveGroups() { @@ -99,14 +104,13 @@ public Response retrieveGroups() { @Path("/group") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Creates group", - response = OidcGroup.class, - code = 201, - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Creates group", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "201", content = @Content(schema = @Schema(implementation = OidcGroup.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response createGroup(final OidcGroup jsonGroup) { @@ -130,13 +134,13 @@ public Response createGroup(final OidcGroup jsonGroup) { @Path("/group") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Updates group", - response = OidcGroup.class, - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Updates group", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = OidcGroup.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response updateGroup(final OidcGroup jsonGroup) { @@ -162,17 +166,17 @@ public Response updateGroup(final OidcGroup jsonGroup) { @DELETE @Path("/group/{uuid}") @Consumes(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Deletes a group", - code = 204, - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Deletes a group", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The group could not be found") + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The group could not be found") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) - public Response deleteGroup(@ApiParam(value = "The UUID of the group to delete", format = "uuid", required = true) + public Response deleteGroup(@Parameter(description = "The UUID of the group to delete", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid final String uuid) { try (QueryManager qm = new QueryManager()) { final OidcGroup group = qm.getObjectByUuid(OidcGroup.class, uuid); @@ -190,18 +194,17 @@ public Response deleteGroup(@ApiParam(value = "The UUID of the group to delete", @GET @Path("/group/{uuid}/team") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of teams associated with the specified group", - response = Team.class, - responseContainer = "List", - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Returns a list of teams associated with the specified group", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the mapping could not be found"), + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = Team.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the mapping could not be found"), }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) - public Response retrieveTeamsMappedToGroup(@ApiParam(value = "The UUID of the mapping to retrieve the team for", format = "uuid", required = true) + public Response retrieveTeamsMappedToGroup(@Parameter(description = "The UUID of the mapping to retrieve the team for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid final String uuid) { try (final QueryManager qm = new QueryManager()) { final OidcGroup oidcGroup = qm.getObjectByUuid(OidcGroup.class, uuid); @@ -220,15 +223,15 @@ public Response retrieveTeamsMappedToGroup(@ApiParam(value = "The UUID of the ma @PUT @Path("/mapping") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Adds a mapping", - response = MappedOidcGroup.class, - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Adds a mapping", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the team or group could not be found"), - @ApiResponse(code = 409, message = "A mapping with the same team and group name already exists") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = MappedOidcGroup.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the team or group could not be found"), + @ApiResponse(responseCode = "409", description = "A mapping with the same team and group name already exists") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response addMapping(final MappedOidcGroupRequest request) { @@ -262,17 +265,17 @@ public Response addMapping(final MappedOidcGroupRequest request) { @DELETE @Path("/mapping/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Deletes a mapping", - code = 204, - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Deletes a mapping", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the mapping could not be found"), + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the mapping could not be found"), }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) - public Response deleteMappingByUuid(@ApiParam(value = "The UUID of the mapping to delete", format = "uuid", required = true) + public Response deleteMappingByUuid(@Parameter(description = "The UUID of the mapping to delete", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid final String uuid) { try (QueryManager qm = new QueryManager()) { final MappedOidcGroup mapping = qm.getObjectByUuid(MappedOidcGroup.class, uuid); @@ -289,19 +292,19 @@ public Response deleteMappingByUuid(@ApiParam(value = "The UUID of the mapping t @DELETE @Path("/group/{groupUuid}/team/{teamUuid}/mapping") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Deletes a mapping", - code = 204, - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Deletes a mapping", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the mapping could not be found"), + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the mapping could not be found"), }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) - public Response deleteMapping(@ApiParam(value = "The UUID of the group to delete a mapping for", format = "uuid", required = true) + public Response deleteMapping(@Parameter(description = "The UUID of the group to delete a mapping for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("groupUuid") @ValidUuid final String groupUuid, - @ApiParam(value = "The UUID of the team to delete a mapping for", format = "uuid", required = true) + @Parameter(description = "The UUID of the team to delete a mapping for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("teamUuid") @ValidUuid final String teamUuid) { try (QueryManager qm = new QueryManager()) { final Team team = qm.getObjectByUuid(Team.class, teamUuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java b/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java index 02e2cc168f..418a928fe8 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java @@ -24,12 +24,15 @@ import alpine.model.UserPrincipal; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; @@ -53,21 +56,24 @@ * @since 3.0.0 */ @Path("/v1/permission") -@Api(value = "permission", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "permission") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class PermissionResource extends AlpineResource { private static final Logger LOGGER = Logger.getLogger(PermissionResource.class); @GET @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all permissions", - response = alpine.model.Permission.class, - responseContainer = "List", - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Returns a list of all permissions", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Permissions.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response getAllPermissions() { @@ -81,21 +87,21 @@ public Response getAllPermissions() { @Path("/{permission}/user/{username}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Adds the permission to the specified username.", - response = UserPrincipal.class, - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Adds the permission to the specified username.", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 304, message = "The user already has the specified permission assigned"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The user could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = UserPrincipal.class))), + @ApiResponse(responseCode = "304", description = "The user already has the specified permission assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The user could not be found") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response addPermissionToUser( - @ApiParam(value = "A valid username", required = true) + @Parameter(description = "A valid username", required = true) @PathParam("username") String username, - @ApiParam(value = "A valid permission", required = true) + @Parameter(description = "A valid permission", required = true) @PathParam("permission") String permissionName) { try (QueryManager qm = new QueryManager()) { UserPrincipal principal = qm.getUserPrincipal(username); @@ -122,21 +128,21 @@ public Response addPermissionToUser( @Path("/{permission}/user/{username}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Removes the permission from the user.", - response = UserPrincipal.class, - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Removes the permission from the user.", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 304, message = "The user already has the specified permission assigned"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The user could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = UserPrincipal.class))), + @ApiResponse(responseCode = "304", description = "The user already has the specified permission assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The user could not be found") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response removePermissionFromUser( - @ApiParam(value = "A valid username", required = true) + @Parameter(description = "A valid username", required = true) @PathParam("username") String username, - @ApiParam(value = "A valid permission", required = true) + @Parameter(description = "A valid permission", required = true) @PathParam("permission") String permissionName) { try (QueryManager qm = new QueryManager()) { UserPrincipal principal = qm.getUserPrincipal(username); @@ -163,21 +169,21 @@ public Response removePermissionFromUser( @Path("/{permission}/team/{uuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Adds the permission to the specified team.", - response = Team.class, - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Adds the permission to the specified team.", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 304, message = "The team already has the specified permission assigned"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The team could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Team.class))), + @ApiResponse(responseCode = "304", description = "The team already has the specified permission assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The team could not be found") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response addPermissionToTeam( - @ApiParam(value = "A valid team uuid", format = "uuid", required = true) + @Parameter(description = "A valid team uuid", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "A valid permission", required = true) + @Parameter(description = "A valid permission", required = true) @PathParam("permission") String permissionName) { try (QueryManager qm = new QueryManager()) { Team team = qm.getObjectByUuid(Team.class, uuid); @@ -204,21 +210,21 @@ public Response addPermissionToTeam( @Path("/{permission}/team/{uuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Removes the permission from the team.", - response = Team.class, - notes = "

    Requires permission ACCESS_MANAGEMENT

    " + @Operation( + summary = "Removes the permission from the team.", + description = "

    Requires permission ACCESS_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 304, message = "The team already has the specified permission assigned"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The team could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Team.class))), + @ApiResponse(responseCode = "304", description = "The team already has the specified permission assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The team could not be found") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response removePermissionFromTeam( - @ApiParam(value = "A valid team uuid", format = "uuid", required = true) + @Parameter(description = "A valid team uuid", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "A valid permission", required = true) + @Parameter(description = "A valid permission", required = true) @PathParam("permission") String permissionName) { try (QueryManager qm = new QueryManager()) { Team team = qm.getObjectByUuid(Team.class, uuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java b/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java index 326dbc7f5a..5c045f77be 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java @@ -21,12 +21,15 @@ import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Policy; @@ -52,26 +55,29 @@ * @since 4.0.0 */ @Path("/v1/policy") -@Api(value = "policyCondition", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "policyCondition") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class PolicyConditionResource extends AlpineResource { @PUT @Path("/{uuid}/condition") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Creates a new policy condition", - response = PolicyCondition.class, - code = 201, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Creates a new policy condition", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the policy could not be found") + @ApiResponse(responseCode = "201", content = @Content(schema = @Schema(implementation = PolicyCondition.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the policy could not be found") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response createPolicyCondition( - @ApiParam(value = "The UUID of the policy", format = "uuid", required = true) + @Parameter(description = "The UUID of the policy", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, PolicyCondition jsonPolicyCondition) { final Validator validator = super.getValidator(); @@ -94,14 +100,14 @@ public Response createPolicyCondition( @Path("/condition") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Updates a policy condition", - response = PolicyCondition.class, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Updates a policy condition", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the policy condition could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = PolicyCondition.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the policy condition could not be found") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response updatePolicyCondition(PolicyCondition jsonPolicyCondition) { @@ -124,18 +130,18 @@ public Response updatePolicyCondition(PolicyCondition jsonPolicyCondition) { @Path("/condition/{uuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Deletes a policy condition", - code = 204, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Deletes a policy condition", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the policy condition could not be found") + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the policy condition could not be found") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response deletePolicyCondition( - @ApiParam(value = "The UUID of the policy condition to delete", format = "uuid", required = true) + @Parameter(description = "The UUID of the policy condition to delete", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final PolicyCondition pc = qm.getObjectByUuid(PolicyCondition.class, uuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java b/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java index c382720831..b455ac94db 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java @@ -21,13 +21,16 @@ import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; -import io.swagger.annotations.ResponseHeader; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Policy; @@ -57,21 +60,27 @@ * @since 4.0.0 */ @Path("/v1/policy") -@Api(value = "policy", authorizations = @Authorization(value = "X-Api-Key")) +@io.swagger.v3.oas.annotations.tags.Tag(name = "policy") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class PolicyResource extends AlpineResource { @GET @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all policies", - response = Policy.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policies"), - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Returns a list of all policies", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of policies", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Policy.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response getPolicies() { @@ -84,18 +93,18 @@ public Response getPolicies() { @GET @Path("/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a specific policy", - response = Policy.class, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Returns a specific policy", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The policy could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Policy.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The policy could not be found") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response getPolicy( - @ApiParam(value = "The UUID of the policy to retrieve", format = "uuid", required = true) + @Parameter(description = "The UUID of the policy to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Policy policy = qm.getObjectByUuid(Policy.class, uuid); @@ -110,15 +119,14 @@ public Response getPolicy( @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Creates a new policy", - response = Policy.class, - code = 201, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Creates a new policy", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 409, message = "A policy with the specified name already exists") + @ApiResponse(responseCode = "201", content = @Content(schema = @Schema(implementation = Policy.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "409", description = "A policy with the specified name already exists") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response createPolicy(Policy jsonPolicy) { @@ -151,14 +159,14 @@ public Response createPolicy(Policy jsonPolicy) { @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Updates a policy", - response = Policy.class, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Updates a policy", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The policy could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Policy.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The policy could not be found") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response updatePolicy(Policy jsonPolicy) { @@ -185,18 +193,18 @@ public Response updatePolicy(Policy jsonPolicy) { @Path("/{uuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Deletes a policy", - code = 204, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Deletes a policy", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The UUID of the policy could not be found") + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The UUID of the policy could not be found") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response deletePolicy( - @ApiParam(value = "The UUID of the policy to delete", format = "uuid", required = true) + @Parameter(description = "The UUID of the policy to delete", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Policy policy = qm.getObjectByUuid(Policy.class, uuid); @@ -213,21 +221,21 @@ public Response deletePolicy( @Path("/{policyUuid}/project/{projectUuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Adds a project to a policy", - response = Policy.class, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Adds a project to a policy", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 304, message = "The policy already has the specified project assigned"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The policy or project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Policy.class))), + @ApiResponse(responseCode = "304", description = "The policy already has the specified project assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The policy or project could not be found") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response addProjectToPolicy( - @ApiParam(value = "The UUID of the policy to add a project to", format = "uuid", required = true) + @Parameter(description = "The UUID of the policy to add a project to", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("policyUuid") @ValidUuid String policyUuid, - @ApiParam(value = "The UUID of the project to add to the rule", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to add to the rule", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("projectUuid") @ValidUuid String projectUuid) { try (QueryManager qm = new QueryManager()) { final Policy policy = qm.getObjectByUuid(Policy.class, policyUuid); @@ -252,21 +260,21 @@ public Response addProjectToPolicy( @Path("/{policyUuid}/project/{projectUuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Removes a project from a policy", - response = Policy.class, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Removes a project from a policy", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 304, message = "The policy does not have the specified project assigned"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The policy or project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Policy.class))), + @ApiResponse(responseCode = "304", description = "The policy does not have the specified project assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The policy or project could not be found") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response removeProjectFromPolicy( - @ApiParam(value = "The UUID of the policy to remove the project from", format = "uuid", required = true) + @Parameter(description = "The UUID of the policy to remove the project from", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("policyUuid") @ValidUuid String policyUuid, - @ApiParam(value = "The UUID of the project to remove from the policy", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to remove from the policy", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("projectUuid") @ValidUuid String projectUuid) { try (QueryManager qm = new QueryManager()) { final Policy policy = qm.getObjectByUuid(Policy.class, policyUuid); @@ -291,21 +299,21 @@ public Response removeProjectFromPolicy( @Path("/{policyUuid}/tag/{tagName}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Adds a tag to a policy", - response = Policy.class, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Adds a tag to a policy", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 304, message = "The policy already has the specified tag assigned"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The policy or tag could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Policy.class))), + @ApiResponse(responseCode = "304", description = "The policy already has the specified tag assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The policy or tag could not be found") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response addTagToPolicy( - @ApiParam(value = "The UUID of the policy to add a project to", format = "uuid", required = true) + @Parameter(description = "The UUID of the policy to add a project to", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("policyUuid") @ValidUuid String policyUuid, - @ApiParam(value = "The name of the tag to add to the rule", required = true) + @Parameter(description = "The name of the tag to add to the rule", required = true) @PathParam("tagName")String tagName) { try (QueryManager qm = new QueryManager()) { final Policy policy = qm.getObjectByUuid(Policy.class, policyUuid); @@ -331,21 +339,20 @@ public Response addTagToPolicy( @Path("/{policyUuid}/tag/{tagName}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Removes a tag from a policy", - response = Policy.class, - notes = "

    Requires permission POLICY_MANAGEMENT

    " + @Operation( + summary = "Removes a tag from a policy", + description = "

    Requires permission POLICY_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 304, message = "The policy does not have the specified tag assigned"), - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 404, message = "The policy or tag could not be found") + @ApiResponse(responseCode = "304", description = "The policy does not have the specified tag assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The policy or tag could not be found") }) @PermissionRequired(Permissions.Constants.POLICY_MANAGEMENT) public Response removeTagFromPolicy( - @ApiParam(value = "The UUID of the policy to remove the tag from", format = "uuid", required = true) + @Parameter(description = "The UUID of the policy to remove the tag from", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("policyUuid") @ValidUuid String policyUuid, - @ApiParam(value = "The name of the tag to remove from the policy", required = true) + @Parameter(description = "The name of the tag to remove from the policy", required = true) @PathParam("tagName") String tagName) { try (QueryManager qm = new QueryManager()) { final Policy policy = qm.getObjectByUuid(Policy.class, policyUuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java b/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java index 0f2c89b85d..f6ac7a1590 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java @@ -21,13 +21,17 @@ import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; -import io.swagger.annotations.ResponseHeader; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Component; import org.dependencytrack.model.PolicyViolation; @@ -54,24 +58,30 @@ * @since 4.0.0 */ @Path("/v1/violation") -@Api(value = "violation", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "violation") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class PolicyViolationResource extends AlpineResource { @GET @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all policy violations for the entire portfolio", - response = PolicyViolation.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policy violations"), - notes = "

    Requires permission VIEW_POLICY_VIOLATION

    " + @Operation( + summary = "Returns a list of all policy violations for the entire portfolio", + description = "

    Requires permission VIEW_POLICY_VIOLATION

    " ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of policy violations", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = PolicyViolation.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_POLICY_VIOLATION) - public Response getViolations(@ApiParam(value = "Optionally includes suppressed violations") + public Response getViolations(@Parameter(description = "Optionally includes suppressed violations") @QueryParam("suppressed") boolean suppressed) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final PaginatedResult result = qm.getPolicyViolations(suppressed); @@ -84,23 +94,25 @@ public Response getViolations(@ApiParam(value = "Optionally includes suppressed @GET @Path("/project/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all policy violations for a specific project", - response = PolicyViolation.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policy violations"), - notes = "

    Requires permission VIEW_POLICY_VIOLATION

    " + @Operation( + summary = "Returns a list of all policy violations for a specific project", + description = "

    Requires permission VIEW_POLICY_VIOLATION

    " ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of policy violations", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = PolicyViolation.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_POLICY_VIOLATION) - public Response getViolationsByProject(@ApiParam(value = "The UUID of the project", format = "uuid", required = true) + public Response getViolationsByProject(@Parameter(description = "The UUID of the project", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "Optionally includes suppressed violations") + @Parameter(description = "Optionally includes suppressed violations") @QueryParam("suppressed") boolean suppressed) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); @@ -122,23 +134,25 @@ public Response getViolationsByProject(@ApiParam(value = "The UUID of the projec @GET @Path("/component/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all policy violations for a specific component", - response = PolicyViolation.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of policy violations"), - notes = "

    Requires permission VIEW_POLICY_VIOLATION

    " + @Operation( + summary = "Returns a list of all policy violations for a specific component", + description = "

    Requires permission VIEW_POLICY_VIOLATION

    " ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified component is forbidden"), - @ApiResponse(code = 404, message = "The component could not be found") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of policy violations", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = PolicyViolation.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse(responseCode = "404", description = "The component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_POLICY_VIOLATION) - public Response getViolationsByComponent(@ApiParam(value = "The UUID of the component", format = "uuid", required = true) + public Response getViolationsByComponent(@Parameter(description = "The UUID of the component", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @ApiParam(value = "Optionally includes suppressed violations") + @Parameter(description = "Optionally includes suppressed violations") @QueryParam("suppressed") boolean suppressed) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Component component = qm.getObjectByUuid(Component.class, uuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java index e322bae29e..d95b0467ea 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java @@ -19,12 +19,16 @@ package org.dependencytrack.resources.v1; import alpine.server.auth.PermissionRequired; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Project; @@ -52,25 +56,28 @@ * @since 3.4.0 */ @Path("/v1/project/{uuid}/property") -@Api(value = "projectProperty", authorizations = @Authorization(value = "X-Api-Key")) +@Tag(name = "projectProperty") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class ProjectPropertyResource extends AbstractConfigPropertyResource { @GET @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all ProjectProperties for the specified project", - response = ProjectProperty.class, - responseContainer = "List", - notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " + @Operation( + summary = "Returns a list of all ProjectProperties for the specified project", + description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ProjectProperty.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response getProperties( - @ApiParam(value = "The UUID of the project to retrieve properties for", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to retrieve properties for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); @@ -100,21 +107,20 @@ public Response getProperties( @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Creates a new project property", - response = ProjectProperty.class, - code = 201, - notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " + @Operation( + summary = "Creates a new project property", + description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found"), - @ApiResponse(code = 409, message = "A property with the specified project/group/name combination already exists") + @ApiResponse(responseCode = "201", content = @Content(schema = @Schema(implementation = ProjectProperty.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found"), + @ApiResponse(responseCode = "409", description = "A property with the specified project/group/name combination already exists") }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response createProperty( - @ApiParam(value = "The UUID of the project to create a property for", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to create a property for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, ProjectProperty json) { final Validator validator = super.getValidator(); @@ -158,19 +164,19 @@ public Response createProperty( @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Updates a project property", - response = ProjectProperty.class, - notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " + @Operation( + summary = "Updates a project property", + description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found"), + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ProjectProperty.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found"), }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response updateProperty( - @ApiParam(value = "The UUID of the project to create a property for", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to create a property for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, ProjectProperty json) { final Validator validator = super.getValidator(); @@ -201,19 +207,19 @@ public Response updateProperty( @DELETE @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Deletes a config property", - response = ProjectProperty.class, - notes = "

    Requires permission PORTFOLIO_MANAGEMENT

    " + @Operation( + summary = "Deletes a config property", + description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project or project property could not be found"), + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project or project property could not be found"), }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response deleteProperty( - @ApiParam(value = "The UUID of the project to delete a property from", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to delete a property from", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, ProjectProperty json) { final Validator validator = super.getValidator(); diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index 64d9693c14..0a24e8632d 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -25,13 +25,16 @@ import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; import io.jsonwebtoken.lang.Collections; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.Authorization; -import io.swagger.annotations.ResponseHeader; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.CloneProjectEvent; @@ -71,32 +74,38 @@ * @since 3.0.0 */ @Path("/v1/project") -@Api(value = "project", authorizations = @Authorization(value = "X-Api-Key")) +@io.swagger.v3.oas.annotations.tags.Tag(name = "project") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) public class ProjectResource extends AlpineResource { private static final Logger LOGGER = Logger.getLogger(ProjectResource.class); @GET @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all projects", - response = Project.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects"), - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns a list of all projects", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response getProjects(@ApiParam(value = "The optional name of the project to query on", required = false) + public Response getProjects(@Parameter(description = "The optional name of the project to query on", required = false) @QueryParam("name") String name, - @ApiParam(value = "Optionally excludes inactive projects from being returned", required = false) + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive, - @ApiParam(value = "Optionally excludes children projects from being returned", required = false) + @Parameter(description = "Optionally excludes children projects from being returned", required = false) @QueryParam("onlyRoot") boolean onlyRoot, - @ApiParam(value = "The UUID of the team which projects shall be excluded", format = "uuid", required = false) + @Parameter(description = "The UUID of the team which projects shall be excluded", schema = @Schema(type = "string", format = "uuid"), required = false) @QueryParam("notAssignedToTeamWithUuid") @ValidUuid String notAssignedToTeamWithUuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { Team notAssignedToTeam = null; @@ -115,19 +124,19 @@ public Response getProjects(@ApiParam(value = "The optional name of the project @GET @Path("/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a specific project", - response = Project.class, - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns a specific project", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Project.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProject( - @ApiParam(value = "The UUID of the project to retrieve", format = "uuid", required = true) + @Parameter(description = "The UUID of the project to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getProject(uuid); @@ -146,22 +155,22 @@ public Response getProject( @GET @Path("/lookup") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a specific project by its name and version", - response = Project.class, - nickname = "getProjectByNameAndVersion", - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns a specific project by its name and version", + operationId = "getProjectByNameAndVersion", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 403, message = "Access to the specified project is forbidden"), - @ApiResponse(code = 404, message = "The project could not be found") + @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Project.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProject( - @ApiParam(value = "The name of the project to query on", required = true) + @Parameter(description = "The name of the project to query on", required = true) @QueryParam("name") String name, - @ApiParam(value = "The version of the project to query on", required = true) + @Parameter(description = "The version of the project to query on", required = true) @QueryParam("version") String version) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getProject(name, version); @@ -180,24 +189,26 @@ public Response getProject( @GET @Path("/tag/{tag}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all projects by tag", - response = Project.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects with the tag"), - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns a list of all projects by tag", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProjectsByTag( - @ApiParam(value = "The tag to query on", required = true) + @Parameter(description = "The tag to query on", required = true) @PathParam("tag") String tagString, - @ApiParam(value = "Optionally excludes inactive projects from being returned", required = false) + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive, - @ApiParam(value = "Optionally excludes children projects from being returned", required = false) + @Parameter(description = "Optionally excludes children projects from being returned", required = false) @QueryParam("onlyRoot") boolean onlyRoot) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Tag tag = qm.getTagByName(tagString); @@ -209,24 +220,26 @@ public Response getProjectsByTag( @GET @Path("/classifier/{classifier}") @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Returns a list of all projects by classifier", - response = Project.class, - responseContainer = "List", - responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of projects of the specified classifier"), - notes = "

    Requires permission VIEW_PORTFOLIO

    " + @Operation( + summary = "Returns a list of all projects by classifier", + description = "

    Requires permission VIEW_PORTFOLIO

    " ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized") + @ApiResponse( + responseCode = "200", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProjectsByClassifier( - @ApiParam(value = "The classifier to query on", required = true) + @Parameter(description = "The classifier to query on", required = true) @PathParam("classifier") String classifierString, - @ApiParam(value = "Optionally excludes inactive projects from being returned", required = false) + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive, - @ApiParam(value = "Optionally excludes children projects from being returned", required = false) + @Parameter(description = "Optionally excludes children projects from being returned", required = false) @QueryParam("onlyRoot") boolean onlyRoot) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Classifier classifier = Classifier.valueOf(classifierString); @@ -240,18 +253,17 @@ public Response getProjectsByClassifier( @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Creates a new project", - notes = """ + @Operation( + summary = "Creates a new project", + description = """

    If a parent project exists, parent.uuid is required

    Requires permission PORTFOLIO_MANAGEMENT

    - """, - response = Project.class, - code = 201 + """ ) @ApiResponses(value = { - @ApiResponse(code = 401, message = "Unauthorized"), - @ApiResponse(code = 409, message = """ + @ApiResponse(responseCode = "201", content = @Content(schema = @Schema(implementation = Project.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "409", description = """