From f8236dd392fbbf5f70dd9f522ad439783799011c Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 7 Nov 2023 15:30:54 +0100 Subject: [PATCH 01/16] Add `manufacturer` and `supplier` to `Bom` model This maps to `metadata.manufacturer` and `metadata.supplier` in CycloneDX. Signed-off-by: nscuro --- .../java/org/dependencytrack/model/Bom.java | 24 +++++++++++++++++++ .../persistence/BomQueryManager.java | 6 ++++- .../persistence/QueryManager.java | 6 +++-- .../dependencytrack/model/ProjectTest.java | 2 +- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/Bom.java b/src/main/java/org/dependencytrack/model/Bom.java index 5dba408764..5d2e90c668 100644 --- a/src/main/java/org/dependencytrack/model/Bom.java +++ b/src/main/java/org/dependencytrack/model/Bom.java @@ -92,6 +92,14 @@ public String getFormatLongName() { private String serialNumber; @Persistent(defaultFetchGroup = "true") + @Column(name = "SUPPLIER", allowsNull = "true") + private OrganizationalEntity supplier; + + @Persistent(defaultFetchGroup = "true") + @Column(name = "MANUFACTURER", allowsNull = "true") + private OrganizationalEntity manufacturer; + + @Persistent @Column(name = "PROJECT_ID", allowsNull = "false") @NotNull private Project project; @@ -150,6 +158,22 @@ public void setSerialNumber(String serialNumber) { this.serialNumber = serialNumber; } + public OrganizationalEntity getSupplier() { + return supplier; + } + + public void setSupplier(final OrganizationalEntity supplier) { + this.supplier = supplier; + } + + public OrganizationalEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(final OrganizationalEntity manufacturer) { + this.manufacturer = manufacturer; + } + public Project getProject() { return project; } diff --git a/src/main/java/org/dependencytrack/persistence/BomQueryManager.java b/src/main/java/org/dependencytrack/persistence/BomQueryManager.java index 7bce2f1d94..1d26d9c6d2 100644 --- a/src/main/java/org/dependencytrack/persistence/BomQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/BomQueryManager.java @@ -20,6 +20,7 @@ import alpine.resources.AlpineRequest; import org.dependencytrack.model.Bom; +import org.dependencytrack.model.OrganizationalEntity; import org.dependencytrack.model.Project; import javax.jdo.PersistenceManager; @@ -52,7 +53,8 @@ final class BomQueryManager extends QueryManager implements IQueryManager { * @param imported the Date when the bom was imported * @return a new Bom object */ - public Bom createBom(Project project, Date imported, Bom.Format format, String specVersion, Integer bomVersion, String serialNumber) { + public Bom createBom(Project project, Date imported, Bom.Format format, String specVersion, Integer bomVersion, + String serialNumber, OrganizationalEntity manufacturer, OrganizationalEntity supplier) { final Bom bom = new Bom(); bom.setImported(imported); bom.setProject(project); @@ -60,6 +62,8 @@ public Bom createBom(Project project, Date imported, Bom.Format format, String s bom.setSpecVersion(specVersion); bom.setBomVersion(bomVersion); bom.setSerialNumber(serialNumber); + bom.setManufacturer(manufacturer); + bom.setSupplier(supplier); return persist(bom); } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index e44d74ff4a..6cd812abfa 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -53,6 +53,7 @@ import org.dependencytrack.model.LicenseGroup; import org.dependencytrack.model.NotificationPublisher; import org.dependencytrack.model.NotificationRule; +import org.dependencytrack.model.OrganizationalEntity; import org.dependencytrack.model.Policy; import org.dependencytrack.model.PolicyCondition; import org.dependencytrack.model.PolicyViolation; @@ -472,8 +473,9 @@ public List getProjectProperties(final Project project) { return getProjectQueryManager().getProjectProperties(project); } - public Bom createBom(Project project, Date imported, Bom.Format format, String specVersion, Integer bomVersion, String serialNumber) { - return getBomQueryManager().createBom(project, imported, format, specVersion, bomVersion, serialNumber); + public Bom createBom(Project project, Date imported, Bom.Format format, String specVersion, Integer bomVersion, + String serialNumber, OrganizationalEntity manufacturer, OrganizationalEntity supplier) { + return getBomQueryManager().createBom(project, imported, format, specVersion, bomVersion, serialNumber, manufacturer, supplier); } public List getAllBoms(Project project) { diff --git a/src/test/java/org/dependencytrack/model/ProjectTest.java b/src/test/java/org/dependencytrack/model/ProjectTest.java index 52e232c22f..0ac64da2bb 100644 --- a/src/test/java/org/dependencytrack/model/ProjectTest.java +++ b/src/test/java/org/dependencytrack/model/ProjectTest.java @@ -31,7 +31,7 @@ public class ProjectTest extends PersistenceCapableTest { public void testProjectPersistence() { Project p1 = qm.createProject("Example Project 1", "Description 1", "1.0", null, null, null, true, false); Project p2 = qm.createProject("Example Project 2", "Description 2", "1.1", null, null, null, true, false); - Bom bom = qm.createBom(p1, new Date(), Bom.Format.CYCLONEDX, "1.1", 1, UUID.randomUUID().toString()); + Bom bom = qm.createBom(p1, new Date(), Bom.Format.CYCLONEDX, "1.1", 1, UUID.randomUUID().toString(), null, null); Assert.assertEquals("Example Project 1", p1.getName()); Assert.assertEquals("Example Project 2", p2.getName()); From f266d3726867d8b8f62fd835c6deb505059c179c Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 7 Nov 2023 15:33:12 +0100 Subject: [PATCH 02/16] Remove `manufacturer` from `Component` and `Project` models Components do not have manufacturers in CycloneDX, but they do have suppliers: https://cyclonedx.org/docs/1.5/json/#components Additionally, add `supplier` to the default fetch group, as otherwise it would be lazy-loaded. Signed-off-by: nscuro --- .../org/dependencytrack/model/Component.java | 26 ++++--------------- .../org/dependencytrack/model/Project.java | 20 ++------------ 2 files changed, 7 insertions(+), 39 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/Component.java b/src/main/java/org/dependencytrack/model/Component.java index 90379169a3..c3e23af7ac 100644 --- a/src/main/java/org/dependencytrack/model/Component.java +++ b/src/main/java/org/dependencytrack/model/Component.java @@ -30,6 +30,7 @@ import org.apache.commons.lang3.StringUtils; import org.dependencytrack.model.validation.ValidSpdxExpression; import org.dependencytrack.resources.v1.serializers.CustomPackageURLSerializer; + import javax.jdo.annotations.Column; import javax.jdo.annotations.Element; import javax.jdo.annotations.Extension; @@ -53,8 +54,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.UUID; import java.util.Set; +import java.util.UUID; /** * Model class for tracking individual components. @@ -115,18 +116,9 @@ public enum FetchGroup { @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The publisher may only contain printable characters") private String publisher; - @Persistent /**Issue #2373, #2737 */ - @Column(name = "MANUFACTURE", allowsNull = "true") - @Serialized - @Size(max = 255) - @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The manufacture may only contain printable characters") - private OrganizationalEntity manufacture; - - @Persistent /**Issue #2373, #2737 */ + @Persistent(defaultFetchGroup = "true") @Column(name = "SUPPLIER", allowsNull = "true") @Serialized - @Size(max = 255) - @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The supplier may only contain printable characters") private OrganizationalEntity supplier; @Persistent @@ -398,22 +390,14 @@ public void setPublisher(String publisher) { this.publisher = publisher; } - public OrganizationalEntity getSupplier() { /**Issue #2373, #2737 */ + public OrganizationalEntity getSupplier() { return supplier; } - public void setSupplier(OrganizationalEntity supplier) {/**Issue #2373, #2737 */ + public void setSupplier(OrganizationalEntity supplier) { this.supplier = supplier; } - public OrganizationalEntity getManufacturer() { /**Issue #2373, #2737 */ - return manufacture; - } - - public void setManufacturer(OrganizationalEntity manufacture) {/**Issue #2373, #2737 */ - this.manufacture = manufacture; - } - public String getGroup() { return group; } diff --git a/src/main/java/org/dependencytrack/model/Project.java b/src/main/java/org/dependencytrack/model/Project.java index 286c1fcf95..36c57df601 100644 --- a/src/main/java/org/dependencytrack/model/Project.java +++ b/src/main/java/org/dependencytrack/model/Project.java @@ -32,6 +32,7 @@ import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; import org.dependencytrack.resources.v1.serializers.CustomPackageURLSerializer; + import javax.jdo.annotations.Column; import javax.jdo.annotations.Element; import javax.jdo.annotations.Extension; @@ -130,18 +131,10 @@ public enum FetchGroup { @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The publisher may only contain printable characters") private String publisher; - @Persistent /**Issue #2373, #2737 */ + @Persistent(defaultFetchGroup = "true") @Column(name = "SUPPLIER", allowsNull = "true") - @Size(max = 255) - @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The supplier may only contain printable characters") private OrganizationalEntity supplier; - @Persistent /**Issue #2373, #2737 */ - @Column(name = "MANUFACTURE", allowsNull = "true") - @Size(max = 255) - @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The manufacturer may only contain printable characters") - private OrganizationalEntity manufacture; - @Persistent @Column(name = "GROUP", jdbcType = "VARCHAR") @Index(name = "PROJECT_GROUP_IDX") @@ -306,15 +299,6 @@ public void setSupplier(OrganizationalEntity supplier) { this.supplier = supplier; } - public OrganizationalEntity getManufacturer() { /**Issue #2373, #2737 */ - return manufacture; - } - - public void setManufacturer(OrganizationalEntity manufacture) {/**Issue #2373, #2737 */ - this.manufacture = manufacture; - } - - public String getGroup() { return group; } From 0ea040f18e7ba76466c3b44022d5a39e0e352ac7 Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 7 Nov 2023 15:33:56 +0100 Subject: [PATCH 03/16] Reduce duplication in mapping of `OrganizationalEntity` Signed-off-by: nscuro --- .../parser/cyclonedx/util/ModelConverter.java | 173 +++++++----------- .../tasks/BomUploadProcessingTask.java | 51 ++---- .../tasks/BomUploadProcessingTaskTest.java | 23 ++- 3 files changed, 103 insertions(+), 144 deletions(-) diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java index 7958832102..65a32757e9 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -99,7 +99,6 @@ public static List convertComponents(final QueryManager qm, final Bom } /**Convert from CycloneDX to DT */ - @SuppressWarnings("deprecation") public static Component convert(final QueryManager qm, final org.cyclonedx.model.Component cycloneDxComponent, final Project project) { Component component = qm.matchSingleIdentity(project, new ComponentIdentity(cycloneDxComponent)); if (component == null) { @@ -109,32 +108,7 @@ public static Component convert(final QueryManager qm, final org.cyclonedx.model component.setAuthor(StringUtils.trimToNull(cycloneDxComponent.getAuthor())); component.setBomRef(StringUtils.trimToNull(cycloneDxComponent.getBomRef())); component.setPublisher(StringUtils.trimToNull(cycloneDxComponent.getPublisher())); - - /**Issue #2373, #2737 */ - if (cycloneDxComponent.getSupplier() != null) { - OrganizationalEntity deptrackOrgEntity = new OrganizationalEntity(); - deptrackOrgEntity.setName(cycloneDxComponent.getSupplier().getName()); - deptrackOrgEntity.setUrls(cycloneDxComponent.getSupplier().getUrls().toArray(new String[0])); - // to do convert contacts - // deptrackOrgEntity.setContacts(cycloneDxComponent.getSupplier().getContacts()); - - if (cycloneDxComponent.getSupplier().getContacts() != null) { - List contacts = new ArrayList<>(); - for (org.cyclonedx.model.OrganizationalContact organizationalContact: cycloneDxComponent.getSupplier().getContacts()) { - OrganizationalContact contact = new OrganizationalContact(); - contact.setName(organizationalContact.getName()); - contact.setEmail(organizationalContact.getEmail()); - contact.setPhone(organizationalContact.getPhone()); - contacts.add(contact); - } - deptrackOrgEntity.setContacts(contacts); - } else { - deptrackOrgEntity.setContacts(null); - } - component.setSupplier(deptrackOrgEntity); - } /**Issue #2373, #2737 */ - - + component.setSupplier(convert(cycloneDxComponent.getSupplier())); component.setGroup(StringUtils.trimToNull(cycloneDxComponent.getGroup())); component.setName(StringUtils.trimToNull(cycloneDxComponent.getName())); component.setVersion(StringUtils.trimToNull(cycloneDxComponent.getVersion())); @@ -241,7 +215,7 @@ else if (StringUtils.isNotBlank(cycloneLicense.getName())) } } - if (cycloneDxComponent.getExternalReferences() != null && cycloneDxComponent.getExternalReferences().size() > 0) { + if (cycloneDxComponent.getExternalReferences() != null && !cycloneDxComponent.getExternalReferences().isEmpty()) { List references = new ArrayList<>(); for (org.cyclonedx.model.ExternalReference cycloneDxRef: cycloneDxComponent.getExternalReferences()) { ExternalReference ref = new ExternalReference(); @@ -269,9 +243,66 @@ else if (StringUtils.isNotBlank(cycloneLicense.getName())) } return component; } + + public static OrganizationalEntity convert(final org.cyclonedx.model.OrganizationalEntity cdxEntity) { + if (cdxEntity == null) { + return null; + } + + final var dtEntity = new OrganizationalEntity(); + dtEntity.setName(StringUtils.trimToNull(cdxEntity.getName())); + if (cdxEntity.getContacts() != null && !cdxEntity.getContacts().isEmpty()) { + dtEntity.setContacts(cdxEntity.getContacts().stream().map(ModelConverter::convert).toList()); + } + if (cdxEntity.getUrls() != null && !cdxEntity.getUrls().isEmpty()) { + dtEntity.setUrls(cdxEntity.getUrls().toArray(new String[0])); + } + + return dtEntity; + } + + private static OrganizationalContact convert(final org.cyclonedx.model.OrganizationalContact cdxContact) { + if (cdxContact == null) { + return null; + } + + final var dtContact = new OrganizationalContact(); + dtContact.setName(StringUtils.trimToNull(cdxContact.getName())); + dtContact.setEmail(StringUtils.trimToNull(cdxContact.getEmail())); + dtContact.setPhone(StringUtils.trimToNull(cdxContact.getPhone())); + return dtContact; + } + + private static org.cyclonedx.model.OrganizationalEntity convert(final OrganizationalEntity dtEntity) { + if (dtEntity == null) { + return null; + } + + final var cdxEntity = new org.cyclonedx.model.OrganizationalEntity(); + cdxEntity.setName(StringUtils.trimToNull(dtEntity.getName())); + if (dtEntity.getContacts() != null && !dtEntity.getContacts().isEmpty()) { + cdxEntity.setContacts(dtEntity.getContacts().stream().map(ModelConverter::convert).toList()); + } + if (dtEntity.getUrls() != null && dtEntity.getUrls().length > 0) { + cdxEntity.setUrls(Arrays.stream(dtEntity.getUrls()).toList()); + } + + return cdxEntity; + } + + private static org.cyclonedx.model.OrganizationalContact convert(final OrganizationalContact dtContact) { + if (dtContact == null) { + return null; + } + + final var cdxContact = new org.cyclonedx.model.OrganizationalContact(); + cdxContact.setName(StringUtils.trimToNull(dtContact.getName())); + cdxContact.setEmail(StringUtils.trimToNull(dtContact.getEmail())); + cdxContact.setPhone(StringUtils.trimToNull(cdxContact.getPhone())); + return cdxContact; + } /**Convert from DT to CycloneDX */ - @SuppressWarnings("deprecation") public static org.cyclonedx.model.Component convert(final QueryManager qm, final Component component) { final org.cyclonedx.model.Component cycloneComponent = new org.cyclonedx.model.Component(); cycloneComponent.setBomRef(component.getUuid().toString()); @@ -412,9 +443,9 @@ public static org.cyclonedx.model.Metadata createMetadata(final Project project) } else { cycloneComponent.setType(org.cyclonedx.model.Component.Type.LIBRARY); } - if (project.getExternalReferences() != null && project.getExternalReferences().size() > 0) { + if (project.getExternalReferences() != null && !project.getExternalReferences().isEmpty()) { List references = new ArrayList<>(); - project.getExternalReferences().stream().forEach(externalReference -> { + project.getExternalReferences().forEach(externalReference -> { org.cyclonedx.model.ExternalReference ref = new org.cyclonedx.model.ExternalReference(); ref.setUrl(externalReference.getUrl()); ref.setType(externalReference.getType()); @@ -423,31 +454,7 @@ public static org.cyclonedx.model.Metadata createMetadata(final Project project) }); cycloneComponent.setExternalReferences(references); } - /*Issue #2737: Adding Supplier contact functionality */ - if (project.getSupplier() != null) { - org.cyclonedx.model.OrganizationalEntity supplier = new org.cyclonedx.model.OrganizationalEntity(); - supplier.setName(project.getSupplier().getName()); - - if (project.getSupplier().getUrls() != null) { - supplier.setUrls(Arrays.asList(project.getSupplier().getUrls())); - } else { - supplier.setUrls(null); - } - if (project.getSupplier().getContacts() != null) { - List contacts = new ArrayList<>(); - for (OrganizationalContact organizationalContact: project.getSupplier().getContacts()) { - org.cyclonedx.model.OrganizationalContact contact = new org.cyclonedx.model.OrganizationalContact(); - contact.setName(organizationalContact.getName()); - contact.setEmail(organizationalContact.getEmail()); - contact.setPhone(organizationalContact.getPhone()); - contacts.add(contact); - } - supplier.setContacts(contacts); - } - cycloneComponent.setSupplier(supplier); - } else { - cycloneComponent.setSupplier(null); - } + cycloneComponent.setSupplier(convert(project.getSupplier())); metadata.setComponent(cycloneComponent); } return metadata; @@ -478,41 +485,19 @@ public static ServiceComponent convert(final QueryManager qm, final org.cycloned service.setProject(project); } service.setBomRef(StringUtils.trimToNull(cycloneDxService.getBomRef())); - if (cycloneDxService.getProvider() != null) { - OrganizationalEntity provider = new OrganizationalEntity();; - provider.setName(cycloneDxService.getProvider().getName()); - if (cycloneDxService.getProvider().getUrls() != null && cycloneDxService.getProvider().getUrls().size() > 0) { - provider.setUrls(cycloneDxService.getProvider().getUrls().toArray(new String[0])); - } else { - provider.setUrls(null); - } - if (cycloneDxService.getProvider().getContacts() != null) { - List contacts = new ArrayList<>(); - for (org.cyclonedx.model.OrganizationalContact cycloneDxContact: cycloneDxService.getProvider().getContacts()) { - OrganizationalContact contact = new OrganizationalContact(); - contact.setName(cycloneDxContact.getName()); - contact.setEmail(cycloneDxContact.getEmail()); - contact.setPhone(cycloneDxContact.getPhone()); - contacts.add(contact); - } - provider.setContacts(contacts); - } - service.setProvider(provider); - } else { - service.setProvider(null); - } + service.setProvider(convert(cycloneDxService.getProvider())); service.setGroup(StringUtils.trimToNull(cycloneDxService.getGroup())); service.setName(StringUtils.trimToNull(cycloneDxService.getName())); service.setVersion(StringUtils.trimToNull(cycloneDxService.getVersion())); service.setDescription(StringUtils.trimToNull(cycloneDxService.getDescription())); - if (cycloneDxService.getEndpoints() != null && cycloneDxService.getEndpoints().size() > 0) { + if (cycloneDxService.getEndpoints() != null && !cycloneDxService.getEndpoints().isEmpty()) { service.setEndpoints(cycloneDxService.getEndpoints().toArray(new String[0])); } else { service.setEndpoints(null); } service.setAuthenticated(cycloneDxService.getAuthenticated()); service.setCrossesTrustBoundary(cycloneDxService.getxTrustBoundary()); - if (cycloneDxService.getData() != null && cycloneDxService.getData().size() > 0) { + if (cycloneDxService.getData() != null && !cycloneDxService.getData().isEmpty()) { List dataClassifications = new ArrayList<>(); for (org.cyclonedx.model.ServiceData data: cycloneDxService.getData()) { DataClassification dc = new DataClassification(); @@ -524,7 +509,7 @@ public static ServiceComponent convert(final QueryManager qm, final org.cycloned } else { service.setData(null); } - if (cycloneDxService.getExternalReferences() != null && cycloneDxService.getExternalReferences().size() > 0) { + if (cycloneDxService.getExternalReferences() != null && !cycloneDxService.getExternalReferences().isEmpty()) { List references = new ArrayList<>(); for (org.cyclonedx.model.ExternalReference cycloneDxRef: cycloneDxService.getExternalReferences()) { ExternalReference ref = new ExternalReference(); @@ -571,25 +556,7 @@ public static ServiceComponent convert(final QueryManager qm, final org.cycloned public static org.cyclonedx.model.Service convert(final QueryManager qm, final ServiceComponent service) { final org.cyclonedx.model.Service cycloneService = new org.cyclonedx.model.Service(); cycloneService.setBomRef(service.getUuid().toString()); - if (service.getProvider() != null) { - org.cyclonedx.model.OrganizationalEntity cycloneEntity = new org.cyclonedx.model.OrganizationalEntity(); - cycloneEntity.setName(service.getProvider().getName()); - if (service.getProvider().getUrls() != null) { - cycloneEntity.setUrls(Arrays.asList(service.getProvider().getUrls())); - } - if (service.getProvider().getContacts() != null && service.getProvider().getContacts().size() > 0) { - List contacts = new ArrayList<>(); - for (OrganizationalContact contact: service.getProvider().getContacts()) { - org.cyclonedx.model.OrganizationalContact cycloneContact = new org.cyclonedx.model.OrganizationalContact(); - cycloneContact.setName(contact.getName()); - cycloneContact.setEmail(contact.getEmail()); - cycloneContact.setPhone(contact.getPhone()); - contacts.add(cycloneContact); - } - cycloneEntity.setContacts(contacts); - } - cycloneService.setProvider(cycloneEntity); - } + cycloneService.setProvider(convert(service.getProvider())); cycloneService.setGroup(StringUtils.trimToNull(service.getGroup())); cycloneService.setName(StringUtils.trimToNull(service.getName())); cycloneService.setVersion(StringUtils.trimToNull(service.getVersion())); @@ -599,13 +566,13 @@ public static org.cyclonedx.model.Service convert(final QueryManager qm, final S } cycloneService.setAuthenticated(service.getAuthenticated()); cycloneService.setxTrustBoundary(service.getCrossesTrustBoundary()); - if (service.getData() != null && service.getData().size() > 0) { + if (service.getData() != null && !service.getData().isEmpty()) { for (DataClassification dc: service.getData()) { org.cyclonedx.model.ServiceData sd = new org.cyclonedx.model.ServiceData(dc.getDirection().name(), dc.getName()); cycloneService.addServiceData(sd); } } - if (service.getExternalReferences() != null && service.getExternalReferences().size() > 0) { + if (service.getExternalReferences() != null && !service.getExternalReferences().isEmpty()) { for (ExternalReference ref : service.getExternalReferences()) { org.cyclonedx.model.ExternalReference cycloneRef = new org.cyclonedx.model.ExternalReference(); cycloneRef.setType(ref.getType()); diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java index 38ef7d7fe5..947988f691 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java @@ -35,7 +35,6 @@ import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.OrganizationalEntity; -import org.dependencytrack.model.OrganizationalContact; import org.dependencytrack.model.Project; import org.dependencytrack.model.ServiceComponent; import org.dependencytrack.notification.NotificationConstants; @@ -47,11 +46,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; /** * Subscriber task that performs processing of bill-of-material (bom) @@ -98,6 +97,8 @@ public void inform(final Event e) { final String bomSpecVersion; final Integer bomVersion; final String serialNumnber; + OrganizationalEntity bomSupplier = null; + OrganizationalEntity bomManufacturer = null; org.cyclonedx.model.Bom cycloneDxBom = null; if (BomParserFactory.looksLikeCycloneDX(bomBytes)) { if (qm.isEnabled(ConfigPropertyConstants.ACCEPT_ARTIFACT_CYCLONEDX)) { @@ -109,42 +110,26 @@ public void inform(final Event e) { bomSpecVersion = cycloneDxBom.getSpecVersion(); bomProcessingFailedBomVersion = bomSpecVersion; bomVersion = cycloneDxBom.getVersion(); + if (cycloneDxBom.getMetadata() != null) { + bomSupplier = ModelConverter.convert(cycloneDxBom.getMetadata().getSupplier()); + bomManufacturer = ModelConverter.convert(cycloneDxBom.getMetadata().getManufacture()); + if (cycloneDxBom.getMetadata().getComponent() != null) { + final org.cyclonedx.model.Component cdxMetadataComponent = cycloneDxBom.getMetadata().getComponent(); + if (cdxMetadataComponent.getType() != null && project.getClassifier() == null) { + project.setClassifier(Classifier.valueOf(cdxMetadataComponent.getType().name())); + } + if (cdxMetadataComponent.getSupplier() != null) { + project.setSupplier(ModelConverter.convert(cdxMetadataComponent.getSupplier())); + } + } + } if (project.getClassifier() == null) { - final var classifier = Optional.ofNullable(cycloneDxBom.getMetadata()) - .map(org.cyclonedx.model.Metadata::getComponent) - .map(org.cyclonedx.model.Component::getType) - .map(org.cyclonedx.model.Component.Type::name) - .map(Classifier::valueOf) - .orElse(Classifier.APPLICATION); - project.setClassifier(classifier); + project.setClassifier(Classifier.APPLICATION); } project.setExternalReferences(ModelConverter.convertBomMetadataExternalReferences(cycloneDxBom)); serialNumnber = (cycloneDxBom.getSerialNumber() != null) ? cycloneDxBom.getSerialNumber().replaceFirst("urn:uuid:", "") : null; components = ModelConverter.convertComponents(qm, cycloneDxBom, project); services = ModelConverter.convertServices(qm, cycloneDxBom, project); - /**Issue #2373, #2737 */ - if (cycloneDxBom.getMetadata() != null) { - if (cycloneDxBom.getMetadata().getManufacture() != null) { - OrganizationalEntity manufacturer = new OrganizationalEntity(); - manufacturer.setName(cycloneDxBom.getMetadata().getManufacture().getName()); - manufacturer.setUrls(cycloneDxBom.getMetadata().getManufacture().getUrls().toArray(new String[0])); - if (cycloneDxBom.getMetadata().getManufacture().getContacts() != null){ - List contacts = new ArrayList<>(); - for (org.cyclonedx.model.OrganizationalContact organizationalContact: cycloneDxBom.getMetadata().getManufacture().getContacts()) { - OrganizationalContact contact = new OrganizationalContact(); - contact.setName(organizationalContact.getName()); - contact.setEmail(organizationalContact.getEmail()); - contact.setPhone(organizationalContact.getPhone()); - contacts.add(contact); - } - manufacturer.setContacts(contacts); - } else { - manufacturer.setContacts(null); - } - project.setManufacturer(manufacturer); - } - } /**Issue #2373, #2737 */ - } else { LOGGER.warn("A CycloneDX BOM was uploaded but accepting CycloneDX BOMs is disabled. Aborting"); return; @@ -162,7 +147,7 @@ public void inform(final Event e) { .content("A " + bomFormat.getFormatShortName() + " BOM was consumed and will be processed") .subject(new BomConsumedOrProcessed(copyOfProject, Base64.getEncoder().encodeToString(bomBytes), bomFormat, bomSpecVersion))); final Date date = new Date(); - final Bom bom = qm.createBom(project, date, bomFormat, bomSpecVersion, bomVersion, serialNumnber); + final Bom bom = qm.createBom(project, date, bomFormat, bomSpecVersion, bomVersion, serialNumnber, bomSupplier, bomManufacturer); for (final Component component: components) { processComponent(qm, component, flattenedComponents, newComponents); } diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index f5d11683f0..dd57171d58 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -136,23 +136,30 @@ public void informTest() throws Exception { assertThat(project.getLastBomImport()).isNotNull(); assertThat(project.getExternalReferences()).isNotNull(); assertThat(project.getExternalReferences()).hasSize(4); + assertThat(project.getSupplier()).isNotNull(); + assertThat(project.getSupplier().getName()).isEqualTo("Foo Incorporated"); + assertThat(project.getSupplier().getUrls()).containsOnly("https://foo.bar.com"); + assertThat(project.getSupplier().getContacts()).satisfiesExactly(contact -> { + assertThat(contact.getName()).isEqualTo("Foo Jr."); + assertThat(contact.getEmail()).isEqualTo("foojr@bar.com"); + assertThat(contact.getPhone()).isEqualTo("123-456-7890"); + }); final List components = qm.getAllComponents(project); assertThat(components).hasSize(1); final Component component = components.get(0); - - assertThat(component.getSupplier().getName()).isEqualTo("Foo Incorporated"); /*Issue #2373, #2737 - Adding support for Supplier*/ + assertThat(component.getSupplier().getName()).isEqualTo("Foo Incorporated"); assertThat(component.getSupplier().getUrls()[0]).isEqualTo("https://foo.bar.com"); assertThat(component.getSupplier().getContacts().get(0).getEmail()).isEqualTo("foojr@bar.com"); assertThat(component.getSupplier().getContacts().get(0).getPhone()).isEqualTo("123-456-7890"); - assertThat(project.getManufacturer().getName()).isEqualTo("Foo Incorporated"); - assertThat(project.getManufacturer().getUrls()[0]).isEqualTo("https://foo.bar.com"); - assertThat(project.getManufacturer().getContacts().get(0).getName()).isEqualTo("Foo Sr."); - assertThat(project.getManufacturer().getContacts().get(0).getEmail()).isEqualTo("foo@bar.com"); - assertThat(project.getManufacturer().getContacts().get(0).getPhone()).isEqualTo("800-123-4567"); - + final Bom bom = qm.getAllBoms(project).get(0); + assertThat(bom.getManufacturer().getName()).isEqualTo("Foo Incorporated"); + assertThat(bom.getManufacturer().getUrls()[0]).isEqualTo("https://foo.bar.com"); + assertThat(bom.getManufacturer().getContacts().get(0).getName()).isEqualTo("Foo Sr."); + assertThat(bom.getManufacturer().getContacts().get(0).getEmail()).isEqualTo("foo@bar.com"); + assertThat(bom.getManufacturer().getContacts().get(0).getPhone()).isEqualTo("800-123-4567"); assertThat(component.getAuthor()).isEqualTo("Sometimes this field is long because it is composed of a list of authors......................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................"); assertThat(component.getPublisher()).isEqualTo("Example Incorporated"); From 910f0cc59952e730852c14ea6c4f5f088b5eeb67 Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 7 Nov 2023 15:39:26 +0100 Subject: [PATCH 04/16] Add support for new component types introduced in CDX v1.5 Signed-off-by: nscuro --- .../java/org/dependencytrack/model/Classifier.java | 6 +++++- .../tasks/BomUploadProcessingTask.java | 11 ++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/Classifier.java b/src/main/java/org/dependencytrack/model/Classifier.java index 3c2085274f..321a96eb6d 100644 --- a/src/main/java/org/dependencytrack/model/Classifier.java +++ b/src/main/java/org/dependencytrack/model/Classifier.java @@ -32,5 +32,9 @@ public enum Classifier { OPERATING_SYSTEM, DEVICE, FIRMWARE, - FILE + FILE, + PLATFORM, + DEVICE_DRIVER, + MACHINE_LEARNING_MODEL, + DATA } diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java index 947988f691..ecbedd64e8 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java @@ -48,9 +48,11 @@ import org.dependencytrack.util.InternalComponentIdentificationUtil; import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; import java.util.Date; import java.util.List; +import java.util.stream.Collectors; /** * Subscriber task that performs processing of bill-of-material (bom) @@ -116,7 +118,14 @@ public void inform(final Event e) { if (cycloneDxBom.getMetadata().getComponent() != null) { final org.cyclonedx.model.Component cdxMetadataComponent = cycloneDxBom.getMetadata().getComponent(); if (cdxMetadataComponent.getType() != null && project.getClassifier() == null) { - project.setClassifier(Classifier.valueOf(cdxMetadataComponent.getType().name())); + try { + project.setClassifier(Classifier.valueOf(cdxMetadataComponent.getType().name())); + } catch (IllegalArgumentException ex) { + LOGGER.warn(""" + The metadata.component element of the BOM is of unknown type %s. \ + Known types are %s.""".formatted(cdxMetadataComponent.getType(), + Arrays.stream(Classifier.values()).map(Enum::name).collect(Collectors.joining(", ")))); + } } if (cdxMetadataComponent.getSupplier() != null) { project.setSupplier(ModelConverter.convert(cdxMetadataComponent.getSupplier())); From e6189b3069ed86cfc01a1ef4d79d6696802cd296 Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 7 Nov 2023 15:52:55 +0100 Subject: [PATCH 05/16] Ensure suppliers are present when exporting BOMs Signed-off-by: nscuro --- .../parser/cyclonedx/util/ModelConverter.java | 1 + .../resources/v1/BomResourceTest.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java index 65a32757e9..c4fb4bfa8c 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -313,6 +313,7 @@ public static org.cyclonedx.model.Component convert(final QueryManager qm, final cycloneComponent.setCopyright(StringUtils.trimToNull(component.getCopyright())); cycloneComponent.setCpe(StringUtils.trimToNull(component.getCpe())); cycloneComponent.setAuthor(StringUtils.trimToNull(component.getAuthor())); + cycloneComponent.setSupplier(convert(component.getSupplier())); if (component.getSwidTagId() != null) { final Swid swid = new Swid(); diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index 3cfc7cddbe..40f6987090 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -29,6 +29,7 @@ import org.dependencytrack.model.AnalysisState; import org.dependencytrack.model.Classifier; import org.dependencytrack.model.Component; +import org.dependencytrack.model.OrganizationalEntity; import org.dependencytrack.model.Project; import org.dependencytrack.model.Severity; import org.dependencytrack.model.Vulnerability; @@ -103,15 +104,23 @@ public void exportProjectAsCycloneDxInventoryTest() { vulnerability.setSeverity(Severity.HIGH); vulnerability = qm.createVulnerability(vulnerability, false); + final var projectSupplier = new OrganizationalEntity(); + projectSupplier.setName("projectSupplier"); + var project = new Project(); project.setName("acme-app"); project.setClassifier(Classifier.APPLICATION); + project.setSupplier(projectSupplier); project = qm.createProject(project, null, false); + final var componentSupplier = new OrganizationalEntity(); + componentSupplier.setName("componentSupplier"); + var componentWithoutVuln = new Component(); componentWithoutVuln.setProject(project); componentWithoutVuln.setName("acme-lib-a"); componentWithoutVuln.setVersion("1.0.0"); + componentWithoutVuln.setSupplier(componentSupplier); componentWithoutVuln.setDirectDependencies("[]"); componentWithoutVuln = qm.createComponent(componentWithoutVuln, false); @@ -177,6 +186,9 @@ public void exportProjectAsCycloneDxInventoryTest() { "component": { "type": "application", "bom-ref": "${json-unit.matches:projectUuid}", + "supplier": { + "name": "projectSupplier" + }, "name": "acme-app", "version": "SNAPSHOT" }, @@ -192,6 +204,9 @@ public void exportProjectAsCycloneDxInventoryTest() { { "type": "library", "bom-ref": "${json-unit.matches:componentWithoutVulnUuid}", + "supplier": { + "name": "componentSupplier" + }, "name": "acme-lib-a", "version": "1.0.0" }, From 4ad675671fb55bad8f2efd81fb440a187160119b Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 8 Nov 2023 00:03:04 +0100 Subject: [PATCH 06/16] Introduce `ProjectMetadata` to store `authors`, `manufacturer`, and `supplier` Use JSON for serialization instead of DataNucleus' `@Serialized`, as this makes it possible for non-DataNucleus and non-Java applications to access and query the data. Most RDBMSes also support JSON queries, which can come in handy here for analytics queries. Signed-off-by: nscuro --- .../java/org/dependencytrack/model/Bom.java | 24 ---- .../org/dependencytrack/model/Component.java | 6 +- .../org/dependencytrack/model/Project.java | 20 +++- .../model/ProjectMetadata.java | 109 ++++++++++++++++++ .../parser/cyclonedx/util/ModelConverter.java | 22 ++++ .../persistence/BomQueryManager.java | 6 +- .../persistence/QueryManager.java | 6 +- .../converter/AbstractJsonConverter.java | 66 +++++++++++ .../OrganizationalContactsJsonConverter.java | 47 ++++++++ .../OrganizationalEntityJsonConverter.java | 45 ++++++++ .../tasks/BomUploadProcessingTask.java | 29 +++-- src/main/resources/META-INF/persistence.xml | 1 + .../dependencytrack/model/ProjectTest.java | 2 +- .../resources/v1/BomResourceTest.java | 29 ++++- .../tasks/BomUploadProcessingTaskTest.java | 33 ++++-- src/test/resources/bom-1.xml | 7 ++ 16 files changed, 399 insertions(+), 53 deletions(-) create mode 100644 src/main/java/org/dependencytrack/model/ProjectMetadata.java create mode 100644 src/main/java/org/dependencytrack/persistence/converter/AbstractJsonConverter.java create mode 100644 src/main/java/org/dependencytrack/persistence/converter/OrganizationalContactsJsonConverter.java create mode 100644 src/main/java/org/dependencytrack/persistence/converter/OrganizationalEntityJsonConverter.java diff --git a/src/main/java/org/dependencytrack/model/Bom.java b/src/main/java/org/dependencytrack/model/Bom.java index 5d2e90c668..bf33e77f6a 100644 --- a/src/main/java/org/dependencytrack/model/Bom.java +++ b/src/main/java/org/dependencytrack/model/Bom.java @@ -91,14 +91,6 @@ public String getFormatLongName() { @Column(name = "SERIAL_NUMBER") private String serialNumber; - @Persistent(defaultFetchGroup = "true") - @Column(name = "SUPPLIER", allowsNull = "true") - private OrganizationalEntity supplier; - - @Persistent(defaultFetchGroup = "true") - @Column(name = "MANUFACTURER", allowsNull = "true") - private OrganizationalEntity manufacturer; - @Persistent @Column(name = "PROJECT_ID", allowsNull = "false") @NotNull @@ -158,22 +150,6 @@ public void setSerialNumber(String serialNumber) { this.serialNumber = serialNumber; } - public OrganizationalEntity getSupplier() { - return supplier; - } - - public void setSupplier(final OrganizationalEntity supplier) { - this.supplier = supplier; - } - - public OrganizationalEntity getManufacturer() { - return manufacturer; - } - - public void setManufacturer(final OrganizationalEntity manufacturer) { - this.manufacturer = manufacturer; - } - public Project getProject() { return project; } diff --git a/src/main/java/org/dependencytrack/model/Component.java b/src/main/java/org/dependencytrack/model/Component.java index c3e23af7ac..4cc6aa1483 100644 --- a/src/main/java/org/dependencytrack/model/Component.java +++ b/src/main/java/org/dependencytrack/model/Component.java @@ -29,9 +29,11 @@ import com.github.packageurl.PackageURL; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.model.validation.ValidSpdxExpression; +import org.dependencytrack.persistence.converter.OrganizationalEntityJsonConverter; import org.dependencytrack.resources.v1.serializers.CustomPackageURLSerializer; import javax.jdo.annotations.Column; +import javax.jdo.annotations.Convert; import javax.jdo.annotations.Element; import javax.jdo.annotations.Extension; import javax.jdo.annotations.FetchGroup; @@ -117,8 +119,8 @@ public enum FetchGroup { private String publisher; @Persistent(defaultFetchGroup = "true") - @Column(name = "SUPPLIER", allowsNull = "true") - @Serialized + @Convert(OrganizationalEntityJsonConverter.class) + @Column(name = "SUPPLIER", jdbcType = "CLOB", allowsNull = "true") private OrganizationalEntity supplier; @Persistent diff --git a/src/main/java/org/dependencytrack/model/Project.java b/src/main/java/org/dependencytrack/model/Project.java index 36c57df601..b06d005169 100644 --- a/src/main/java/org/dependencytrack/model/Project.java +++ b/src/main/java/org/dependencytrack/model/Project.java @@ -31,9 +31,11 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; +import org.dependencytrack.persistence.converter.OrganizationalEntityJsonConverter; import org.dependencytrack.resources.v1.serializers.CustomPackageURLSerializer; import javax.jdo.annotations.Column; +import javax.jdo.annotations.Convert; import javax.jdo.annotations.Element; import javax.jdo.annotations.Extension; import javax.jdo.annotations.FetchGroup; @@ -89,6 +91,9 @@ @Persistent(name = "tags"), @Persistent(name = "accessTeams") }), + @FetchGroup(name = "METADATA", members = { + @Persistent(name = "metadata") + }), @FetchGroup(name = "METRICS_UPDATE", members = { @Persistent(name = "id"), @Persistent(name = "lastInheritedRiskScore"), @@ -108,6 +113,7 @@ public class Project implements Serializable { */ public enum FetchGroup { ALL, + METADATA, METRICS_UPDATE, PARENT } @@ -132,7 +138,8 @@ public enum FetchGroup { private String publisher; @Persistent(defaultFetchGroup = "true") - @Column(name = "SUPPLIER", allowsNull = "true") + @Convert(OrganizationalEntityJsonConverter.class) + @Column(name = "SUPPLIER", jdbcType = "CLOB", allowsNull = "true") private OrganizationalEntity supplier; @Persistent @@ -263,6 +270,9 @@ public enum FetchGroup { @Serialized private List externalReferences; + @Persistent(mappedBy = "project") + private ProjectMetadata metadata; + private transient ProjectMetrics metrics; private transient List versions; private transient List dependencyGraph; @@ -495,6 +505,14 @@ public void addAccessTeam(Team accessTeam) { this.accessTeams.add(accessTeam); } + public ProjectMetadata getMetadata() { + return metadata; + } + + public void setMetadata(final ProjectMetadata metadata) { + this.metadata = metadata; + } + @JsonIgnore public List getDependencyGraph() { return dependencyGraph; diff --git a/src/main/java/org/dependencytrack/model/ProjectMetadata.java b/src/main/java/org/dependencytrack/model/ProjectMetadata.java new file mode 100644 index 0000000000..e54f39691a --- /dev/null +++ b/src/main/java/org/dependencytrack/model/ProjectMetadata.java @@ -0,0 +1,109 @@ +/* + * 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.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import org.dependencytrack.persistence.converter.OrganizationalContactsJsonConverter; +import org.dependencytrack.persistence.converter.OrganizationalEntityJsonConverter; + +import javax.jdo.annotations.Column; +import javax.jdo.annotations.Convert; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; +import javax.jdo.annotations.Unique; +import java.util.List; + +/** + * @since 4.10.0 + */ +@PersistenceCapable(table = "PROJECT_METADATA") +@JsonInclude(Include.NON_NULL) +public class ProjectMetadata { + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + @Persistent + @Unique(name = "PROJECT_METADATA_PROJECT_ID_IDX") + @Column(name = "PROJECT_ID", allowsNull = "false") + @JsonIgnore + private Project project; + + @Persistent + @Convert(OrganizationalEntityJsonConverter.class) + @Column(name = "MANUFACTURER", jdbcType = "CLOB", allowsNull = "true") + private OrganizationalEntity manufacturer; + + @Persistent + @Convert(OrganizationalEntityJsonConverter.class) + @Column(name = "SUPPLIER", jdbcType = "CLOB", allowsNull = "true") + private OrganizationalEntity supplier; + + @Persistent + @Convert(OrganizationalContactsJsonConverter.class) + @Column(name = "AUTHORS", jdbcType = "CLOB", allowsNull = "true") + private List authors; + + public long getId() { + return id; + } + + public void setId(final long id) { + this.id = id; + } + + public Project getProject() { + return project; + } + + public void setProject(final Project project) { + this.project = project; + } + + public OrganizationalEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(final OrganizationalEntity manufacturer) { + this.manufacturer = manufacturer; + } + + public OrganizationalEntity getSupplier() { + return supplier; + } + + public void setSupplier(final OrganizationalEntity supplier) { + this.supplier = supplier; + } + + public List getAuthors() { + return authors; + } + + public void setAuthors(final List authors) { + this.authors = authors; + } + +} diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java index c4fb4bfa8c..13dd68ea20 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -261,6 +261,14 @@ public static OrganizationalEntity convert(final org.cyclonedx.model.Organizatio return dtEntity; } + public static List convertCdxContacts(final List cdxContacts) { + if (cdxContacts == null) { + return null; + } + + return cdxContacts.stream().map(ModelConverter::convert).toList(); + } + private static OrganizationalContact convert(final org.cyclonedx.model.OrganizationalContact cdxContact) { if (cdxContact == null) { return null; @@ -273,6 +281,14 @@ private static OrganizationalContact convert(final org.cyclonedx.model.Organizat return dtContact; } + private static List convertContacts(final List dtContacts) { + if (dtContacts == null) { + return null; + } + + return dtContacts.stream().map(ModelConverter::convert).toList(); + } + private static org.cyclonedx.model.OrganizationalEntity convert(final OrganizationalEntity dtEntity) { if (dtEntity == null) { return null; @@ -457,6 +473,12 @@ public static org.cyclonedx.model.Metadata createMetadata(final Project project) } cycloneComponent.setSupplier(convert(project.getSupplier())); metadata.setComponent(cycloneComponent); + + if (project.getMetadata() != null) { + metadata.setAuthors(convertContacts(project.getMetadata().getAuthors())); + metadata.setManufacture(convert(project.getMetadata().getManufacturer())); + metadata.setSupplier(convert(project.getMetadata().getSupplier())); + } } return metadata; } diff --git a/src/main/java/org/dependencytrack/persistence/BomQueryManager.java b/src/main/java/org/dependencytrack/persistence/BomQueryManager.java index 1d26d9c6d2..7bce2f1d94 100644 --- a/src/main/java/org/dependencytrack/persistence/BomQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/BomQueryManager.java @@ -20,7 +20,6 @@ import alpine.resources.AlpineRequest; import org.dependencytrack.model.Bom; -import org.dependencytrack.model.OrganizationalEntity; import org.dependencytrack.model.Project; import javax.jdo.PersistenceManager; @@ -53,8 +52,7 @@ final class BomQueryManager extends QueryManager implements IQueryManager { * @param imported the Date when the bom was imported * @return a new Bom object */ - public Bom createBom(Project project, Date imported, Bom.Format format, String specVersion, Integer bomVersion, - String serialNumber, OrganizationalEntity manufacturer, OrganizationalEntity supplier) { + public Bom createBom(Project project, Date imported, Bom.Format format, String specVersion, Integer bomVersion, String serialNumber) { final Bom bom = new Bom(); bom.setImported(imported); bom.setProject(project); @@ -62,8 +60,6 @@ public Bom createBom(Project project, Date imported, Bom.Format format, String s bom.setSpecVersion(specVersion); bom.setBomVersion(bomVersion); bom.setSerialNumber(serialNumber); - bom.setManufacturer(manufacturer); - bom.setSupplier(supplier); return persist(bom); } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 6cd812abfa..e44d74ff4a 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -53,7 +53,6 @@ import org.dependencytrack.model.LicenseGroup; import org.dependencytrack.model.NotificationPublisher; import org.dependencytrack.model.NotificationRule; -import org.dependencytrack.model.OrganizationalEntity; import org.dependencytrack.model.Policy; import org.dependencytrack.model.PolicyCondition; import org.dependencytrack.model.PolicyViolation; @@ -473,9 +472,8 @@ public List getProjectProperties(final Project project) { return getProjectQueryManager().getProjectProperties(project); } - public Bom createBom(Project project, Date imported, Bom.Format format, String specVersion, Integer bomVersion, - String serialNumber, OrganizationalEntity manufacturer, OrganizationalEntity supplier) { - return getBomQueryManager().createBom(project, imported, format, specVersion, bomVersion, serialNumber, manufacturer, supplier); + public Bom createBom(Project project, Date imported, Bom.Format format, String specVersion, Integer bomVersion, String serialNumber) { + return getBomQueryManager().createBom(project, imported, format, specVersion, bomVersion, serialNumber); } public List getAllBoms(Project project) { diff --git a/src/main/java/org/dependencytrack/persistence/converter/AbstractJsonConverter.java b/src/main/java/org/dependencytrack/persistence/converter/AbstractJsonConverter.java new file mode 100644 index 0000000000..d80e1e4031 --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/converter/AbstractJsonConverter.java @@ -0,0 +1,66 @@ +/* + * 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.persistence.converter; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import javax.jdo.AttributeConverter; + +/** + * @since 4.10.0 + */ +abstract class AbstractJsonConverter implements AttributeConverter { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private final TypeReference typeReference; + + AbstractJsonConverter(final TypeReference typeReference) { + this.typeReference = typeReference; + } + + @Override + public String convertToDatastore(final T attributeValue) { + if (attributeValue == null) { + return null; + } + + try { + return OBJECT_MAPPER.writeValueAsString(attributeValue); + } catch (JacksonException e) { + throw new RuntimeException(e); + } + } + + @Override + public T convertToAttribute(final String datastoreValue) { + if (datastoreValue == null) { + return null; + } + + try { + return OBJECT_MAPPER.readValue(datastoreValue, typeReference); + } catch (JacksonException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/org/dependencytrack/persistence/converter/OrganizationalContactsJsonConverter.java b/src/main/java/org/dependencytrack/persistence/converter/OrganizationalContactsJsonConverter.java new file mode 100644 index 0000000000..a22b7fd1d7 --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/converter/OrganizationalContactsJsonConverter.java @@ -0,0 +1,47 @@ +/* + * 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.persistence.converter; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.dependencytrack.model.OrganizationalContact; + +import java.util.List; + +/** + * @since 4.10.0 + */ +public class OrganizationalContactsJsonConverter extends AbstractJsonConverter> { + + public OrganizationalContactsJsonConverter() { + super(new TypeReference<>() {}); + } + + @Override + public String convertToDatastore(final List attributeValue) { + // Overriding is required for DataNucleus to correctly detect the return type. + return super.convertToDatastore(attributeValue); + } + + @Override + public List convertToAttribute(final String datastoreValue) { + // Overriding is required for DataNucleus to correctly detect the return type. + return super.convertToAttribute(datastoreValue); + } + +} diff --git a/src/main/java/org/dependencytrack/persistence/converter/OrganizationalEntityJsonConverter.java b/src/main/java/org/dependencytrack/persistence/converter/OrganizationalEntityJsonConverter.java new file mode 100644 index 0000000000..c63ca2b56e --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/converter/OrganizationalEntityJsonConverter.java @@ -0,0 +1,45 @@ +/* + * 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.persistence.converter; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.dependencytrack.model.OrganizationalEntity; + +/** + * @since 4.10.0 + */ +public class OrganizationalEntityJsonConverter extends AbstractJsonConverter { + + public OrganizationalEntityJsonConverter() { + super(new TypeReference<>() {}); + } + + @Override + public String convertToDatastore(final OrganizationalEntity attributeValue) { + // Overriding is required for DataNucleus to correctly detect the return type. + return super.convertToDatastore(attributeValue); + } + + @Override + public OrganizationalEntity convertToAttribute(final String datastoreValue) { + // Overriding is required for DataNucleus to correctly detect the return type. + return super.convertToAttribute(datastoreValue); + } + +} diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java index ecbedd64e8..1297397729 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java @@ -34,8 +34,8 @@ import org.dependencytrack.model.Classifier; import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; -import org.dependencytrack.model.OrganizationalEntity; import org.dependencytrack.model.Project; +import org.dependencytrack.model.ProjectMetadata; import org.dependencytrack.model.ServiceComponent; import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; @@ -47,6 +47,7 @@ import org.dependencytrack.util.CompressUtil; import org.dependencytrack.util.InternalComponentIdentificationUtil; +import javax.jdo.FetchPlan; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -78,7 +79,8 @@ public void inform(final Event e) { final byte[] bomBytes = CompressUtil.optionallyDecompress(event.getBom()); final QueryManager qm = new QueryManager(); try { - final Project project = qm.getObjectByUuid(Project.class, event.getProjectUuid()); + final Project project = qm.getObjectByUuid(Project.class, event.getProjectUuid(), + List.of(FetchPlan.DEFAULT, Project.FetchGroup.METADATA.name())); bomProcessingFailedProject = project; if (project == null) { @@ -99,8 +101,6 @@ public void inform(final Event e) { final String bomSpecVersion; final Integer bomVersion; final String serialNumnber; - OrganizationalEntity bomSupplier = null; - OrganizationalEntity bomManufacturer = null; org.cyclonedx.model.Bom cycloneDxBom = null; if (BomParserFactory.looksLikeCycloneDX(bomBytes)) { if (qm.isEnabled(ConfigPropertyConstants.ACCEPT_ARTIFACT_CYCLONEDX)) { @@ -113,8 +113,23 @@ public void inform(final Event e) { bomProcessingFailedBomVersion = bomSpecVersion; bomVersion = cycloneDxBom.getVersion(); if (cycloneDxBom.getMetadata() != null) { - bomSupplier = ModelConverter.convert(cycloneDxBom.getMetadata().getSupplier()); - bomManufacturer = ModelConverter.convert(cycloneDxBom.getMetadata().getManufacture()); + final var projectMetadata = new ProjectMetadata(); + projectMetadata.setManufacturer(ModelConverter.convert(cycloneDxBom.getMetadata().getManufacture())); + projectMetadata.setSupplier(ModelConverter.convert(cycloneDxBom.getMetadata().getSupplier())); + projectMetadata.setAuthors(ModelConverter.convertCdxContacts(cycloneDxBom.getMetadata().getAuthors())); + if (project.getMetadata() != null) { + qm.runInTransaction(() -> { + project.getMetadata().setManufacturer(projectMetadata.getManufacturer()); + project.getMetadata().setSupplier(projectMetadata.getSupplier()); + project.getMetadata().setAuthors(projectMetadata.getAuthors()); + }); + } else { + qm.runInTransaction(() -> { + projectMetadata.setProject(project); + qm.getPersistenceManager().makePersistent(projectMetadata); + }); + } + if (cycloneDxBom.getMetadata().getComponent() != null) { final org.cyclonedx.model.Component cdxMetadataComponent = cycloneDxBom.getMetadata().getComponent(); if (cdxMetadataComponent.getType() != null && project.getClassifier() == null) { @@ -156,7 +171,7 @@ public void inform(final Event e) { .content("A " + bomFormat.getFormatShortName() + " BOM was consumed and will be processed") .subject(new BomConsumedOrProcessed(copyOfProject, Base64.getEncoder().encodeToString(bomBytes), bomFormat, bomSpecVersion))); final Date date = new Date(); - final Bom bom = qm.createBom(project, date, bomFormat, bomSpecVersion, bomVersion, serialNumnber, bomSupplier, bomManufacturer); + final Bom bom = qm.createBom(project, date, bomFormat, bomSpecVersion, bomVersion, serialNumnber); for (final Component component: components) { processComponent(qm, component, flattenedComponents, newComponents); } diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml index 07857f33dd..915df94b39 100644 --- a/src/main/resources/META-INF/persistence.xml +++ b/src/main/resources/META-INF/persistence.xml @@ -39,6 +39,7 @@ org.dependencytrack.model.PolicyViolation org.dependencytrack.model.PortfolioMetrics org.dependencytrack.model.Project + org.dependencytrack.model.ProjectMetadata org.dependencytrack.model.ProjectMetrics org.dependencytrack.model.ProjectProperty org.dependencytrack.model.Repository diff --git a/src/test/java/org/dependencytrack/model/ProjectTest.java b/src/test/java/org/dependencytrack/model/ProjectTest.java index 0ac64da2bb..52e232c22f 100644 --- a/src/test/java/org/dependencytrack/model/ProjectTest.java +++ b/src/test/java/org/dependencytrack/model/ProjectTest.java @@ -31,7 +31,7 @@ public class ProjectTest extends PersistenceCapableTest { public void testProjectPersistence() { Project p1 = qm.createProject("Example Project 1", "Description 1", "1.0", null, null, null, true, false); Project p2 = qm.createProject("Example Project 2", "Description 2", "1.1", null, null, null, true, false); - Bom bom = qm.createBom(p1, new Date(), Bom.Format.CYCLONEDX, "1.1", 1, UUID.randomUUID().toString(), null, null); + Bom bom = qm.createBom(p1, new Date(), Bom.Format.CYCLONEDX, "1.1", 1, UUID.randomUUID().toString()); Assert.assertEquals("Example Project 1", p1.getName()); Assert.assertEquals("Example Project 2", p2.getName()); diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index 40f6987090..9f894f3526 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -29,8 +29,10 @@ import org.dependencytrack.model.AnalysisState; import org.dependencytrack.model.Classifier; import org.dependencytrack.model.Component; +import org.dependencytrack.model.OrganizationalContact; import org.dependencytrack.model.OrganizationalEntity; import org.dependencytrack.model.Project; +import org.dependencytrack.model.ProjectMetadata; import org.dependencytrack.model.Severity; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.resources.v1.vo.BomSubmitRequest; @@ -49,6 +51,7 @@ import javax.ws.rs.core.Response; import java.io.File; import java.util.Base64; +import java.util.List; import java.util.UUID; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; @@ -106,16 +109,27 @@ public void exportProjectAsCycloneDxInventoryTest() { final var projectSupplier = new OrganizationalEntity(); projectSupplier.setName("projectSupplier"); - var project = new Project(); project.setName("acme-app"); project.setClassifier(Classifier.APPLICATION); project.setSupplier(projectSupplier); project = qm.createProject(project, null, false); + final var bomSupplier = new OrganizationalEntity(); + bomSupplier.setName("bomSupplier"); + final var bomManufacturer = new OrganizationalEntity(); + bomManufacturer.setName("bomManufacturer"); + final var bomAuthor = new OrganizationalContact(); + bomAuthor.setName("bomAuthor"); + final var projectMetadata = new ProjectMetadata(); + projectMetadata.setProject(project); + projectMetadata.setAuthors(List.of(bomAuthor)); + projectMetadata.setManufacturer(bomManufacturer); + projectMetadata.setSupplier(bomSupplier); + qm.persist(projectMetadata); + final var componentSupplier = new OrganizationalEntity(); componentSupplier.setName("componentSupplier"); - var componentWithoutVuln = new Component(); componentWithoutVuln.setProject(project); componentWithoutVuln.setName("acme-lib-a"); @@ -183,6 +197,11 @@ public void exportProjectAsCycloneDxInventoryTest() { "version": 1, "metadata": { "timestamp": "${json-unit.any-string}", + "authors": [ + { + "name": "bomAuthor" + } + ], "component": { "type": "application", "bom-ref": "${json-unit.matches:projectUuid}", @@ -192,6 +211,12 @@ public void exportProjectAsCycloneDxInventoryTest() { "name": "acme-app", "version": "SNAPSHOT" }, + "manufacture": { + "name": "bomManufacturer" + }, + "supplier": { + "name": "bomSupplier" + }, "tools": [ { "vendor": "OWASP", diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index dd57171d58..633b2b8483 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -131,6 +131,7 @@ public void informTest() throws Exception { new BomUploadProcessingTask().inform(new BomUploadEvent(project.getUuid(), bomBytes)); assertConditionWithTimeout(() -> NOTIFICATIONS.size() >= 6, Duration.ofSeconds(5)); + qm.getPersistenceManager().refresh(project); assertThat(project.getClassifier()).isEqualTo(Classifier.APPLICATION); assertThat(project.getLastBomImport()).isNotNull(); @@ -145,6 +146,31 @@ public void informTest() throws Exception { assertThat(contact.getPhone()).isEqualTo("123-456-7890"); }); + assertThat(project.getMetadata()).isNotNull(); + assertThat(project.getMetadata().getAuthors()).satisfiesExactly(contact -> { + assertThat(contact.getName()).isEqualTo("Author"); + assertThat(contact.getEmail()).isEqualTo("author@example.com"); + assertThat(contact.getPhone()).isEqualTo("123-456-7890"); + }); + assertThat(project.getMetadata().getManufacturer()).satisfies(supplier -> { + assertThat(supplier.getName()).isEqualTo("Foo Incorporated"); + assertThat(supplier.getUrls()).containsOnly("https://foo.bar.com"); + assertThat(supplier.getContacts()).satisfiesExactly(contact -> { + assertThat(contact.getName()).isEqualTo("Foo Sr."); + assertThat(contact.getEmail()).isEqualTo("foo@bar.com"); + assertThat(contact.getPhone()).isEqualTo("800-123-4567"); + }); + }); + assertThat(project.getMetadata().getSupplier()).satisfies(manufacturer -> { + assertThat(manufacturer.getName()).isEqualTo("Foo Incorporated"); + assertThat(manufacturer.getUrls()).containsOnly("https://foo.bar.com"); + assertThat(manufacturer.getContacts()).satisfiesExactly(contact -> { + assertThat(contact.getName()).isEqualTo("Foo Jr."); + assertThat(contact.getEmail()).isEqualTo("foojr@bar.com"); + assertThat(contact.getPhone()).isEqualTo("123-456-7890"); + }); + }); + final List components = qm.getAllComponents(project); assertThat(components).hasSize(1); @@ -153,13 +179,6 @@ public void informTest() throws Exception { assertThat(component.getSupplier().getUrls()[0]).isEqualTo("https://foo.bar.com"); assertThat(component.getSupplier().getContacts().get(0).getEmail()).isEqualTo("foojr@bar.com"); assertThat(component.getSupplier().getContacts().get(0).getPhone()).isEqualTo("123-456-7890"); - - final Bom bom = qm.getAllBoms(project).get(0); - assertThat(bom.getManufacturer().getName()).isEqualTo("Foo Incorporated"); - assertThat(bom.getManufacturer().getUrls()[0]).isEqualTo("https://foo.bar.com"); - assertThat(bom.getManufacturer().getContacts().get(0).getName()).isEqualTo("Foo Sr."); - assertThat(bom.getManufacturer().getContacts().get(0).getEmail()).isEqualTo("foo@bar.com"); - assertThat(bom.getManufacturer().getContacts().get(0).getPhone()).isEqualTo("800-123-4567"); assertThat(component.getAuthor()).isEqualTo("Sometimes this field is long because it is composed of a list of authors......................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................"); assertThat(component.getPublisher()).isEqualTo("Example Incorporated"); diff --git a/src/test/resources/bom-1.xml b/src/test/resources/bom-1.xml index a5a8f0ca1d..e48509ebce 100644 --- a/src/test/resources/bom-1.xml +++ b/src/test/resources/bom-1.xml @@ -1,6 +1,13 @@ + + + Author + author@example.com + 123-456-7890 + + Foo Incorporated From c87e496777bc0f78b44551f6af2a2303764ab1df Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 8 Nov 2023 00:04:48 +0100 Subject: [PATCH 07/16] Revert unrelated change Signed-off-by: nscuro --- src/main/java/org/dependencytrack/model/Bom.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/model/Bom.java b/src/main/java/org/dependencytrack/model/Bom.java index bf33e77f6a..5dba408764 100644 --- a/src/main/java/org/dependencytrack/model/Bom.java +++ b/src/main/java/org/dependencytrack/model/Bom.java @@ -91,7 +91,7 @@ public String getFormatLongName() { @Column(name = "SERIAL_NUMBER") private String serialNumber; - @Persistent + @Persistent(defaultFetchGroup = "true") @Column(name = "PROJECT_ID", allowsNull = "false") @NotNull private Project project; From 885c244b015f29b23c2e63e5112994afbf9970fe Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 8 Nov 2023 15:46:19 +0100 Subject: [PATCH 08/16] Add serialized fields of `ProjectMetadata` to default fetch group Signed-off-by: nscuro --- .../java/org/dependencytrack/model/ProjectMetadata.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/ProjectMetadata.java b/src/main/java/org/dependencytrack/model/ProjectMetadata.java index e54f39691a..1668869450 100644 --- a/src/main/java/org/dependencytrack/model/ProjectMetadata.java +++ b/src/main/java/org/dependencytrack/model/ProjectMetadata.java @@ -51,17 +51,17 @@ public class ProjectMetadata { @JsonIgnore private Project project; - @Persistent + @Persistent(defaultFetchGroup = "true") @Convert(OrganizationalEntityJsonConverter.class) @Column(name = "MANUFACTURER", jdbcType = "CLOB", allowsNull = "true") private OrganizationalEntity manufacturer; - @Persistent + @Persistent(defaultFetchGroup = "true") @Convert(OrganizationalEntityJsonConverter.class) @Column(name = "SUPPLIER", jdbcType = "CLOB", allowsNull = "true") private OrganizationalEntity supplier; - @Persistent + @Persistent(defaultFetchGroup = "true") @Convert(OrganizationalContactsJsonConverter.class) @Column(name = "AUTHORS", jdbcType = "CLOB", allowsNull = "true") private List authors; From 10691c8897c4b743a460d6666ed7d3088296bb7b Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 8 Nov 2023 15:46:46 +0100 Subject: [PATCH 09/16] Mark `Project#metadata` field as read only in API docs Signed-off-by: nscuro --- src/main/java/org/dependencytrack/model/Project.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/model/Project.java b/src/main/java/org/dependencytrack/model/Project.java index b06d005169..36faf98705 100644 --- a/src/main/java/org/dependencytrack/model/Project.java +++ b/src/main/java/org/dependencytrack/model/Project.java @@ -31,6 +31,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; +import io.swagger.annotations.ApiModelProperty; import org.dependencytrack.persistence.converter.OrganizationalEntityJsonConverter; import org.dependencytrack.resources.v1.serializers.CustomPackageURLSerializer; @@ -89,7 +90,8 @@ @Persistent(name = "children"), @Persistent(name = "properties"), @Persistent(name = "tags"), - @Persistent(name = "accessTeams") + @Persistent(name = "accessTeams"), + @Persistent(name = "metadata") }), @FetchGroup(name = "METADATA", members = { @Persistent(name = "metadata") @@ -271,6 +273,7 @@ public enum FetchGroup { private List externalReferences; @Persistent(mappedBy = "project") + @ApiModelProperty(accessMode = ApiModelProperty.AccessMode.READ_ONLY) private ProjectMetadata metadata; private transient ProjectMetrics metrics; From 785f1e485e685cab7bbc4a7b3f39f2aef616d024 Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 8 Nov 2023 15:47:25 +0100 Subject: [PATCH 10/16] Handle supplier in update, patch, and clone operations of components and projects Signed-off-by: nscuro --- .../persistence/ComponentQueryManager.java | 2 ++ .../persistence/ProjectQueryManager.java | 3 +++ .../resources/v1/ProjectResource.java | 25 +++++++++++++++++++ .../resources/v1/ProjectResourceTest.java | 13 ++++++++++ 4 files changed, 43 insertions(+) diff --git a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java index 35125702fe..c2653a555d 100644 --- a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java @@ -376,6 +376,7 @@ public Component cloneComponent(Component sourceComponent, Project destinationPr component.setLicenseUrl(sourceComponent.getLicenseUrl()); component.setResolvedLicense(sourceComponent.getResolvedLicense()); component.setAuthor(sourceComponent.getAuthor()); + component.setSupplier(sourceComponent.getSupplier()); // TODO Add support for parent component and children components component.setProject(destinationProject); return createComponent(component, commitIndex); @@ -410,6 +411,7 @@ public Component updateComponent(Component transientComponent, boolean commitInd component.setPurl(transientComponent.getPurl()); component.setInternal(transientComponent.isInternal()); component.setAuthor(transientComponent.getAuthor()); + component.setSupplier(transientComponent.getSupplier()); final Component result = persist(component); Event.dispatch(new IndexEvent(IndexEvent.Action.UPDATE, result)); commitSearchIndex(commitIndex, Component.class); diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 35d9a1772a..be9e55b516 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -49,6 +49,7 @@ import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.util.NotificationUtil; + import javax.jdo.FetchPlan; import javax.jdo.PersistenceManager; import javax.jdo.Query; @@ -611,6 +612,7 @@ public Project clone(UUID from, String newVersion, boolean includeTags, boolean } Project project = new Project(); project.setAuthor(source.getAuthor()); + project.setSupplier(source.getSupplier()); project.setPublisher(source.getPublisher()); project.setGroup(source.getGroup()); project.setName(source.getName()); @@ -748,6 +750,7 @@ public void recursivelyDelete(final Project project, final boolean commitIndex) deleteVexs(project); removeProjectFromNotificationRules(project); removeProjectFromPolicies(project); + delete(project.getMetadata()); delete(project.getProperties()); delete(getAllBoms(project)); delete(project.getChildren()); diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index 4ecbf962d5..49f245b32c 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -35,6 +35,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.CloneProjectEvent; import org.dependencytrack.model.Classifier; +import org.dependencytrack.model.OrganizationalEntity; import org.dependencytrack.model.Project; import org.dependencytrack.model.Tag; import org.dependencytrack.persistence.QueryManager; @@ -55,8 +56,10 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.security.Principal; +import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Function; @@ -401,6 +404,10 @@ public Response patchProject( modified = true; project.setExternalReferences(jsonProject.getExternalReferences()); } + if (isOrganizationalEntityModified(jsonProject.getSupplier(), project.getSupplier())) { + modified = true; + project.setSupplier(jsonProject.getSupplier()); + } if (modified) { try { project = qm.updateProject(project, true); @@ -419,6 +426,24 @@ public Response patchProject( } } + private static boolean isOrganizationalEntityModified(final OrganizationalEntity updated, final OrganizationalEntity original) { + if (updated == null) { + return false; + } + if (original == null) { + return true; + } + + if (!Objects.equals(updated.getName(), original.getName())) { + return true; + } + if (!Arrays.equals(updated.getUrls(), original.getUrls())) { + return true; + } + + return !Collections.isEmpty(updated.getContacts()) || !Collections.isEmpty(original.getContacts()); + } + /** * returns `true` if the given [updated] collection should be considered an update of the [original] collection. */ diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index f4219274f9..ec95e6fcda 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -33,6 +33,7 @@ import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.ExternalReference; +import org.dependencytrack.model.OrganizationalEntity; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectProperty; import org.dependencytrack.model.ServiceComponent; @@ -798,9 +799,13 @@ public void getProjectsWithoutDescendantsOfTest() { public void cloneProjectTest() { EventService.getInstance().subscribe(CloneProjectEvent.class, CloneProjectTask.class); + final var projectSupplier = new OrganizationalEntity(); + projectSupplier.setName("projectSupplier"); + final var project = new Project(); project.setName("acme-app"); project.setVersion("1.0.0"); + project.setSupplier(projectSupplier); project.setAccessTeams(List.of(team)); qm.persist(project); @@ -811,10 +816,14 @@ public void cloneProjectTest() { qm.createTag("tag-b") )); + final var componentSupplier = new OrganizationalEntity(); + componentSupplier.setName("componentSupplier"); + final var component = new Component(); component.setProject(project); component.setName("acme-lib"); component.setVersion("2.0.0"); + component.setSupplier(componentSupplier); qm.persist(component); final var service = new ServiceComponent(); @@ -858,6 +867,8 @@ public void cloneProjectTest() { final Project clonedProject = qm.getProject("acme-app", "1.1.0"); assertThat(clonedProject).isNotNull(); assertThat(clonedProject.getUuid()).isNotEqualTo(project.getUuid()); + assertThat(clonedProject.getSupplier()).isNotNull(); + assertThat(clonedProject.getName()).isEqualTo("projectSupplier"); assertThat(clonedProject.getAccessTeams()).containsOnly(team); final List clonedProperties = qm.getProjectProperties(clonedProject); @@ -877,6 +888,8 @@ public void cloneProjectTest() { assertThat(clonedComponent.getUuid()).isNotEqualTo(component.getUuid()); assertThat(clonedComponent.getName()).isEqualTo("acme-lib"); assertThat(clonedComponent.getVersion()).isEqualTo("2.0.0"); + assertThat(clonedComponent.getSupplier()).isNotNull(); + assertThat(clonedComponent.getSupplier().getName()).isEqualTo("componentSupplier"); assertThat(qm.getAllVulnerabilities(clonedComponent)).containsOnly(vuln); From ab000176e2a1fb282de28d26ec0918be9ab0f3cd Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 8 Nov 2023 15:50:27 +0100 Subject: [PATCH 11/16] Add unit tests for attribute converters Signed-off-by: nscuro --- ...ganizationalContactsJsonConverterTest.java | 79 ++++++++++++++ ...OrganizationalEntityJsonConverterTest.java | 100 ++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 src/test/java/org/dependencytrack/persistence/converter/OrganizationalContactsJsonConverterTest.java create mode 100644 src/test/java/org/dependencytrack/persistence/converter/OrganizationalEntityJsonConverterTest.java diff --git a/src/test/java/org/dependencytrack/persistence/converter/OrganizationalContactsJsonConverterTest.java b/src/test/java/org/dependencytrack/persistence/converter/OrganizationalContactsJsonConverterTest.java new file mode 100644 index 0000000000..c7db6029e4 --- /dev/null +++ b/src/test/java/org/dependencytrack/persistence/converter/OrganizationalContactsJsonConverterTest.java @@ -0,0 +1,79 @@ +/* + * 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.persistence.converter; + +import org.dependencytrack.model.OrganizationalContact; +import org.junit.Test; + +import java.util.List; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +public class OrganizationalContactsJsonConverterTest { + + @Test + public void testConvertToDatastore() { + final var contact = new OrganizationalContact(); + contact.setName("Foo"); + contact.setEmail("foo@example.com"); + contact.setPhone("123456789"); + + assertThatJson(new OrganizationalContactsJsonConverter().convertToDatastore(List.of(contact))) + .isEqualTo(""" + [ + { + "name": "Foo", + "email": "foo@example.com", + "phone": "123456789" + } + ] + """); + } + + @Test + public void testConvertToAttribute() { + final List contacts = new OrganizationalContactsJsonConverter().convertToAttribute(""" + [ + { + "name": "Foo", + "email": "foo@example.com", + "phone": "123456789" + } + ] + """); + + assertThat(contacts).satisfiesExactly(contact -> { + assertThat(contact.getName()).isEqualTo("Foo"); + assertThat(contact.getEmail()).isEqualTo("foo@example.com"); + assertThat(contact.getPhone()).isEqualTo("123456789"); + }); + } + + @Test + public void testConvertToDatastoreNull() { + assertThat(new OrganizationalContactsJsonConverter().convertToDatastore(null)).isNull(); + } + + @Test + public void testConvertToAttributeNull() { + assertThat(new OrganizationalContactsJsonConverter().convertToAttribute(null)).isNull(); + } + +} \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/persistence/converter/OrganizationalEntityJsonConverterTest.java b/src/test/java/org/dependencytrack/persistence/converter/OrganizationalEntityJsonConverterTest.java new file mode 100644 index 0000000000..e2fb40783c --- /dev/null +++ b/src/test/java/org/dependencytrack/persistence/converter/OrganizationalEntityJsonConverterTest.java @@ -0,0 +1,100 @@ +/* + * 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.persistence.converter; + +import org.dependencytrack.model.OrganizationalContact; +import org.dependencytrack.model.OrganizationalEntity; +import org.junit.Test; + +import java.util.List; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +public class OrganizationalEntityJsonConverterTest { + + @Test + public void testConvertToDatastore() { + final var contact = new OrganizationalContact(); + contact.setName("Foo"); + contact.setEmail("foo@example.com"); + contact.setPhone("123456789"); + + final var entity = new OrganizationalEntity(); + entity.setName("foo"); + entity.setUrls(new String[]{"https://example.com"}); + entity.setContacts(List.of(contact)); + + assertThatJson(new OrganizationalEntityJsonConverter().convertToDatastore(entity)) + .isEqualTo(""" + { + "name": "foo", + "urls": [ + "https://example.com" + ], + "contacts": [ + { + "name": "Foo", + "email": "foo@example.com", + "phone": "123456789" + } + ] + } + """); + } + + @Test + public void testConvertToAttribute() { + final OrganizationalEntity entity = new OrganizationalEntityJsonConverter().convertToAttribute(""" + { + "name": "foo", + "urls": [ + "https://example.com" + ], + "contacts": [ + { + "name": "Foo", + "email": "foo@example.com", + "phone": "123456789" + } + ] + } + """); + + assertThat(entity).isNotNull(); + assertThat(entity.getName()).isEqualTo("foo"); + assertThat(entity.getUrls()).containsOnly("https://example.com"); + assertThat(entity.getContacts()).satisfiesExactly(contact -> { + assertThat(contact.getName()).isEqualTo("Foo"); + assertThat(contact.getEmail()).isEqualTo("foo@example.com"); + assertThat(contact.getPhone()).isEqualTo("123456789"); + }); + } + + @Test + public void testConvertToDatastoreNull() { + assertThat(new OrganizationalEntityJsonConverter().convertToDatastore(null)).isNull(); + } + + @Test + public void testConvertToAttributeNull() { + assertThat(new OrganizationalEntityJsonConverter().convertToAttribute(null)).isNull(); + } + +} \ No newline at end of file From 9ebe704af93fe3c91b4f18d8052ed0eeae1f5cae Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 14 Nov 2023 15:24:38 +0100 Subject: [PATCH 12/16] Fix incorrect assertion in `cloneProjectTest` Signed-off-by: nscuro --- .../org/dependencytrack/resources/v1/ProjectResourceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index ec95e6fcda..8c9c9bcf7f 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -868,7 +868,7 @@ public void cloneProjectTest() { assertThat(clonedProject).isNotNull(); assertThat(clonedProject.getUuid()).isNotEqualTo(project.getUuid()); assertThat(clonedProject.getSupplier()).isNotNull(); - assertThat(clonedProject.getName()).isEqualTo("projectSupplier"); + assertThat(clonedProject.getSupplier().getName()).isEqualTo("projectSupplier"); assertThat(clonedProject.getAccessTeams()).containsOnly(team); final List clonedProperties = qm.getProjectProperties(clonedProject); From 91c1040f77f2aaa1061d8e98f3a3563361c6de7a Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 14 Nov 2023 15:37:38 +0100 Subject: [PATCH 13/16] Include metadata when cloning projects Signed-off-by: nscuro --- .../persistence/ProjectQueryManager.java | 10 ++++++++ .../resources/v1/ProjectResourceTest.java | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index be9e55b516..03918793b1 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -40,6 +40,7 @@ import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.FindingAttribution; import org.dependencytrack.model.Project; +import org.dependencytrack.model.ProjectMetadata; import org.dependencytrack.model.ProjectProperty; import org.dependencytrack.model.ProjectVersion; import org.dependencytrack.model.ServiceComponent; @@ -629,6 +630,15 @@ public Project clone(UUID from, String newVersion, boolean includeTags, boolean project.setParent(source.getParent()); project = persist(project); + if (source.getMetadata() != null) { + final var metadata = new ProjectMetadata(); + metadata.setProject(project); + metadata.setAuthors(source.getMetadata().getAuthors()); + metadata.setManufacturer(source.getMetadata().getManufacturer()); + metadata.setSupplier(source.getMetadata().getSupplier()); + persist(metadata); + } + if (includeTags) { for (final Tag tag: source.getTags()) { tag.getProjects().add(project); diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index 8c9c9bcf7f..a05d6eed4c 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -33,8 +33,10 @@ import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.ExternalReference; +import org.dependencytrack.model.OrganizationalContact; import org.dependencytrack.model.OrganizationalEntity; import org.dependencytrack.model.Project; +import org.dependencytrack.model.ProjectMetadata; import org.dependencytrack.model.ProjectProperty; import org.dependencytrack.model.ServiceComponent; import org.dependencytrack.model.Tag; @@ -816,6 +818,19 @@ public void cloneProjectTest() { qm.createTag("tag-b") )); + final var metadataAuthor = new OrganizationalContact(); + metadataAuthor.setName("metadataAuthor"); + final var metadataManufacturer = new OrganizationalEntity(); + metadataManufacturer.setName("metadataManufacturer"); + final var metadataSupplier = new OrganizationalEntity(); + metadataSupplier.setName("metadataSupplier"); + final var metadata = new ProjectMetadata(); + metadata.setProject(project); + metadata.setAuthors(List.of(metadataAuthor)); + metadata.setManufacturer(metadataManufacturer); + metadata.setSupplier(metadataSupplier); + qm.persist(metadata); + final var componentSupplier = new OrganizationalEntity(); componentSupplier.setName("componentSupplier"); @@ -884,6 +899,15 @@ public void cloneProjectTest() { assertThat(clonedProject.getTags()).extracting(Tag::getName) .containsOnly("tag-a", "tag-b"); + final ProjectMetadata clonedMetadata = clonedProject.getMetadata(); + assertThat(clonedMetadata).isNotNull(); + assertThat(clonedMetadata.getAuthors()) + .satisfiesExactly(contact -> assertThat(contact.getName()).isEqualTo("metadataAuthor")); + assertThat(clonedMetadata.getManufacturer()) + .satisfies(entity -> assertThat(entity.getName()).isEqualTo("metadataManufacturer")); + assertThat(clonedMetadata.getSupplier()) + .satisfies(entity -> assertThat(entity.getName()).isEqualTo("metadataSupplier")); + assertThat(qm.getAllComponents(clonedProject)).satisfiesExactly(clonedComponent -> { assertThat(clonedComponent.getUuid()).isNotEqualTo(component.getUuid()); assertThat(clonedComponent.getName()).isEqualTo("acme-lib"); From 609c4f8092c390e30ac32ef5929c09b9ba69181d Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 27 Nov 2023 20:07:51 +0100 Subject: [PATCH 14/16] Ensure `project.supplier` can be `PATCH`ed Signed-off-by: nscuro --- .../model/OrganizationalContact.java | 15 +++++ .../model/OrganizationalEntity.java | 18 ++++++ .../persistence/ProjectQueryManager.java | 1 + .../resources/v1/ProjectResource.java | 26 +-------- .../resources/v1/ProjectResourceTest.java | 56 +++++++++++++++---- 5 files changed, 81 insertions(+), 35 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/OrganizationalContact.java b/src/main/java/org/dependencytrack/model/OrganizationalContact.java index f03f09f01e..42e7ce3696 100644 --- a/src/main/java/org/dependencytrack/model/OrganizationalContact.java +++ b/src/main/java/org/dependencytrack/model/OrganizationalContact.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import java.io.Serializable; +import java.util.Objects; /** * Model class for tracking organizational contacts. @@ -67,4 +68,18 @@ public String getPhone() { public void setPhone(String phone) { this.phone = phone; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final OrganizationalContact that = (OrganizationalContact) o; + return Objects.equals(name, that.name) && Objects.equals(email, that.email) && Objects.equals(phone, that.phone); + } + + @Override + public int hashCode() { + return Objects.hash(name, email, phone); + } + } diff --git a/src/main/java/org/dependencytrack/model/OrganizationalEntity.java b/src/main/java/org/dependencytrack/model/OrganizationalEntity.java index 134fc7cbfb..d47cd80777 100644 --- a/src/main/java/org/dependencytrack/model/OrganizationalEntity.java +++ b/src/main/java/org/dependencytrack/model/OrganizationalEntity.java @@ -25,7 +25,9 @@ import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Objects; /** * Model class for tracking organizational entities (provider, supplier, manufacturer, etc). @@ -76,4 +78,20 @@ public void addContact(OrganizationalContact contact) { public void setContacts(List contacts) { this.contacts = contacts; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final OrganizationalEntity that = (OrganizationalEntity) o; + return Objects.equals(name, that.name) && Arrays.equals(urls, that.urls) && Objects.equals(contacts, that.contacts); + } + + @Override + public int hashCode() { + int result = Objects.hash(name, contacts); + result = 31 * result + Arrays.hashCode(urls); + return result; + } + } diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 03918793b1..9aa6aad106 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -553,6 +553,7 @@ public Project updateProject(Project transientProject, boolean commitIndex) { final Project project = getObjectByUuid(Project.class, transientProject.getUuid()); project.setAuthor(transientProject.getAuthor()); project.setPublisher(transientProject.getPublisher()); + project.setSupplier(transientProject.getSupplier()); project.setGroup(transientProject.getGroup()); project.setName(transientProject.getName()); project.setDescription(transientProject.getDescription()); diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index 49f245b32c..6a30590dd3 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -35,7 +35,6 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.CloneProjectEvent; import org.dependencytrack.model.Classifier; -import org.dependencytrack.model.OrganizationalEntity; import org.dependencytrack.model.Project; import org.dependencytrack.model.Tag; import org.dependencytrack.persistence.QueryManager; @@ -56,10 +55,8 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.security.Principal; -import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Function; @@ -385,6 +382,7 @@ public Response patchProject( modified |= setIfDifferent(jsonProject, project, Project::getPurl, Project::setPurl); modified |= setIfDifferent(jsonProject, project, Project::getSwidTagId, Project::setSwidTagId); modified |= setIfDifferent(jsonProject, project, Project::isActive, Project::setActive); + modified |= setIfDifferent(jsonProject, project, Project::getSupplier, Project::setSupplier); if (jsonProject.getParent() != null && jsonProject.getParent().getUuid() != null) { final Project parent = qm.getObjectByUuid(Project.class, jsonProject.getParent().getUuid()); if (parent == null) { @@ -404,10 +402,6 @@ public Response patchProject( modified = true; project.setExternalReferences(jsonProject.getExternalReferences()); } - if (isOrganizationalEntityModified(jsonProject.getSupplier(), project.getSupplier())) { - modified = true; - project.setSupplier(jsonProject.getSupplier()); - } if (modified) { try { project = qm.updateProject(project, true); @@ -426,24 +420,6 @@ public Response patchProject( } } - private static boolean isOrganizationalEntityModified(final OrganizationalEntity updated, final OrganizationalEntity original) { - if (updated == null) { - return false; - } - if (original == null) { - return true; - } - - if (!Objects.equals(updated.getName(), original.getName())) { - return true; - } - if (!Arrays.equals(updated.getUrls(), original.getUrls())) { - return true; - } - - return !Collections.isEmpty(updated.getContacts()) || !Collections.isEmpty(original.getContacts()); - } - /** * returns `true` if the given [updated] collection should be considered an update of the [original] collection. */ diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index a05d6eed4c..bada619280 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -66,8 +66,10 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.equalTo; public class ProjectResourceTest extends ResourceTest { @@ -585,6 +587,14 @@ public void patchProjectNotFoundTest() { public void patchProjectSuccessfullyPatchedTest() { final var tags = Stream.of("tag1", "tag2").map(qm::createTag).collect(Collectors.toUnmodifiableList()); final var p1 = qm.createProject("ABC", "Test project", "1.0", tags, null, null, true, false); + final var projectSupplierContact = new OrganizationalContact(); + projectSupplierContact.setName("supplierContactName"); + final var projectSupplier = new OrganizationalEntity(); + projectSupplier.setName("supplierName"); + projectSupplier.setUrls(new String[]{"https://supplier.example.com"}); + projectSupplier.setContacts(List.of(projectSupplierContact)); + p1.setSupplier(projectSupplier); + qm.persist(p1); final var jsonProject = new Project(); jsonProject.setActive(false); jsonProject.setName("new name"); @@ -594,22 +604,48 @@ public void patchProjectSuccessfullyPatchedTest() { t.setName(name); return t; }).collect(Collectors.toUnmodifiableList())); + final var jsonProjectSupplierContact = new OrganizationalContact(); + jsonProjectSupplierContact.setName("newSupplierContactName"); + final var jsonProjectSupplier = new OrganizationalEntity(); + jsonProjectSupplier.setName("supplierName"); + jsonProjectSupplier.setUrls(new String[]{"https://supplier.example.com"}); + jsonProjectSupplier.setContacts(List.of(jsonProjectSupplierContact)); + jsonProject.setSupplier(jsonProjectSupplier); final var response = target(V1_PROJECT + "/" + p1.getUuid()) .request() .header(X_API_KEY, apiKey) .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true) .method("PATCH", Entity.json(jsonProject)); Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); - final var json = parseJsonObject(response); - Assert.assertEquals(p1.getUuid().toString(), json.getString("uuid")); - Assert.assertEquals(p1.getDescription(), json.getString("description")); - Assert.assertEquals(p1.getVersion(), json.getString("version")); - Assert.assertEquals(jsonProject.getName(), json.getString("name")); - Assert.assertEquals(jsonProject.getPublisher(), json.getString("publisher")); - Assert.assertEquals(false, json.getBoolean("active")); - final var jsonTags = json.getJsonArray("tags"); - Assert.assertEquals(1, jsonTags.size()); - Assert.assertEquals("tag4", jsonTags.get(0).asJsonObject().getString("name")); + assertThatJson(getPlainTextBody(response)) + .withMatcher("projectUuid", equalTo(p1.getUuid().toString())) + .isEqualTo(""" + { + "publisher": "new publisher", + "supplier": { + "name": "supplierName", + "urls": [ + "https://supplier.example.com" + ], + "contacts": [ + { + "name": "newSupplierContactName" + } + ] + }, + "name": "new name", + "description": "Test project", + "version": "1.0", + "uuid": "${json-unit.matches:projectUuid}", + "properties": [], + "tags": [ + { + "name": "tag4" + } + ], + "active": false + } + """); } @Test From b6952ca22f8907383dabe8356585ddd0d585ffd4 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 27 Nov 2023 20:09:50 +0100 Subject: [PATCH 15/16] Remove unused `ProjectQueryManager#updateProject` method Signed-off-by: nscuro --- .../persistence/ProjectQueryManager.java | 34 ------------------- .../persistence/QueryManager.java | 4 --- 2 files changed, 38 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 9aa6aad106..07a077643c 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -508,40 +508,6 @@ public Project createProject(final Project project, List tags, boolean comm return result; } - /** - * Updates an existing Project. - * @param uuid the uuid of the project to update - * @param name the name of the project - * @param description a description of the project - * @param version the project version - * @param tags a List of Tags - these will be resolved if necessary - * @param purl an optional Package URL - * @param active specified if the project is active - * @param commitIndex specifies if the search index should be committed (an expensive operation) - * @return the updated Project - */ - @Override - public Project updateProject(UUID uuid, String name, String description, String version, List tags, PackageURL purl, boolean active, boolean commitIndex) { - final Project project = getObjectByUuid(Project.class, uuid); - project.setName(name); - project.setDescription(description); - project.setVersion(version); - project.setPurl(purl); - - if (!active && Boolean.TRUE.equals(project.isActive()) && hasActiveChild(project)){ - throw new IllegalArgumentException("Project cannot be set to inactive, if active children are present."); - } - project.setActive(active); - - final List resolvedTags = resolveTags(tags); - bind(project, resolvedTags); - - final Project result = persist(project); - Event.dispatch(new IndexEvent(IndexEvent.Action.UPDATE, result)); - commitSearchIndex(commitIndex, Project.class); - return result; - } - /** * Updates an existing Project. * @param transientProject the project to update diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index e44d74ff4a..3df1f76761 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -431,10 +431,6 @@ public Project createProject(final Project project, List tags, boolean comm return getProjectQueryManager().createProject(project, tags, commitIndex); } - public Project updateProject(UUID uuid, String name, String description, String version, List tags, PackageURL purl, boolean active, boolean commitIndex) { - return getProjectQueryManager().updateProject(uuid, name, description, version, tags, purl, active, commitIndex); - } - public Project updateProject(Project transientProject, boolean commitIndex) { return getProjectQueryManager().updateProject(transientProject, commitIndex); } From b5a0bbf5fe6c5638b1d51136b7f8831d2fc14a91 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 27 Nov 2023 21:47:02 +0100 Subject: [PATCH 16/16] Move `manufacturer` from `ProjectMetadata` to `Project` As per CycloneDX specification, `metadata.manufacturer` refers to `metadata.component`, whereas `metadata.supplier` and `metadata.authors` refer to the BOM itself. Keeping `manufacturer` in `ProjectMetadata` is awkward and confusing. Signed-off-by: nscuro --- .../org/dependencytrack/model/Project.java | 13 +++++++ .../model/ProjectMetadata.java | 18 +++------- .../parser/cyclonedx/util/ModelConverter.java | 3 +- .../persistence/ProjectQueryManager.java | 3 +- .../resources/v1/ProjectResource.java | 1 + .../tasks/BomUploadProcessingTask.java | 4 +-- .../resources/v1/BomResourceTest.java | 8 ++--- .../resources/v1/ProjectResourceTest.java | 35 ++++++++++++++++--- .../tasks/BomUploadProcessingTaskTest.java | 33 ++++++++--------- 9 files changed, 76 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/Project.java b/src/main/java/org/dependencytrack/model/Project.java index 36faf98705..b50a23c220 100644 --- a/src/main/java/org/dependencytrack/model/Project.java +++ b/src/main/java/org/dependencytrack/model/Project.java @@ -139,6 +139,11 @@ public enum FetchGroup { @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The publisher may only contain printable characters") private String publisher; + @Persistent(defaultFetchGroup = "true") + @Convert(OrganizationalEntityJsonConverter.class) + @Column(name = "MANUFACTURER", jdbcType = "CLOB", allowsNull = "true") + private OrganizationalEntity manufacturer; + @Persistent(defaultFetchGroup = "true") @Convert(OrganizationalEntityJsonConverter.class) @Column(name = "SUPPLIER", jdbcType = "CLOB", allowsNull = "true") @@ -304,6 +309,14 @@ public void setPublisher(String publisher) { this.publisher = publisher; } + public OrganizationalEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(final OrganizationalEntity manufacturer) { + this.manufacturer = manufacturer; + } + public OrganizationalEntity getSupplier() { return supplier; } diff --git a/src/main/java/org/dependencytrack/model/ProjectMetadata.java b/src/main/java/org/dependencytrack/model/ProjectMetadata.java index 1668869450..e8841b57b0 100644 --- a/src/main/java/org/dependencytrack/model/ProjectMetadata.java +++ b/src/main/java/org/dependencytrack/model/ProjectMetadata.java @@ -34,6 +34,11 @@ import java.util.List; /** + * Metadata that relates to, but does not directly describe, a {@link Project}. + *

+ * In CycloneDX terms, {@link ProjectMetadata} represents data from the {@code metadata} node + * of a BOM (except {@code metadata.component}, which represents a {@link Project} in Dependency-Track). + * * @since 4.10.0 */ @PersistenceCapable(table = "PROJECT_METADATA") @@ -51,11 +56,6 @@ public class ProjectMetadata { @JsonIgnore private Project project; - @Persistent(defaultFetchGroup = "true") - @Convert(OrganizationalEntityJsonConverter.class) - @Column(name = "MANUFACTURER", jdbcType = "CLOB", allowsNull = "true") - private OrganizationalEntity manufacturer; - @Persistent(defaultFetchGroup = "true") @Convert(OrganizationalEntityJsonConverter.class) @Column(name = "SUPPLIER", jdbcType = "CLOB", allowsNull = "true") @@ -82,14 +82,6 @@ public void setProject(final Project project) { this.project = project; } - public OrganizationalEntity getManufacturer() { - return manufacturer; - } - - public void setManufacturer(final OrganizationalEntity manufacturer) { - this.manufacturer = manufacturer; - } - public OrganizationalEntity getSupplier() { return supplier; } diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java index 13dd68ea20..abbe25f466 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -432,6 +432,8 @@ public static org.cyclonedx.model.Metadata createMetadata(final Project project) tool.setVersion(alpine.Config.getInstance().getApplicationVersion()); metadata.setTools(Collections.singletonList(tool)); if (project != null) { + metadata.setManufacture(convert(project.getManufacturer())); + final org.cyclonedx.model.Component cycloneComponent = new org.cyclonedx.model.Component(); cycloneComponent.setBomRef(project.getUuid().toString()); cycloneComponent.setAuthor(StringUtils.trimToNull(project.getAuthor())); @@ -476,7 +478,6 @@ public static org.cyclonedx.model.Metadata createMetadata(final Project project) if (project.getMetadata() != null) { metadata.setAuthors(convertContacts(project.getMetadata().getAuthors())); - metadata.setManufacture(convert(project.getMetadata().getManufacturer())); metadata.setSupplier(convert(project.getMetadata().getSupplier())); } } diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 07a077643c..e07e44b476 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -519,6 +519,7 @@ public Project updateProject(Project transientProject, boolean commitIndex) { final Project project = getObjectByUuid(Project.class, transientProject.getUuid()); project.setAuthor(transientProject.getAuthor()); project.setPublisher(transientProject.getPublisher()); + project.setManufacturer(transientProject.getManufacturer()); project.setSupplier(transientProject.getSupplier()); project.setGroup(transientProject.getGroup()); project.setName(transientProject.getName()); @@ -580,6 +581,7 @@ public Project clone(UUID from, String newVersion, boolean includeTags, boolean } Project project = new Project(); project.setAuthor(source.getAuthor()); + project.setManufacturer(source.getManufacturer()); project.setSupplier(source.getSupplier()); project.setPublisher(source.getPublisher()); project.setGroup(source.getGroup()); @@ -601,7 +603,6 @@ public Project clone(UUID from, String newVersion, boolean includeTags, boolean final var metadata = new ProjectMetadata(); metadata.setProject(project); metadata.setAuthors(source.getMetadata().getAuthors()); - metadata.setManufacturer(source.getMetadata().getManufacturer()); metadata.setSupplier(source.getMetadata().getSupplier()); persist(metadata); } diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index 6a30590dd3..66516b3270 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -382,6 +382,7 @@ public Response patchProject( modified |= setIfDifferent(jsonProject, project, Project::getPurl, Project::setPurl); modified |= setIfDifferent(jsonProject, project, Project::getSwidTagId, Project::setSwidTagId); modified |= setIfDifferent(jsonProject, project, Project::isActive, Project::setActive); + modified |= setIfDifferent(jsonProject, project, Project::getManufacturer, Project::setManufacturer); modified |= setIfDifferent(jsonProject, project, Project::getSupplier, Project::setSupplier); if (jsonProject.getParent() != null && jsonProject.getParent().getUuid() != null) { final Project parent = qm.getObjectByUuid(Project.class, jsonProject.getParent().getUuid()); diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java index 1297397729..8fc656ef2c 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java @@ -113,13 +113,13 @@ public void inform(final Event e) { bomProcessingFailedBomVersion = bomSpecVersion; bomVersion = cycloneDxBom.getVersion(); if (cycloneDxBom.getMetadata() != null) { + project.setManufacturer(ModelConverter.convert(cycloneDxBom.getMetadata().getManufacture())); + final var projectMetadata = new ProjectMetadata(); - projectMetadata.setManufacturer(ModelConverter.convert(cycloneDxBom.getMetadata().getManufacture())); projectMetadata.setSupplier(ModelConverter.convert(cycloneDxBom.getMetadata().getSupplier())); projectMetadata.setAuthors(ModelConverter.convertCdxContacts(cycloneDxBom.getMetadata().getAuthors())); if (project.getMetadata() != null) { qm.runInTransaction(() -> { - project.getMetadata().setManufacturer(projectMetadata.getManufacturer()); project.getMetadata().setSupplier(projectMetadata.getSupplier()); project.getMetadata().setAuthors(projectMetadata.getAuthors()); }); diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index 9f894f3526..014a0046e6 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -107,24 +107,24 @@ public void exportProjectAsCycloneDxInventoryTest() { vulnerability.setSeverity(Severity.HIGH); vulnerability = qm.createVulnerability(vulnerability, false); + final var projectManufacturer = new OrganizationalEntity(); + projectManufacturer.setName("projectManufacturer"); final var projectSupplier = new OrganizationalEntity(); projectSupplier.setName("projectSupplier"); var project = new Project(); project.setName("acme-app"); project.setClassifier(Classifier.APPLICATION); + project.setManufacturer(projectManufacturer); project.setSupplier(projectSupplier); project = qm.createProject(project, null, false); final var bomSupplier = new OrganizationalEntity(); bomSupplier.setName("bomSupplier"); - final var bomManufacturer = new OrganizationalEntity(); - bomManufacturer.setName("bomManufacturer"); final var bomAuthor = new OrganizationalContact(); bomAuthor.setName("bomAuthor"); final var projectMetadata = new ProjectMetadata(); projectMetadata.setProject(project); projectMetadata.setAuthors(List.of(bomAuthor)); - projectMetadata.setManufacturer(bomManufacturer); projectMetadata.setSupplier(bomSupplier); qm.persist(projectMetadata); @@ -212,7 +212,7 @@ public void exportProjectAsCycloneDxInventoryTest() { "version": "SNAPSHOT" }, "manufacture": { - "name": "bomManufacturer" + "name": "projectManufacturer" }, "supplier": { "name": "bomSupplier" diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index bada619280..4763ce4cce 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -587,6 +587,13 @@ public void patchProjectNotFoundTest() { public void patchProjectSuccessfullyPatchedTest() { final var tags = Stream.of("tag1", "tag2").map(qm::createTag).collect(Collectors.toUnmodifiableList()); final var p1 = qm.createProject("ABC", "Test project", "1.0", tags, null, null, true, false); + final var projectManufacturerContact = new OrganizationalContact(); + projectManufacturerContact.setName("manufacturerContactName"); + final var projectManufacturer = new OrganizationalEntity(); + projectManufacturer.setName("manufacturerName"); + projectManufacturer.setUrls(new String[]{"https://manufacturer.example.com"}); + projectManufacturer.setContacts(List.of(projectManufacturerContact)); + p1.setManufacturer(projectManufacturer); final var projectSupplierContact = new OrganizationalContact(); projectSupplierContact.setName("supplierContactName"); final var projectSupplier = new OrganizationalEntity(); @@ -604,6 +611,13 @@ public void patchProjectSuccessfullyPatchedTest() { t.setName(name); return t; }).collect(Collectors.toUnmodifiableList())); + final var jsonProjectManufacturerContact = new OrganizationalContact(); + jsonProjectManufacturerContact.setName("newManufacturerContactName"); + final var jsonProjectManufacturer = new OrganizationalEntity(); + jsonProjectManufacturer.setName("manufacturerName"); + jsonProjectManufacturer.setUrls(new String[]{"https://manufacturer.example.com"}); + jsonProjectManufacturer.setContacts(List.of(jsonProjectManufacturerContact)); + jsonProject.setManufacturer(jsonProjectManufacturer); final var jsonProjectSupplierContact = new OrganizationalContact(); jsonProjectSupplierContact.setName("newSupplierContactName"); final var jsonProjectSupplier = new OrganizationalEntity(); @@ -622,6 +636,17 @@ public void patchProjectSuccessfullyPatchedTest() { .isEqualTo(""" { "publisher": "new publisher", + "manufacturer": { + "name": "manufacturerName", + "urls": [ + "https://manufacturer.example.com" + ], + "contacts": [ + { + "name": "newManufacturerContactName" + } + ] + }, "supplier": { "name": "supplierName", "urls": [ @@ -837,12 +862,15 @@ public void getProjectsWithoutDescendantsOfTest() { public void cloneProjectTest() { EventService.getInstance().subscribe(CloneProjectEvent.class, CloneProjectTask.class); + final var projectManufacturer = new OrganizationalEntity(); + projectManufacturer.setName("projectManufacturer"); final var projectSupplier = new OrganizationalEntity(); projectSupplier.setName("projectSupplier"); final var project = new Project(); project.setName("acme-app"); project.setVersion("1.0.0"); + project.setManufacturer(projectManufacturer); project.setSupplier(projectSupplier); project.setAccessTeams(List.of(team)); qm.persist(project); @@ -856,14 +884,11 @@ public void cloneProjectTest() { final var metadataAuthor = new OrganizationalContact(); metadataAuthor.setName("metadataAuthor"); - final var metadataManufacturer = new OrganizationalEntity(); - metadataManufacturer.setName("metadataManufacturer"); final var metadataSupplier = new OrganizationalEntity(); metadataSupplier.setName("metadataSupplier"); final var metadata = new ProjectMetadata(); metadata.setProject(project); metadata.setAuthors(List.of(metadataAuthor)); - metadata.setManufacturer(metadataManufacturer); metadata.setSupplier(metadataSupplier); qm.persist(metadata); @@ -920,6 +945,8 @@ public void cloneProjectTest() { assertThat(clonedProject.getUuid()).isNotEqualTo(project.getUuid()); assertThat(clonedProject.getSupplier()).isNotNull(); assertThat(clonedProject.getSupplier().getName()).isEqualTo("projectSupplier"); + assertThat(clonedProject.getManufacturer()).isNotNull(); + assertThat(clonedProject.getManufacturer().getName()).isEqualTo("projectManufacturer"); assertThat(clonedProject.getAccessTeams()).containsOnly(team); final List clonedProperties = qm.getProjectProperties(clonedProject); @@ -939,8 +966,6 @@ public void cloneProjectTest() { assertThat(clonedMetadata).isNotNull(); assertThat(clonedMetadata.getAuthors()) .satisfiesExactly(contact -> assertThat(contact.getName()).isEqualTo("metadataAuthor")); - assertThat(clonedMetadata.getManufacturer()) - .satisfies(entity -> assertThat(entity.getName()).isEqualTo("metadataManufacturer")); assertThat(clonedMetadata.getSupplier()) .satisfies(entity -> assertThat(entity.getName()).isEqualTo("metadataSupplier")); diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index 633b2b8483..2bf72da74e 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -137,30 +137,31 @@ public void informTest() throws Exception { assertThat(project.getLastBomImport()).isNotNull(); assertThat(project.getExternalReferences()).isNotNull(); assertThat(project.getExternalReferences()).hasSize(4); - assertThat(project.getSupplier()).isNotNull(); - assertThat(project.getSupplier().getName()).isEqualTo("Foo Incorporated"); - assertThat(project.getSupplier().getUrls()).containsOnly("https://foo.bar.com"); - assertThat(project.getSupplier().getContacts()).satisfiesExactly(contact -> { - assertThat(contact.getName()).isEqualTo("Foo Jr."); - assertThat(contact.getEmail()).isEqualTo("foojr@bar.com"); - assertThat(contact.getPhone()).isEqualTo("123-456-7890"); - }); - - assertThat(project.getMetadata()).isNotNull(); - assertThat(project.getMetadata().getAuthors()).satisfiesExactly(contact -> { - assertThat(contact.getName()).isEqualTo("Author"); - assertThat(contact.getEmail()).isEqualTo("author@example.com"); - assertThat(contact.getPhone()).isEqualTo("123-456-7890"); - }); - assertThat(project.getMetadata().getManufacturer()).satisfies(supplier -> { + assertThat(project.getSupplier()).satisfies(supplier -> { assertThat(supplier.getName()).isEqualTo("Foo Incorporated"); assertThat(supplier.getUrls()).containsOnly("https://foo.bar.com"); assertThat(supplier.getContacts()).satisfiesExactly(contact -> { + assertThat(contact.getName()).isEqualTo("Foo Jr."); + assertThat(contact.getEmail()).isEqualTo("foojr@bar.com"); + assertThat(contact.getPhone()).isEqualTo("123-456-7890"); + }); + }); + assertThat(project.getManufacturer()).satisfies(manufacturer -> { + assertThat(manufacturer.getName()).isEqualTo("Foo Incorporated"); + assertThat(manufacturer.getUrls()).containsOnly("https://foo.bar.com"); + assertThat(manufacturer.getContacts()).satisfiesExactly(contact -> { assertThat(contact.getName()).isEqualTo("Foo Sr."); assertThat(contact.getEmail()).isEqualTo("foo@bar.com"); assertThat(contact.getPhone()).isEqualTo("800-123-4567"); }); }); + + assertThat(project.getMetadata()).isNotNull(); + assertThat(project.getMetadata().getAuthors()).satisfiesExactly(contact -> { + assertThat(contact.getName()).isEqualTo("Author"); + assertThat(contact.getEmail()).isEqualTo("author@example.com"); + assertThat(contact.getPhone()).isEqualTo("123-456-7890"); + }); assertThat(project.getMetadata().getSupplier()).satisfies(manufacturer -> { assertThat(manufacturer.getName()).isEqualTo("Foo Incorporated"); assertThat(manufacturer.getUrls()).containsOnly("https://foo.bar.com");