diff --git a/src/main/java/org/dependencytrack/model/Component.java b/src/main/java/org/dependencytrack/model/Component.java index f0e0b65dc..cc33f9531 100644 --- a/src/main/java/org/dependencytrack/model/Component.java +++ b/src/main/java/org/dependencytrack/model/Component.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.github.packageurl.MalformedPackageURLException; @@ -114,17 +115,20 @@ public enum FetchGroup { @Persistent @Column(name = "AUTHOR", jdbcType = "CLOB") @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The author may only contain printable characters") + @JsonView(JsonViews.MetadataTools.class) private String author; @Persistent @Column(name = "PUBLISHER", jdbcType = "VARCHAR") @Size(max = 255) @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The publisher may only contain printable characters") + @JsonView(JsonViews.MetadataTools.class) private String publisher; @Persistent(defaultFetchGroup = "true") @Convert(OrganizationalEntityJsonConverter.class) @Column(name = "SUPPLIER", jdbcType = "CLOB", allowsNull = "true") + @JsonView(JsonViews.MetadataTools.class) private OrganizationalEntity supplier; @Persistent @@ -132,6 +136,7 @@ public enum FetchGroup { @Index(name = "COMPONENT_GROUP_IDX") @Size(max = 255) @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The group may only contain printable characters") + @JsonView(JsonViews.MetadataTools.class) private String group; @Persistent @@ -141,6 +146,7 @@ public enum FetchGroup { @Size(min = 1, max = 255) @JsonDeserialize(using = TrimmedStringDeserializer.class) @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The name may only contain printable characters") + @JsonView(JsonViews.MetadataTools.class) private String name; @Persistent @@ -148,12 +154,14 @@ public enum FetchGroup { @Size(max = 255) @JsonDeserialize(using = TrimmedStringDeserializer.class) @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The version may only contain printable characters") + @JsonView(JsonViews.MetadataTools.class) private String version; @Persistent @Column(name = "CLASSIFIER", jdbcType = "VARCHAR") @Index(name = "COMPONENT_CLASSIFIER_IDX") @Extension(vendorName = "datanucleus", key = "enum-check-constraint", value = "true") + @JsonView(JsonViews.MetadataTools.class) private Classifier classifier; @Persistent @@ -174,72 +182,84 @@ public enum FetchGroup { @Index(name = "COMPONENT_MD5_IDX") @Column(name = "MD5", jdbcType = "VARCHAR", length = 32) @Pattern(regexp = "^[0-9a-fA-F]{32}$", message = "The MD5 hash must be a valid 32 character HEX number") + @JsonView(JsonViews.MetadataTools.class) private String md5; @Persistent @Index(name = "COMPONENT_SHA1_IDX") @Column(name = "SHA1", jdbcType = "VARCHAR", length = 40) @Pattern(regexp = "^[0-9a-fA-F]{40}$", message = "The SHA1 hash must be a valid 40 character HEX number") + @JsonView(JsonViews.MetadataTools.class) private String sha1; @Persistent @Index(name = "COMPONENT_SHA256_IDX") @Column(name = "SHA_256", jdbcType = "VARCHAR", length = 64) @Pattern(regexp = "^[0-9a-fA-F]{64}$", message = "The SHA-256 hash must be a valid 64 character HEX number") + @JsonView(JsonViews.MetadataTools.class) private String sha256; @Persistent @Index(name = "COMPONENT_SHA384_IDX") @Column(name = "SHA_384", jdbcType = "VARCHAR", length = 96) @Pattern(regexp = "^[0-9a-fA-F]{96}$", message = "The SHA-384 hash must be a valid 96 character HEX number") + @JsonView(JsonViews.MetadataTools.class) private String sha384; @Persistent @Index(name = "COMPONENT_SHA512_IDX") @Column(name = "SHA_512", jdbcType = "VARCHAR", length = 128) @Pattern(regexp = "^[0-9a-fA-F]{128}$", message = "The SHA-512 hash must be a valid 128 character HEX number") + @JsonView(JsonViews.MetadataTools.class) private String sha512; @Persistent @Index(name = "COMPONENT_SHA3_256_IDX") @Column(name = "SHA3_256", jdbcType = "VARCHAR", length = 64) @Pattern(regexp = "^[0-9a-fA-F]{64}$", message = "The SHA3-256 hash must be a valid 64 character HEX number") + @JsonView(JsonViews.MetadataTools.class) private String sha3_256; @Persistent @Index(name = "COMPONENT_SHA3_384_IDX") @Column(name = "SHA3_384", jdbcType = "VARCHAR", length = 96) @Pattern(regexp = "^[0-9a-fA-F]{96}$", message = "The SHA3-384 hash must be a valid 96 character HEX number") + @JsonView(JsonViews.MetadataTools.class) private String sha3_384; @Persistent @Index(name = "COMPONENT_SHA3_512_IDX") @Column(name = "SHA3_512", jdbcType = "VARCHAR", length = 128) @Pattern(regexp = "^[0-9a-fA-F]{128}$", message = "The SHA3-512 hash must be a valid 128 character HEX number") + @JsonView(JsonViews.MetadataTools.class) private String sha3_512; @Persistent @Index(name = "COMPONENT_BLAKE2B_256_IDX") @Column(name = "BLAKE2B_256", jdbcType = "VARCHAR", length = 64) @Pattern(regexp = RegexSequence.Definition.HASH_SHA256, message = "The BLAKE2b hash must be a valid 64 character HEX number") + @JsonView(JsonViews.MetadataTools.class) private String blake2b_256; @Persistent @Index(name = "COMPONENT_BLAKE2B_384_IDX") @Column(name = "BLAKE2B_384", jdbcType = "VARCHAR", length = 96) @Pattern(regexp = RegexSequence.Definition.HASH_SHA384, message = "The BLAKE2b hash must be a valid 96 character HEX number") + @JsonView(JsonViews.MetadataTools.class) private String blake2b_384; @Persistent @Index(name = "COMPONENT_BLAKE2B_512_IDX") @Column(name = "BLAKE2B_512", jdbcType = "VARCHAR", length = 128) @Pattern(regexp = RegexSequence.Definition.HASH_SHA512, message = "The BLAKE2b hash must be a valid 128 character HEX number") + @JsonView(JsonViews.MetadataTools.class) private String blake2b_512; @Persistent @Index(name = "COMPONENT_BLAKE3_IDX") @Column(name = "BLAKE3", jdbcType = "VARCHAR", length = 255) @Pattern(regexp = RegexSequence.Definition.HEXADECIMAL, message = "The BLAKE3 hash must be a valid HEX number") + @JsonView(JsonViews.MetadataTools.class) private String blake3; @Persistent @@ -248,6 +268,7 @@ public enum FetchGroup { @Size(max = 255) //Patterns obtained from https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd @Pattern(regexp = "(cpe:2\\.3:[aho\\*\\-](:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!\"#$$%&'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|}~]))+(\\?*|\\*?))|[\\*\\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\\*\\-]))(:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!\"#$$%&'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|}~]))+(\\?*|\\*?))|[\\*\\-])){4})|([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\\._\\-~%]*){0,6})", message = "The CPE must conform to the CPE v2.2 or v2.3 specification defined by NIST") + @JsonView(JsonViews.MetadataTools.class) private String cpe; @Persistent(defaultFetchGroup = "true") @@ -256,6 +277,7 @@ public enum FetchGroup { @Size(max = 1024) @com.github.packageurl.validator.PackageURL @JsonDeserialize(using = TrimmedStringDeserializer.class) + @JsonView(JsonViews.MetadataTools.class) private String purl; @Persistent(defaultFetchGroup = "true") @@ -270,6 +292,7 @@ public enum FetchGroup { @Index(name = "COMPONENT_SWID_TAGID_IDX") @Size(max = 255) @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The SWID tagId may only contain printable characters") + @JsonView(JsonViews.MetadataTools.class) private String swidTagId; @Persistent @@ -323,6 +346,7 @@ public enum FetchGroup { @Persistent(defaultFetchGroup = "true") @Column(name = "EXTERNAL_REFERENCES") @Serialized + @JsonView(JsonViews.MetadataTools.class) private List externalReferences; @Persistent diff --git a/src/main/java/org/dependencytrack/model/DataClassification.java b/src/main/java/org/dependencytrack/model/DataClassification.java index 5092e777d..8238dc5ce 100644 --- a/src/main/java/org/dependencytrack/model/DataClassification.java +++ b/src/main/java/org/dependencytrack/model/DataClassification.java @@ -19,6 +19,7 @@ package org.dependencytrack.model; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonView; import java.io.Serializable; @@ -50,7 +51,10 @@ private Direction(String name) { } } + @JsonView(JsonViews.MetadataTools.class) private Direction direction; + + @JsonView(JsonViews.MetadataTools.class) private String name; public Direction getDirection() { diff --git a/src/main/java/org/dependencytrack/model/ExternalReference.java b/src/main/java/org/dependencytrack/model/ExternalReference.java index 1bd2925ac..e1b7a344b 100644 --- a/src/main/java/org/dependencytrack/model/ExternalReference.java +++ b/src/main/java/org/dependencytrack/model/ExternalReference.java @@ -21,6 +21,7 @@ import alpine.common.validation.RegexSequence; import alpine.server.json.TrimmedStringDeserializer; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.validation.constraints.NotBlank; @@ -38,14 +39,17 @@ public class ExternalReference implements Serializable { private static final long serialVersionUID = -5885851731192037664L; + @JsonView(JsonViews.MetadataTools.class) private org.cyclonedx.model.ExternalReference.Type type; @NotBlank @JsonDeserialize(using = TrimmedStringDeserializer.class) + @JsonView(JsonViews.MetadataTools.class) private String url; @JsonDeserialize(using = TrimmedStringDeserializer.class) @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The comment may only contain printable characters") + @JsonView(JsonViews.MetadataTools.class) private String comment; public org.cyclonedx.model.ExternalReference.Type getType() { diff --git a/src/main/java/org/dependencytrack/model/JsonViews.java b/src/main/java/org/dependencytrack/model/JsonViews.java new file mode 100644 index 000000000..9401cae31 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/JsonViews.java @@ -0,0 +1,16 @@ +package org.dependencytrack.model; + +import com.fasterxml.jackson.annotation.JsonView; + +/** + * Marker interfaces to be used in conjunction with Jackson's {@link JsonView} annotation. + */ +public class JsonViews { + + /** + * Marks fields to be included when (de-)serializing {@link Tools}. + */ + public interface MetadataTools { + } + +} diff --git a/src/main/java/org/dependencytrack/model/OrganizationalContact.java b/src/main/java/org/dependencytrack/model/OrganizationalContact.java index 347bfd89e..ddd069009 100644 --- a/src/main/java/org/dependencytrack/model/OrganizationalContact.java +++ b/src/main/java/org/dependencytrack/model/OrganizationalContact.java @@ -20,6 +20,7 @@ import alpine.server.json.TrimmedStringDeserializer; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import java.io.Serializable; @@ -37,12 +38,15 @@ public class OrganizationalContact implements Serializable { private static final long serialVersionUID = -1026863376484187244L; @JsonDeserialize(using = TrimmedStringDeserializer.class) + @JsonView(JsonViews.MetadataTools.class) private String name; @JsonDeserialize(using = TrimmedStringDeserializer.class) + @JsonView(JsonViews.MetadataTools.class) private String email; @JsonDeserialize(using = TrimmedStringDeserializer.class) + @JsonView(JsonViews.MetadataTools.class) private String phone; public String getName() { diff --git a/src/main/java/org/dependencytrack/model/OrganizationalEntity.java b/src/main/java/org/dependencytrack/model/OrganizationalEntity.java index e8f682bac..2ab46493c 100644 --- a/src/main/java/org/dependencytrack/model/OrganizationalEntity.java +++ b/src/main/java/org/dependencytrack/model/OrganizationalEntity.java @@ -21,6 +21,7 @@ import alpine.server.json.TrimmedStringArrayDeserializer; import alpine.server.json.TrimmedStringDeserializer; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import java.io.Serializable; @@ -41,11 +42,14 @@ public class OrganizationalEntity implements Serializable { private static final long serialVersionUID = 5333594855427723634L; @JsonDeserialize(using = TrimmedStringDeserializer.class) + @JsonView(JsonViews.MetadataTools.class) private String name; @JsonDeserialize(using = TrimmedStringArrayDeserializer.class) + @JsonView(JsonViews.MetadataTools.class) private String[] urls; + @JsonView(JsonViews.MetadataTools.class) private List contacts; public String getName() { diff --git a/src/main/java/org/dependencytrack/model/ProjectMetadata.java b/src/main/java/org/dependencytrack/model/ProjectMetadata.java index ddc71647e..5378d91dd 100644 --- a/src/main/java/org/dependencytrack/model/ProjectMetadata.java +++ b/src/main/java/org/dependencytrack/model/ProjectMetadata.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import org.dependencytrack.persistence.converter.OrganizationalContactsJsonConverter; import org.dependencytrack.persistence.converter.OrganizationalEntityJsonConverter; +import org.dependencytrack.persistence.converter.ToolsJsonConverter; import javax.jdo.annotations.Column; import javax.jdo.annotations.Convert; @@ -66,6 +67,11 @@ public class ProjectMetadata { @Column(name = "AUTHORS", jdbcType = "CLOB", allowsNull = "true") private List authors; + @Persistent(defaultFetchGroup = "true") + @Convert(ToolsJsonConverter.class) + @Column(name = "TOOLS", jdbcType = "CLOB", allowsNull = "true") + private Tools tools; + public long getId() { return id; } @@ -98,4 +104,12 @@ public void setAuthors(final List authors) { this.authors = authors; } + public Tools getTools() { + return tools; + } + + public void setTools(final Tools tools) { + this.tools = tools; + } + } \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/model/ServiceComponent.java b/src/main/java/org/dependencytrack/model/ServiceComponent.java index eeae2042b..5f34b968c 100644 --- a/src/main/java/org/dependencytrack/model/ServiceComponent.java +++ b/src/main/java/org/dependencytrack/model/ServiceComponent.java @@ -23,6 +23,7 @@ import alpine.server.json.TrimmedStringDeserializer; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.jdo.annotations.Column; @@ -86,12 +87,14 @@ public enum FetchGroup { @Persistent(defaultFetchGroup = "true") @Column(name = "PROVIDER_ID") @Serialized + @JsonView(JsonViews.MetadataTools.class) private OrganizationalEntity provider; @Persistent @Column(name = "GROUP", jdbcType = "VARCHAR") @Size(max = 255) @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The group may only contain printable characters") + @JsonView(JsonViews.MetadataTools.class) private String group; @Persistent @@ -100,6 +103,7 @@ public enum FetchGroup { @Size(min = 1, max = 255) @JsonDeserialize(using = TrimmedStringDeserializer.class) @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The name may only contain printable characters") + @JsonView(JsonViews.MetadataTools.class) private String name; @Persistent @@ -107,6 +111,7 @@ public enum FetchGroup { @Size(max = 255) @JsonDeserialize(using = TrimmedStringDeserializer.class) @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The version may only contain printable characters") + @JsonView(JsonViews.MetadataTools.class) private String version; @Persistent @@ -114,25 +119,30 @@ public enum FetchGroup { @Size(max = 1024) @JsonDeserialize(using = TrimmedStringDeserializer.class) @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The description may only contain printable characters") + @JsonView(JsonViews.MetadataTools.class) private String description; @Persistent(defaultFetchGroup = "true") @Serialized @Column(name = "ENDPOINTS", jdbcType = "LONGVARBINARY") @JsonDeserialize(using = TrimmedStringArrayDeserializer.class) + @JsonView(JsonViews.MetadataTools.class) private String[] endpoints; @Persistent @Column(name = "AUTHENTICATED") + @JsonView(JsonViews.MetadataTools.class) private Boolean authenticated; @Persistent @Column(name = "X_TRUST_BOUNDARY") + @JsonView(JsonViews.MetadataTools.class) private Boolean crossesTrustBoundary; @Persistent(defaultFetchGroup = "true") @Column(name = "DATA") @Serialized + @JsonView(JsonViews.MetadataTools.class) private List data; //TODO add license support once Component license support is refactored @@ -140,6 +150,7 @@ public enum FetchGroup { @Persistent(defaultFetchGroup = "true") @Column(name = "EXTERNAL_REFERENCES") @Serialized + @JsonView(JsonViews.MetadataTools.class) private List externalReferences; @Persistent diff --git a/src/main/java/org/dependencytrack/model/Tools.java b/src/main/java/org/dependencytrack/model/Tools.java new file mode 100644 index 000000000..0d0753a74 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/Tools.java @@ -0,0 +1,12 @@ +package org.dependencytrack.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonView; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record Tools( + @JsonView(JsonViews.MetadataTools.class) List components, + @JsonView(JsonViews.MetadataTools.class) List services) { +} diff --git a/src/main/java/org/dependencytrack/model/mapping/PolicyProtoMapper.java b/src/main/java/org/dependencytrack/model/mapping/PolicyProtoMapper.java index 03513167c..df4b9d485 100644 --- a/src/main/java/org/dependencytrack/model/mapping/PolicyProtoMapper.java +++ b/src/main/java/org/dependencytrack/model/mapping/PolicyProtoMapper.java @@ -2,6 +2,9 @@ import com.google.protobuf.Timestamp; import com.google.protobuf.util.Timestamps; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.License; +import org.dependencytrack.model.LicenseGroup; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerabilityAlias; @@ -20,6 +23,43 @@ */ public class PolicyProtoMapper { + public static org.dependencytrack.proto.policy.v1.Component mapToProto(final Component component) { + if (component == null) { + return org.dependencytrack.proto.policy.v1.Component.getDefaultInstance(); + } + + // An object attached to a persistence context could do lazy loading of fields when accessing them. + // Ensure this can't happen, as it could cause massive performance degradation. + assertNonPersistent(component, "component must not be persistent"); + + final org.dependencytrack.proto.policy.v1.Component.Builder protoBuilder = + org.dependencytrack.proto.policy.v1.Component.newBuilder(); + maybeSet(asString(component.getUuid()), protoBuilder::setUuid); + maybeSet(component::getGroup, protoBuilder::setGroup); + maybeSet(component::getName, protoBuilder::setName); + maybeSet(component::getVersion, protoBuilder::setVersion); + maybeSet(asString(component.getClassifier()), protoBuilder::setClassifier); + maybeSet(component::getCpe, protoBuilder::setCpe); + maybeSet(component::getPurl, purl -> protoBuilder.setPurl(purl.canonicalize())); + maybeSet(component::getSwidTagId, protoBuilder::setSwidTagId); + maybeSet(component::isInternal, protoBuilder::setIsInternal); + maybeSet(component::getMd5, protoBuilder::setMd5); + maybeSet(component::getSha1, protoBuilder::setSha1); + maybeSet(component::getSha256, protoBuilder::setSha256); + maybeSet(component::getSha384, protoBuilder::setSha384); + maybeSet(component::getSha512, protoBuilder::setSha512); + maybeSet(component::getSha3_256, protoBuilder::setSha3256); + maybeSet(component::getSha3_384, protoBuilder::setSha3384); + maybeSet(component::getSha3_512, protoBuilder::setSha3512); + maybeSet(component::getBlake2b_256, protoBuilder::setBlake2B256); + maybeSet(component::getBlake2b_384, protoBuilder::setBlake2B384); + maybeSet(component::getBlake2b_512, protoBuilder::setBlake2B512); + maybeSet(component::getBlake3, protoBuilder::setBlake3); + maybeSet(component::getResolvedLicense, license -> protoBuilder.setResolvedLicense(mapToProto(license))); + + return protoBuilder.build(); + } + public static org.dependencytrack.proto.policy.v1.Vulnerability mapToProto(final Vulnerability vuln) { if (vuln == null) { return org.dependencytrack.proto.policy.v1.Vulnerability.getDefaultInstance(); @@ -61,6 +101,46 @@ public static org.dependencytrack.proto.policy.v1.Vulnerability mapToProto(final return protoBuilder.build(); } + private static org.dependencytrack.proto.policy.v1.License mapToProto(final License license) { + if (license == null) { + return org.dependencytrack.proto.policy.v1.License.getDefaultInstance(); + } + + // An object attached to a persistence context could do lazy loading of fields when accessing them. + // Ensure this can't happen, as it could cause massive performance degradation. + assertNonPersistent(license, "license must not be persistent"); + + final org.dependencytrack.proto.policy.v1.License.Builder protoBuilder = + org.dependencytrack.proto.policy.v1.License.newBuilder(); + maybeSet(asString(license.getUuid()), protoBuilder::setUuid); + maybeSet(license::getLicenseId, protoBuilder::setId); + maybeSet(license::getName, protoBuilder::setName); + maybeSet(license::isOsiApproved, protoBuilder::setIsOsiApproved); + maybeSet(license::isFsfLibre, protoBuilder::setIsFsfLibre); + maybeSet(license::isDeprecatedLicenseId, protoBuilder::setIsDeprecatedId); + maybeSet(license::isCustomLicense, protoBuilder::setIsCustom); + maybeSet(license::getLicenseGroups, licenseGroups -> licenseGroups.stream() + .map(PolicyProtoMapper::mapToProto).forEach(protoBuilder::addGroups)); + + return protoBuilder.build(); + } + + private static org.dependencytrack.proto.policy.v1.License.Group mapToProto(final LicenseGroup licenseGroup) { + if (licenseGroup == null) { + return org.dependencytrack.proto.policy.v1.License.Group.getDefaultInstance(); + } + + // An object attached to a persistence context could do lazy loading of fields when accessing them. + // Ensure this can't happen, as it could cause massive performance degradation. + assertNonPersistent(licenseGroup, "licenseGroup must not be persistent"); + + final org.dependencytrack.proto.policy.v1.License.Group.Builder protoBuilder = + org.dependencytrack.proto.policy.v1.License.Group.newBuilder(); + maybeSet(asString(licenseGroup.getUuid()), protoBuilder::setUuid); + maybeSet(licenseGroup::getName, protoBuilder::setName); + return protoBuilder.build(); + } + private static Stream mapToProtos(final VulnerabilityAlias alias) { if (alias == null) { return Stream.empty(); 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 d3a41ec6e..a9e11aa88 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -27,6 +27,7 @@ import org.cyclonedx.model.LicenseChoice; import org.cyclonedx.model.Metadata; import org.cyclonedx.model.Swid; +import org.cyclonedx.model.Tool; import org.dependencytrack.model.Analysis; import org.dependencytrack.model.AnalysisJustification; import org.dependencytrack.model.AnalysisResponse; @@ -43,6 +44,7 @@ import org.dependencytrack.model.ProjectMetadata; import org.dependencytrack.model.ServiceComponent; import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Tools; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.parser.common.resolver.CweResolver; import org.dependencytrack.parser.cyclonedx.CycloneDXExporter; @@ -83,14 +85,49 @@ private ModelConverter() { } public static ProjectMetadata convertToProjectMetadata(final Metadata cdxMetadata) { + if (cdxMetadata == null) { + return null; + } + final var projectMetadata = new ProjectMetadata(); projectMetadata.setSupplier(ModelConverter.convert(cdxMetadata.getSupplier())); projectMetadata.setAuthors(ModelConverter.convertCdxContacts(cdxMetadata.getAuthors())); + + final var toolComponents = new ArrayList(); + final var toolServices = new ArrayList(); + if (cdxMetadata.getTools() != null) { + cdxMetadata.getTools().stream().map(ModelConverter::convert).forEach(toolComponents::add); + } + if (cdxMetadata.getToolChoice() != null) { + if (cdxMetadata.getToolChoice().getComponents() != null) { + cdxMetadata.getToolChoice().getComponents().stream().map(ModelConverter::convertComponent).forEach(toolComponents::add); + } + if (cdxMetadata.getToolChoice().getServices() != null) { + cdxMetadata.getToolChoice().getServices().stream().map(ModelConverter::convertService).forEach(toolServices::add); + } + } + if (!toolComponents.isEmpty() || !toolServices.isEmpty()) { + projectMetadata.setTools(new Tools( + toolComponents.isEmpty() ? null : toolComponents, + toolServices.isEmpty() ? null : toolServices + )); + } + return projectMetadata; } - public static Project convertToProject(final Metadata cdxMetadata, final ProjectMetadata projectMetadata) { - final var cdxComponent = cdxMetadata.getComponent(); + public static Project convertToProject(final org.cyclonedx.model.Metadata cdxMetadata) { + if (cdxMetadata == null || cdxMetadata.getComponent() == null) { + return null; + } + + final Project project = convertToProject(cdxMetadata.getComponent()); + project.setManufacturer(convert(cdxMetadata.getManufacture())); + + return project; + } + + public static Project convertToProject(final org.cyclonedx.model.Component cdxComponent) { final var project = new Project(); project.setAuthor(trimToNull(cdxComponent.getAuthor())); project.setPublisher(trimToNull(cdxComponent.getPublisher())); @@ -100,9 +137,7 @@ public static Project convertToProject(final Metadata cdxMetadata, final Project project.setVersion(trimToNull(cdxComponent.getVersion())); project.setDescription(trimToNull(cdxComponent.getDescription())); project.setExternalReferences(convertExternalReferences(cdxComponent.getExternalReferences())); - project.setManufacturer(ModelConverter.convert(cdxMetadata.getManufacture())); project.setSupplier(ModelConverter.convert(cdxComponent.getSupplier())); - project.setMetadata(projectMetadata); if (cdxComponent.getPurl() != null) { try { @@ -229,6 +264,47 @@ public static Component convertComponent(final org.cyclonedx.model.Component cdx return component; } + private static Component convert(@SuppressWarnings("deprecation") final Tool tool) { + if (tool == null) { + return null; + } + + final var component = new Component(); + if (tool.getVendor() != null && !tool.getVendor().isBlank()) { + final var supplier = new OrganizationalEntity(); + supplier.setName(trimToNull(tool.getVendor())); + component.setSupplier(supplier); + } + component.setName(trimToNull(tool.getName())); + component.setVersion(trimToNull(tool.getVersion())); + component.setExternalReferences(convertExternalReferences(tool.getExternalReferences())); + + if (tool.getHashes() != null && !tool.getHashes().isEmpty()) { + for (final org.cyclonedx.model.Hash cdxHash : tool.getHashes()) { + final Consumer hashSetter = switch (cdxHash.getAlgorithm().toLowerCase()) { + case "md5" -> component::setMd5; + case "sha-1" -> component::setSha1; + case "sha-256" -> component::setSha256; + case "sha-384" -> component::setSha384; + case "sha-512" -> component::setSha512; + case "sha3-256" -> component::setSha3_256; + case "sha3-384" -> component::setSha3_384; + case "sha3-512" -> component::setSha3_512; + case "blake2b-256" -> component::setBlake2b_256; + case "blake2b-384" -> component::setBlake2b_384; + case "blake2b-512" -> component::setBlake2b_512; + case "blake3" -> component::setBlake3; + default -> null; + }; + if (hashSetter != null) { + hashSetter.accept(cdxHash.getValue()); + } + } + } + + return component; + } + public static OrganizationalEntity convert(final org.cyclonedx.model.OrganizationalEntity cdxEntity) { if (cdxEntity == null) { return null; diff --git a/src/main/java/org/dependencytrack/persistence/converter/AbstractJsonConverter.java b/src/main/java/org/dependencytrack/persistence/converter/AbstractJsonConverter.java index c4f9fe19f..317157ee3 100644 --- a/src/main/java/org/dependencytrack/persistence/converter/AbstractJsonConverter.java +++ b/src/main/java/org/dependencytrack/persistence/converter/AbstractJsonConverter.java @@ -19,22 +19,35 @@ package org.dependencytrack.persistence.converter; import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.json.JsonMapper; import javax.jdo.AttributeConverter; +import java.io.IOException; /** * @since 4.10.0 */ abstract class AbstractJsonConverter implements AttributeConverter { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final JsonMapper JSON_MAPPER = JsonMapper.builder() + .disable(MapperFeature.DEFAULT_VIEW_INCLUSION) + .build(); private final TypeReference typeReference; + private final Class jsonView; AbstractJsonConverter(final TypeReference typeReference) { + this(typeReference, null); + } + + AbstractJsonConverter(final TypeReference typeReference, final Class jsonView) { this.typeReference = typeReference; + this.jsonView = jsonView; } @Override @@ -43,8 +56,15 @@ public String convertToDatastore(final T attributeValue) { return null; } + final ObjectWriter objectWriter; + if (jsonView == null) { + objectWriter = JSON_MAPPER.writer(); + } else { + objectWriter = JSON_MAPPER.writerWithView(jsonView); + } + try { - return OBJECT_MAPPER.writeValueAsString(attributeValue); + return objectWriter.writeValueAsString(attributeValue); } catch (JacksonException e) { throw new RuntimeException(e); } @@ -56,9 +76,17 @@ public T convertToAttribute(final String datastoreValue) { return null; } + final ObjectReader objectReader; + if (jsonView == null) { + objectReader = JSON_MAPPER.reader(); + } else { + objectReader = JSON_MAPPER.readerWithView(jsonView); + } + try { - return OBJECT_MAPPER.readValue(datastoreValue, typeReference); - } catch (JacksonException e) { + final JsonParser jsonParser = objectReader.createParser(datastoreValue); + return objectReader.readValue(jsonParser, typeReference); + } catch (IOException e) { throw new RuntimeException(e); } } diff --git a/src/main/java/org/dependencytrack/persistence/converter/ToolsJsonConverter.java b/src/main/java/org/dependencytrack/persistence/converter/ToolsJsonConverter.java new file mode 100644 index 000000000..aadd43184 --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/converter/ToolsJsonConverter.java @@ -0,0 +1,25 @@ +package org.dependencytrack.persistence.converter; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.dependencytrack.model.JsonViews; +import org.dependencytrack.model.Tools; + +public class ToolsJsonConverter extends AbstractJsonConverter { + + public ToolsJsonConverter() { + super(new TypeReference<>() {}, JsonViews.MetadataTools.class); + } + + @Override + public String convertToDatastore(final Tools attributeValue) { + // Overriding is required for DataNucleus to correctly detect the return type. + return super.convertToDatastore(attributeValue); + } + + @Override + public Tools 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/policy/cel/CelCommonPolicyLibrary.java b/src/main/java/org/dependencytrack/policy/cel/CelCommonPolicyLibrary.java index 37e8f6e3c..f5ec3d27a 100644 --- a/src/main/java/org/dependencytrack/policy/cel/CelCommonPolicyLibrary.java +++ b/src/main/java/org/dependencytrack/policy/cel/CelCommonPolicyLibrary.java @@ -10,6 +10,7 @@ import org.dependencytrack.proto.policy.v1.Component; import org.dependencytrack.proto.policy.v1.License; import org.dependencytrack.proto.policy.v1.Project; +import org.dependencytrack.proto.policy.v1.Tools; import org.dependencytrack.proto.policy.v1.VersionDistance; import org.dependencytrack.proto.policy.v1.Vulnerability; import org.jdbi.v3.core.Handle; @@ -128,7 +129,9 @@ public List getCompileOptions() { License.getDefaultInstance(), License.Group.getDefaultInstance(), Project.getDefaultInstance(), + Project.Metadata.getDefaultInstance(), Project.Property.getDefaultInstance(), + Tools.getDefaultInstance(), Vulnerability.getDefaultInstance(), Vulnerability.Alias.getDefaultInstance(), VersionDistance.getDefaultInstance() diff --git a/src/main/java/org/dependencytrack/policy/cel/CelPolicyEngine.java b/src/main/java/org/dependencytrack/policy/cel/CelPolicyEngine.java index 85b3392b7..f38fbefae 100644 --- a/src/main/java/org/dependencytrack/policy/cel/CelPolicyEngine.java +++ b/src/main/java/org/dependencytrack/policy/cel/CelPolicyEngine.java @@ -42,8 +42,6 @@ import org.dependencytrack.policy.cel.compat.VulnerabilityIdCelPolicyScriptSourceBuilder; import org.dependencytrack.policy.cel.mapping.ComponentProjection; import org.dependencytrack.policy.cel.mapping.LicenseProjection; -import org.dependencytrack.policy.cel.mapping.ProjectProjection; -import org.dependencytrack.policy.cel.mapping.ProjectPropertyProjection; import org.dependencytrack.policy.cel.mapping.VulnerabilityProjection; import org.dependencytrack.policy.cel.persistence.CelPolicyDao; import org.dependencytrack.proto.policy.v1.Vulnerability; @@ -383,52 +381,6 @@ private static List evaluatePolicyOperators(final Collection properties = - OBJECT_MAPPER.readValue(projection.propertiesJson, new TypeReference<>() { - }); - for (final ProjectPropertyProjection property : properties) { - builder.addProperties(org.dependencytrack.proto.policy.v1.Project.Property.newBuilder() - .setGroup(trimToEmpty(property.group)) - .setName(trimToEmpty(property.name)) - .setValue(trimToEmpty(property.value)) - .setType(trimToEmpty(property.type)) - .build()); - } - } catch (JacksonException e) { - LOGGER.warn("Failed to parse properties from %s for project %s" - .formatted(projection.propertiesJson, projection.id), e); - } - } - - if (projection.tagsJson != null) { - try { - final List tags = OBJECT_MAPPER.readValue(projection.tagsJson, new TypeReference<>() { - }); - builder.addAllTags(tags); - } catch (JacksonException e) { - LOGGER.warn("Failed to parse tags from %s for project %s" - .formatted(projection.tagsJson, projection.id), e); - } - } - - return builder.build(); - } - private static org.dependencytrack.proto.policy.v1.Component mapToProto(final ComponentProjection projection, final Map protoLicenseById) { final org.dependencytrack.proto.policy.v1.Component.Builder componentBuilder = diff --git a/src/main/java/org/dependencytrack/policy/cel/CelVulnerabilityPolicyEvaluator.java b/src/main/java/org/dependencytrack/policy/cel/CelVulnerabilityPolicyEvaluator.java index 21238d817..68afa82d3 100644 --- a/src/main/java/org/dependencytrack/policy/cel/CelVulnerabilityPolicyEvaluator.java +++ b/src/main/java/org/dependencytrack/policy/cel/CelVulnerabilityPolicyEvaluator.java @@ -41,6 +41,7 @@ import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_LICENSE; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_LICENSE_GROUP; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_PROJECT; +import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_PROJECT_METADATA; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_PROJECT_PROPERTY; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_VULNERABILITY; @@ -282,6 +283,11 @@ private static String buildCacheKey(final Project project, final MultiValuedMap< cacheKeyParts.add("property.%s".formatted(propertyFieldName)); } } + if (cacheKeyParts.contains("metadata") && requirements.containsKey(TYPE_PROJECT_METADATA)) { + for (final String metadataFieldName : requirements.get(TYPE_PROJECT_METADATA)) { + cacheKeyParts.add("metadata.%s".formatted(metadataFieldName)); + } + } final String rawCacheKey = "%s|%s" .formatted(project.getUuid(), cacheKeyParts.stream().sorted().collect(Collectors.joining("|"))); diff --git a/src/main/java/org/dependencytrack/policy/cel/definition/CelPolicyTypes.java b/src/main/java/org/dependencytrack/policy/cel/definition/CelPolicyTypes.java index 672ce4e3e..821841e85 100644 --- a/src/main/java/org/dependencytrack/policy/cel/definition/CelPolicyTypes.java +++ b/src/main/java/org/dependencytrack/policy/cel/definition/CelPolicyTypes.java @@ -4,6 +4,7 @@ import org.dependencytrack.proto.policy.v1.Component; import org.dependencytrack.proto.policy.v1.License; import org.dependencytrack.proto.policy.v1.Project; +import org.dependencytrack.proto.policy.v1.Tools; import org.dependencytrack.proto.policy.v1.Vulnerability; import org.dependencytrack.proto.policy.v1.VersionDistance; import org.projectnessie.cel.checker.Decls; @@ -14,7 +15,9 @@ public class CelPolicyTypes { public static final Type TYPE_LICENSE = Decls.newObjectType(License.getDescriptor().getFullName()); public static final Type TYPE_LICENSE_GROUP = Decls.newObjectType(License.Group.getDescriptor().getFullName()); public static final Type TYPE_PROJECT = Decls.newObjectType(Project.getDescriptor().getFullName()); + public static final Type TYPE_PROJECT_METADATA = Decls.newObjectType(Project.Metadata.getDescriptor().getFullName()); public static final Type TYPE_PROJECT_PROPERTY = Decls.newObjectType(Project.Property.getDescriptor().getFullName()); + public static final Type TYPE_TOOLS = Decls.newObjectType(Tools.getDescriptor().getFullName()); public static final Type TYPE_VULNERABILITY = Decls.newObjectType(Vulnerability.getDescriptor().getFullName()); public static final Type TYPE_VULNERABILITIES = Decls.newListType(TYPE_VULNERABILITY); public static final Type TYPE_VULNERABILITY_ALIAS = Decls.newObjectType(Vulnerability.Alias.getDescriptor().getFullName()); diff --git a/src/main/java/org/dependencytrack/policy/cel/mapping/ProjectProjection.java b/src/main/java/org/dependencytrack/policy/cel/mapping/ProjectProjection.java index 40f9ab980..525bba031 100644 --- a/src/main/java/org/dependencytrack/policy/cel/mapping/ProjectProjection.java +++ b/src/main/java/org/dependencytrack/policy/cel/mapping/ProjectProjection.java @@ -38,7 +38,4 @@ public class ProjectProjection { @MappedField(protoFieldName = "last_bom_import", sqlColumnName = "LAST_BOM_IMPORTED") public Date lastBomImport; - public String propertiesJson; - public String tagsJson; - } diff --git a/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyDao.java b/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyDao.java index 3c8802f0e..f8c58bb0e 100644 --- a/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyDao.java +++ b/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyDao.java @@ -28,6 +28,7 @@ import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_COMPONENT; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_PROJECT; +import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_PROJECT_METADATA; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_PROJECT_PROPERTY; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_VULNERABILITY; import static org.dependencytrack.policy.cel.mapping.FieldMappingUtil.getFieldMappings; @@ -41,6 +42,10 @@ public interface CelPolicyDao { ${fetchColumns?join(", ")} FROM "PROJECT" AS "P" + <#if fetchColumns?filter(col -> col?contains("\\"metadata_tools\\""))?size gt 0> + INNER JOIN + "PROJECT_METADATA" AS "PM" ON "PM"."PROJECT_ID" = "P"."ID" + <#if fetchPropertyColumns?size gt 0> LEFT JOIN LATERAL ( SELECT @@ -154,6 +159,13 @@ default Project loadRequiredFields(final Project project, final MultiValuedMap fieldsToLoad.contains(fieldMapping.protoFieldName())) .map(fieldMapping -> "\"P\".\"%s\" AS \"%s\"".formatted(fieldMapping.sqlColumnName(), fieldMapping.protoFieldName())) .collect(Collectors.toList()); + + if (fieldsToLoad.contains("metadata") + && requirements.containsKey(TYPE_PROJECT_METADATA) + && requirements.get(TYPE_PROJECT_METADATA).contains("tools")) { + sqlSelectColumns.add("\"PM\".\"TOOLS\" AS \"metadata_tools\""); + } + final var sqlPropertySelectColumns = new ArrayList(); if (fieldsToLoad.contains("properties") && requirements.containsKey(TYPE_PROJECT_PROPERTY)) { sqlSelectColumns.add("\"properties\""); diff --git a/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyProjectRowMapper.java b/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyProjectRowMapper.java index 443465014..5e21eb566 100644 --- a/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyProjectRowMapper.java +++ b/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyProjectRowMapper.java @@ -3,8 +3,10 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.google.protobuf.util.JsonFormat; +import org.dependencytrack.model.mapping.PolicyProtoMapper; import org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil; import org.dependencytrack.proto.policy.v1.Project; +import org.dependencytrack.proto.policy.v1.Tools; import org.jdbi.v3.core.mapper.RowMapper; import org.jdbi.v3.core.result.UnableToProduceResultException; import org.jdbi.v3.core.statement.StatementContext; @@ -18,6 +20,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.OBJECT_MAPPER; +import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.hasColumn; import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet; public class CelPolicyProjectRowMapper implements RowMapper { @@ -37,10 +40,44 @@ public Project map(final ResultSet rs, final StatementContext ctx) throws SQLExc maybeSet(rs, "last_bom_import", RowMapperUtil::nullableTimestamp, builder::setLastBomImport); maybeSet(rs, "tags", RowMapperUtil::stringArray, builder::addAllTags); maybeSet(rs, "properties", CelPolicyProjectRowMapper::maybeConvertProperties, builder::addAllProperties); + + if (hasColumn(rs, "metadata_tools")) { + builder.setMetadata(Project.Metadata.newBuilder() + .setTools(convertMetadataTools(rs)) + .build()); + } + return builder.build(); } - private static List maybeConvertProperties(final ResultSet rs, String columnName) throws SQLException { + private static Tools convertMetadataTools(final ResultSet rs) throws SQLException { + final String jsonString = rs.getString("metadata_tools"); + if (isBlank(jsonString)) { + return Tools.getDefaultInstance(); + } + + final org.dependencytrack.model.Tools modelTools; + try { + modelTools = OBJECT_MAPPER.readValue(jsonString, org.dependencytrack.model.Tools.class); + } catch (IOException e) { + throw new UnableToProduceResultException(e); + } + + if (modelTools == null) { + return Tools.getDefaultInstance(); + } + + final var toolsBuilder = Tools.newBuilder(); + if (modelTools.components() != null) { + modelTools.components().stream() + .map(PolicyProtoMapper::mapToProto) + .forEach(toolsBuilder::addComponents); + } + + return toolsBuilder.build(); + } + + private static List maybeConvertProperties(final ResultSet rs, final String columnName) throws SQLException { final String jsonString = rs.getString(columnName); if (isBlank(jsonString)) { return Collections.emptyList(); diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java index 58cf22f36..5133e628b 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java @@ -92,6 +92,7 @@ import static org.apache.commons.lang3.StringUtils.trimToNull; import static org.datanucleus.PropertyNames.PROPERTY_FLUSH_MODE; import static org.datanucleus.PropertyNames.PROPERTY_PERSISTENCE_BY_REACHABILITY_AT_COMMIT; +import static org.datanucleus.PropertyNames.PROPERTY_RETAIN_VALUES; import static org.dependencytrack.common.ConfigKey.BOM_UPLOAD_PROCESSING_TRX_FLUSH_THRESHOLD; import static org.dependencytrack.event.kafka.componentmeta.RepoMetaConstants.SUPPORTED_PACKAGE_URLS_FOR_INTEGRITY_CHECK; import static org.dependencytrack.event.kafka.componentmeta.RepoMetaConstants.TIME_SPAN; @@ -220,15 +221,11 @@ private void processBom(final Context ctx, final File bomFile) throws BomConsump // Note: One identity can point to multiple BOM refs, due to component and service de-duplication. final var bomRefsByIdentity = new HashSetValuedHashMap(); - Project metadataComponent = null; - ProjectMetadata projectMetadata = null; + final ProjectMetadata projectMetadata = convertToProjectMetadata(cdxBom.getMetadata()); + final Project project = convertToProject(cdxBom.getMetadata()); List components = new ArrayList<>(); - if (cdxBom.getMetadata() != null) { - projectMetadata = convertToProjectMetadata(cdxBom.getMetadata()); - if (cdxBom.getMetadata().getComponent() != null) { - metadataComponent = convertToProject(cdxBom.getMetadata(), projectMetadata); - components.addAll(convertComponents(cdxBom.getMetadata().getComponent().getComponents())); - } + if (cdxBom.getMetadata() != null && cdxBom.getMetadata().getComponent() != null) { + components.addAll(convertComponents(cdxBom.getMetadata().getComponent().getComponents())); } components.addAll(convertComponents(cdxBom.getComponents())); components = flatten(components, Component::getChildren, Component::setChildren); @@ -304,30 +301,27 @@ private void processBom(final Context ctx, final File bomFile) throws BomConsump // BomUploadProcessingTaskTest#informWithBloatedBomTest can be used to profile the impact on large BOMs. pm.setProperty(PROPERTY_FLUSH_MODE, FlushMode.MANUAL.name()); + // Prevent object fields from being unloaded upon commit. + // + // DataNucleus transitions objects into the "hollow" state after the transaction is committed. + // In hollow state, all fields except the ID are unloaded. Accessing fields afterward triggers + // one or more database queries to load them again. + // See https://www.datanucleus.org/products/accessplatform_6_0/jdo/persistence.html#lifecycle + qm.getPersistenceManager().setProperty(PROPERTY_RETAIN_VALUES, "true"); + LOGGER.info("Processing %d components and %d services from BOM (%s)" .formatted(components.size(), services.size(), ctx)); final Transaction trx = pm.currentTransaction(); try { trx.begin(); - final Project project = processMetadataComponent(ctx, pm, metadataComponent); - if (projectMetadata != null) { - if (project.getMetadata() == null) { - projectMetadata.setProject(project); - qm.getPersistenceManager().makePersistent(projectMetadata); - } else { - project.getMetadata().setSupplier(projectMetadata.getSupplier()); - project.getMetadata().setAuthors(projectMetadata.getAuthors() != null - ? new ArrayList<>(projectMetadata.getAuthors()) - : null); - } - } + final Project persistentProject = processProject(ctx, pm, project, projectMetadata); final Map persistentComponents = - processComponents(qm, project, components, identitiesByBomRef, bomRefsByIdentity); + processComponents(qm, persistentProject, components, identitiesByBomRef, bomRefsByIdentity); final Map persistentServices = - processServices(qm, project, services, identitiesByBomRef, bomRefsByIdentity); - processDependencyGraph(ctx, pm, cdxBom, project, persistentComponents, persistentServices, identitiesByBomRef); - recordBomImport(ctx, pm, project); + processServices(qm, persistentProject, services, identitiesByBomRef, bomRefsByIdentity); + processDependencyGraph(ctx, pm, cdxBom, persistentProject, persistentComponents, persistentServices, identitiesByBomRef); + recordBomImport(ctx, pm, persistentProject); // BOM ref <-> ComponentIdentity indexes are no longer needed. // Let go of their contents to make it eligible for GC sooner. @@ -484,42 +478,58 @@ private static org.cyclonedx.model.Bom parseBom(final Context ctx, final File bo return bom; } - private static Project processMetadataComponent(final Context ctx, final PersistenceManager pm, final Project metadataComponent) throws BomProcessingException { + private static Project processProject(final Context ctx, final PersistenceManager pm, + final Project project, final ProjectMetadata projectMetadata) throws BomProcessingException { final Query query = pm.newQuery(Project.class); query.setFilter("uuid == :uuid"); query.setParameters(ctx.project.getUuid()); - final Project project; + final Project persistentProject; try { - project = query.executeUnique(); + persistentProject = query.executeUnique(); } finally { query.closeAll(); } - if (project == null) { + if (persistentProject == null) { throw new BomProcessingException(ctx, "Project does not exist"); } - if (metadataComponent != null) { - boolean changed = false; - changed |= applyIfChanged(project, metadataComponent, Project::getAuthor, project::setAuthor); - changed |= applyIfChanged(project, metadataComponent, Project::getPublisher, project::setPublisher); - changed |= applyIfChanged(project, metadataComponent, Project::getClassifier, project::setClassifier); - changed |= applyIfChanged(project, metadataComponent, Project::getSupplier, project::setSupplier); - changed |= applyIfChanged(project, metadataComponent, Project::getManufacturer, project::setManufacturer); + boolean hasChanged = false; + if (project != null) { + hasChanged |= applyIfChanged(persistentProject, project, Project::getAuthor, persistentProject::setAuthor); + hasChanged |= applyIfChanged(persistentProject, project, Project::getPublisher, persistentProject::setPublisher); + hasChanged |= applyIfChanged(persistentProject, project, Project::getClassifier, persistentProject::setClassifier); + hasChanged |= applyIfChanged(persistentProject, project, Project::getSupplier, persistentProject::setSupplier); + hasChanged |= applyIfChanged(persistentProject, project, Project::getManufacturer, persistentProject::setManufacturer); // TODO: Currently these properties are "decoupled" from the BOM and managed directly by DT users. // Perhaps there could be a flag for BOM uploads saying "use BOM properties" or something? - // changed |= applyIfChanged(project, metadataComponent, Project::getGroup, project::setGroup); - // changed |= applyIfChanged(project, metadataComponent, Project::getName, project::setName); - // changed |= applyIfChanged(project, metadataComponent, Project::getVersion, project::setVersion); - // changed |= applyIfChanged(project, metadataComponent, Project::getDescription, project::setDescription); - changed |= applyIfChanged(project, metadataComponent, Project::getExternalReferences, project::setExternalReferences); - changed |= applyIfChanged(project, metadataComponent, Project::getPurl, project::setPurl); - changed |= applyIfChanged(project, metadataComponent, Project::getSwidTagId, project::setSwidTagId); - if (changed) { - pm.flush(); + // hasChanged |= applyIfChanged(persistentProject, project, Project::getGroup, persistentProject::setGroup); + // hasChanged |= applyIfChanged(persistentProject, project, Project::getName, persistentProject::setName); + // hasChanged |= applyIfChanged(persistentProject, project, Project::getVersion, persistentProject::setVersion); + // hasChanged |= applyIfChanged(persistentProject, project, Project::getDescription, persistentProject::setDescription); + hasChanged |= applyIfChanged(persistentProject, project, Project::getExternalReferences, persistentProject::setExternalReferences); + hasChanged |= applyIfChanged(persistentProject, project, Project::getPurl, persistentProject::setPurl); + hasChanged |= applyIfChanged(persistentProject, project, Project::getSwidTagId, persistentProject::setSwidTagId); + } + + if (projectMetadata != null) { + if (persistentProject.getMetadata() == null) { + projectMetadata.setProject(persistentProject); + pm.makePersistent(projectMetadata); + hasChanged = true; + } else { + hasChanged |= applyIfChanged(persistentProject.getMetadata(), projectMetadata, ProjectMetadata::getAuthors, + authors -> persistentProject.getMetadata().setAuthors(authors != null ? new ArrayList<>(authors) : null)); + hasChanged |= applyIfChanged(persistentProject.getMetadata(), projectMetadata, ProjectMetadata::getSupplier, persistentProject.getMetadata()::setSupplier); + hasChanged |= applyIfChanged(persistentProject.getMetadata(), projectMetadata, ProjectMetadata::getTools, persistentProject.getMetadata()::setTools); } } - return project; + + if (hasChanged) { + pm.flush(); + } + + return persistentProject; } private static Map processComponents(final QueryManager qm, diff --git a/src/main/proto/org/dependencytrack/policy/v1/policy.proto b/src/main/proto/org/dependencytrack/policy/v1/policy.proto index 545a9b74e..c4fe36ba8 100644 --- a/src/main/proto/org/dependencytrack/policy/v1/policy.proto +++ b/src/main/proto/org/dependencytrack/policy/v1/policy.proto @@ -97,6 +97,11 @@ message Project { optional string purl = 10; optional string swid_tag_id = 11; optional google.protobuf.Timestamp last_bom_import = 12; + optional Metadata metadata = 13; + + message Metadata { + optional Tools tools = 1; + } message Property { string group = 1; @@ -106,6 +111,13 @@ message Project { } } +message Tools { + // Components used as tools. + repeated Component components = 1; + + // TODO: Add services. +} + message Vulnerability { string uuid = 1; string id = 2; diff --git a/src/main/resources/migration/changelog-main.xml b/src/main/resources/migration/changelog-main.xml index 58a4a9b67..5ab794a8e 100644 --- a/src/main/resources/migration/changelog-main.xml +++ b/src/main/resources/migration/changelog-main.xml @@ -10,5 +10,6 @@ http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> + \ No newline at end of file diff --git a/src/main/resources/migration/changelog-v5.4.0.xml b/src/main/resources/migration/changelog-v5.4.0.xml new file mode 100644 index 000000000..56e32f357 --- /dev/null +++ b/src/main/resources/migration/changelog-v5.4.0.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/model/mapping/PolicyProtoMapperTest.java b/src/test/java/org/dependencytrack/model/mapping/PolicyProtoMapperTest.java index 05002134f..e048618a0 100644 --- a/src/test/java/org/dependencytrack/model/mapping/PolicyProtoMapperTest.java +++ b/src/test/java/org/dependencytrack/model/mapping/PolicyProtoMapperTest.java @@ -112,7 +112,7 @@ public void testMapVulnerabilityWithNoFieldsSet() throws Exception { @Test public void testMapVulnerabilityToProtoWhenNull() { - assertThat(PolicyProtoMapper.mapToProto(null)) + assertThat(PolicyProtoMapper.mapToProto((Vulnerability) null)) .isEqualTo(org.dependencytrack.proto.policy.v1.Vulnerability.getDefaultInstance()); } diff --git a/src/test/java/org/dependencytrack/persistence/converter/ToolsJsonConverterTest.java b/src/test/java/org/dependencytrack/persistence/converter/ToolsJsonConverterTest.java new file mode 100644 index 000000000..e6d057c42 --- /dev/null +++ b/src/test/java/org/dependencytrack/persistence/converter/ToolsJsonConverterTest.java @@ -0,0 +1,305 @@ +package org.dependencytrack.persistence.converter; + +import org.dependencytrack.model.Classifier; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.DataClassification; +import org.dependencytrack.model.ExternalReference; +import org.dependencytrack.model.OrganizationalEntity; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.ServiceComponent; +import org.dependencytrack.model.Tools; +import org.dependencytrack.model.Vulnerability; +import org.junit.Test; + +import java.util.List; +import java.util.UUID; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +public class ToolsJsonConverterTest { + + @Test + public void testConvertToDatastore() { + final var project = new Project(); + project.setName("acme-app"); + + final var componentSupplier = new OrganizationalEntity(); + componentSupplier.setName("componentSupplierName"); + + final var externalReference = new ExternalReference(); + externalReference.setType(org.cyclonedx.model.ExternalReference.Type.DOCUMENTATION); + externalReference.setUrl("https://example.com"); + + final var vuln = new Vulnerability(); + vuln.setVulnId("INT-001"); + vuln.setSource(Vulnerability.Source.INTERNAL); + + final var component = new Component(); + component.setProject(project); + component.setId(123); + component.setUuid(UUID.randomUUID()); + component.setAuthor("componentAuthor"); + component.setPublisher("componentPublisher"); + component.setSupplier(componentSupplier); + component.setGroup("componentGroup"); + component.setName("componentName"); + component.setVersion("componentVersion"); + component.setClassifier(Classifier.LIBRARY); + component.setFilename("componentFilename"); + component.setExtension("componentExtension"); + component.setMd5("componentMd5"); + component.setSha1("componentSha1"); + component.setSha256("componentSha256"); + component.setSha384("componentSha384"); + component.setSha512("componentSha512"); + component.setSha3_256("componentSha3_256"); + component.setSha3_384("componentSha3_384"); + component.setSha3_512("componentSha3_512"); + component.setBlake2b_256("componentBlake2b_256"); + component.setBlake2b_384("componentBlake2b_384"); + component.setBlake2b_512("componentBlake2b_512"); + component.setBlake3("componentBlake3"); + component.setCpe("componentCpe"); + component.setPurl("pkg:maven/componentGroup/componentName@componentVersion?foo=bar"); + component.setPurlCoordinates("pkg:maven/componentGroup/componentName@componentVersion"); + component.setSwidTagId("componentSwidTagId"); + component.setInternal(true); + component.setDescription("componentDescription"); + component.setCopyright("componentCopyright"); + component.setLicense("componentLicense"); + component.setLicenseExpression("componentLicenseExpression"); + component.setLicenseUrl("componentLicenseUrl"); + component.setDirectDependencies("componentDirectDependencies"); + component.setExternalReferences(List.of(externalReference)); + component.setParent(component); + component.setChildren(List.of(component)); + component.setVulnerabilities(List.of(vuln)); + component.setLastInheritedRiskScore(10.0); + component.setNotes("componentNotes"); + + final var serviceProvider = new OrganizationalEntity(); + serviceProvider.setName("serviceProviderName"); + + final var serviceDataClassification = new DataClassification(); + serviceDataClassification.setDirection(DataClassification.Direction.OUTBOUND); + serviceDataClassification.setName("serviceDataClassificationName"); + + final var service = new ServiceComponent(); + service.setProject(project); + service.setId(123); + service.setUuid(UUID.randomUUID()); + service.setProvider(serviceProvider); + service.setGroup("serviceGroup"); + service.setName("serviceName"); + service.setVersion("serviceVersion"); + service.setDescription("serviceDescription"); + service.setEndpoints(new String[]{"https://example.com"}); + service.setAuthenticated(true); + service.setCrossesTrustBoundary(true); + service.setData(List.of(serviceDataClassification)); + service.setExternalReferences(List.of(externalReference)); + service.setParent(service); + service.setChildren(List.of(service)); + service.setVulnerabilities(List.of(vuln)); + service.setLastInheritedRiskScore(11.0); + service.setNotes("serviceNotes"); + + assertThatJson(new ToolsJsonConverter().convertToDatastore(new Tools(List.of(component), List.of(service)))) + .isEqualTo(""" + { + "components": [ + { + "author": "componentAuthor", + "blake2b_256": "componentBlake2b_256", + "blake2b_384": "componentBlake2b_384", + "blake2b_512": "componentBlake2b_512", + "blake3": "componentBlake3", + "classifier": "LIBRARY", + "cpe": "componentCpe", + "externalReferences": [ + { + "type": "documentation", + "url": "https://example.com" + } + ], + "group": "componentGroup", + "md5": "componentmd5", + "name": "componentName", + "publisher": "componentPublisher", + "purl": "pkg:maven/componentGroup/componentName@componentVersion?foo=bar", + "sha1": "componentsha1", + "sha256": "componentsha256", + "sha384": "componentsha384", + "sha3_256": "componentsha3_256", + "sha3_384": "componentsha3_384", + "sha3_512": "componentsha3_512", + "sha512": "componentsha512", + "supplier": { + "name": "componentSupplierName" + }, + "swidTagId": "componentSwidTagId", + "version": "componentVersion" + } + ], + "services": [ + { + "provider": { + "name": "serviceProviderName" + }, + "group": "serviceGroup", + "name": "serviceName", + "version": "serviceVersion", + "description": "serviceDescription", + "endpoints": [ + "https://example.com" + ], + "authenticated": true, + "crossesTrustBoundary": true, + "data": [ + { + "direction": "OUTBOUND", + "name": "serviceDataClassificationName" + } + ], + "externalReferences": [ + { + "type": "documentation", + "url": "https://example.com" + } + ] + } + ] + } + """); + } + + @Test + public void testConvertToAttribute() { + final Tools tools = new ToolsJsonConverter().convertToAttribute(""" + { + "components": [ + { + "author": "componentAuthor", + "blake2b_256": "componentBlake2b_256", + "blake2b_384": "componentBlake2b_384", + "blake2b_512": "componentBlake2b_512", + "blake3": "componentBlake3", + "classifier": "LIBRARY", + "cpe": "componentCpe", + "externalReferences": [ + { + "type": "documentation", + "url": "https://example.com" + } + ], + "group": "componentGroup", + "md5": "componentmd5", + "name": "componentName", + "publisher": "componentPublisher", + "purl": "pkg:maven/componentGroup/componentName@componentVersion?foo=bar", + "sha1": "componentsha1", + "sha256": "componentsha256", + "sha384": "componentsha384", + "sha3_256": "componentsha3_256", + "sha3_384": "componentsha3_384", + "sha3_512": "componentsha3_512", + "sha512": "componentsha512", + "supplier": { + "name": "componentSupplierName" + }, + "swidTagId": "componentSwidTagId", + "version": "componentVersion" + } + ], + "services": [ + { + "provider": { + "name": "serviceProviderName" + }, + "group": "serviceGroup", + "name": "serviceName", + "version": "serviceVersion", + "description": "serviceDescription", + "endpoints": [ + "https://example.com" + ], + "authenticated": true, + "crossesTrustBoundary": true, + "data": [ + { + "direction": "OUTBOUND", + "name": "serviceDataClassificationName" + } + ], + "externalReferences": [ + { + "type": "documentation", + "url": "https://example.com" + } + ] + } + ] + } + """); + + assertThat(tools).isNotNull(); + assertThat(tools.components()).satisfiesExactly(component -> { + assertThat(component.getAuthor()).isEqualTo("componentAuthor"); + assertThat(component.getBlake2b_256()).isEqualTo("componentBlake2b_256"); + assertThat(component.getBlake2b_384()).isEqualTo("componentBlake2b_384"); + assertThat(component.getBlake2b_512()).isEqualTo("componentBlake2b_512"); + assertThat(component.getBlake3()).isEqualTo("componentBlake3"); + assertThat(component.getClassifier()).isEqualTo(Classifier.LIBRARY); + assertThat(component.getCpe()).isEqualTo("componentCpe"); + assertThat(component.getExternalReferences()).satisfiesExactly(externalReference -> { + assertThat(externalReference.getType()).isEqualTo(org.cyclonedx.model.ExternalReference.Type.DOCUMENTATION); + assertThat(externalReference.getUrl()).isEqualTo("https://example.com"); + }); + assertThat(component.getGroup()).isEqualTo("componentGroup"); + assertThat(component.getMd5()).isEqualTo("componentmd5"); + assertThat(component.getName()).isEqualTo("componentName"); + assertThat(component.getPublisher()).isEqualTo("componentPublisher"); + assertThat(component.getPurl()).asString().isEqualTo("pkg:maven/componentGroup/componentName@componentVersion?foo=bar"); + assertThat(component.getSha1()).isEqualTo("componentsha1"); + assertThat(component.getSha256()).isEqualTo("componentsha256"); + assertThat(component.getSha384()).isEqualTo("componentsha384"); + assertThat(component.getSha512()).isEqualTo("componentsha512"); + assertThat(component.getSha3_256()).isEqualTo("componentsha3_256"); + assertThat(component.getSha3_384()).isEqualTo("componentsha3_384"); + assertThat(component.getSha3_512()).isEqualTo("componentsha3_512"); + assertThat(component.getSupplier()).satisfies(supplier -> assertThat(supplier.getName()).isEqualTo("componentSupplierName")); + assertThat(component.getSwidTagId()).isEqualTo("componentSwidTagId"); + assertThat(component.getVersion()).isEqualTo("componentVersion"); + }); + assertThat(tools.services()).satisfiesExactly(service -> { + assertThat(service.getProvider()).satisfies(provider -> assertThat(provider.getName()).isEqualTo("serviceProviderName")); + assertThat(service.getGroup()).isEqualTo("serviceGroup"); + assertThat(service.getName()).isEqualTo("serviceName"); + assertThat(service.getVersion()).isEqualTo("serviceVersion"); + assertThat(service.getDescription()).isEqualTo("serviceDescription"); + assertThat(service.getEndpoints()).containsOnly("https://example.com"); + assertThat(service.getAuthenticated()).isTrue(); + assertThat(service.getCrossesTrustBoundary()).isTrue(); + assertThat(service.getData()).satisfiesExactly(classification -> { + assertThat(classification.getDirection()).isEqualTo(DataClassification.Direction.OUTBOUND); + assertThat(classification.getName()).isEqualTo("serviceDataClassificationName"); + }); + assertThat(service.getExternalReferences()).satisfiesExactly(externalReference -> { + assertThat(externalReference.getType()).isEqualTo(org.cyclonedx.model.ExternalReference.Type.DOCUMENTATION); + assertThat(externalReference.getUrl()).isEqualTo("https://example.com"); + }); + }); + } + + @Test + public void testConvertToDatastoreNull() { + assertThat(new ToolsJsonConverter().convertToDatastore(null)).isNull(); + } + + @Test + public void testConvertToAttributeNull() { + assertThat(new ToolsJsonConverter().convertToAttribute(null)).isNull(); + } + +} \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java b/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java index 9469a9468..a4e216d4f 100644 --- a/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java +++ b/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java @@ -18,10 +18,12 @@ import org.dependencytrack.model.PolicyCondition; import org.dependencytrack.model.PolicyViolation; import org.dependencytrack.model.Project; +import org.dependencytrack.model.ProjectMetadata; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; import org.dependencytrack.model.Severity; import org.dependencytrack.model.Tag; +import org.dependencytrack.model.Tools; import org.dependencytrack.model.ViolationAnalysisState; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerabilityAlias; @@ -69,7 +71,7 @@ public void before() throws Exception { * Data being available means: *
    *
  • Expression requirements were analyzed correctly
  • - *
  • Database was retrieved from the database correctly
  • + *
  • Data was retrieved from the database correctly
  • *
  • The mapping from DB data to CEL Protobuf models worked as expected
  • *
*/ @@ -88,6 +90,43 @@ public void testEvaluateProjectWithAllFields() { project.setLastBomImport(new java.util.Date()); qm.persist(project); + final var toolComponentLicense = new License(); + toolComponentLicense.setUuid(UUID.randomUUID()); + toolComponentLicense.setLicenseId("toolComponentLicenseId"); + + final var toolComponent = new Component(); + toolComponent.setGroup("toolComponentGroup"); + toolComponent.setName("toolComponentName"); + toolComponent.setVersion("toolComponentVersion"); + toolComponent.setClassifier(Classifier.APPLICATION); + toolComponent.setCpe("toolComponentCpe"); + toolComponent.setPurl("pkg:maven/toolComponentGroup/toolComponentName@toolComponentVersion"); // NB: Must be valid PURL, otherwise it's being JSON serialized as null + toolComponent.setSwidTagId("toolComponentSwidTagId"); + toolComponent.setInternal(true); // NB: Currently ignored for tool components. + toolComponent.setMd5("toolComponentMd5"); + toolComponent.setSha1("toolComponentSha1"); + toolComponent.setSha256("toolComponentSha256"); + toolComponent.setSha384("toolComponentSha384"); + toolComponent.setSha512("toolComponentSha512"); + toolComponent.setSha3_256("toolComponentSha3_256"); + toolComponent.setSha3_384("toolComponentSha3_384"); + toolComponent.setSha3_512("toolComponentSha3_512"); + toolComponent.setBlake2b_256("toolComponentBlake2b_256"); + toolComponent.setBlake2b_384("toolComponentBlake2b_384"); + toolComponent.setBlake2b_512("toolComponentBlake2b_512"); + toolComponent.setBlake3("toolComponentBlake3"); + // NB: License data is currently ignored for tool components. + // Including it in the test for documentation purposes. + toolComponent.setLicense("toolComponentLicense"); + toolComponent.setLicenseExpression("toolComponentLicenseExpression"); + toolComponent.setLicenseUrl("toolComponentLicenseUrl"); + toolComponent.setResolvedLicense(toolComponentLicense); + + final var projectMetadata = new ProjectMetadata(); + projectMetadata.setProject(project); + projectMetadata.setTools(new Tools(List.of(toolComponent), null)); + qm.persist(projectMetadata); + qm.createProjectProperty(project, "propertyGroup", "propertyName", "propertyValue", IConfigProperty.PropertyType.STRING, null); qm.bind(project, List.of( @@ -231,6 +270,31 @@ public void testEvaluateProjectWithAllFields() { && project.purl == "projectPurl" && project.swid_tag_id == "projectSwidTagId" && has(project.last_bom_import) + && project.metadata.tools.components.all(tool, + tool.group == "toolComponentGroup" + && tool.name == "toolComponentName" + && tool.version == "toolComponentVersion" + && tool.classifier == "APPLICATION" + && tool.cpe == "toolComponentCpe" + && tool.purl == "pkg:maven/toolComponentGroup/toolComponentName@toolComponentVersion" + && tool.swid_tag_id == "toolComponentSwidTagId" + && !tool.is_internal + && tool.md5 == "toolcomponentmd5" + && tool.sha1 == "toolcomponentsha1" + && tool.sha256 == "toolcomponentsha256" + && tool.sha384 == "toolcomponentsha384" + && tool.sha512 == "toolcomponentsha512" + && tool.sha3_256 == "toolcomponentsha3_256" + && tool.sha3_384 == "toolcomponentsha3_384" + && tool.sha3_512 == "toolcomponentsha3_512" + && tool.blake2b_256 == "toolComponentBlake2b_256" + && tool.blake2b_384 == "toolComponentBlake2b_384" + && tool.blake2b_512 == "toolComponentBlake2b_512" + && tool.blake3 == "toolComponentBlake3" + && !has(tool.license_name) + && !has(tool.license_expression) + && !has(tool.resolved_license) + ) && "projecttaga" in project.tags && project.properties.all(property, property.group == "propertyGroup" @@ -1582,7 +1646,7 @@ public void testEvaluateProjectWithFuncMatchesRangeWithInvalidRange() { qm.createPolicyCondition(policy, PolicyCondition.Subject.EXPRESSION, PolicyCondition.Operator.MATCHES, """ project.matches_range("foo") && component.matches_range("bar") - """); + """, PolicyViolation.Type.OPERATIONAL); final var project = new Project(); project.setName("acme-app"); @@ -1606,6 +1670,45 @@ public void testEvaluateProjectWithFuncMatchesRangeWithInvalidRange() { assertThat(qm.getAllPolicyViolations(componentB)).isEmpty(); } + @Test + public void testEvaluateProjectWithToolMetadata() { + final var policy = qm.createPolicy("policy", Policy.Operator.ANY, Policy.ViolationState.FAIL); + qm.createPolicyCondition(policy, PolicyCondition.Subject.EXPRESSION, PolicyCondition.Operator.MATCHES, """ + project.metadata.tools.components.exists(tool, + tool.name == "toolName" && tool.matches_range("vers:generic/>=1.2.3|<3")) + """, PolicyViolation.Type.OPERATIONAL); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("0.1"); + qm.persist(project); + + final var toolComponent = new Component(); + toolComponent.setName("toolName"); + toolComponent.setVersion("2.3.1"); + + final var projectMetadata = new ProjectMetadata(); + projectMetadata.setProject(project); + projectMetadata.setTools(new Tools(List.of(toolComponent), null)); + qm.persist(projectMetadata); + + final var componentA = new Component(); + componentA.setProject(project); + componentA.setName("acme-lib-a"); + componentA.setVersion("v1.9.3"); + qm.persist(componentA); + + assertThatNoException().isThrownBy(() -> new CelPolicyEngine().evaluateProject(project.getUuid())); + assertThat(qm.getAllPolicyViolations(componentA)).hasSize(1); + + toolComponent.setVersion("3.1"); + projectMetadata.setTools(new Tools(List.of(toolComponent), null)); + qm.persist(projectMetadata); + + assertThatNoException().isThrownBy(() -> new CelPolicyEngine().evaluateProject(project.getUuid())); + assertThat(qm.getAllPolicyViolations(componentA)).isEmpty(); + } + @Test public void testEvaluateProjectWhenProjectDoesNotExist() { assertThatNoException().isThrownBy(() -> new CelPolicyEngine().evaluateProject(UUID.randomUUID())); diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index 86c1c8cd3..876ef1503 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -907,6 +907,63 @@ public void informWithBomContainingServiceTest() throws Exception { assertThat(qm.getAllServiceComponents(project)).isNotEmpty(); } + @Test + public void informWithBomContainingMetadataToolsDeprecatedTest() throws Exception { + final Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + + final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), createTempBomFile("bom-metadata-tool-deprecated.json")); + qm.createWorkflowSteps(bomUploadEvent.getChainIdentifier()); + new BomUploadProcessingTask().inform(bomUploadEvent); + assertBomProcessedNotification(); + + qm.getPersistenceManager().refresh(project); + assertThat(project.getMetadata()).isNotNull(); + assertThat(project.getMetadata().getTools()).isNotNull(); + assertThat(project.getMetadata().getTools().components()).satisfiesExactly(component -> { + assertThat(component.getSupplier()).isNotNull(); + assertThat(component.getSupplier().getName()).isEqualTo("Awesome Vendor"); + assertThat(component.getName()).isEqualTo("Awesome Tool"); + assertThat(component.getVersion()).isEqualTo("9.1.2"); + assertThat(component.getSha1()).isEqualTo("25ed8e31b995bb927966616df2a42b979a2717f0"); + assertThat(component.getSha256()).isEqualTo("a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df"); + }); + assertThat(project.getMetadata().getTools().services()).isNull(); + } + + @Test + public void informWithBomContainingMetadataToolsTest() throws Exception { + final Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + + final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), createTempBomFile("bom-metadata-tool.json")); + qm.createWorkflowSteps(bomUploadEvent.getChainIdentifier()); + new BomUploadProcessingTask().inform(bomUploadEvent); + assertBomProcessedNotification(); + + qm.getPersistenceManager().refresh(project); + assertThat(project.getMetadata()).isNotNull(); + assertThat(project.getMetadata().getTools()).isNotNull(); + assertThat(project.getMetadata().getTools().components()).satisfiesExactly(component -> { + assertThat(component.getGroup()).isEqualTo("Awesome Vendor"); + assertThat(component.getName()).isEqualTo("Awesome Tool"); + assertThat(component.getVersion()).isEqualTo("9.1.2"); + assertThat(component.getSha1()).isEqualTo("25ed8e31b995bb927966616df2a42b979a2717f0"); + assertThat(component.getSha256()).isEqualTo("a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df"); + }); + assertThat(project.getMetadata().getTools().services()).satisfiesExactly(service -> { + assertThat(service.getProvider()).isNotNull(); + assertThat(service.getProvider().getName()).isEqualTo("Acme Org"); + assertThat(service.getProvider().getUrls()).containsOnly("https://example.com"); + assertThat(service.getGroup()).isEqualTo("com.example"); + assertThat(service.getName()).isEqualTo("Acme Signing Server"); + assertThat(service.getDescription()).isEqualTo("Signs artifacts"); + assertThat(service.getEndpoints()).containsExactlyInAnyOrder( + "https://example.com/sign", + "https://example.com/verify", + "https://example.com/tsa" + ); + }); + } + private void assertBomProcessedNotification() throws Exception { try { assertThat(kafkaMockProducer.history()).anySatisfy(record -> { diff --git a/src/test/resources/unit/bom-metadata-tool-deprecated.json b/src/test/resources/unit/bom-metadata-tool-deprecated.json new file mode 100644 index 000000000..7e578d7f5 --- /dev/null +++ b/src/test/resources/unit/bom-metadata-tool-deprecated.json @@ -0,0 +1,26 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "tools": [ + { + "vendor": "Awesome Vendor", + "name": "Awesome Tool", + "version": "9.1.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "25ed8e31b995bb927966616df2a42b979a2717f0" + }, + { + "alg": "SHA-256", + "content": "a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df" + } + ] + } + ] + }, + "components": [] +} \ No newline at end of file diff --git a/src/test/resources/unit/bom-metadata-tool.json b/src/test/resources/unit/bom-metadata-tool.json new file mode 100644 index 000000000..aa55d6765 --- /dev/null +++ b/src/test/resources/unit/bom-metadata-tool.json @@ -0,0 +1,47 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "tools": { + "components": [ + { + "type": "application", + "group": "Awesome Vendor", + "name": "Awesome Tool", + "version": "9.1.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "25ed8e31b995bb927966616df2a42b979a2717f0" + }, + { + "alg": "SHA-256", + "content": "a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df" + } + ] + } + ], + "services": [ + { + "provider": { + "name": "Acme Org", + "url": [ + "https://example.com" + ] + }, + "group": "com.example", + "name": "Acme Signing Server", + "description": "Signs artifacts", + "endpoints": [ + "https://example.com/sign", + "https://example.com/verify", + "https://example.com/tsa" + ] + } + ] + } + }, + "components": [] +} \ No newline at end of file