Skip to content

Commit

Permalink
Implement component property ingest for legacy BOM processing task
Browse files Browse the repository at this point in the history
Signed-off-by: nscuro <[email protected]>
  • Loading branch information
nscuro committed Apr 14, 2024
1 parent 523b5f0 commit 11020d1
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,13 @@ else if (StringUtils.isNotBlank(cycloneLicense.getName()))
component.setExternalReferences(null);
}

final List<ComponentProperty> 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<Component> components = new ArrayList<>();
for (int i = 0; i < cycloneDxComponent.getComponents().size(); i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -886,4 +891,55 @@ public long deleteComponentPropertyByUuid(final Component component, final UUID
}
}

public void synchronizeComponentProperties(final Component component, final List<ComponentProperty> 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<ComponentProperty.Identity>();
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);
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ComponentProperty> properties) {
getComponentQueryManager().synchronizeComponentProperties(component, properties);
}

public PaginatedResult getLicenses() {
return getLicenseQueryManager().getLicenses();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -442,12 +439,10 @@ private Map<ComponentIdentity, Component> 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);
Expand All @@ -469,58 +464,6 @@ private Map<ComponentIdentity, Component> processComponents(final QueryManager q
return persistentComponents;
}

private void processComponentProperties(final QueryManager qm, final Component component, final List<ComponentProperty> 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<ComponentProperty.Identity>();
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<ComponentIdentity, ServiceComponent> processServices(final QueryManager qm,
final Project project,
final List<ServiceComponent> services,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,31 +241,29 @@ public void informTest() throws Exception {
assertThat(component.getPurl().canonicalize()).isEqualTo("pkg:maven/com.example/[email protected]?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(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 11020d1

Please sign in to comment.