diff --git a/api/src/main/openapi/paths/components.yaml b/api/src/main/openapi/paths/components.yaml
index d1acc5eed6..b5b77f2a98 100644
--- a/api/src/main/openapi/paths/components.yaml
+++ b/api/src/main/openapi/paths/components.yaml
@@ -56,90 +56,94 @@ get:
operationId: listComponents
summary: List all components
description: |-
- Retrieves a list of all components that have the specified component identity. This resource accepts coordinates (group, name, version) , `purl`, `cpe`, `swidTagId`, `project_uuid`, or `hash`.
-
+ Retrieves a list of all components matching the provided filter criteria.
+
+ Text filters are case-insensitive.
+
### Sortable fields
-
+
Sorting is supported for the following fields:
-
+
* `name`
* `version`
* `group`
* `purl`
* `cpe`
- * `swid_tag_id`
* `last_inherited_risk_score`
-
+
Requires permission VIEW_PORTFOLIO
tags:
- - Components
+ - Components
parameters:
- - name: project_uuid
- in: query
- description: The UUID of the project to retrieve components for
- schema:
- type: string
- format: uuid
- - name: group
- in: query
- description: The group of the component
- schema:
- type: string
- - name: name
- in: query
- description: The name of the component
- schema:
- type: string
- - name: version
- in: query
- description: The version of the component
- schema:
- type: string
- - name: purl
- in: query
- description: The PURL of the component
- schema:
- type: string
- - name: cpe
- in: query
- description: The CPE of the component
- schema:
- type: string
- - name: swid_tag_id
- in: query
- description: The SWID Tag ID of the component
- schema:
- type: string
- - name: hash_type
- in: query
- description: The MD5, SHA1, SHA_256, SHA_384, SHA_512, SHA3_256, SHA3_384, SHA3_512, BLAKE2B_256, BLAKE2B_384, BLAKE2B_512, or BLAKE3 hash type of the component
- schema:
- type: string
- enum:
- - MD5
- - SHA1
- - SHA_256
- - SHA_384
- - SHA_512
- - SHA3_256
- - SHA3_384
- - SHA3_512
- - BLAKE2B_256
- - BLAKE2B_384
- - BLAKE2B_512
- - BLAKE3
- - name: hash
- in: query
- description: The hash value of the component
- schema:
- type: string
- - $ref: "../components/parameters/pagination-limit.yaml"
- - $ref: "../components/parameters/page-token.yaml"
- - $ref: "../components/parameters/sort-direction.yaml"
- - $ref: "../components/parameters/sort-by.yaml"
+ - name: group_contains
+ in: query
+ description: Filter by group (substring match)
+ schema:
+ type: string
+ - name: name_contains
+ in: query
+ description: Filter by name (substring match)
+ schema:
+ type: string
+ - name: version_contains
+ in: query
+ description: Filter by version (substring match)
+ schema:
+ type: string
+ - name: purl_prefix
+ in: query
+ description: |-
+ Filter by PURL (prefix match).
+
+ Must be a valid PURL, with at least `pkg:/` populated.
+ schema:
+ type: string
+ - name: cpe
+ in: query
+ description: |-
+ Filter by CPE (exact match).
+
+ Must be a valid CPE.
+ schema:
+ type: string
+ - name: swid_tag_id_contains
+ in: query
+ description: Filter by SWID Tag ID (substring match)
+ schema:
+ type: string
+ - name: hash_type
+ in: query
+ description: The hash type to filter by
+ schema:
+ type: string
+ enum:
+ - MD5
+ - SHA1
+ - SHA_256
+ - SHA_384
+ - SHA_512
+ - SHA3_256
+ - SHA3_384
+ - SHA3_512
+ - BLAKE2B_256
+ - BLAKE2B_384
+ - BLAKE2B_512
+ - BLAKE3
+ - name: hash
+ in: query
+ description: |-
+ Filter by hash value (exact match).
+
+ Requires `hash_type` to be set.
+ schema:
+ type: string
+ - $ref: "../components/parameters/pagination-limit.yaml"
+ - $ref: "../components/parameters/page-token.yaml"
+ - $ref: "../components/parameters/sort-direction.yaml"
+ - $ref: "../components/parameters/sort-by.yaml"
responses:
"200":
- description: A list of all components for a given identity
+ description: A list of components matching the provided filters
content:
application/json:
schema:
@@ -150,13 +154,11 @@ get:
application/problem+json:
schema:
anyOf:
- - $ref: "../components/schemas/json-schema-validation-problem-details.yaml"
- - $ref: "../components/schemas/problem-details.yaml"
+ - $ref: "../components/schemas/json-schema-validation-problem-details.yaml"
+ - $ref: "../components/schemas/problem-details.yaml"
"401":
$ref: "../components/responses/generic-unauthorized-error.yaml"
"403":
$ref: "../components/responses/generic-forbidden-error.yaml"
- "404":
- $ref: "../components/responses/generic-not-found-error.yaml"
default:
$ref: "../components/responses/generic-error.yaml"
\ No newline at end of file
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ComponentDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ComponentDao.java
index cb3ee49363..2850c3e524 100644
--- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ComponentDao.java
+++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ComponentDao.java
@@ -249,7 +249,7 @@ default Page listComponents(
queryParams.put("componentPurl", componentPurl);
}
if (componentCpe != null) {
- whereConditions.add("LOWER(\"C\".\"CPE\") LIKE ('%' || LOWER(:componentCpe) || '%')");
+ whereConditions.add("LOWER(\"C\".\"CPE\") = LOWER(:componentCpe)");
queryParams.put("componentCpe", componentCpe);
}
if (componentSwidTagId != null) {
@@ -298,7 +298,6 @@ default Page listComponents(
case "group" -> SortBy.GROUP;
case "purl" -> SortBy.PURL;
case "cpe" -> SortBy.CPE;
- case "swid_tag_id" -> SortBy.SWIDTAGID;
case "last_inherited_risk_score" -> SortBy.LAST_RISKSCORE;
case null, default -> null;
};
@@ -321,7 +320,6 @@ default Page listComponents(
case SortBy.GROUP -> lastRow.getGroup();
case SortBy.PURL -> lastRow.getPurl();
case SortBy.CPE -> lastRow.getCpe();
- case SortBy.SWIDTAGID -> lastRow.getSwidTagId();
case SortBy.LAST_RISKSCORE -> lastRow.getLastInheritedRiskScore();
case null -> lastRow.getName();
};
@@ -450,7 +448,6 @@ enum SortBy {
GROUP,
PURL,
CPE,
- SWIDTAGID,
LAST_RISKSCORE
}
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/ComponentsResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/ComponentsResource.java
index 2f007f4e63..f7ab56a2f8 100644
--- a/apiserver/src/main/java/org/dependencytrack/resources/v2/ComponentsResource.java
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/ComponentsResource.java
@@ -44,7 +44,6 @@
import org.dependencytrack.model.Project;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.persistence.jdbi.ComponentDao;
-import org.dependencytrack.persistence.jdbi.ProjectDao;
import org.dependencytrack.resources.AbstractApiResource;
import org.dependencytrack.util.InternalComponentIdentifier;
import org.dependencytrack.util.PurlUtil;
@@ -109,23 +108,15 @@ public Response createComponent(final CreateComponentRequest request) {
@Override
@PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO)
- public Response listComponents(UUID projectUuid, String group, String name, String version, String purl, String cpe,
- String swidTagId, String hashType, String hash, Integer limit, String pageToken, SortDirection sortDirection, String sortBy) {
+ public Response listComponents(String groupContains, String nameContains, String versionContains, String purlPrefix, String cpe,
+ String swidTagIdContains, String hashType, String hash, Integer limit, String pageToken, SortDirection sortDirection, String sortBy) {
return inJdbiTransaction(getAlpineRequest(), handle -> {
- Long projectId = null;
- if (projectUuid != null) {
- projectId = handle.attach(ProjectDao.class).getProjectId(projectUuid);
- if (projectId == null) {
- throw new NotFoundException();
- }
- requireProjectAccess(handle, projectUuid);
- }
PackageURL packageURL = null;
- if (purl != null) {
+ if (purlPrefix != null) {
try {
- packageURL = new PackageURL(StringUtils.trimToNull(purl));
+ packageURL = new PackageURL(StringUtils.trimToNull(purlPrefix));
} catch (MalformedPackageURLException e) {
- throw new BadRequestException("Invalid package URL: %s".formatted(purl));
+ throw new BadRequestException("Invalid package URL: %s".formatted(purlPrefix));
}
}
if (cpe != null) {
@@ -144,9 +135,9 @@ public Response listComponents(UUID projectUuid, String group, String name, Stri
}
}
final Page componentsPage = handle.attach(ComponentDao.class)
- .listComponents(projectId, true, packageURL != null ? packageURL.canonicalize().toLowerCase() : null, StringUtils.trimToNull(cpe),
- StringUtils.trimToNull(swidTagId), StringUtils.trimToNull(group), StringUtils.trimToNull(name),
- StringUtils.trimToNull(version), hashTypeEnum, StringUtils.trimToNull(hash), limit, pageToken, sortBy, mapSortDirection(sortDirection));
+ .listComponents(null, true, packageURL != null ? packageURL.canonicalize().toLowerCase() : null, StringUtils.trimToNull(cpe),
+ StringUtils.trimToNull(swidTagIdContains), StringUtils.trimToNull(groupContains), StringUtils.trimToNull(nameContains),
+ StringUtils.trimToNull(versionContains), hashTypeEnum, StringUtils.trimToNull(hash), limit, pageToken, sortBy, mapSortDirection(sortDirection));
final var response = ListComponentsResponse.builder()
.items(componentsPage.items().stream()
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/ComponentsResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/ComponentsResourceTest.java
index a6c40e8a49..cd3692618c 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v2/ComponentsResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/ComponentsResourceTest.java
@@ -31,8 +31,6 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
-import java.util.UUID;
-
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static org.assertj.core.api.Assertions.assertThat;
@@ -302,9 +300,9 @@ public void listComponentsSortingTest() {
public void listComponentsWithCoordinatesTest() {
prepareComponents();
Response response = jersey.target("/components")
- .queryParam("group", "B")
- .queryParam("name", "B")
- .queryParam("version", "versionB")
+ .queryParam("group_contains", "B")
+ .queryParam("name_contains", "B")
+ .queryParam("version_contains", "versionB")
.queryParam("limit", 2)
.request()
.header(X_API_KEY, apiKey)
@@ -341,7 +339,7 @@ public void listComponentsWithCoordinatesTest() {
public void listComponentsWithPurlTest() {
prepareComponents();
Response response = jersey.target("/components")
- .queryParam("purl", "pkg:maven/groupB/nameB@versionB")
+ .queryParam("purl_prefix", "pkg:maven/groupB/nameB@versionB")
.queryParam("limit", 2)
.request()
.header(X_API_KEY, apiKey)
@@ -423,63 +421,13 @@ public void listComponentsWithCpeTest() {
""");
}
- @Test
- public void listComponentsWithProjectTest() {
- prepareComponents();
- Response response = jersey.target("/components")
- .queryParam("project_uuid", qm.getProject("projectA", "1.0").getUuid())
- .queryParam("limit", 2)
- .request()
- .header(X_API_KEY, apiKey)
- .get();
- assertThat(response.getStatus()).isEqualTo(200);
- final JsonObject responseJson = parseJsonObject(response);
- assertThatJson(responseJson.toString()).isEqualTo(/* language=JSON */ """
- {
- "items" : [ {
- "name": "nameA",
- "version": "versionA",
- "group": "groupA",
- "cpe": "cpe:2.3:a:groupA:nameA:versionA:*:*:*:*:*:*:*",
- "purl":"pkg:maven/groupA/nameA@versionA?foo=bar",
- "internal": false,
- "uuid": "${json-unit.any-string}",
- "project": {
- "name": "projectA",
- "version": "1.0",
- "uuid": "${json-unit.any-string}"
- }
- }
- ],
- "total": {
- "count": 1,
- "type": "EXACT"
- }
- }
- """);
- }
-
- @Test
- public void listComponentsWithProjectWhenProjectDoesNotExistTest() {
- prepareComponents();
- Response response = jersey.target("/components")
- .queryParam("project_uuid", UUID.randomUUID())
- .queryParam("limit", 2)
- .request()
- .header(X_API_KEY, apiKey)
- .get();
- assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_NOT_FOUND);
- assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull();
- assertThat(getPlainTextBody(response)).contains("Not Found");
- }
-
@Test
public void listComponentsAclTest() {
enablePortfolioAccessControl();
initializeWithPermissions(Permissions.PORTFOLIO_MANAGEMENT);
prepareComponents();
Response response = jersey.target("/components")
- .queryParam("name", "name")
+ .queryParam("name_contains", "name")
.queryParam("limit", 2)
.request()
.header(X_API_KEY, apiKey)