Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port: Add "Show in Dependency-Graph" Button in "Affected Projects" List #671

Merged
merged 5 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import static org.dependencytrack.model.sqlmapping.ComponentProjection.mapToComponent;
Expand Down Expand Up @@ -885,28 +884,17 @@ private void preprocessACLs(final Query<Component> query, final String inputFilt
}
}

public Map<String, Component> getDependencyGraphForComponent(Project project, Component component) {
public Map<String, Component> getDependencyGraphForComponents(Project project, List<Component> components) {
Map<String, Component> dependencyGraph = new HashMap<>();
if (project.getDirectDependencies() == null || project.getDirectDependencies().isBlank()) {
return dependencyGraph;
}
String queryUuid = ".*" + component.getUuid().toString() + ".*";
final Query<Component> query = pm.newQuery(Component.class, "directDependencies.matches(:queryUuid) && project == :project");
List<Component> components = (List<Component>) query.executeWithArray(queryUuid, project);
for (Component parentNodeComponent : components) {
parentNodeComponent.setExpandDependencyGraph(true);
if (dependencyGraph.containsKey(parentNodeComponent.getUuid().toString())) {
parentNodeComponent.getDependencyGraph().add(component.getUuid().toString());
} else {
dependencyGraph.put(parentNodeComponent.getUuid().toString(), parentNodeComponent);
Set<String> set = new HashSet<>();
set.add(component.getUuid().toString());
parentNodeComponent.setDependencyGraph(set);
}
getParentDependenciesOfComponent(project, parentNodeComponent, dependencyGraph, component);
}
if (!dependencyGraph.isEmpty() || project.getDirectDependencies().contains(component.getUuid().toString())) {

for(Component component : components) {
dependencyGraph.put(component.getUuid().toString(), component);
getParentDependenciesOfComponent(project, component, dependencyGraph);
}
if (!dependencyGraph.isEmpty()){
getRootDependencies(dependencyGraph, project);
getDirectDependenciesForPathDependencies(dependencyGraph);
}
Expand Down Expand Up @@ -949,34 +937,30 @@ public List<DependencyGraphResponse> getDependencyGraphByUUID(final List<UUID> u
return List.copyOf(query.executeResultList(DependencyGraphResponse.class));
}

private void getParentDependenciesOfComponent(Project project, Component parentNode, Map<String, Component> dependencyGraph, Component searchedComponent) {
String queryUuid = ".*" + parentNode.getUuid().toString() + ".*";
private void getParentDependenciesOfComponent(Project project, Component childComponent, Map<String, Component> dependencyGraph) {
String queryUuid = ".*" + childComponent.getUuid().toString() + ".*";
final Query<Component> query = pm.newQuery(Component.class, "directDependencies.matches(:queryUuid) && project == :project");
List<Component> components = (List<Component>) query.executeWithArray(queryUuid, project);
for (Component component : components) {
if (component.getUuid() != searchedComponent.getUuid()) {
component.setExpandDependencyGraph(true);
if (dependencyGraph.containsKey(component.getUuid().toString())) {
if (component.getDependencyGraph().add(component.getUuid().toString())) {
getParentDependenciesOfComponent(project, component, dependencyGraph, searchedComponent);
}
} else {
dependencyGraph.put(component.getUuid().toString(), component);
Set<String> set = new HashSet<>();
set.add(component.getUuid().toString());
component.setDependencyGraph(set);
getParentDependenciesOfComponent(project, component, dependencyGraph, searchedComponent);
}
List<Component> parentComponents = (List<Component>) query.executeWithArray(queryUuid, project);
for (Component parentComponent : parentComponents) {
parentComponent.setExpandDependencyGraph(true);
if(parentComponent.getDependencyGraph() == null) {
parentComponent.setDependencyGraph(new HashSet<>());
}
parentComponent.getDependencyGraph().add(childComponent.getUuid().toString());
if (!dependencyGraph.containsKey(parentComponent.getUuid().toString())) {
dependencyGraph.put(parentComponent.getUuid().toString(), parentComponent);
getParentDependenciesOfComponent(project, parentComponent, dependencyGraph);
}
}
}

private void getRootDependencies(Map<String, Component> dependencyGraph, Project project) {
JsonArray directDependencies = Json.createReader(new StringReader(project.getDirectDependencies())).readArray();
for (JsonValue directDependency : directDependencies) {
if (!dependencyGraph.containsKey(directDependency.asJsonObject().getString("uuid"))) {
Component component = this.getObjectByUuid(Component.class, directDependency.asJsonObject().getString("uuid"));
dependencyGraph.put(component.getUuid().toString(), component);
String uuid = directDependency.asJsonObject().getString("uuid");
if (!dependencyGraph.containsKey(uuid)) {
Component component = this.getObjectByUuid(Component.class, uuid);
dependencyGraph.put(uuid, component);
}
}
getDirectDependenciesForPathDependencies(dependencyGraph);
Expand All @@ -991,12 +975,13 @@ private void getDirectDependenciesForPathDependencies(Map<String, Component> dep
if (component.getDependencyGraph() == null) {
component.setDependencyGraph(new HashSet<>());
}
if (!dependencyGraph.containsKey(directDependency.asJsonObject().getString("uuid")) && !addToDependencyGraph.containsKey(directDependency.asJsonObject().getString("uuid"))) {
Component childNode = this.getObjectByUuid(Component.class, directDependency.asJsonObject().getString("uuid"));
String uuid = directDependency.asJsonObject().getString("uuid");
if (!dependencyGraph.containsKey(uuid) && !addToDependencyGraph.containsKey(uuid)) {
Component childNode = this.getObjectByUuid(Component.class, uuid);
addToDependencyGraph.put(childNode.getUuid().toString(), childNode);
component.getDependencyGraph().add(childNode.getUuid().toString());
} else {
component.getDependencyGraph().add(directDependency.asJsonObject().getString("uuid"));
component.getDependencyGraph().add(uuid);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
import org.dependencytrack.proto.vulnanalysis.v1.ScanResult;
import org.dependencytrack.proto.vulnanalysis.v1.ScanStatus;
import org.dependencytrack.proto.vulnanalysis.v1.ScannerResult;
import org.dependencytrack.resources.v1.vo.AffectedProject;
import org.dependencytrack.resources.v1.vo.DependencyGraphResponse;
import org.dependencytrack.tasks.IntegrityMetaInitializerTask;

Expand Down Expand Up @@ -675,8 +676,8 @@ public void recursivelyDelete(Component component, boolean commitIndex) {
getComponentQueryManager().recursivelyDelete(component, commitIndex);
}

public Map<String, Component> getDependencyGraphForComponent(Project project, Component component) {
return getComponentQueryManager().getDependencyGraphForComponent(project, component);
public Map<String, Component> getDependencyGraphForComponents(Project project, List<Component> components) {
return getComponentQueryManager().getDependencyGraphForComponents(project, components);
}

public PaginatedResult getLicenses() {
Expand Down Expand Up @@ -1130,8 +1131,8 @@ public long getSuppressedCount(Project project, Component component) {
return getFindingsQueryManager().getSuppressedCount(project, component);
}

public List<Project> getProjects(final Vulnerability vulnerability, final Set<String> fetchGroups) {
return getVulnerabilityQueryManager().getProjects(vulnerability, fetchGroups);
public List<AffectedProject> getAffectedProjects(Vulnerability vulnerability) {
return getVulnerabilityQueryManager().getAffectedProjects(vulnerability);
}

public VulnerabilityAlias synchronizeVulnerabilityAlias(VulnerabilityAlias alias) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.model.VulnerabilityAlias;
import org.dependencytrack.model.VulnerableSoftware;
import org.dependencytrack.resources.v1.vo.AffectedProject;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
Expand All @@ -46,6 +48,8 @@
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.UUID;
import java.util.function.Function;

final class VulnerabilityQueryManager extends QueryManager implements IQueryManager {

Expand Down Expand Up @@ -342,7 +346,7 @@ public PaginatedResult getVulnerabilities(Component component, boolean includeSu
Map<String, Epss> matchedEpssList = getEpssForCveIds(
result.getList(Vulnerability.class).stream().map(vuln -> vuln.getVulnId()).distinct().toList());
for (final Vulnerability vulnerability: result.getList(Vulnerability.class)) {
//vulnerability.setAffectedProjectCount(this.getProjects(vulnerability).size());
vulnerability.setAffectedProjectCount(this.getAffectedProjects(vulnerability).size());
vulnerability.setAliases(getVulnerabilityAliases(vulnerability));
vulnerability.setEpss(matchedEpssList.get(vulnerability.getVulnId()));
}
Expand Down Expand Up @@ -524,6 +528,48 @@ public List<Project> getProjects(final Vulnerability vulnerability, final Set<St
return getObjectsById(Project.class, affectedProjectIds, fetchGroups);
}

/**
* Returns a List of Projects affected by a specific vulnerability.
* @param vulnerability the vulnerability to query on
* @return a List of AffectedProjects
*/
public List<AffectedProject> getAffectedProjects(Vulnerability vulnerability) {
// Fetch all projects affected by the given vulnerability.
// Group them by their UUID to ease additional enrichment.
final Map<String, AffectedProject> affectedProjects = getProjects(vulnerability, Set.of(Project.FetchGroup.IDENTIFIERS.name())).stream()
.map(project -> new AffectedProject(
project.getUuid(),
project.getDirectDependencies() != null,
project.getName(),
project.getVersion(),
null
))
.collect(Collectors.toMap(affectedProject -> affectedProject.getUuid().toString(), Function.identity()));

// Fetch UUIDs of components that are part of the affected projects,
// and affected by the given vulnerability. Return both the component's UUID,
// but also the UUID of the project it belongs to, so we can correlate.
final Query<Component> query = pm.newQuery(Component.class);
query.setFilter(":projectUuids.contains(project.uuid) && vulnerabilities.contains(:vulnerability)");
query.setParameters(affectedProjects.keySet(), vulnerability);
query.setResult("uuid, project.uuid");
final List<Object[]> resultRows;
try {
resultRows = List.copyOf(query.executeResultList(Object[].class));
} finally {
query.closeAll();
}

// "Enrich" affectedProjects with component UUIDs.
for (final Object[] resultRow : resultRows) {
final String componentUuid = String.valueOf(resultRow[0]);
final String projectUuid = String.valueOf(resultRow[1]);
affectedProjects.get(projectUuid).getAffectedComponentUuids().add(UUID.fromString(componentUuid));
}

return new ArrayList<>(affectedProjects.values());
}

/**
* Returns the number of {@link Project}s affected by a given {@link Vulnerability}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

Expand Down Expand Up @@ -593,7 +595,7 @@ public Response identifyInternalComponents() {
}

@GET
@Path("/project/{projectUuid}/dependencyGraph/{componentUuid}")
@Path("/project/{projectUuid}/dependencyGraph/{componentUuids}")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(
value = "Returns the expanded dependency graph to every occurrence of a component",
Expand All @@ -608,24 +610,29 @@ public Response identifyInternalComponents() {
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 component to get the expanded dependency graph for", required = true)
@PathParam("componentUuid") String componentUuid) {
@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()) {
final Project project = qm.getObjectByUuid(Project.class, projectUuid);
if (project != null) {
if (!qm.hasAccess(super.getPrincipal(), project)) {
return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden.").build();
}
final Component component = qm.getObjectByUuid(Component.class, componentUuid);
if (component != null) {
Map<String, Component> dependencyGraph = qm.getDependencyGraphForComponent(project, component);
return Response.ok(dependencyGraph).build();
} else {
if(project == null) {
return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build();
}

if (!qm.hasAccess(super.getPrincipal(), project)) {
return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden.").build();
}

final String[] componentUuidsSplit = componentUuids.split("\\|");
final List<Component> components = new ArrayList<>();
for(String uuid : componentUuidsSplit) {
final Component component = qm.getObjectByUuid(Component.class, uuid);
if(component == null) {
return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the component could not be found.").build();
}
} else {
return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build();
components.add(component);
}
Map<String, Component> dependencyGraph = qm.getDependencyGraphForComponents(project, components);
return Response.ok(dependencyGraph).build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.dependencytrack.model.VulnerableSoftware;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.resources.v1.vo.AffectedComponent;
import org.dependencytrack.resources.v1.vo.AffectedProject;
import org.dependencytrack.util.VulnerabilityUtil;
import us.springett.cvss.Cvss;
import us.springett.cvss.Score;
Expand All @@ -60,7 +61,6 @@
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
* JAX-RS resources for processing vulnerabilities.
Expand Down Expand Up @@ -213,7 +213,7 @@ public Response getAffectedProject(@PathParam("source") String source,
try (QueryManager qm = new QueryManager(getAlpineRequest())) {
final Vulnerability vulnerability = qm.getVulnerabilityByVulnId(source, vuln);
if (vulnerability != null) {
final List<Project> projects = qm.detach(qm.getProjects(vulnerability, Set.of(Project.FetchGroup.IDENTIFIERS.name())));
final List<AffectedProject> projects = qm.getAffectedProjects(vulnerability);
final long totalCount = projects.size();
return Response.ok(projects).header(TOTAL_COUNT_HEADER, totalCount).build();
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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.vo;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
* Describes a project that is affected by a specific vulnerability, including a list of UUIDs of the components
* affected by the vulnerability within this project.
*
* @author Ralf King
* @since 4.11.0
*/
public class AffectedProject {
private final UUID uuid;

private final boolean dependencyGraphAvailable;

private final String name;

private final String version;

private final List<UUID> affectedComponentUuids;

public AffectedProject(UUID uuid, boolean dependencyGraphAvailable, String name, String version, List<UUID> affectedComponentUuids) {
this.uuid = uuid;
this.dependencyGraphAvailable = dependencyGraphAvailable;
this.name = name;
this.version = version;
this.affectedComponentUuids = affectedComponentUuids == null ? new ArrayList<>() : affectedComponentUuids;
}

public UUID getUuid() {
return uuid;
}

public boolean isDependencyGraphAvailable() {
return dependencyGraphAvailable;
}
public String getName() {
return name;
}

public String getVersion() {
return version;
}

public List<UUID> getAffectedComponentUuids() {
return affectedComponentUuids;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ public void getAffectedProjectTest() throws Exception {
Assert.assertNotNull(json);
Assert.assertEquals("Project 1", json.getJsonObject(0).getString("name"));
Assert.assertEquals(sampleData.p1.getUuid().toString(), json.getJsonObject(0).getString("uuid"));
Assert.assertEquals(sampleData.c1.getUuid().toString(), json.getJsonObject(0).getJsonArray("affectedComponentUuids").getJsonString(0).getString());
}

@Test
Expand Down
Loading