Skip to content

Commit

Permalink
Ingest component properties from BOM
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 1fbd88f commit 0cd4332
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.dependencytrack.parser.cyclonedx.util;

import alpine.common.logging.Logger;
import alpine.model.IConfigProperty.PropertyType;
import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import org.apache.commons.collections4.CollectionUtils;
Expand All @@ -38,6 +39,7 @@
import org.dependencytrack.model.Classifier;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.ComponentIdentity;
import org.dependencytrack.model.ComponentProperty;
import org.dependencytrack.model.Cwe;
import org.dependencytrack.model.DataClassification;
import org.dependencytrack.model.ExternalReference;
Expand Down Expand Up @@ -168,6 +170,7 @@ public static Component convertComponent(final org.cyclonedx.model.Component cdx
component.setCopyright(trimToNull(cdxComponent.getCopyright()));
component.setCpe(trimToNull(cdxComponent.getCpe()));
component.setExternalReferences(convertExternalReferences(cdxComponent.getExternalReferences()));
component.setProperties(convertToComponentProperties(cdxComponent.getProperties()));

if (cdxComponent.getPurl() != null) {
try {
Expand Down Expand Up @@ -255,6 +258,48 @@ public static Component convertComponent(final org.cyclonedx.model.Component cdx
return component;
}

private static List<ComponentProperty> convertToComponentProperties(final List<org.cyclonedx.model.Property> cdxProperties) {
if (cdxProperties == null || cdxProperties.isEmpty()) {
return Collections.emptyList();
}

return cdxProperties.stream()
.map(ModelConverter::convertToComponentProperty)
.filter(Objects::nonNull)
.toList();
}

private static ComponentProperty convertToComponentProperty(final org.cyclonedx.model.Property cdxProperty) {
if (cdxProperty == null) {
return null;
}

final var property = new ComponentProperty();
property.setPropertyValue(trimToNull(cdxProperty.getValue()));
property.setPropertyType(PropertyType.STRING);

final String cdxPropertyName = trimToNull(cdxProperty.getName());
if (cdxPropertyName == null) {
// TODO: What to do here?
// * Generate groupName and propertyName?
// * Log a warning and ignore?
return null;
}

// Treat property names according to the CycloneDX namespace syntax:
// https://cyclonedx.github.io/cyclonedx-property-taxonomy/
final int lastSeparatorIndex = cdxPropertyName.lastIndexOf(':');
if (lastSeparatorIndex < 0) {
property.setGroupName("internal");
property.setPropertyName(cdxPropertyName);
} else {
property.setGroupName(cdxPropertyName.substring(0, lastSeparatorIndex));
property.setPropertyName(cdxPropertyName.substring(lastSeparatorIndex + 1));
}

return property;
}

public static List<ServiceComponent> convertServices(final List<org.cyclonedx.model.Service> cdxServices) {
if (cdxServices == null || cdxServices.isEmpty()) {
return Collections.emptyList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
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 @@ -74,6 +75,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -436,12 +438,15 @@ private Map<ComponentIdentity, Component> processComponents(final QueryManager q
applyIfChanged(persistentComponent, component, Component::getResolvedLicense, persistentComponent::setResolvedLicense);
applyIfChanged(persistentComponent, component, Component::getLicense, persistentComponent::setLicense);
applyIfChanged(persistentComponent, component, Component::getLicenseUrl, persistentComponent::setLicenseUrl);
applyIfChanged(persistentComponent, component, Component::getLicenseExpression, persistentComponent::setLicenseExpression);
applyIfChanged(persistentComponent, component, Component::isInternal, persistentComponent::setInternal);
applyIfChanged(persistentComponent, component, Component::getExternalReferences, persistentComponent::setExternalReferences);

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 @@ -463,6 +468,39 @@ private Map<ComponentIdentity, Component> processComponents(final QueryManager q
return persistentComponents;
}

private void processComponentProperties(final QueryManager qm, final Component component, final List<ComponentProperty> properties) {
if (properties == null || properties.isEmpty()) {
// TODO: Should we delete pre-existing properties that no longer exist in the BOM?
return;
}

if (component.getProperties() == null || component.getProperties().isEmpty()) {
for (final ComponentProperty property : properties) {
property.setComponent(component);
qm.getPersistenceManager().makePersistent(property);
}

return;
}

for (final ComponentProperty property : component.getProperties()) {
final Optional<ComponentProperty> optionalPersistentProperty = component.getProperties().stream()
.filter(persistentProperty -> Objects.equals(persistentProperty.getGroupName(), property.getGroupName()))
.filter(persistentProperty -> Objects.equals(persistentProperty.getPropertyName(), property.getPropertyName()))
.findFirst();
if (optionalPersistentProperty.isEmpty()) {
property.setComponent(component);
qm.getPersistenceManager().makePersistent(property);
continue;
}

final ComponentProperty persistentProperty = optionalPersistentProperty.get();
applyIfChanged(persistentProperty, property, ComponentProperty::getPropertyValue, persistentProperty::setPropertyValue);
applyIfChanged(persistentProperty, property, ComponentProperty::getPropertyType, persistentProperty::setPropertyType);
applyIfChanged(persistentProperty, property, ComponentProperty::getDescription, persistentProperty::setDescription);
}
}

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 @@ -20,6 +20,7 @@

import alpine.event.framework.Event;
import alpine.event.framework.EventService;
import alpine.model.IConfigProperty.PropertyType;
import alpine.notification.Notification;
import alpine.notification.NotificationLevel;
import alpine.notification.NotificationService;
Expand Down Expand Up @@ -177,7 +178,7 @@ public void informTest() throws Exception {
final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()),
resourceToByteArray("/unit/bom-1.xml"));
bomUploadProcessingTaskSupplier.get().inform(bomUploadEvent);
assertConditionWithTimeout(() -> NOTIFICATIONS.size() >= 6, Duration.ofSeconds(5));
awaitBomProcessedNotification();

qm.getPersistenceManager().refresh(project);
assertThat(project.getClassifier()).isEqualTo(Classifier.APPLICATION);
Expand Down Expand Up @@ -237,6 +238,22 @@ public void informTest() throws Exception {
assertThat(component.getCpe()).isEqualTo("cpe:/a:example:xmlutil:1.0.0");
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");
assertThat(component.getProperties()).satisfiesExactly(
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()).isEqualTo("internal");
assertThat(property.getPropertyName()).isEqualTo("foo");
assertThat(property.getPropertyValue()).isEqualTo("bar");
assertThat(property.getPropertyType()).isEqualTo(PropertyType.STRING);
assertThat(property.getDescription()).isNull();
}
);

assertThat(qm.getAllVulnerabilities(component)).hasSize(2);
assertThat(NOTIFICATIONS).satisfiesExactly(
Expand Down
5 changes: 5 additions & 0 deletions src/test/resources/unit/bom-1.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@
<cpe>cpe:/a:example:xmlutil:1.0.0</cpe>
<purl>pkg:maven/com.example/[email protected]?packaging=jar</purl>
<modified>false</modified>
<properties>
<property name="">foo</property>
<property name="foo">bar</property>
<property name="foo:bar">baz</property>
</properties>
</component>
</components>
</bom>

0 comments on commit 0cd4332

Please sign in to comment.