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/model/Component.java b/src/main/java/org/dependencytrack/model/Component.java index 90379169a3..4cc6aa1483 100644 --- a/src/main/java/org/dependencytrack/model/Component.java +++ b/src/main/java/org/dependencytrack/model/Component.java @@ -29,8 +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; @@ -53,8 +56,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 +118,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 */ - @Column(name = "SUPPLIER", allowsNull = "true") - @Serialized - @Size(max = 255) - @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The supplier may only contain printable characters") + @Persistent(defaultFetchGroup = "true") + @Convert(OrganizationalEntityJsonConverter.class) + @Column(name = "SUPPLIER", jdbcType = "CLOB", allowsNull = "true") private OrganizationalEntity supplier; @Persistent @@ -398,22 +392,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/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/model/Project.java b/src/main/java/org/dependencytrack/model/Project.java index 286c1fcf95..b50a23c220 100644 --- a/src/main/java/org/dependencytrack/model/Project.java +++ b/src/main/java/org/dependencytrack/model/Project.java @@ -31,8 +31,12 @@ 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; + 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; @@ -86,7 +90,11 @@ @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") }), @FetchGroup(name = "METRICS_UPDATE", members = { @Persistent(name = "id"), @@ -107,6 +115,7 @@ public class Project implements Serializable { */ public enum FetchGroup { ALL, + METADATA, METRICS_UPDATE, PARENT } @@ -130,17 +139,15 @@ 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 = "SUPPLIER", allowsNull = "true") - @Size(max = 255) - @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The supplier may only contain printable characters") - private OrganizationalEntity supplier; + @Persistent(defaultFetchGroup = "true") + @Convert(OrganizationalEntityJsonConverter.class) + @Column(name = "MANUFACTURER", jdbcType = "CLOB", allowsNull = "true") + private OrganizationalEntity manufacturer; - @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(defaultFetchGroup = "true") + @Convert(OrganizationalEntityJsonConverter.class) + @Column(name = "SUPPLIER", jdbcType = "CLOB", allowsNull = "true") + private OrganizationalEntity supplier; @Persistent @Column(name = "GROUP", jdbcType = "VARCHAR") @@ -270,6 +277,10 @@ public enum FetchGroup { @Serialized private List externalReferences; + @Persistent(mappedBy = "project") + @ApiModelProperty(accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private ProjectMetadata metadata; + private transient ProjectMetrics metrics; private transient List versions; private transient List dependencyGraph; @@ -298,23 +309,22 @@ public void setPublisher(String publisher) { this.publisher = publisher; } - public OrganizationalEntity getSupplier() { - return supplier; + public OrganizationalEntity getManufacturer() { + return manufacturer; } - public void setSupplier(OrganizationalEntity supplier) { - this.supplier = supplier; + public void setManufacturer(final OrganizationalEntity manufacturer) { + this.manufacturer = manufacturer; } - public OrganizationalEntity getManufacturer() { /**Issue #2373, #2737 */ - return manufacture; + public OrganizationalEntity getSupplier() { + return supplier; } - public void setManufacturer(OrganizationalEntity manufacture) {/**Issue #2373, #2737 */ - this.manufacture = manufacture; + public void setSupplier(OrganizationalEntity supplier) { + this.supplier = supplier; } - public String getGroup() { return group; } @@ -511,6 +521,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..e8841b57b0 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/ProjectMetadata.java @@ -0,0 +1,101 @@ +/* + * 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; + +/** + * 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") +@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(defaultFetchGroup = "true") + @Convert(OrganizationalEntityJsonConverter.class) + @Column(name = "SUPPLIER", jdbcType = "CLOB", allowsNull = "true") + private OrganizationalEntity supplier; + + @Persistent(defaultFetchGroup = "true") + @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 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 7958832102..abbe25f466 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,82 @@ 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; + } + + 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; + } + + 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 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; + } + + 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()); @@ -282,6 +329,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(); @@ -384,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())); @@ -412,9 +462,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,32 +473,13 @@ 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); + + if (project.getMetadata() != null) { + metadata.setAuthors(convertContacts(project.getMetadata().getAuthors())); + metadata.setSupplier(convert(project.getMetadata().getSupplier())); + } } return metadata; } @@ -478,41 +509,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 +533,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 +580,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 +590,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/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..e07e44b476 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; @@ -49,6 +50,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; @@ -506,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 @@ -551,6 +519,8 @@ 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()); project.setDescription(transientProject.getDescription()); @@ -611,6 +581,8 @@ 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()); project.setName(source.getName()); @@ -627,6 +599,14 @@ 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.setSupplier(source.getMetadata().getSupplier()); + persist(metadata); + } + if (includeTags) { for (final Tag tag: source.getTags()) { tag.getProjects().add(project); @@ -748,6 +728,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/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); } 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/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index 4ecbf962d5..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,8 @@ 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()); if (parent == null) { diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java index 38ef7d7fe5..8fc656ef2c 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java @@ -34,9 +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.OrganizationalContact; 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,11 +46,14 @@ import org.dependencytrack.persistence.QueryManager; 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; import java.util.Date; import java.util.List; -import java.util.Optional; +import java.util.stream.Collectors; /** * Subscriber task that performs processing of bill-of-material (bom) @@ -77,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) { @@ -109,42 +112,48 @@ public void inform(final Event e) { bomSpecVersion = cycloneDxBom.getSpecVersion(); bomProcessingFailedBomVersion = bomSpecVersion; bomVersion = cycloneDxBom.getVersion(); + if (cycloneDxBom.getMetadata() != null) { + project.setManufacturer(ModelConverter.convert(cycloneDxBom.getMetadata().getManufacture())); + + final var projectMetadata = new ProjectMetadata(); + projectMetadata.setSupplier(ModelConverter.convert(cycloneDxBom.getMetadata().getSupplier())); + projectMetadata.setAuthors(ModelConverter.convertCdxContacts(cycloneDxBom.getMetadata().getAuthors())); + if (project.getMetadata() != null) { + qm.runInTransaction(() -> { + 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) { + 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())); + } + } + } 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; 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/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 diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index 3cfc7cddbe..014a0046e6 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -29,7 +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; @@ -48,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; @@ -103,15 +107,34 @@ 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 bomAuthor = new OrganizationalContact(); + bomAuthor.setName("bomAuthor"); + final var projectMetadata = new ProjectMetadata(); + projectMetadata.setProject(project); + projectMetadata.setAuthors(List.of(bomAuthor)); + 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"); componentWithoutVuln.setVersion("1.0.0"); + componentWithoutVuln.setSupplier(componentSupplier); componentWithoutVuln.setDirectDependencies("[]"); componentWithoutVuln = qm.createComponent(componentWithoutVuln, false); @@ -174,12 +197,26 @@ public void exportProjectAsCycloneDxInventoryTest() { "version": 1, "metadata": { "timestamp": "${json-unit.any-string}", + "authors": [ + { + "name": "bomAuthor" + } + ], "component": { "type": "application", "bom-ref": "${json-unit.matches:projectUuid}", + "supplier": { + "name": "projectSupplier" + }, "name": "acme-app", "version": "SNAPSHOT" }, + "manufacture": { + "name": "projectManufacturer" + }, + "supplier": { + "name": "bomSupplier" + }, "tools": [ { "vendor": "OWASP", @@ -192,6 +229,9 @@ public void exportProjectAsCycloneDxInventoryTest() { { "type": "library", "bom-ref": "${json-unit.matches:componentWithoutVulnUuid}", + "supplier": { + "name": "componentSupplier" + }, "name": "acme-lib-a", "version": "1.0.0" }, diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index f4219274f9..4763ce4cce 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -33,7 +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; @@ -63,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 { @@ -582,6 +587,21 @@ 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(); + 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"); @@ -591,22 +611,66 @@ 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(); + 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", + "manufacturer": { + "name": "manufacturerName", + "urls": [ + "https://manufacturer.example.com" + ], + "contacts": [ + { + "name": "newManufacturerContactName" + } + ] + }, + "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 @@ -798,9 +862,16 @@ 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); @@ -811,10 +882,24 @@ public void cloneProjectTest() { qm.createTag("tag-b") )); + final var metadataAuthor = new OrganizationalContact(); + metadataAuthor.setName("metadataAuthor"); + final var metadataSupplier = new OrganizationalEntity(); + metadataSupplier.setName("metadataSupplier"); + final var metadata = new ProjectMetadata(); + metadata.setProject(project); + metadata.setAuthors(List.of(metadataAuthor)); + metadata.setSupplier(metadataSupplier); + qm.persist(metadata); + + 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 +943,10 @@ 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.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); @@ -873,10 +962,19 @@ 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.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"); assertThat(clonedComponent.getVersion()).isEqualTo("2.0.0"); + assertThat(clonedComponent.getSupplier()).isNotNull(); + assertThat(clonedComponent.getSupplier().getName()).isEqualTo("componentSupplier"); assertThat(qm.getAllVulnerabilities(clonedComponent)).containsOnly(vuln); diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index f5d11683f0..2bf72da74e 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -131,28 +131,55 @@ 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(); assertThat(project.getExternalReferences()).isNotNull(); assertThat(project.getExternalReferences()).hasSize(4); + 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"); + 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); 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"); - 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