Skip to content

Commit 03bae1e

Browse files
authored
Add project filters to component identity search (#6085)
* Add project filters to component identity search Adds onlyActive and onlyLatestVersion query parameters to GET /v1/component/identity, allowing portfolio-wide component searches to be scoped to active projects and/or projects flagged as the latest version. Reduces noise from archived projects and older project versions when triaging components across the portfolio. Closes #4570 Signed-off-by: hoobio <7289249+hoobio@users.noreply.github.com> * Document component search project filters Adds a short note to the Impact Analysis page describing the "Only active projects" and "Only latest project versions" toggles on the Components search page, and the matching onlyActive and onlyLatestVersion query parameters on /api/v1/component/identity. Refs #4570 Signed-off-by: hoobio <7289249+hoobio@users.noreply.github.com> * Tighten javadoc and parameter descriptions Trim wording in the new ComponentQueryManager overload, the new @parameter descriptions on /v1/component/identity, and the Impact Analysis doc paragraph added in the previous commit. Signed-off-by: hoobio <7289249+hoobio@users.noreply.github.com> * Rename query params to disambiguate on /component endpoint Per PR review: onlyActive and onlyLatestVersion read as if they filter on component attributes when the endpoint is scoped to components. Rename to: - excludeInactiveProjects, mirroring the existing convention used by /v1/project, /v1/vulnerability, and others. - onlyLatestProjectVersion, mirroring the existing field on Policy and its use in PolicyEngine. Refs #4570 Signed-off-by: hoobio <7289249+hoobio@users.noreply.github.com> * Update Impact Analysis doc to match toggle framing Reflect the renamed query parameters and the toggle relabel (Show inactive projects / Show all project versions, both on by default) in the doc paragraph added by the previous commit. Signed-off-by: hoobio <7289249+hoobio@users.noreply.github.com> * Update Impact Analysis doc for renamed latest-version toggle Track the frontend label change from "Show all project versions" to "Show non-latest project versions". Signed-off-by: hoobio <7289249+hoobio@users.noreply.github.com> * Pluralize onlyLatestProjectVersions query parameter Per PR review, the parameter takes effect across all matching projects (one latest per name, multiple in aggregate), so the plural form reads more accurately. Renames the corresponding internal Java identifiers, test methods, test query strings, and the Impact Analysis doc reference. Signed-off-by: hoobio <7289249+hoobio@users.noreply.github.com> --------- Signed-off-by: hoobio <7289249+hoobio@users.noreply.github.com>
1 parent bf3b39b commit 03bae1e

5 files changed

Lines changed: 151 additions & 2 deletions

File tree

docs/_docs/usage/impact-analysis.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,10 @@ Alternatively, if the component name and version are known, then performing a se
2727
reveal a list of vulnerabilities, as well as a list of all projects that have a dependency on the component.
2828

2929
![incident response](/images/screenshots/vulnerable-component.png)
30+
31+
Two toggles on the Components search page narrow results during incident response:
32+
33+
- **Show inactive projects**: when off, hides components from inactive projects (usually archived). On by default.
34+
- **Only show latest project versions**: when on, hides components from project versions not flagged as the latest. Off by default.
35+
36+
Combine with **Show inactive projects** (off) and **Only show latest project versions** (on) to scope to active, latest-version projects only. They map to the `excludeInactiveProjects` and `onlyLatestProjectVersions` query parameters on `GET /api/v1/component/identity`.

src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,21 @@ public PaginatedResult getComponents(ComponentIdentity identity, boolean include
251251
* @return a list of components
252252
*/
253253
public PaginatedResult getComponents(ComponentIdentity identity, Project project, boolean includeMetrics) {
254+
return getComponents(identity, project, includeMetrics, false, false);
255+
}
256+
257+
/**
258+
* Returns components by their identity, optionally scoped to active projects, latest-version
259+
* projects, or both.
260+
* @param identity the ComponentIdentity to query against
261+
* @param project the {@link Project} the {@link Component}s belong to
262+
* @param includeMetrics whether to include component metrics
263+
* @param excludeInactiveProjects when {@code true}, return only components from active projects
264+
* @param onlyLatestProjectVersions when {@code true}, return only components from projects flagged as the latest version
265+
* @return a list of components
266+
*/
267+
public PaginatedResult getComponents(ComponentIdentity identity, Project project, boolean includeMetrics,
268+
boolean excludeInactiveProjects, boolean onlyLatestProjectVersions) {
254269
if (identity == null) {
255270
return null;
256271
}
@@ -262,6 +277,12 @@ public PaginatedResult getComponents(ComponentIdentity identity, Project project
262277
queryFilterElements.add(" project == :project ");
263278
queryParams.put("project", project);
264279
}
280+
if (excludeInactiveProjects) {
281+
queryFilterElements.add(" project.active == true ");
282+
}
283+
if (onlyLatestProjectVersions) {
284+
queryFilterElements.add(" project.isLatest == true ");
285+
}
265286

266287
final PaginatedResult result;
267288
if (identity.getGroup() != null || identity.getName() != null || identity.getVersion() != null) {

src/main/java/org/dependencytrack/persistence/QueryManager.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,11 @@ public PaginatedResult getComponents(ComponentIdentity identity, Project project
567567
return getComponentQueryManager().getComponents(identity, project, includeMetrics);
568568
}
569569

570+
public PaginatedResult getComponents(ComponentIdentity identity, Project project, boolean includeMetrics,
571+
boolean excludeInactiveProjects, boolean onlyLatestProjectVersions) {
572+
return getComponentQueryManager().getComponents(identity, project, includeMetrics, excludeInactiveProjects, onlyLatestProjectVersions);
573+
}
574+
570575
public Component createComponent(Component component, boolean commitIndex) {
571576
return getComponentQueryManager().createComponent(component, commitIndex);
572577
}

src/main/java/org/dependencytrack/resources/v1/ComponentResource.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,11 @@ public Response getComponentByIdentity(@Parameter(description = "The group of th
202202
@Parameter(description = "The swidTagId of the component")
203203
@QueryParam("swidTagId") String swidTagId,
204204
@Parameter(description = "The project the component belongs to", schema = @Schema(type = "string", format = "uuid"))
205-
@QueryParam("project") @ValidUuid String projectUuid) {
205+
@QueryParam("project") @ValidUuid String projectUuid,
206+
@Parameter(description = "When true, only return components from active projects")
207+
@QueryParam("excludeInactiveProjects") boolean excludeInactiveProjects,
208+
@Parameter(description = "When true, only return components from projects flagged as the latest version")
209+
@QueryParam("onlyLatestProjectVersions") boolean onlyLatestProjectVersions) {
206210
try (QueryManager qm = new QueryManager(getAlpineRequest())) {
207211
Project project = null;
208212
if (projectUuid != null) {
@@ -229,7 +233,7 @@ public Response getComponentByIdentity(@Parameter(description = "The group of th
229233
&& identity.getPurl() == null && identity.getCpe() == null && identity.getSwidTagId() == null) {
230234
return Response.ok().header(TOTAL_COUNT_HEADER, 0).build();
231235
} else {
232-
final PaginatedResult result = qm.getComponents(identity, project, true);
236+
final PaginatedResult result = qm.getComponents(identity, project, true, excludeInactiveProjects, onlyLatestProjectVersions);
233237
return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build();
234238
}
235239
}

src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,118 @@ void getComponentByIdentityWithProjectWhenProjectDoesNotExistTest() {
518518
assertThat(getPlainTextBody(response)).contains("The project could not be found");
519519
}
520520

521+
@Test
522+
void getComponentByIdentityExcludeInactiveProjectsTest() {
523+
final Project activeProject = qm.createProject("activeProject", null, "1.0", null, null, null, true, false);
524+
var activeComponent = new Component();
525+
activeComponent.setProject(activeProject);
526+
activeComponent.setGroup("acme");
527+
activeComponent.setName("library");
528+
activeComponent.setVersion("1.0");
529+
activeComponent.setPurl("pkg:maven/acme/library@1.0");
530+
activeComponent = qm.createComponent(activeComponent, false);
531+
532+
final Project inactiveProject = qm.createProject("inactiveProject", null, "1.0", null, null, null, false, false);
533+
var inactiveComponent = new Component();
534+
inactiveComponent.setProject(inactiveProject);
535+
inactiveComponent.setGroup("acme");
536+
inactiveComponent.setName("library");
537+
inactiveComponent.setVersion("1.0");
538+
inactiveComponent.setPurl("pkg:maven/acme/library@1.0");
539+
qm.createComponent(inactiveComponent, false);
540+
541+
final Response response = jersey.target(V1_COMPONENT + "/identity")
542+
.queryParam("name", "library")
543+
.queryParam("excludeInactiveProjects", "true")
544+
.request()
545+
.header(X_API_KEY, apiKey)
546+
.get(Response.class);
547+
assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK);
548+
assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("1");
549+
550+
final JsonArray json = parseJsonArray(response);
551+
assertThat(json).hasSize(1);
552+
assertThat(json.getJsonObject(0).getString("uuid")).isEqualTo(activeComponent.getUuid().toString());
553+
}
554+
555+
@Test
556+
void getComponentByIdentityOnlyLatestProjectVersionTest() {
557+
final Project latestProject = qm.createProject("latestProject", null, "2.0", null, null, null, true, true, false);
558+
var latestComponent = new Component();
559+
latestComponent.setProject(latestProject);
560+
latestComponent.setGroup("acme");
561+
latestComponent.setName("library");
562+
latestComponent.setVersion("1.0");
563+
latestComponent.setPurl("pkg:maven/acme/library@1.0");
564+
latestComponent = qm.createComponent(latestComponent, false);
565+
566+
final Project olderProject = qm.createProject("olderProject", null, "1.0", null, null, null, true, false);
567+
var olderComponent = new Component();
568+
olderComponent.setProject(olderProject);
569+
olderComponent.setGroup("acme");
570+
olderComponent.setName("library");
571+
olderComponent.setVersion("1.0");
572+
olderComponent.setPurl("pkg:maven/acme/library@1.0");
573+
qm.createComponent(olderComponent, false);
574+
575+
final Response response = jersey.target(V1_COMPONENT + "/identity")
576+
.queryParam("name", "library")
577+
.queryParam("onlyLatestProjectVersions", "true")
578+
.request()
579+
.header(X_API_KEY, apiKey)
580+
.get(Response.class);
581+
assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK);
582+
assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("1");
583+
584+
final JsonArray json = parseJsonArray(response);
585+
assertThat(json).hasSize(1);
586+
assertThat(json.getJsonObject(0).getString("uuid")).isEqualTo(latestComponent.getUuid().toString());
587+
}
588+
589+
@Test
590+
void getComponentByIdentityExcludeInactiveAndOnlyLatestProjectVersionTest() {
591+
final Project activeLatest = qm.createProject("activeLatest", null, "2.0", null, null, null, true, true, false);
592+
var activeLatestComponent = new Component();
593+
activeLatestComponent.setProject(activeLatest);
594+
activeLatestComponent.setGroup("acme");
595+
activeLatestComponent.setName("library");
596+
activeLatestComponent.setVersion("1.0");
597+
activeLatestComponent.setPurl("pkg:maven/acme/library@1.0");
598+
activeLatestComponent = qm.createComponent(activeLatestComponent, false);
599+
600+
final Project activeOlder = qm.createProject("activeOlder", null, "1.0", null, null, null, true, false);
601+
var activeOlderComponent = new Component();
602+
activeOlderComponent.setProject(activeOlder);
603+
activeOlderComponent.setGroup("acme");
604+
activeOlderComponent.setName("library");
605+
activeOlderComponent.setVersion("1.0");
606+
activeOlderComponent.setPurl("pkg:maven/acme/library@1.0");
607+
qm.createComponent(activeOlderComponent, false);
608+
609+
final Project inactiveLatest = qm.createProject("inactiveLatest", null, "2.0", null, null, null, false, true, false);
610+
var inactiveLatestComponent = new Component();
611+
inactiveLatestComponent.setProject(inactiveLatest);
612+
inactiveLatestComponent.setGroup("acme");
613+
inactiveLatestComponent.setName("library");
614+
inactiveLatestComponent.setVersion("1.0");
615+
inactiveLatestComponent.setPurl("pkg:maven/acme/library@1.0");
616+
qm.createComponent(inactiveLatestComponent, false);
617+
618+
final Response response = jersey.target(V1_COMPONENT + "/identity")
619+
.queryParam("name", "library")
620+
.queryParam("excludeInactiveProjects", "true")
621+
.queryParam("onlyLatestProjectVersions", "true")
622+
.request()
623+
.header(X_API_KEY, apiKey)
624+
.get(Response.class);
625+
assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK);
626+
assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("1");
627+
628+
final JsonArray json = parseJsonArray(response);
629+
assertThat(json).hasSize(1);
630+
assertThat(json.getJsonObject(0).getString("uuid")).isEqualTo(activeLatestComponent.getUuid().toString());
631+
}
632+
521633
@Test
522634
void getComponentByHashTest() {
523635
Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false);

0 commit comments

Comments
 (0)