diff --git a/src/main/java/org/dependencytrack/event/PolicyEvaluationEvent.java b/src/main/java/org/dependencytrack/event/PolicyEvaluationEvent.java new file mode 100644 index 0000000000..3a480293e2 --- /dev/null +++ b/src/main/java/org/dependencytrack/event/PolicyEvaluationEvent.java @@ -0,0 +1,88 @@ +/* + * 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.event; + +import java.util.ArrayList; +import java.util.List; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Project; +import alpine.event.framework.AbstractChainableEvent; + +/** + * Defines a general purpose event to analyze components for vulnerabilities. + * Additional logic in the event handler performs analysis on what specific + * type of analysis should take place. + * + * @author Steve Springett + * @since 3.0.0 + */ +public class PolicyEvaluationEvent extends AbstractChainableEvent { + + private List components = new ArrayList<>(); + private Project project; + + /** + * Default constructed used to signal that a portfolio analysis + * should be performed on all components. + */ + public PolicyEvaluationEvent() { + + } + + /** + * Creates an event to analyze the specified components. + * @param components the components to analyze + */ + public PolicyEvaluationEvent(final List components) { + this.components = components; + } + + /** + * Creates an event to analyze the specified component. + * @param component the component to analyze + */ + public PolicyEvaluationEvent(final Component component) { + this.components.add(component); + } + + /** + * Returns the list of components to analyze. + * @return the list of components to analyze + */ + public List getComponents() { + return this.components; + } + + /** + * Fluent method that sets the project these components are + * optionally a part of and returns this instance. + */ + public PolicyEvaluationEvent project(final Project project) { + this.project = project; + return this; + } + + /** + * Returns the project these components are optionally a part of. + */ + public Project getProject() { + return project; + } + +} diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java index 5050699767..2d45fd83ec 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java @@ -18,36 +18,8 @@ */ package org.dependencytrack.resources.v1; -import alpine.event.framework.Event; -import alpine.persistence.PaginatedResult; -import alpine.server.auth.PermissionRequired; -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 org.apache.commons.lang3.StringUtils; -import org.dependencytrack.auth.Permissions; -import org.dependencytrack.event.InternalComponentIdentificationEvent; -import org.dependencytrack.event.RepositoryMetaEvent; -import org.dependencytrack.event.VulnerabilityAnalysisEvent; -import org.dependencytrack.model.Component; -import org.dependencytrack.model.ComponentIdentity; -import org.dependencytrack.model.License; -import org.dependencytrack.model.Project; -import org.dependencytrack.model.RepositoryType; -import org.dependencytrack.model.RepositoryMetaComponent; -import org.dependencytrack.persistence.QueryManager; -import org.dependencytrack.util.InternalComponentIdentificationUtil; - import java.util.List; import java.util.Map; - import javax.validation.Validator; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -60,6 +32,33 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import org.apache.commons.lang3.StringUtils; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.event.InternalComponentIdentificationEvent; +import org.dependencytrack.event.PolicyEvaluationEvent; +import org.dependencytrack.event.RepositoryMetaEvent; +import org.dependencytrack.event.VulnerabilityAnalysisEvent; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.ComponentIdentity; +import org.dependencytrack.model.License; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.RepositoryMetaComponent; +import org.dependencytrack.model.RepositoryType; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.InternalComponentIdentificationUtil; +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; +import alpine.event.framework.Event; +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; /** * JAX-RS resources for processing components. @@ -310,8 +309,13 @@ public Response createComponent(@PathParam("uuid") String uuid, Component jsonCo component.setNotes(StringUtils.trimToNull(jsonComponent.getNotes())); component = qm.createComponent(component, true); - Event.dispatch(new VulnerabilityAnalysisEvent(component)); - Event.dispatch(new RepositoryMetaEvent(List.of(component))); + Event.dispatch( + new VulnerabilityAnalysisEvent(component) + // Wait for RepositoryMetaEvent after VulnerabilityAnalysisEvent, + // as both might be needed in policy evaluation + .onSuccess(new RepositoryMetaEvent(List.of(component))) + .onSuccess(new PolicyEvaluationEvent(component)) + ); return Response.status(Response.Status.CREATED).entity(component).build(); } } @@ -394,8 +398,13 @@ public Response updateComponent(Component jsonComponent) { component.setNotes(StringUtils.trimToNull(jsonComponent.getNotes())); component = qm.updateComponent(component, true); - Event.dispatch(new VulnerabilityAnalysisEvent(component)); - Event.dispatch(new RepositoryMetaEvent(List.of(component))); + Event.dispatch( + new VulnerabilityAnalysisEvent(component) + // Wait for RepositoryMetaEvent after VulnerabilityAnalysisEvent, +// as both might be needed in policy evaluation + .onSuccess(new RepositoryMetaEvent(List.of(component))) + .onSuccess(new PolicyEvaluationEvent(component)) + ); return Response.ok(component).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the component could not be found.").build(); diff --git a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index f8ed552d1d..2fc4ced508 100644 --- a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -18,6 +18,28 @@ */ package org.dependencytrack.resources.v1; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +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 org.dependencytrack.auth.Permissions; +import org.dependencytrack.event.PolicyEvaluationEvent; +import org.dependencytrack.event.RepositoryMetaEvent; +import org.dependencytrack.event.VulnerabilityAnalysisEvent; +import org.dependencytrack.integrations.FindingPackagingFormat; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Finding; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.persistence.QueryManager; import alpine.common.logging.Logger; import alpine.event.framework.Event; import alpine.server.auth.PermissionRequired; @@ -29,27 +51,6 @@ import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; import io.swagger.annotations.ResponseHeader; -import org.dependencytrack.auth.Permissions; -import org.dependencytrack.event.VulnerabilityAnalysisEvent; -import org.dependencytrack.integrations.FindingPackagingFormat; -import org.dependencytrack.model.Component; -import org.dependencytrack.model.Finding; -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; -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 java.util.Collections; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; /** * JAX-RS resources for processing findings. @@ -160,6 +161,10 @@ public Response analyzeProject( final List detachedComponents = qm.detach(qm.getAllComponents(project)); final Project detachedProject = qm.detach(Project.class, project.getId()); final VulnerabilityAnalysisEvent vae = new VulnerabilityAnalysisEvent(detachedComponents).project(detachedProject); + // Wait for RepositoryMetaEvent after VulnerabilityAnalysisEvent, + // as both might be needed in policy evaluation + vae.onSuccess(new RepositoryMetaEvent(detachedComponents)); + vae.onSuccess(new PolicyEvaluationEvent(detachedComponents).project(detachedProject)); Event.dispatch(vae); return Response.ok(Collections.singletonMap("token", vae.getChainIdentifier())).build(); diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java index 7e98882abb..0de1d0c9cf 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java @@ -18,15 +18,16 @@ */ package org.dependencytrack.tasks; -import alpine.common.logging.Logger; -import alpine.event.framework.Event; -import alpine.event.framework.Subscriber; -import alpine.notification.Notification; -import alpine.notification.NotificationLevel; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.Optional; import org.cyclonedx.BomParserFactory; import org.cyclonedx.parsers.Parser; import org.dependencytrack.event.BomUploadEvent; import org.dependencytrack.event.NewVulnerableDependencyAnalysisEvent; +import org.dependencytrack.event.PolicyEvaluationEvent; import org.dependencytrack.event.RepositoryMetaEvent; import org.dependencytrack.event.VulnerabilityAnalysisEvent; import org.dependencytrack.model.Bom; @@ -43,12 +44,11 @@ import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.util.CompressUtil; import org.dependencytrack.util.InternalComponentIdentificationUtil; - -import java.util.ArrayList; -import java.util.Base64; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import alpine.common.logging.Logger; +import alpine.event.framework.Event; +import alpine.event.framework.Subscriber; +import alpine.notification.Notification; +import alpine.notification.NotificationLevel; /** * Subscriber task that performs processing of bill-of-material (bom) @@ -71,12 +71,12 @@ public void inform(final Event e) { final QueryManager qm = new QueryManager(); try { final Project project = qm.getObjectByUuid(Project.class, event.getProjectUuid()); - + if (project == null) { LOGGER.warn("Ignoring BOM Upload event for no longer existing project " + event.getProjectUuid()); return; } - + final List components; final List newComponents = new ArrayList<>(); final List flattenedComponents = new ArrayList<>(); @@ -161,8 +161,11 @@ public void inform(final Event e) { // vulnerability analysis completed. vae.onSuccess(new NewVulnerableDependencyAnalysisEvent(newComponents)); } + // Wait for RepositoryMetaEvent after VulnerabilityAnalysisEvent, + // as both might be needed in policy evaluation + vae.onSuccess(new RepositoryMetaEvent(detachedFlattenedComponent)); + vae.onSuccess(new PolicyEvaluationEvent(detachedFlattenedComponent).project(detachedProject)); Event.dispatch(vae); - Event.dispatch(new RepositoryMetaEvent(detachedFlattenedComponent)); LOGGER.info("Processed " + flattenedComponents.size() + " components and " + flattenedServices.size() + " services uploaded to project " + event.getProjectUuid()); Notification.dispatch(new Notification() .scope(NotificationScope.PORTFOLIO) diff --git a/src/main/java/org/dependencytrack/tasks/PolicyEvaluationTask.java b/src/main/java/org/dependencytrack/tasks/PolicyEvaluationTask.java new file mode 100644 index 0000000000..380010f9ad --- /dev/null +++ b/src/main/java/org/dependencytrack/tasks/PolicyEvaluationTask.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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.tasks; + +import java.util.ArrayList; +import java.util.List; +import org.dependencytrack.event.PolicyEvaluationEvent; +import org.dependencytrack.event.ProjectMetricsUpdateEvent; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Project; +import org.dependencytrack.policy.PolicyEngine; +import alpine.common.logging.Logger; +import alpine.event.framework.Event; +import alpine.event.framework.Subscriber; + +public class PolicyEvaluationTask implements Subscriber { + + private static final Logger LOGGER = Logger.getLogger(PolicyEvaluationTask.class); + + /** + * {@inheritDoc} + */ + public void inform(final Event e) { + if (e instanceof PolicyEvaluationEvent) { + final PolicyEvaluationEvent event = (PolicyEvaluationEvent) e; + LOGGER.info("Starting policy evaluation"); + if (event.getComponents() != null && !event.getComponents().isEmpty()) { + performPolicyEvaluation(event.getProject(), event.getComponents()); + } else if (event.getProject() != null) { + performPolicyEvaluation(event.getProject(), new ArrayList<>()); + } + LOGGER.info("Policy evaluation complete"); + } + } + + private void performPolicyEvaluation(Project project, List components) { + // Evaluate the components against applicable policies via the PolicyEngine. + final PolicyEngine pe = new PolicyEngine(); + pe.evaluate(components); + if (project != null) { + Event.dispatch(new ProjectMetricsUpdateEvent(project.getUuid())); + } + } + +} diff --git a/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java index 9afadbe8c6..2a68b89a8b 100644 --- a/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java @@ -64,7 +64,7 @@ public class VulnerabilityAnalysisTask implements Subscriber { public void inform(final Event e) { if (e instanceof VulnerabilityAnalysisEvent) { final VulnerabilityAnalysisEvent event = (VulnerabilityAnalysisEvent) e; - + LOGGER.info("Analyzing vulnerabilities"); if (event.getComponents() != null && event.getComponents().size() > 0) { final List components = new ArrayList<>(); try (final QueryManager qm = new QueryManager()) { @@ -76,10 +76,8 @@ public void inform(final Event e) { } analyzeComponents(qm, components, e); } - performPolicyEvaluation(event.getProject(), components); - } else if (event.getProject() != null) { - performPolicyEvaluation(event.getProject(), new ArrayList<>()); } + LOGGER.info("Vulnerability analysis complete"); } else if (e instanceof PortfolioVulnerabilityAnalysisEvent) { final PortfolioVulnerabilityAnalysisEvent event = (PortfolioVulnerabilityAnalysisEvent) e; LOGGER.info("Analyzing portfolio"); diff --git a/src/test/java/org/dependencytrack/event/PolicyEvaluationEventTest.java b/src/test/java/org/dependencytrack/event/PolicyEvaluationEventTest.java new file mode 100644 index 0000000000..1fbcc374b6 --- /dev/null +++ b/src/test/java/org/dependencytrack/event/PolicyEvaluationEventTest.java @@ -0,0 +1,60 @@ +/* + * 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.event; + +import java.util.ArrayList; +import java.util.List; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Project; +import org.junit.Assert; +import org.junit.Test; + +public class PolicyEvaluationEventTest { + + @Test + public void testDefaultConstructor() { + PolicyEvaluationEvent event = new PolicyEvaluationEvent(); + Assert.assertNull(event.getProject()); + Assert.assertEquals(0, event.getComponents().size()); + } + + @Test + public void testComponentConstructor() { + Component component = new Component(); + PolicyEvaluationEvent event = new PolicyEvaluationEvent(component); + Assert.assertEquals(1, event.getComponents().size()); + } + + @Test + public void testComponentsConstructor() { + Component component = new Component(); + List components = new ArrayList<>(); + components.add(component); + PolicyEvaluationEvent event = new PolicyEvaluationEvent(components); + Assert.assertEquals(1, event.getComponents().size()); + } + + @Test + public void testProjectCriteria() { + Project project = new Project(); + PolicyEvaluationEvent event = new PolicyEvaluationEvent().project(project); + Assert.assertEquals(project, event.getProject()); + Assert.assertEquals(0, event.getComponents().size()); + } +}