diff --git a/src/main/java/org/dependencytrack/metrics/Metrics.java b/src/main/java/org/dependencytrack/metrics/Metrics.java index 1164a5c16..6b0b6ed15 100644 --- a/src/main/java/org/dependencytrack/metrics/Metrics.java +++ b/src/main/java/org/dependencytrack/metrics/Metrics.java @@ -87,4 +87,12 @@ public static void updateComponentMetrics(final UUID componentUuid) { .invoke()); } + /** + * Update metrics related to cryptography. + * + * @since 5.5.0 + */ + public static void updateCryptographyMetrics() { + useJdbiHandle(handle -> handle.createCall("CALL \"UPDATE_CRYPTOGRAPHY_METRICS\"()").invoke()); + } } diff --git a/src/main/java/org/dependencytrack/model/CipherSuite.java b/src/main/java/org/dependencytrack/model/CipherSuite.java new file mode 100644 index 000000000..dcb3d76ef --- /dev/null +++ b/src/main/java/org/dependencytrack/model/CipherSuite.java @@ -0,0 +1,88 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +import java.io.Serializable; +import java.util.List; + +import javax.jdo.annotations.Column; +import javax.jdo.annotations.Element; +import javax.jdo.annotations.Extension; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.Join; +import javax.jdo.annotations.Order; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +@PersistenceCapable(table = "CIPHER_SUITE") +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CipherSuite implements Serializable { + + private static final long serialVersionUID = 8548267900098588016L; + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + @Persistent + @Column(name = "NAME", jdbcType = "VARCHAR", length=64) + private String name; + + @Persistent(table = "CIPHER_SUITE_ALGORITHM", defaultFetchGroup = "true") + @Join(column = "CIPHER_SUITE_ID") + @Element(column = "ALGORITHM", dependent = "true") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "id ASC")) + private List algorithms; + + @Persistent(table = "CIPHER_SUITE_IDENTIFIER", defaultFetchGroup = "true") + @Join(column = "CIPHER_SUITE_ID") + @Element(column = "IDENTIFIER", dependent = "true") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "id ASC")) + private List identifiers; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getAlgorithms() { + return algorithms; + } + + public void setAlgorithms(List algorithms) { + this.algorithms = algorithms; + } + + public List getIdentifiers() { + return identifiers; + } + + public void setIdentifiers(List identifiers) { + this.identifiers = identifiers; + } +} + diff --git a/src/main/java/org/dependencytrack/model/Classifier.java b/src/main/java/org/dependencytrack/model/Classifier.java index 7f63c63d5..6405e1310 100644 --- a/src/main/java/org/dependencytrack/model/Classifier.java +++ b/src/main/java/org/dependencytrack/model/Classifier.java @@ -36,5 +36,6 @@ public enum Classifier { PLATFORM, DEVICE_DRIVER, MACHINE_LEARNING_MODEL, - DATA + DATA, + CRYPTOGRAPHIC_ASSET } diff --git a/src/main/java/org/dependencytrack/model/Component.java b/src/main/java/org/dependencytrack/model/Component.java index 2a3c1dd36..975e7bb09 100644 --- a/src/main/java/org/dependencytrack/model/Component.java +++ b/src/main/java/org/dependencytrack/model/Component.java @@ -78,6 +78,7 @@ @Persistent(name = "children"), @Persistent(name = "properties"), @Persistent(name = "vulnerabilities"), + @Persistent(name = "cryptoAssetProperties") }), @FetchGroup(name = "IDENTITY", members = { @Persistent(name = "id"), @@ -390,6 +391,17 @@ public enum FetchGroup { @NotNull private UUID uuid; + @Persistent(defaultFetchGroup = "true", dependent = "true") + @Index(name = "COMPONENT_CRYPTO_PROPERTIES_ID_IDX") + @Column(name = "CRYPTO_PROPERTIES_ID", allowsNull = "true") + private CryptoAssetProperties cryptoAssetProperties; + + @Persistent(table = "COMPONENT_OCCURRENCES") + @Join(column = "COMPONENT_ID") + @Element(column = "OCCURRENCE_ID", dependent = "true") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "id ASC")) + private List occurrences; + private transient String bomRef; private transient List licenseCandidates; private transient DependencyMetrics metrics; @@ -886,6 +898,21 @@ public String getAuthor(){ public void setAuthor(String author){ this.author=author; } + public CryptoAssetProperties getCryptoAssetProperties() { + return cryptoAssetProperties; + } + + public void setCryptoAssetProperties(CryptoAssetProperties cryptoAssetProperties) { + this.cryptoAssetProperties = cryptoAssetProperties; + } + + public List getOccurrences() { + return occurrences; + } + + public void setOccurrences(List occurrences) { + this.occurrences = occurrences; + } @Override public String toString() { diff --git a/src/main/java/org/dependencytrack/model/ComponentIdentity.java b/src/main/java/org/dependencytrack/model/ComponentIdentity.java index f34f20631..939e2cda1 100644 --- a/src/main/java/org/dependencytrack/model/ComponentIdentity.java +++ b/src/main/java/org/dependencytrack/model/ComponentIdentity.java @@ -20,6 +20,8 @@ import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; + +import org.cyclonedx.model.component.crypto.enums.AssetType; import org.dependencytrack.util.PurlUtil; import org.json.JSONObject; @@ -47,6 +49,8 @@ public enum ObjectType { private String name; private String version; private UUID uuid; + private String oid; + private AssetType assetType; public ComponentIdentity(final PackageURL purl, final String cpe, final String swidTagId, final String group, final String name, final String version) { @@ -70,6 +74,8 @@ public ComponentIdentity(final Component component) { this.version = component.getVersion(); this.uuid = component.getUuid(); this.objectType = ObjectType.COMPONENT; + this.assetType = component.getCryptoAssetProperties() != null ? component.getCryptoAssetProperties().getAssetType() : null; + this.oid = component.getCryptoAssetProperties() != null ? component.getCryptoAssetProperties().getOid() : null; } public ComponentIdentity(final org.cyclonedx.model.Component component) { @@ -85,6 +91,8 @@ public ComponentIdentity(final org.cyclonedx.model.Component component) { this.name = component.getName(); this.version = component.getVersion(); this.objectType = ObjectType.COMPONENT; + this.assetType = component.getCryptoProperties() != null ? component.getCryptoProperties().getAssetType() : null; + this.oid = component.getCryptoProperties() != null ? component.getCryptoProperties().getOid() : null; } public ComponentIdentity(final ServiceComponent service) { @@ -102,6 +110,12 @@ public ComponentIdentity(final org.cyclonedx.model.Service service) { this.objectType = ObjectType.SERVICE; } + // search crypto assets by asset type + public ComponentIdentity(AssetType assetType) { + this.objectType = ObjectType.COMPONENT; + this.assetType = assetType; + } + public ObjectType getObjectType() { return objectType; } @@ -138,17 +152,43 @@ public UUID getUuid() { return uuid; } - @Override + public AssetType getAssetType() { + return assetType; + } + + public void setAssetType(AssetType assetType) { + this.assetType = assetType; + } + + public String getOid() { + return oid; + } + + public void setOid(String oid) { + this.oid = oid; + } + + @Override public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final ComponentIdentity that = (ComponentIdentity) o; - return objectType == that.objectType && Objects.equals(purl, that.purl) && Objects.equals(purlCoordinates, that.purlCoordinates) && Objects.equals(cpe, that.cpe) && Objects.equals(swidTagId, that.swidTagId) && Objects.equals(group, that.group) && Objects.equals(name, that.name) && Objects.equals(version, that.version) && Objects.equals(uuid, that.uuid); + return objectType == that.objectType && + Objects.equals(purl, that.purl) && + Objects.equals(purlCoordinates, that.purlCoordinates) && + Objects.equals(cpe, that.cpe) && + Objects.equals(swidTagId, that.swidTagId) && + Objects.equals(group, that.group) && + Objects.equals(name, that.name) && + Objects.equals(version, that.version) && + Objects.equals(uuid, that.uuid) && + assetType == that.assetType && + Objects.equals(oid, that.oid); } @Override public int hashCode() { - return Objects.hash(objectType, purl, purlCoordinates, cpe, swidTagId, group, name, version, uuid); + return Objects.hash(objectType, purl, purlCoordinates, cpe, swidTagId, group, name, version, uuid, assetType, oid); } public JSONObject toJSON() { diff --git a/src/main/java/org/dependencytrack/model/CryptoAlgorithmProperties.java b/src/main/java/org/dependencytrack/model/CryptoAlgorithmProperties.java new file mode 100644 index 000000000..8546bed09 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/CryptoAlgorithmProperties.java @@ -0,0 +1,179 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +import java.io.Serializable; +import java.util.List; + +import javax.jdo.annotations.Column; +import javax.jdo.annotations.Element; +import javax.jdo.annotations.Extension; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.Join; +import javax.jdo.annotations.Order; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; + +import org.cyclonedx.model.component.crypto.enums.CertificationLevel; +import org.cyclonedx.model.component.crypto.enums.CryptoFunction; +import org.cyclonedx.model.component.crypto.enums.ExecutionEnvironment; +import org.cyclonedx.model.component.crypto.enums.ImplementationPlatform; +import org.cyclonedx.model.component.crypto.enums.Mode; +import org.cyclonedx.model.component.crypto.enums.Padding; +import org.cyclonedx.model.component.crypto.enums.Primitive; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +@PersistenceCapable(table= "ALGORITHM_PROPERTIES") +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CryptoAlgorithmProperties implements Serializable { + + private static final long serialVersionUID = 6421255046930674722L; + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + // @Persistent + // @Column(name = "CRYPTO_PROPERTIES_ID") + // @JsonIgnore + // private CryptoAssetProperties cryptoAssetProperties; + + // public CryptoAssetProperties getCryptoAssetProperties() { + // return cryptoAssetProperties; + // } + + // public void setCryptoAssetProperties(CryptoAssetProperties cryptoAssetProperties) { + // this.cryptoAssetProperties = cryptoAssetProperties; + // } + + /* + * algorithmProperties + */ + @Persistent + @Column(name = "PRIMITIVE", jdbcType = "VARCHAR", length=32) + private Primitive primitive; + @Persistent + @Column(name = "PARAMETER_SET_ID", jdbcType = "VARCHAR", length=32) + private String parameterSetIdentifier; + @Persistent + @Column(name = "CURVE", jdbcType = "VARCHAR", length=32) + private String curve; + @Persistent + @Column(name = "EXECUTION_ENV", jdbcType = "VARCHAR", length=32) + private ExecutionEnvironment executionEnvironment; + @Persistent + @Column(name = "IMPLEMENTATION_PLATFORM", jdbcType = "VARCHAR", length=32) + private ImplementationPlatform implementationPlatform; + @Persistent + @Column(name = "CERTIFICATION_LEVEL", jdbcType = "VARCHAR", length=32) + private CertificationLevel certificationLevel; + @Persistent + @Column(name = "MODE", jdbcType = "VARCHAR", length=16) + private Mode mode; + @Persistent + @Column(name = "PADDING", jdbcType = "VARCHAR", length=16) + private Padding padding; + + @Persistent(table = "CRYPTO_FUNCTIONS", defaultFetchGroup = "true") + @Join(column="ALGORITHM_PROPERTY_ID") + @Element(column="CRYPTO_FUNCTION", dependent = "true") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "id ASC")) + private List cryptoFunctions; + + @Persistent + @Column(name = "CLASSICAL_SECURITY_LEVEL") + private Integer classicalSecurityLevel; + @Persistent + @Column(name = "NIST_QUANTUM_SECURITY_LEVEL") + private Integer nistQuantumSecurityLevel; + + public Primitive getPrimitive() { + return primitive; + } + public void setPrimitive(Primitive primitive) { + this.primitive = primitive; + } + public String getParameterSetIdentifier() { + return parameterSetIdentifier; + } + public void setParameterSetIdentifier(String parameterSetIdentifier) { + this.parameterSetIdentifier = parameterSetIdentifier; + } + public String getCurve() { + return curve; + } + public void setCurve(String curve) { + this.curve = curve; + } + public ExecutionEnvironment getExecutionEnvironment() { + return executionEnvironment; + } + public void setExecutionEnvironment(ExecutionEnvironment executionEnvironment) { + this.executionEnvironment = executionEnvironment; + } + public ImplementationPlatform getImplementationPlatform() { + return implementationPlatform; + } + public void setImplementationPlatform(ImplementationPlatform implementationPlatform) { + this.implementationPlatform = implementationPlatform; + } + public CertificationLevel getCertificationLevel() { + return certificationLevel; + } + public void setCertificationLevel(CertificationLevel certificationLevel) { + this.certificationLevel = certificationLevel; + } + public Mode getMode() { + return mode; + } + public void setMode(Mode mode) { + this.mode = mode; + } + public Padding getPadding() { + return padding; + } + public void setPadding(Padding padding) { + this.padding = padding; + } + public List getCryptoFunctions() { + return cryptoFunctions; + } + public void setCryptoFunctions(List cryptoFunctions) { + this.cryptoFunctions = cryptoFunctions; + } + public Integer getClassicalSecurityLevel() { + return classicalSecurityLevel; + } + public void setClassicalSecurityLevel(Integer classicalSecurityLevel) { + this.classicalSecurityLevel = classicalSecurityLevel; + } + public Integer getNistQuantumSecurityLevel() { + return nistQuantumSecurityLevel; + } + public void setNistQuantumSecurityLevel(Integer nistQuantumSecurityLevel) { + this.nistQuantumSecurityLevel = nistQuantumSecurityLevel; + } + + + +} \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/model/CryptoAssetProperties.java b/src/main/java/org/dependencytrack/model/CryptoAssetProperties.java new file mode 100644 index 000000000..d71aec0cd --- /dev/null +++ b/src/main/java/org/dependencytrack/model/CryptoAssetProperties.java @@ -0,0 +1,131 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +import java.io.Serializable; + +import javax.jdo.annotations.Column; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.Index; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; + +import jakarta.validation.constraints.Pattern; + +import org.cyclonedx.model.component.crypto.enums.AssetType; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +//import alpine.common.logging.Logger; + + +@PersistenceCapable(table= "CRYPTO_PROPERTIES") +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CryptoAssetProperties implements Serializable { + + private static final long serialVersionUID = 6421255046930674702L; + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + @Persistent + @Column(name = "ASSET_TYPE", jdbcType = "VARCHAR", length=32) + private AssetType assetType; + + @Persistent(defaultFetchGroup = "true", dependent = "true") + @Index(name = "COMPONENT_ALGORITHM_ID_IDX") + @Column(name = "ALGORITHM_PROPERTIES_ID", allowsNull = "true") + private CryptoAlgorithmProperties algorithmProperties; + + @Persistent(defaultFetchGroup = "true", dependent = "true") + @Index(name = "COMPONENT_CERTIFICATE_ID_IDX") + @Column(name = "CERTIFICATE_PROPERTIES_ID", allowsNull = "true") + private CryptoCertificateProperties certificateProperties; + + @Persistent(defaultFetchGroup = "true", dependent = "true") + @Index(name = "COMPONENT_RELATED_MATERIAL_ID_IDX") + @Column(name = "RELATED_MATERIAL_PROPERTIES_ID", allowsNull = "true") + private CryptoRelatedMaterialProperties relatedMaterialProperties; + + @Persistent(defaultFetchGroup = "true", dependent = "true") + @Index(name = "COMPONENT_PROTOCOL_ID_IDX") + @Column(name = "PROTOCOL_PROPERTIES_ID", allowsNull = "true") + private CryptoProtocolProperties protocolProperties; + + @Persistent + @Column(name = "OID", jdbcType = "VARCHAR", length=255) + @Pattern(regexp = "^([0-2])((\\.0)|(\\.[1-9][0-9]*))*$", message = "The OID must be a valid") + private String oid; + + public long getId() { + return id; + } + + public AssetType getAssetType() { + return assetType; + } + + public void setAssetType(AssetType assetType) { + this.assetType = assetType; + } + + public CryptoAlgorithmProperties getAlgorithmProperties() { + return algorithmProperties; + } + + public void setAlgorithmProperties(CryptoAlgorithmProperties algorithmProperties) { + this.algorithmProperties = algorithmProperties; + } + + public CryptoCertificateProperties getCertificateProperties() { + return certificateProperties; + } + + public void setCertificateProperties(CryptoCertificateProperties certificateProperties) { + this.certificateProperties = certificateProperties; + } + + public CryptoRelatedMaterialProperties getRelatedMaterialProperties() { + return relatedMaterialProperties; + } + + public void setRelatedMaterialProperties(CryptoRelatedMaterialProperties relatedMaterialProperties) { + this.relatedMaterialProperties = relatedMaterialProperties; + } + + public CryptoProtocolProperties getProtocolProperties() { + return protocolProperties; + } + + public void setProtocolProperties(CryptoProtocolProperties protocolProperties) { + this.protocolProperties = protocolProperties; + } + + public String getOid() { + return oid; + } + + public void setOid(String oid) { + this.oid = oid; + } +} diff --git a/src/main/java/org/dependencytrack/model/CryptoCertificateProperties.java b/src/main/java/org/dependencytrack/model/CryptoCertificateProperties.java new file mode 100644 index 000000000..6c0bb1afd --- /dev/null +++ b/src/main/java/org/dependencytrack/model/CryptoCertificateProperties.java @@ -0,0 +1,143 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +import java.io.Serializable; +import java.util.Date; + +import javax.jdo.annotations.Column; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; + +import org.dependencytrack.resources.v1.serializers.Iso8601DateSerializer; +import org.dependencytrack.util.DateUtil; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +@PersistenceCapable(table= "CERTIFICATE_PROPERTIES") +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CryptoCertificateProperties implements Serializable { + + private static final long serialVersionUID = 6421255046930674723L; + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + + /* + * certificateProperties + */ + @Persistent + @Column(name = "SUBJECT_NAME", jdbcType = "VARCHAR", length=255) + private String subjectName; + @Persistent + @Column(name = "ISSUER_NAME", jdbcType = "VARCHAR", length=255) + private String issuerName; + @Persistent + @Column(name = "NOT_VALID_BEFORE", jdbcType = "TIMESTAMP") + @JsonSerialize(using = Iso8601DateSerializer.class) + private Date notValidBefore; + @Persistent + @Column(name = "NOT_VALID_AFTER", jdbcType = "TIMESTAMP") + @JsonSerialize(using = Iso8601DateSerializer.class) + private Date notValidAfter; + @Persistent + @Column(name = "SIGNATURE_ALGORITHM_REF", jdbcType = "VARCHAR", length=64) + private String signatureAlgorithmRef; + @Persistent + @Column(name = "SUBJECT_PUBLIC_KEY_REF", jdbcType = "VARCHAR", length=64) + private String subjectPublicKeyRef; + @Persistent + @Column(name = "CERTIFICATE_FORMAT", jdbcType = "VARCHAR", length=32) + private String certificateFormat; + @Persistent + @Column(name = "CERTIFICATE_EXTENSION", jdbcType = "VARCHAR", length=32) + private String certificateExtension; + + + public String getSubjectName() { + return subjectName; + } + + public void setSubjectName(String subjectName) { + this.subjectName = subjectName; + } + + public String getIssuerName() { + return issuerName; + } + + public void setIssuerName(String issuerName) { + this.issuerName = issuerName; + } + + public Date getNotValidBefore() { + return notValidBefore; + } + + public void setNotValidBefore(String notValidBefore) { + this.notValidBefore = DateUtil.fromISO8601(notValidBefore); + } + + public Date getNotValidAfter() { + return notValidAfter; + } + + public void setNotValidAfter(String notValidAfter) { + this.notValidAfter = DateUtil.fromISO8601(notValidAfter); + } + + public String getSignatureAlgorithmRef() { + return signatureAlgorithmRef; + } + + public void setSignatureAlgorithmRef(String signatureAlgorithmRef) { + this.signatureAlgorithmRef = signatureAlgorithmRef; + } + + public String getSubjectPublicKeyRef() { + return subjectPublicKeyRef; + } + + public void setSubjectPublicKeyRef(String subjectPublicKeyRef) { + this.subjectPublicKeyRef = subjectPublicKeyRef; + } + + public String getCertificateFormat() { + return certificateFormat; + } + + public void setCertificateFormat(String certificateFormat) { + this.certificateFormat = certificateFormat; + } + + public String getCertificateExtension() { + return certificateExtension; + } + + public void setCertificateExtension(String certificateExtension) { + this.certificateExtension = certificateExtension; + } +} diff --git a/src/main/java/org/dependencytrack/model/CryptoProtocolProperties.java b/src/main/java/org/dependencytrack/model/CryptoProtocolProperties.java new file mode 100644 index 000000000..f374e2c8d --- /dev/null +++ b/src/main/java/org/dependencytrack/model/CryptoProtocolProperties.java @@ -0,0 +1,118 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +import java.io.Serializable; +import java.util.List; + +import javax.jdo.annotations.Column; +import javax.jdo.annotations.Element; +import javax.jdo.annotations.Extension; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.Join; +import javax.jdo.annotations.Order; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; + +import org.cyclonedx.model.component.crypto.enums.ProtocolType; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +@PersistenceCapable(table= "PROTOCOL_PROPERTIES") +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CryptoProtocolProperties implements Serializable { + + private static final long serialVersionUID = 6421255046930674725L; + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + + /* + * protocolProperties + */ + @Persistent + @Column(name = "TYPE", jdbcType = "VARCHAR", length=16) + private ProtocolType type; + @Persistent + @Column(name = "VERSION", jdbcType = "VARCHAR", length=16) + private String version; + + @Persistent(table = "PROTOCOL_CIPHER_SUITES", defaultFetchGroup = "true") + @Join(column = "PROTOCOL_PROPERTY_ID") + @Element(column = "CIPHER_SUITE_ID", dependent = "true") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "id ASC")) + private List cipherSuites; + + @Persistent(table = "PROTOCOL_IKEV2_TYPES") + @Join(column = "PROTOCOL_PROPERTY_ID") + @Element(column = "IKEV2_TYPE_ID", dependent = "true") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "id ASC")) + private List ikev2Types; + + @Persistent(table = "CRYPTO_REFS") + @Join(column = "PROTOCOL_PROPERTY_ID") + @Element(column = "CRYPTO_REF", dependent = "true") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "id ASC")) + private List cryptoRefs; + + public ProtocolType getType() { + return type; + } + + public void setType(ProtocolType type) { + this.type = type; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public List getCipherSuites() { + return cipherSuites; + } + + public void setCipherSuites(List cipherSuites) { + this.cipherSuites = cipherSuites; + } + + public List getIkev2Types() { + return ikev2Types; + } + + public void setIkev2Types(List ikev2Types) { + this.ikev2Types = ikev2Types; + } + + public List getCryptoRefs() { + return cryptoRefs; + } + + public void setCryptoRefs(List cryptoRefs) { + this.cryptoRefs = cryptoRefs; + } +} diff --git a/src/main/java/org/dependencytrack/model/CryptoRelatedMaterialProperties.java b/src/main/java/org/dependencytrack/model/CryptoRelatedMaterialProperties.java new file mode 100644 index 000000000..630608e94 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/CryptoRelatedMaterialProperties.java @@ -0,0 +1,203 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +import java.io.Serializable; +import java.util.Date; + +import javax.jdo.annotations.Column; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; + +import org.cyclonedx.model.component.crypto.enums.Mechanism; +import org.cyclonedx.model.component.crypto.enums.RelatedCryptoMaterialType; +import org.cyclonedx.model.component.crypto.enums.State; +import org.dependencytrack.resources.v1.serializers.Iso8601DateSerializer; +import org.dependencytrack.util.DateUtil; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +@PersistenceCapable(table= "RELATED_CRYPTO_MATERIAL_PROPERTIES") +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CryptoRelatedMaterialProperties implements Serializable { + + private static final long serialVersionUID = 6421255046930674724L; + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + + /* + * relatedCryptoMaterialProperties + */ + @Persistent + @Column(name = "TYPE") + private RelatedCryptoMaterialType type; + @Persistent + @Column(name = "IDENTIFIER", jdbcType = "VARCHAR", length=64) + private String identifier; + @Persistent + @Column(name = "STATE", jdbcType = "VARCHAR", length=16) + private State state; + @Persistent + @Column(name = "ALGORITHM_REF", jdbcType = "VARCHAR", length=64) + private String algorithmRef; + @Persistent + @Column(name = "CREATION_DATE", jdbcType = "TIMESTAMP") + @JsonSerialize(using = Iso8601DateSerializer.class) + private Date creationDate; + @Persistent + @Column(name = "ACTIVATION_DATE", jdbcType = "TIMESTAMP") + @JsonSerialize(using = Iso8601DateSerializer.class) + private Date activationDate; + @Persistent + @Column(name = "UPDATE_DATE", jdbcType = "TIMESTAMP") + @JsonSerialize(using = Iso8601DateSerializer.class) + private Date updateDate; + @Persistent + @Column(name = "EXPIRATION_DATE", jdbcType = "TIMESTAMP") + @JsonSerialize(using = Iso8601DateSerializer.class) + private Date expirationDate; + @Persistent + @Column(name = "VALUE", jdbcType = "VARCHAR", length=32) + private String value; + @Persistent + @Column(name = "SIZE") + private Integer size; + @Persistent + @Column(name = "FORMAT", jdbcType = "VARCHAR", length=8) + private String format; + @Persistent + @Column(name = "SECURED_BY_MECHANISM", jdbcType = "VARCHAR", length=16) + private Mechanism securedByMechanism; + @Persistent + @Column(name = "SECURED_BY_ALGORITHM_REF", jdbcType = "VARCHAR", length=64) + private String securedByAlgorithmRef; + + + public RelatedCryptoMaterialType getType() { + return type; + } + + public void setType(RelatedCryptoMaterialType type) { + this.type = type; + } + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String id) { + this.identifier = id; + } + + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } + + public String getAlgorithmRef() { + return algorithmRef; + } + + public void setAlgorithmRef(String algorithmRef) { + this.algorithmRef = algorithmRef; + } + + public Date getCreationDate() { + return creationDate; + } + + public void setCreationDate(String creationDate) { + this.creationDate = DateUtil.fromISO8601(creationDate); + } + + public Date getActivationDate() { + return activationDate; + } + + public void setActivationDate(String activationDate) { + this.activationDate = DateUtil.fromISO8601(activationDate); + } + + public Date getUpdateDate() { + return updateDate; + } + + public void setUpdateDate(String updateDate) { + this.updateDate = DateUtil.fromISO8601(updateDate); + } + + public Date getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(String expirationDate) { + this.expirationDate = DateUtil.fromISO8601(expirationDate); + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public Integer getSize() { + return size; + } + + public void setSize(Integer size) { + this.size = size; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + + public Mechanism getSecuredByMechanism() { + return securedByMechanism; + } + + public void setSecuredByMechanism(Mechanism securedByMechanism) { + this.securedByMechanism = securedByMechanism; + } + + public String getSecuredByAlgorithmRef() { + return securedByAlgorithmRef; + } + + public void setSecuredByAlgorithmRef(String securedByAlgorithmRef) { + this.securedByAlgorithmRef = securedByAlgorithmRef; + } +} diff --git a/src/main/java/org/dependencytrack/model/CryptographyMetrics.java b/src/main/java/org/dependencytrack/model/CryptographyMetrics.java new file mode 100644 index 000000000..8c907791a --- /dev/null +++ b/src/main/java/org/dependencytrack/model/CryptographyMetrics.java @@ -0,0 +1,135 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +import javax.jdo.annotations.Column; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.Index; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; +import jakarta.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Date; + +/** + * Metrics specific for cryptographic assets. + * + * @author Nicklas Körtge + * @since 5.5.0 + */ + +@PersistenceCapable +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CryptographyMetrics implements Serializable { + + private static final long serialVersionUID = 1231893328584979791L; + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + @Persistent + @Column(name = "NUMBER_OF_CRYPTOGRAPHIC_ASSETS") + private int numberOfCryptographicAssets; + + @Persistent + @Column(name = "MOST_USED_ALGORITHM_NAME") + private String mostUsedAlgorithmName; + + @Persistent + @Column(name = "MOST_USED_ALGORITHM_PERCENTAGE") + private double mostUsedAlgorithmPercentage; + + @Persistent + @Column(name = "NUMBER_OF_KEYS") + private int numberOfKeys; + + @Persistent + @Column(name = "FIRST_OCCURRENCE", allowsNull = "false") + @NotNull + @Index(name = "CRYPTOGRAPHY_METRICS_FIRST_OCCURRENCE_IDX") + private Date firstOccurrence; + + @Persistent + @Column(name = "LAST_OCCURRENCE", allowsNull = "false") + @NotNull + @Index(name = "CRYPTOGRAPHY_METRICS_LAST_OCCURRENCE_IDX") + private Date lastOccurrence; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public int getNumberOfCryptographicAssets() { + return numberOfCryptographicAssets; + } + + public void setNumberOfCryptographicAssets(int numberOfCryptographicAssets) { + this.numberOfCryptographicAssets = numberOfCryptographicAssets; + } + + public String getMostUsedAlgorithmName() { + return mostUsedAlgorithmName; + } + + public void setMostUsedAlgorithmName(String mostUsedAlgorithmName) { + this.mostUsedAlgorithmName = mostUsedAlgorithmName; + } + + public double getMostUsedAlgorithmPercentage() { + return mostUsedAlgorithmPercentage; + } + + public void setMostUsedAlgorithmPercentage(double mostUsedAlgorithmPercentage) { + this.mostUsedAlgorithmPercentage = mostUsedAlgorithmPercentage; + } + + public int getNumberOfKeys() { + return numberOfKeys; + } + + public void setNumberOfKeys(int numberOfKeys) { + this.numberOfKeys = numberOfKeys; + } + + public @NotNull Date getFirstOccurrence() { + return firstOccurrence; + } + + public void setFirstOccurrence(@NotNull Date firstOccurrence) { + this.firstOccurrence = firstOccurrence; + } + + public @NotNull Date getLastOccurrence() { + return lastOccurrence; + } + + public void setLastOccurrence(@NotNull Date lastOccurrence) { + this.lastOccurrence = lastOccurrence; + } +} diff --git a/src/main/java/org/dependencytrack/model/Ikev2Type.java b/src/main/java/org/dependencytrack/model/Ikev2Type.java new file mode 100644 index 000000000..580898fba --- /dev/null +++ b/src/main/java/org/dependencytrack/model/Ikev2Type.java @@ -0,0 +1,74 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +import java.io.Serializable; +import java.util.List; + +import javax.jdo.annotations.Column; +import javax.jdo.annotations.Element; +import javax.jdo.annotations.Extension; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.Join; +import javax.jdo.annotations.Order; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +@PersistenceCapable(table = "IKEV2_TYPE") +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Ikev2Type implements Serializable { + + private static final long serialVersionUID = 8548267900098588061L; + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + @Persistent + @Column(name = "TYPE", jdbcType = "VARCHAR", length=16) + private String type; + + @Persistent(table = "IKEV2_REF", defaultFetchGroup = "true") + @Join(column = "IKEV2_TYPE_ID") + @Element(column = "REF", dependent = "true") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "id ASC")) + private List refs; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public List getRefs() { + return refs; + } + + public void setRefs(List refs) { + this.refs = refs; + } +} + diff --git a/src/main/java/org/dependencytrack/model/Occurrence.java b/src/main/java/org/dependencytrack/model/Occurrence.java new file mode 100644 index 000000000..98ab4865f --- /dev/null +++ b/src/main/java/org/dependencytrack/model/Occurrence.java @@ -0,0 +1,114 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +import java.io.Serializable; + +import javax.jdo.annotations.Column; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +@PersistenceCapable +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Occurrence implements Serializable { + + private static final long serialVersionUID = 8548267900098587015L; + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + @Persistent + @Column(name = "BOM_REF", jdbcType = "VARCHAR", length = 64) + private String bomRef; + + @Persistent + @Column(name = "LOCATION", jdbcType = "VARCHAR", length = 255) + private String location; + + @Persistent + @Column(name = "LINE") + private Integer line; + + @Persistent + @Column(name = "OFFSET") + private Integer offset; + + @Persistent + @Column(name = "SYMBOL") + private Integer symbol; + + @Persistent + @Column(name = "ADDITIONAL_CONTEXT", jdbcType = "VARCHAR", length = 255) + private String additionalContext; + + public String getBomRef() { + return bomRef; + } + + public void setBomRef(String bomRef) { + this.bomRef = bomRef; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public Integer getLine() { + return line; + } + + public void setLine(Integer line) { + this.line = line; + } + + public Integer getOffset() { + return offset; + } + + public void setOffset(Integer offset) { + this.offset = offset; + } + + public Integer getSymbol() { + return symbol; + } + + public void setSymbol(Integer symbol) { + this.symbol = symbol; + } + + public String getAdditionalContext() { + return additionalContext; + } + + public void setAdditionalContext(String additionalContext) { + this.additionalContext = additionalContext; + } +} diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXExporter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXExporter.java index 62565c89b..5c397d9c1 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXExporter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXExporter.java @@ -26,7 +26,8 @@ import org.dependencytrack.model.Finding; import org.dependencytrack.model.Project; import org.dependencytrack.model.ServiceComponent; -import org.dependencytrack.parser.cyclonedx.util.ModelConverter; +import org.dependencytrack.parser.cyclonedx.util.DependencyUtil; +import org.dependencytrack.parser.cyclonedx.util.ModelExporter; import org.dependencytrack.persistence.QueryManager; import java.util.ArrayList; @@ -78,17 +79,17 @@ private Bom create(Listcomponents, final List servi .filter(component -> !component.getVulnerabilities().isEmpty()) .toList(); } - final List cycloneComponents = (Variant.VEX != variant && components != null) ? components.stream().map(component -> ModelConverter.convert(qm, component)).collect(Collectors.toList()) : null; - final List cycloneServices = (Variant.VEX != variant && services != null) ? services.stream().map(service -> ModelConverter.convert(qm, service)).collect(Collectors.toList()) : null; + final List cycloneComponents = (Variant.VEX != variant && components != null) ? components.stream().map(component -> ModelExporter.convert(qm, component)).collect(Collectors.toList()) : null; + final List cycloneServices = (Variant.VEX != variant && services != null) ? services.stream().map(service -> ModelExporter.convert(qm, service)).collect(Collectors.toList()) : null; final Bom bom = new Bom(); bom.setSerialNumber("urn:uuid:" + UUID.randomUUID()); bom.setVersion(1); - bom.setMetadata(ModelConverter.createMetadata(project)); + bom.setMetadata(ModelExporter.createMetadata(project)); bom.setComponents(cycloneComponents); bom.setServices(cycloneServices); - bom.setVulnerabilities(ModelConverter.generateVulnerabilities(qm, variant, findings)); + bom.setVulnerabilities(ModelExporter.generateVulnerabilities(qm, variant, findings)); if (cycloneComponents != null) { - bom.setDependencies(ModelConverter.generateDependencies(project, components)); + bom.setDependencies(DependencyUtil.generateDependencies(project, components)); } return bom; } @@ -97,10 +98,9 @@ public String export(final Bom bom, final Format format) throws GeneratorExcepti // TODO: The output version should be user-controllable. if (Format.JSON == format) { - return BomGeneratorFactory.createJson(Version.VERSION_15, bom).toJsonString(); + return BomGeneratorFactory.createJson(Version.VERSION_16, bom).toJsonString(); } else { - return BomGeneratorFactory.createXml(Version.VERSION_15, bom).toXmlString(); + return BomGeneratorFactory.createXml(Version.VERSION_16, bom).toXmlString(); } } - } diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/DependencyUtil.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/DependencyUtil.java new file mode 100644 index 000000000..efdcc0e90 --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/DependencyUtil.java @@ -0,0 +1,93 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.parser.cyclonedx.util; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import org.cyclonedx.model.Dependency; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Project; +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONTokener; + +public class DependencyUtil { + + private DependencyUtil() {} + + /** + * Converts {@link Project#getDirectDependencies()} and {@link Component#getDirectDependencies()} + * references to a CycloneDX dependency graph. + * + * @param project The {@link Project} to generate the graph for + * @param components The {@link Component}s belonging to {@code project} + * @return The CycloneDX representation of the {@link Project}'s dependency graph + */ + public static List generateDependencies(final Project project, final List components) { + if (project == null) { + return Collections.emptyList(); + } + + final var dependencies = new ArrayList(); + final var rootDependency = new Dependency(project.getUuid().toString()); + rootDependency.setDependencies(convertDirectDependencies(project.getDirectDependencies(), components)); + if (hasDependecies(rootDependency)) { + dependencies.add(rootDependency); + } + + for (final Component component : components) { + final var dependency = new Dependency(component.getUuid().toString()); + dependency.setDependencies(convertDirectDependencies(component.getDirectDependencies(), components)); + if (hasDependecies(dependency)) { + dependencies.add(dependency); + } + } + + return dependencies; + } + + private static boolean hasDependecies(Dependency dependency) { + return dependency.getDependencies() != null && !dependency.getDependencies().isEmpty(); + } + + private static List convertDirectDependencies(final String directDependenciesRaw, final List components) { + if (directDependenciesRaw == null || directDependenciesRaw.isBlank()) { + return Collections.emptyList(); + } + + final var dependencies = new ArrayList(); + try(final StringReader reader = new StringReader(directDependenciesRaw)) { + final JSONArray directDependenciesJsonArray = new JSONArray(new JSONTokener(reader)); + directDependenciesJsonArray.forEach(o -> { + if (o instanceof final JSONObject directDependencyObject) { + final String componentUuid = directDependencyObject.optString("uuid", null); + if (componentUuid != null && components.stream().map(Component::getUuid).map(UUID::toString).anyMatch(componentUuid::equals)) { + dependencies.add(new Dependency(directDependencyObject.getString("uuid"))); + } + } + }); + } + + return dependencies; + } +} 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 7bb5b84ae..8150a9829 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -22,54 +22,53 @@ import alpine.model.IConfigProperty; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.json.JsonValue; import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.multimap.HashSetValuedHashMap; import org.apache.commons.lang3.StringUtils; import org.cyclonedx.model.BomReference; import org.cyclonedx.model.Dependency; -import org.cyclonedx.model.Hash; -import org.cyclonedx.model.LicenseChoice; import org.cyclonedx.model.Metadata; -import org.cyclonedx.model.Swid; import org.cyclonedx.model.Tool; +import org.cyclonedx.model.component.crypto.AlgorithmProperties; +import org.cyclonedx.model.component.crypto.CertificateProperties; +import org.cyclonedx.model.component.crypto.CryptoProperties; +import org.cyclonedx.model.component.crypto.CryptoRef; +import org.cyclonedx.model.component.crypto.ProtocolProperties; +import org.cyclonedx.model.component.crypto.RelatedCryptoMaterialProperties; +import org.cyclonedx.model.component.crypto.SecuredBy; +import org.cyclonedx.model.component.crypto.enums.Mechanism; import org.cyclonedx.model.license.Expression; -import org.dependencytrack.model.Analysis; import org.dependencytrack.model.AnalysisJustification; import org.dependencytrack.model.AnalysisResponse; import org.dependencytrack.model.AnalysisState; +import org.dependencytrack.model.CipherSuite; import org.dependencytrack.model.Classifier; import org.dependencytrack.model.Component; import org.dependencytrack.model.ComponentProperty; -import org.dependencytrack.model.Cwe; +import org.dependencytrack.model.CryptoAlgorithmProperties; +import org.dependencytrack.model.CryptoAssetProperties; +import org.dependencytrack.model.CryptoCertificateProperties; +import org.dependencytrack.model.CryptoProtocolProperties; +import org.dependencytrack.model.CryptoRelatedMaterialProperties; import org.dependencytrack.model.DataClassification; import org.dependencytrack.model.ExternalReference; -import org.dependencytrack.model.Finding; +import org.dependencytrack.model.Ikev2Type; +import org.dependencytrack.model.Occurrence; import org.dependencytrack.model.OrganizationalContact; import org.dependencytrack.model.OrganizationalEntity; import org.dependencytrack.model.Project; 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; import org.dependencytrack.parser.spdx.expression.SpdxExpressionParser; import org.dependencytrack.parser.spdx.expression.model.SpdxExpression; -import org.dependencytrack.persistence.QueryManager; -import org.dependencytrack.util.VulnerabilityUtil; -import java.io.StringReader; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -82,7 +81,6 @@ import static org.apache.commons.lang3.StringUtils.trim; import static org.apache.commons.lang3.StringUtils.trimToNull; import static org.dependencytrack.util.PurlUtil.silentPurlCoordinatesOnly; - public class ModelConverter { private static final Logger LOGGER = Logger.getLogger(ModelConverter.class); @@ -130,10 +128,7 @@ public static Project convertToProject(final org.cyclonedx.model.Metadata cdxMet return null; } - final Project project = convertToProject(cdxMetadata.getComponent()); - project.setManufacturer(convert(cdxMetadata.getManufacture())); - - return project; + return convertToProject(cdxMetadata.getComponent()); } public static Project convertToProject(final org.cyclonedx.model.Component cdxComponent) { @@ -147,6 +142,8 @@ public static Project convertToProject(final org.cyclonedx.model.Component cdxCo project.setVersion(trimToNull(cdxComponent.getVersion())); project.setDescription(trimToNull(cdxComponent.getDescription())); project.setExternalReferences(convertExternalReferences(cdxComponent.getExternalReferences())); + project.setSupplier(ModelConverter.convert(cdxComponent.getSupplier())); + project.setManufacturer(ModelConverter.convert(cdxComponent.getManufacturer())); List contacts = new ArrayList<>(); if(cdxComponent.getAuthor()!=null){ @@ -294,9 +291,157 @@ public static Component convertComponent(final org.cyclonedx.model.Component cdx component.setChildren(children); } + if (cdxComponent.getCryptoProperties() != null) { + CryptoProperties cryptoProperties = cdxComponent.getCryptoProperties(); + CryptoAssetProperties cryptoAssetProperties = new CryptoAssetProperties(); + + cryptoAssetProperties.setAssetType(cryptoProperties.getAssetType()); + + switch (cryptoAssetProperties.getAssetType()) { + case ALGORITHM: + if (cryptoProperties.getAlgorithmProperties() != null) { + cryptoAssetProperties.setAlgorithmProperties(convert(cryptoProperties.getAlgorithmProperties())); + } + break; + case CERTIFICATE: + if (cryptoProperties.getCertificateProperties() != null) { + cryptoAssetProperties.setCertificateProperties(convert(cryptoProperties.getCertificateProperties())); + } + break; + case RELATED_CRYPTO_MATERIAL: + if (cryptoProperties.getRelatedCryptoMaterialProperties() != null) { + cryptoAssetProperties.setRelatedMaterialProperties(convert(cryptoProperties.getRelatedCryptoMaterialProperties())); + } + break; + case PROTOCOL: + if (cryptoProperties.getProtocolProperties() != null) { + cryptoAssetProperties.setProtocolProperties(convert(cryptoProperties.getProtocolProperties())); + } + break; + default: + break; + } + + cryptoAssetProperties.setOid(cryptoProperties.getOid()); + component.setCryptoAssetProperties(cryptoAssetProperties); + } + + if (cdxComponent.getEvidence() != null) { + List occurrences = cdxComponent.getEvidence().getOccurrences(); + if (occurrences != null &&!occurrences.isEmpty()) { + component.setOccurrences(convertOccurrences(occurrences)); + } + } + return component; } + private static CryptoAlgorithmProperties convert(AlgorithmProperties algorithmProperties) { + CryptoAlgorithmProperties cap = new CryptoAlgorithmProperties(); + cap.setPrimitive(algorithmProperties.getPrimitive()); + cap.setParameterSetIdentifier(algorithmProperties.getParameterSetIdentifier()); + cap.setCurve(algorithmProperties.getCurve()); + cap.setExecutionEnvironment(algorithmProperties.getExecutionEnvironment()); + cap.setImplementationPlatform(algorithmProperties.getImplementationPlatform()); + cap.setCertificationLevel(algorithmProperties.getCertificationLevel()); + cap.setMode(algorithmProperties.getMode()); + cap.setPadding(algorithmProperties.getPadding()); + cap.setCryptoFunctions(algorithmProperties.getCryptoFunctions()); + cap.setClassicalSecurityLevel(algorithmProperties.getClassicalSecurityLevel()); + cap.setNistQuantumSecurityLevel(algorithmProperties.getNistQuantumSecurityLevel()); + return cap; + } + + private static CryptoCertificateProperties convert(CertificateProperties certificateProperties) { + CryptoCertificateProperties ccp = new CryptoCertificateProperties(); + ccp.setSubjectName(certificateProperties.getSubjectName()); + ccp.setIssuerName(certificateProperties.getIssuerName()); + ccp.setNotValidBefore(certificateProperties.getNotValidBefore()); + ccp.setNotValidAfter(certificateProperties.getNotValidAfter()); + ccp.setSignatureAlgorithmRef(certificateProperties.getSignatureAlgorithmRef()); + ccp.setSubjectPublicKeyRef(certificateProperties.getSubjectPublicKeyRef()); + ccp.setCertificateFormat(certificateProperties.getCertificateFormat()); + ccp.setCertificateExtension(certificateProperties.getCertificateExtension()); + return ccp; + } + + private static CryptoRelatedMaterialProperties convert(RelatedCryptoMaterialProperties cryptoMaterialProperties) { + CryptoRelatedMaterialProperties crp = new CryptoRelatedMaterialProperties(); + crp.setType(cryptoMaterialProperties.getType()); + crp.setIdentifier(cryptoMaterialProperties.getId()); + crp.setState(cryptoMaterialProperties.getState()); + crp.setAlgorithmRef(cryptoMaterialProperties.getAlgorithmRef()); + crp.setCreationDate(cryptoMaterialProperties.getCreationDate()); + crp.setActivationDate(cryptoMaterialProperties.getActivationDate()); + crp.setUpdateDate(cryptoMaterialProperties.getUpdateDate()); + crp.setExpirationDate(cryptoMaterialProperties.getExpirationDate()); + crp.setValue(cryptoMaterialProperties.getValue()); + crp.setSize(cryptoMaterialProperties.getSize()); + crp.setFormat(cryptoMaterialProperties.getFormat()); + if (cryptoMaterialProperties.getSecuredBy() != null) { + SecuredBy secBy = cryptoMaterialProperties.getSecuredBy(); + crp.setSecuredByMechanism(Mechanism.valueOf(secBy.getMechanism().toUpperCase())); // allow "None" + crp.setSecuredByAlgorithmRef(secBy.getAlgorithmRef()); + } + return crp; + } + + private static CryptoProtocolProperties convert(ProtocolProperties protocolProperties) { + CryptoProtocolProperties cpp = new CryptoProtocolProperties(); + cpp.setType(protocolProperties.getType()); + cpp.setVersion(protocolProperties.getVersion()); + + if (protocolProperties.getCipherSuites() != null && !protocolProperties.getCipherSuites().isEmpty()) { + final var suites = new ArrayList(); + for (final org.cyclonedx.model.component.crypto.CipherSuite cdxCipherSuite : protocolProperties.getCipherSuites()) { + suites.add(convertCipherSuite(cdxCipherSuite)); + } + cpp.setCipherSuites(suites); + } + + if (protocolProperties.getIkev2TransformTypes() != null) { + Map cxIkev2Types = protocolProperties.getIkev2TransformTypes(); + final List ikev2Types = new ArrayList<>(); + for (Map.Entry e : cxIkev2Types.entrySet()) { + Ikev2Type ikev2Type = new Ikev2Type(); + ikev2Type.setType(e.getKey()); + ikev2Type.setRefs(e.getValue().getRef()); + ikev2Types.add(ikev2Type); + } + cpp.setIkev2Types(ikev2Types); + } + + if (protocolProperties.getCryptoRefArray() != null) { + cpp.setCryptoRefs(protocolProperties.getCryptoRefArray().getRef()); + } + + return cpp; + } + + private static CipherSuite convertCipherSuite(org.cyclonedx.model.component.crypto.CipherSuite cs) { + CipherSuite modelCS = new CipherSuite(); + modelCS.setName(cs.getName()); + modelCS.setAlgorithms(cs.getAlgorithms()); + modelCS.setIdentifiers(cs.getIdentifiers()); + return modelCS; + } + + private static List convertOccurrences( + List occurrences ) { + List occs = new ArrayList<>(); + for(org.cyclonedx.model.component.evidence.Occurrence o: occurrences) { + Occurrence occ = new Occurrence(); + occ.setBomRef(o.getBomRef()); + occ.setLine(o.getLine()); + occ.setLocation(o.getLocation()); + occ.setOffset(o.getOffset()); + occ.setSymbol(o.getSymbol()); + occ.setAdditionalContext(o.getAdditionalContext()); + occs.add(occ); + } + return occs; + } + private static Component convert(@SuppressWarnings("deprecation") final Tool tool) { if (tool == null) { return null; @@ -375,43 +520,6 @@ private static OrganizationalContact convert(final org.cyclonedx.model.Organizat return dtContact; } - private static List convertContacts(final List dtContacts) { - if (dtContacts == null) { - return null; - } - - return dtContacts.stream().map(ModelConverter::convert).toList(); - } - - private static org.cyclonedx.model.OrganizationalEntity convert(final OrganizationalEntity dtEntity) { - if (dtEntity == null) { - return null; - } - - 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; - } - private static List convertToComponentProperties(final List cdxProperties) { if (cdxProperties == null || cdxProperties.isEmpty()) { return Collections.emptyList(); @@ -532,7 +640,8 @@ private static List convertExternalReferences(final List List flatten(final Collection items, return result; } - 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()); - cycloneComponent.setGroup(StringUtils.trimToNull(component.getGroup())); - cycloneComponent.setName(StringUtils.trimToNull(component.getName())); - cycloneComponent.setVersion(StringUtils.trimToNull(component.getVersion())); - cycloneComponent.setDescription(StringUtils.trimToNull(component.getDescription())); - cycloneComponent.setCopyright(StringUtils.trimToNull(component.getCopyright())); - cycloneComponent.setCpe(StringUtils.trimToNull(component.getCpe())); - cycloneComponent.setAuthor(StringUtils.trimToNull(convertContactsToString(component.getAuthors()))); - cycloneComponent.setSupplier(convert(component.getSupplier())); - cycloneComponent.setProperties(convert(component.getProperties())); - - if (component.getSwidTagId() != null) { - final Swid swid = new Swid(); - swid.setTagId(component.getSwidTagId()); - cycloneComponent.setSwid(swid); - } - - if (component.getPurl() != null) { - cycloneComponent.setPurl(component.getPurl().canonicalize()); - } - - if (component.getClassifier() != null) { - cycloneComponent.setType(org.cyclonedx.model.Component.Type.valueOf(component.getClassifier().name())); - } else { - cycloneComponent.setType(org.cyclonedx.model.Component.Type.LIBRARY); - } - - if (component.getMd5() != null) { - cycloneComponent.addHash(new Hash(Hash.Algorithm.MD5, component.getMd5())); - } - if (component.getSha1() != null) { - cycloneComponent.addHash(new Hash(Hash.Algorithm.SHA1, component.getSha1())); - } - if (component.getSha256() != null) { - cycloneComponent.addHash(new Hash(Hash.Algorithm.SHA_256, component.getSha256())); - } - if (component.getSha512() != null) { - cycloneComponent.addHash(new Hash(Hash.Algorithm.SHA_512, component.getSha512())); - } - if (component.getSha3_256() != null) { - cycloneComponent.addHash(new Hash(Hash.Algorithm.SHA3_256, component.getSha3_256())); - } - if (component.getSha3_512() != null) { - cycloneComponent.addHash(new Hash(Hash.Algorithm.SHA3_512, component.getSha3_512())); - } - - final LicenseChoice licenses = new LicenseChoice(); - if (component.getResolvedLicense() != null) { - final org.cyclonedx.model.License license = new org.cyclonedx.model.License(); - if (!component.getResolvedLicense().isCustomLicense()) { - license.setId(component.getResolvedLicense().getLicenseId()); - } else { - license.setName(component.getResolvedLicense().getName()); - } - license.setUrl(component.getLicenseUrl()); - licenses.addLicense(license); - cycloneComponent.setLicenses(licenses); - } else if (component.getLicense() != null) { - final org.cyclonedx.model.License license = new org.cyclonedx.model.License(); - license.setName(component.getLicense()); - license.setUrl(component.getLicenseUrl()); - licenses.addLicense(license); - cycloneComponent.setLicenses(licenses); - } else if (StringUtils.isNotEmpty(component.getLicenseUrl())) { - final org.cyclonedx.model.License license = new org.cyclonedx.model.License(); - license.setUrl(component.getLicenseUrl()); - licenses.addLicense(license); - cycloneComponent.setLicenses(licenses); - } - if (component.getLicenseExpression() != null) { - final var licenseExpression = new Expression(); - licenseExpression.setValue(component.getLicenseExpression()); - licenses.setExpression(licenseExpression); - cycloneComponent.setLicenses(licenses); - } - - if (component.getExternalReferences() != null && !component.getExternalReferences().isEmpty()) { - List references = new ArrayList<>(); - for (ExternalReference ref : component.getExternalReferences()) { - org.cyclonedx.model.ExternalReference cdxRef = new org.cyclonedx.model.ExternalReference(); - cdxRef.setType(ref.getType()); - cdxRef.setUrl(ref.getUrl()); - cdxRef.setComment(ref.getComment()); - references.add(cdxRef); - } - cycloneComponent.setExternalReferences(references); - } else { - cycloneComponent.setExternalReferences(null); - } - - /* - TODO: Assemble child/parent hierarchy. Components come in as flat, resolved dependencies. - */ - /* - if (component.getChildren() != null && component.getChildren().size() > 0) { - final List components = new ArrayList<>(); - final Component[] children = component.getChildren().toArray(new Component[0]); - for (Component child : children) { - components.add(convert(qm, child)); - } - if (children.length > 0) { - cycloneComponent.setComponents(components); - } - } - */ - - return cycloneComponent; - } - - public static String convertContactsToString(List authors) { - if (authors == null || authors.isEmpty()) { - return ""; - } - StringBuilder stringBuilder = new StringBuilder(); - for (OrganizationalContact author : authors) { - if (author != null && author.getName() != null) { - stringBuilder.append(author.getName()).append(", "); - } - } - //remove trailing comma and space - if (stringBuilder.length() > 0) { - stringBuilder.setLength(stringBuilder.length() - 2); - } - return stringBuilder.toString(); - } - - private static List convert(final Collection dtProperties) { - if (dtProperties == null || dtProperties.isEmpty()) { - return Collections.emptyList(); - } - - final List cdxProperties = new ArrayList<>(); - for (final T dtProperty : dtProperties) { - if (dtProperty.getPropertyType() == IConfigProperty.PropertyType.ENCRYPTEDSTRING) { - // We treat encrypted properties as internal. - // They shall not be leaked when exporting. - continue; - } - - final var cdxProperty = new org.cyclonedx.model.Property(); - if (dtProperty.getGroupName() == null) { - cdxProperty.setName(dtProperty.getPropertyName()); - } else { - cdxProperty.setName("%s:%s".formatted(dtProperty.getGroupName(), dtProperty.getPropertyName())); - } - cdxProperty.setValue(dtProperty.getPropertyValue()); - cdxProperties.add(cdxProperty); - } - - return cdxProperties; - } - - public static org.cyclonedx.model.Metadata createMetadata(final Project project) { - final org.cyclonedx.model.Metadata metadata = new org.cyclonedx.model.Metadata(); - final org.cyclonedx.model.Tool tool = new org.cyclonedx.model.Tool(); - tool.setVendor("OWASP"); - tool.setName(alpine.Config.getInstance().getApplicationName()); - 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(convertContactsToString(project.getAuthors()))); - cycloneComponent.setPublisher(StringUtils.trimToNull(project.getPublisher())); - cycloneComponent.setGroup(StringUtils.trimToNull(project.getGroup())); - cycloneComponent.setName(StringUtils.trimToNull(project.getName())); - if (StringUtils.trimToNull(project.getVersion()) == null) { - cycloneComponent.setVersion("SNAPSHOT"); // Version is required per CycloneDX spec - } else { - cycloneComponent.setVersion(StringUtils.trimToNull(project.getVersion())); - } - cycloneComponent.setDescription(StringUtils.trimToNull(project.getDescription())); - cycloneComponent.setCpe(StringUtils.trimToNull(project.getCpe())); - if (project.getPurl() != null) { - cycloneComponent.setPurl(StringUtils.trimToNull(project.getPurl().canonicalize())); - } - if (StringUtils.trimToNull(project.getSwidTagId()) != null) { - final Swid swid = new Swid(); - swid.setTagId(StringUtils.trimToNull(project.getSwidTagId())); - swid.setName(StringUtils.trimToNull(project.getName())); - swid.setVersion(StringUtils.trimToNull(project.getVersion())); - cycloneComponent.setSwid(swid); - } - if (project.getClassifier() != null) { - cycloneComponent.setType(org.cyclonedx.model.Component.Type.valueOf(project.getClassifier().name())); - } else { - cycloneComponent.setType(org.cyclonedx.model.Component.Type.LIBRARY); - } - if (project.getExternalReferences() != null && !project.getExternalReferences().isEmpty()) { - List references = new ArrayList<>(); - project.getExternalReferences().forEach(externalReference -> { - org.cyclonedx.model.ExternalReference ref = new org.cyclonedx.model.ExternalReference(); - ref.setUrl(externalReference.getUrl()); - ref.setType(externalReference.getType()); - ref.setComment(externalReference.getComment()); - references.add(ref); - }); - cycloneComponent.setExternalReferences(references); - } - cycloneComponent.setSupplier(convert(project.getSupplier())); - // NB: Project properties are currently used to configure integrations - // such as Defect Dojo. They can also contain encrypted values that most - // definitely are not safe to share. Before we can include project properties - // in BOM exports, we need a filtering mechanism. - // cycloneComponent.setProperties(convert(project.getProperties())); - metadata.setComponent(cycloneComponent); - - if (project.getMetadata() != null) { - metadata.setAuthors(convertContacts(project.getMetadata().getAuthors())); - metadata.setSupplier(convert(project.getMetadata().getSupplier())); - } - } - return metadata; - } - - 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()); - cycloneService.setProvider(convert(service.getProvider())); - cycloneService.setProvider(convert(service.getProvider())); - cycloneService.setGroup(StringUtils.trimToNull(service.getGroup())); - cycloneService.setName(StringUtils.trimToNull(service.getName())); - cycloneService.setVersion(StringUtils.trimToNull(service.getVersion())); - cycloneService.setDescription(StringUtils.trimToNull(service.getDescription())); - if (service.getEndpoints() != null && service.getEndpoints().length > 0) { - cycloneService.setEndpoints(Arrays.asList(service.getEndpoints().clone())); - } - cycloneService.setAuthenticated(service.getAuthenticated()); - cycloneService.setxTrustBoundary(service.getCrossesTrustBoundary()); - 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().isEmpty()) { - for (ExternalReference ref : service.getExternalReferences()) { - org.cyclonedx.model.ExternalReference cycloneRef = new org.cyclonedx.model.ExternalReference(); - cycloneRef.setType(ref.getType()); - cycloneRef.setUrl(ref.getUrl()); - cycloneRef.setComment(ref.getComment()); - cycloneService.addExternalReference(cycloneRef); - } - } - /* TODO: Add when services support licenses (after component license refactor) - if (component.getResolvedLicense() != null) { - final org.cyclonedx.model.License license = new org.cyclonedx.model.License(); - license.setId(component.getResolvedLicense().getLicenseId()); - final LicenseChoice licenseChoice = new LicenseChoice(); - licenseChoice.addLicense(license); - cycloneComponent.setLicenseChoice(licenseChoice); - } else if (component.getLicense() != null) { - final org.cyclonedx.model.License license = new org.cyclonedx.model.License(); - license.setName(component.getLicense()); - final LicenseChoice licenseChoice = new LicenseChoice(); - licenseChoice.addLicense(license); - cycloneComponent.setLicenseChoice(licenseChoice); - } - */ - - /* - TODO: Assemble child/parent hierarchy. Components come in as flat, resolved dependencies. - */ - /* - if (component.getChildren() != null && component.getChildren().size() > 0) { - final List components = new ArrayList<>(); - final Component[] children = component.getChildren().toArray(new Component[0]); - for (Component child : children) { - components.add(convert(qm, child)); - } - if (children.length > 0) { - cycloneComponent.setComponents(components); - } - } - */ - return cycloneService; - } - - public static org.cyclonedx.model.vulnerability.Vulnerability convert(final QueryManager qm, final CycloneDXExporter.Variant variant, - final Finding finding) { - final Component component = qm.getObjectByUuid(Component.class, finding.getComponent().get("uuid").toString()); - final Project project = component.getProject(); - final Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, finding.getVulnerability().get("uuid").toString()); - - final org.cyclonedx.model.vulnerability.Vulnerability cdxVulnerability = new org.cyclonedx.model.vulnerability.Vulnerability(); - cdxVulnerability.setBomRef(vulnerability.getUuid().toString()); - cdxVulnerability.setId(vulnerability.getVulnId()); - // Add the vulnerability source - org.cyclonedx.model.vulnerability.Vulnerability.Source cdxSource = new org.cyclonedx.model.vulnerability.Vulnerability.Source(); - cdxSource.setName(vulnerability.getSource()); - cdxVulnerability.setSource(convertDtVulnSourceToCdxVulnSource(Vulnerability.Source.valueOf(vulnerability.getSource()))); - if (vulnerability.getCvssV2BaseScore() != null) { - org.cyclonedx.model.vulnerability.Vulnerability.Rating rating = new org.cyclonedx.model.vulnerability.Vulnerability.Rating(); - rating.setSource(convertDtVulnSourceToCdxVulnSource(Vulnerability.Source.valueOf(vulnerability.getSource()))); - rating.setMethod(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Method.CVSSV2); - rating.setScore(vulnerability.getCvssV2BaseScore().doubleValue()); - rating.setVector(vulnerability.getCvssV2Vector()); - if (rating.getScore() >= 7.0) { - rating.setSeverity(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.HIGH); - } else if (rating.getScore() >= 4.0) { - rating.setSeverity(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.MEDIUM); - } else { - rating.setSeverity(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.LOW); - } - cdxVulnerability.addRating(rating); - } - if (vulnerability.getCvssV3BaseScore() != null) { - org.cyclonedx.model.vulnerability.Vulnerability.Rating rating = new org.cyclonedx.model.vulnerability.Vulnerability.Rating(); - rating.setSource(convertDtVulnSourceToCdxVulnSource(Vulnerability.Source.valueOf(vulnerability.getSource()))); - if (vulnerability.getCvssV3Vector() != null && vulnerability.getCvssV3Vector().contains("CVSS:3.0")) { - rating.setMethod(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Method.CVSSV3); - } else { - rating.setMethod(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Method.CVSSV31); - } - rating.setScore(vulnerability.getCvssV3BaseScore().doubleValue()); - rating.setVector(vulnerability.getCvssV3Vector()); - if (rating.getScore() >= 9.0) { - rating.setSeverity(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.CRITICAL); - } else if (rating.getScore() >= 7.0) { - rating.setSeverity(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.HIGH); - } else if (rating.getScore() >= 4.0) { - rating.setSeverity(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.MEDIUM); - } else { - rating.setSeverity(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.LOW); - } - cdxVulnerability.addRating(rating); - } - if (vulnerability.getOwaspRRLikelihoodScore() != null && vulnerability.getOwaspRRTechnicalImpactScore() != null && vulnerability.getOwaspRRBusinessImpactScore() != null) { - org.cyclonedx.model.vulnerability.Vulnerability.Rating rating = new org.cyclonedx.model.vulnerability.Vulnerability.Rating(); - rating.setSeverity(convertDtSeverityToCdxSeverity(VulnerabilityUtil.normalizedOwaspRRScore(vulnerability.getOwaspRRLikelihoodScore().doubleValue(), vulnerability.getOwaspRRTechnicalImpactScore().doubleValue(), vulnerability.getOwaspRRBusinessImpactScore().doubleValue()))); - rating.setSource(convertDtVulnSourceToCdxVulnSource(Vulnerability.Source.valueOf(vulnerability.getSource()))); - rating.setMethod(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Method.OWASP); - rating.setVector(vulnerability.getOwaspRRVector()); - cdxVulnerability.addRating(rating); - } - if (vulnerability.getCvssV2BaseScore() == null && vulnerability.getCvssV3BaseScore() == null && vulnerability.getOwaspRRLikelihoodScore() == null) { - org.cyclonedx.model.vulnerability.Vulnerability.Rating rating = new org.cyclonedx.model.vulnerability.Vulnerability.Rating(); - rating.setSeverity(convertDtSeverityToCdxSeverity(vulnerability.getSeverity())); - rating.setSource(convertDtVulnSourceToCdxVulnSource(Vulnerability.Source.valueOf(vulnerability.getSource()))); - rating.setMethod(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Method.OTHER); - cdxVulnerability.addRating(rating); - } - if (vulnerability.getCwes() != null) { - for (final Integer cweId : vulnerability.getCwes()) { - final Cwe cwe = CweResolver.getInstance().lookup(cweId); - if (cwe != null) { - cdxVulnerability.addCwe(cwe.getCweId()); - } - } - } - cdxVulnerability.setDescription(vulnerability.getDescription()); - cdxVulnerability.setRecommendation(vulnerability.getRecommendation()); - cdxVulnerability.setCreated(vulnerability.getCreated()); - cdxVulnerability.setPublished(vulnerability.getPublished()); - cdxVulnerability.setUpdated(vulnerability.getUpdated()); - - if (CycloneDXExporter.Variant.INVENTORY_WITH_VULNERABILITIES == variant || CycloneDXExporter.Variant.VDR == variant) { - final List affects = new ArrayList<>(); - final org.cyclonedx.model.vulnerability.Vulnerability.Affect affect = new org.cyclonedx.model.vulnerability.Vulnerability.Affect(); - affect.setRef(component.getUuid().toString()); - affects.add(affect); - cdxVulnerability.setAffects(affects); - } else if (CycloneDXExporter.Variant.VEX == variant && project != null) { - final List affects = new ArrayList<>(); - final org.cyclonedx.model.vulnerability.Vulnerability.Affect affect = new org.cyclonedx.model.vulnerability.Vulnerability.Affect(); - affect.setRef(project.getUuid().toString()); - affects.add(affect); - cdxVulnerability.setAffects(affects); - } - - if (CycloneDXExporter.Variant.VEX == variant || CycloneDXExporter.Variant.VDR == variant) { - final Analysis analysis = qm.getAnalysis( - qm.getObjectByUuid(Component.class, component.getUuid()), - qm.getObjectByUuid(Vulnerability.class, vulnerability.getUuid()) - ); - if (analysis != null) { - final org.cyclonedx.model.vulnerability.Vulnerability.Analysis cdxAnalysis = new org.cyclonedx.model.vulnerability.Vulnerability.Analysis(); - if (analysis.getAnalysisResponse() != null) { - final org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Response response = convertDtVulnAnalysisResponseToCdxAnalysisResponse(analysis.getAnalysisResponse()); - if (response != null) { - List responses = new ArrayList<>(); - responses.add(response); - cdxAnalysis.setResponses(responses); - } - } - if (analysis.getAnalysisState() != null) { - cdxAnalysis.setState(convertDtVulnAnalysisStateToCdxAnalysisState(analysis.getAnalysisState())); - } - if (analysis.getAnalysisJustification() != null) { - cdxAnalysis.setJustification(convertDtVulnAnalysisJustificationToCdxAnalysisJustification(analysis.getAnalysisJustification())); - } - cdxAnalysis.setDetail(StringUtils.trimToNull(analysis.getAnalysisDetails())); - cdxVulnerability.setAnalysis(cdxAnalysis); - } - } - - return cdxVulnerability; - } - - /** - * Converts {@link Project#getDirectDependencies()} and {@link Component#getDirectDependencies()} - * references to a CycloneDX dependency graph. - * - * @param project The {@link Project} to generate the graph for - * @param components The {@link Component}s belonging to {@code project} - * @return The CycloneDX representation of the {@link Project}'s dependency graph - */ - public static List generateDependencies(final Project project, final List components) { - if (project == null) { - return Collections.emptyList(); - } - - final var dependencies = new ArrayList(); - final var rootDependency = new Dependency(project.getUuid().toString()); - rootDependency.setDependencies(convertDirectDependencies(project.getDirectDependencies(), components)); - dependencies.add(rootDependency); - - for (final Component component : components) { - final var dependency = new Dependency(component.getUuid().toString()); - dependency.setDependencies(convertDirectDependencies(component.getDirectDependencies(), components)); - dependencies.add(dependency); - } - - return dependencies; - } - - private static List convertDirectDependencies(final String directDependenciesRaw, final List components) { - if (directDependenciesRaw == null || directDependenciesRaw.isBlank()) { - return Collections.emptyList(); - } - - final var dependencies = new ArrayList(); - final JsonValue directDependenciesJson = Json - .createReader(new StringReader(directDependenciesRaw)) - .readValue(); - if (directDependenciesJson instanceof final JsonArray directDependenciesJsonArray) { - for (final JsonValue directDependency : directDependenciesJsonArray) { - if (directDependency instanceof final JsonObject directDependencyObject) { - final String componentUuid = directDependencyObject.getString("uuid", null); - if (componentUuid != null && components.stream().map(Component::getUuid).map(UUID::toString).anyMatch(componentUuid::equals)) { - dependencies.add(new Dependency(directDependencyObject.getString("uuid"))); - } - } - } - } - - return dependencies; - } - - private static org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity convertDtSeverityToCdxSeverity(final Severity severity) { - switch (severity) { - case CRITICAL: - return org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.CRITICAL; - case HIGH: - return org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.HIGH; - case MEDIUM: - return org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.MEDIUM; - case LOW: - return org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.LOW; - default: - return org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.UNKNOWN; - } - } - - private static org.cyclonedx.model.vulnerability.Vulnerability.Source convertDtVulnSourceToCdxVulnSource(final Vulnerability.Source vulnSource) { - org.cyclonedx.model.vulnerability.Vulnerability.Source cdxSource = new org.cyclonedx.model.vulnerability.Vulnerability.Source(); - cdxSource.setName(vulnSource.name()); - switch (vulnSource) { - case NVD: - cdxSource.setUrl("https://nvd.nist.gov/"); - break; - case NPM: - cdxSource.setUrl("https://www.npmjs.com/"); - break; - case GITHUB: - cdxSource.setUrl("https://github.com/advisories"); - break; - case VULNDB: - cdxSource.setUrl("https://vulndb.cyberriskanalytics.com/"); - break; - case OSSINDEX: - cdxSource.setUrl("https://ossindex.sonatype.org/"); - break; - case RETIREJS: - cdxSource.setUrl("https://github.com/RetireJS/retire.js"); - break; - } - return cdxSource; - } - - private static org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Response convertDtVulnAnalysisResponseToCdxAnalysisResponse(final AnalysisResponse analysisResponse) { - if (analysisResponse == null) { - return null; - } - switch (analysisResponse) { - case UPDATE: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Response.UPDATE; - case CAN_NOT_FIX: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Response.CAN_NOT_FIX; - case WILL_NOT_FIX: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Response.WILL_NOT_FIX; - case ROLLBACK: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Response.ROLLBACK; - case WORKAROUND_AVAILABLE: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Response.WORKAROUND_AVAILABLE; - default: - return null; - } - } - public static AnalysisResponse convertCdxVulnAnalysisResponseToDtAnalysisResponse(final org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Response cdxAnalysisResponse) { if (cdxAnalysisResponse == null) { return null; @@ -1134,26 +730,6 @@ public static AnalysisResponse convertCdxVulnAnalysisResponseToDtAnalysisRespons } } - private static org.cyclonedx.model.vulnerability.Vulnerability.Analysis.State convertDtVulnAnalysisStateToCdxAnalysisState(final AnalysisState analysisState) { - if (analysisState == null) { - return null; - } - switch (analysisState) { - case EXPLOITABLE: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.State.EXPLOITABLE; - case FALSE_POSITIVE: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.State.FALSE_POSITIVE; - case IN_TRIAGE: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.State.IN_TRIAGE; - case NOT_AFFECTED: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.State.NOT_AFFECTED; - case RESOLVED: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.State.RESOLVED; - default: - return null; - } - } - public static AnalysisState convertCdxVulnAnalysisStateToDtAnalysisState(final org.cyclonedx.model.vulnerability.Vulnerability.Analysis.State cdxAnalysisState) { if (cdxAnalysisState == null) { return null; @@ -1174,33 +750,6 @@ public static AnalysisState convertCdxVulnAnalysisStateToDtAnalysisState(final o } } - private static org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification convertDtVulnAnalysisJustificationToCdxAnalysisJustification(final AnalysisJustification analysisJustification) { - if (analysisJustification == null) { - return null; - } - switch (analysisJustification) { - case CODE_NOT_PRESENT: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.CODE_NOT_PRESENT; - case CODE_NOT_REACHABLE: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.CODE_NOT_REACHABLE; - case PROTECTED_AT_PERIMETER: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.PROTECTED_AT_PERIMETER; - case PROTECTED_AT_RUNTIME: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.PROTECTED_AT_RUNTIME; - case PROTECTED_BY_COMPILER: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.PROTECTED_BY_COMPILER; - case PROTECTED_BY_MITIGATING_CONTROL: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.PROTECTED_BY_MITIGATING_CONTROL; - case REQUIRES_CONFIGURATION: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.REQUIRES_CONFIGURATION; - case REQUIRES_DEPENDENCY: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.REQUIRES_DEPENDENCY; - case REQUIRES_ENVIRONMENT: - return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.REQUIRES_ENVIRONMENT; - default: - return null; - } - } public static AnalysisJustification convertCdxVulnAnalysisJustificationToDtAnalysisJustification(final org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification cdxAnalysisJustification) { if (cdxAnalysisJustification == null) { @@ -1229,19 +778,4 @@ public static AnalysisJustification convertCdxVulnAnalysisJustificationToDtAnaly return AnalysisJustification.NOT_SET; } } - - public static List generateVulnerabilities( - final QueryManager qm, - final CycloneDXExporter.Variant variant, - final List findings - ) { - if (findings == null) { - return Collections.emptyList(); - } - final var vulnerabilitiesSeen = new HashSet(); - return findings.stream() - .map(finding -> convert(qm, variant, finding)) - .filter(vulnerabilitiesSeen::add) - .toList(); - } } diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelExporter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelExporter.java new file mode 100644 index 000000000..4b35d408e --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelExporter.java @@ -0,0 +1,785 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.parser.cyclonedx.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.cyclonedx.model.Hash; +import org.cyclonedx.model.LicenseChoice; +import org.cyclonedx.model.Swid; +import org.cyclonedx.model.component.crypto.AlgorithmProperties; +import org.cyclonedx.model.component.crypto.CertificateProperties; +import org.cyclonedx.model.component.crypto.CryptoProperties; +import org.cyclonedx.model.component.crypto.CryptoRef; +import org.cyclonedx.model.component.crypto.ProtocolProperties; +import org.cyclonedx.model.component.crypto.RelatedCryptoMaterialProperties; +import org.cyclonedx.model.component.crypto.SecuredBy; +import org.cyclonedx.model.license.Expression; +import org.dependencytrack.model.Analysis; +import org.dependencytrack.model.AnalysisJustification; +import org.dependencytrack.model.AnalysisResponse; +import org.dependencytrack.model.AnalysisState; +import org.dependencytrack.model.CipherSuite; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.CryptoAlgorithmProperties; +import org.dependencytrack.model.CryptoAssetProperties; +import org.dependencytrack.model.CryptoCertificateProperties; +import org.dependencytrack.model.CryptoProtocolProperties; +import org.dependencytrack.model.CryptoRelatedMaterialProperties; +import org.dependencytrack.model.Cwe; +import org.dependencytrack.model.DataClassification; +import org.dependencytrack.model.ExternalReference; +import org.dependencytrack.model.Finding; +import org.dependencytrack.model.Ikev2Type; +import org.dependencytrack.model.Occurrence; +import org.dependencytrack.model.OrganizationalContact; +import org.dependencytrack.model.OrganizationalEntity; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.ServiceComponent; +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.parser.common.resolver.CweResolver; +import org.dependencytrack.parser.cyclonedx.CycloneDXExporter; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.DateUtil; +import org.dependencytrack.util.VulnerabilityUtil; + +import alpine.model.IConfigProperty; + +public class ModelExporter { + + private ModelExporter() {} + + public static org.cyclonedx.model.Metadata createMetadata(final Project project) { + final org.cyclonedx.model.Metadata metadata = new org.cyclonedx.model.Metadata(); + final org.cyclonedx.model.Tool tool = new org.cyclonedx.model.Tool(); + tool.setVendor("OWASP"); + tool.setName(alpine.Config.getInstance().getApplicationName()); + tool.setVersion(alpine.Config.getInstance().getApplicationVersion()); + metadata.setTools(Collections.singletonList(tool)); + if (project != null) { + final org.cyclonedx.model.Component cycloneComponent = new org.cyclonedx.model.Component(); + cycloneComponent.setBomRef(project.getUuid().toString()); + cycloneComponent.setAuthor(StringUtils.trimToNull(convertContactsToString(project.getAuthors()))); + cycloneComponent.setPublisher(StringUtils.trimToNull(project.getPublisher())); + cycloneComponent.setGroup(StringUtils.trimToNull(project.getGroup())); + cycloneComponent.setName(StringUtils.trimToNull(project.getName())); + if (StringUtils.trimToNull(project.getVersion()) == null) { + cycloneComponent.setVersion("SNAPSHOT"); // Version is required per CycloneDX spec + } else { + cycloneComponent.setVersion(StringUtils.trimToNull(project.getVersion())); + } + cycloneComponent.setDescription(StringUtils.trimToNull(project.getDescription())); + cycloneComponent.setCpe(StringUtils.trimToNull(project.getCpe())); + if (project.getPurl() != null) { + cycloneComponent.setPurl(StringUtils.trimToNull(project.getPurl().canonicalize())); + } + if (StringUtils.trimToNull(project.getSwidTagId()) != null) { + final Swid swid = new Swid(); + swid.setTagId(StringUtils.trimToNull(project.getSwidTagId())); + swid.setName(StringUtils.trimToNull(project.getName())); + swid.setVersion(StringUtils.trimToNull(project.getVersion())); + cycloneComponent.setSwid(swid); + } + if (project.getClassifier() != null) { + cycloneComponent.setType(org.cyclonedx.model.Component.Type.valueOf(project.getClassifier().name())); + } else { + cycloneComponent.setType(org.cyclonedx.model.Component.Type.LIBRARY); + } + if (project.getExternalReferences() != null && !project.getExternalReferences().isEmpty()) { + List references = new ArrayList<>(); + project.getExternalReferences().forEach(externalReference -> { + org.cyclonedx.model.ExternalReference ref = new org.cyclonedx.model.ExternalReference(); + ref.setUrl(externalReference.getUrl()); + ref.setType(externalReference.getType()); + ref.setComment(externalReference.getComment()); + references.add(ref); + }); + cycloneComponent.setExternalReferences(references); + } + cycloneComponent.setSupplier(convert(project.getSupplier())); + // NB: Project properties are currently used to configure integrations + // such as Defect Dojo. They can also contain encrypted values that most + // definitely are not safe to share. Before we can include project properties + // in BOM exports, we need a filtering mechanism. + // cycloneComponent.setProperties(convert(project.getProperties())); + cycloneComponent.setManufacturer(convert(project.getManufacturer())); + metadata.setComponent(cycloneComponent); + + if (project.getMetadata() != null) { + metadata.setAuthors(convertContacts(project.getMetadata().getAuthors())); + metadata.setSupplier(convert(project.getMetadata().getSupplier())); + } + } + return metadata; + } + + 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()); + cycloneService.setProvider(convert(service.getProvider())); + cycloneService.setProvider(convert(service.getProvider())); + cycloneService.setGroup(StringUtils.trimToNull(service.getGroup())); + cycloneService.setName(StringUtils.trimToNull(service.getName())); + cycloneService.setVersion(StringUtils.trimToNull(service.getVersion())); + cycloneService.setDescription(StringUtils.trimToNull(service.getDescription())); + if (service.getEndpoints() != null && service.getEndpoints().length > 0) { + cycloneService.setEndpoints(Arrays.asList(service.getEndpoints().clone())); + } + cycloneService.setAuthenticated(service.getAuthenticated()); + cycloneService.setxTrustBoundary(service.getCrossesTrustBoundary()); + 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().isEmpty()) { + for (ExternalReference ref : service.getExternalReferences()) { + org.cyclonedx.model.ExternalReference cycloneRef = new org.cyclonedx.model.ExternalReference(); + cycloneRef.setType(ref.getType()); + cycloneRef.setUrl(ref.getUrl()); + cycloneRef.setComment(ref.getComment()); + cycloneService.addExternalReference(cycloneRef); + } + } + /* TODO: Add when services support licenses (after component license refactor) + if (component.getResolvedLicense() != null) { + final org.cyclonedx.model.License license = new org.cyclonedx.model.License(); + license.setId(component.getResolvedLicense().getLicenseId()); + final LicenseChoice licenseChoice = new LicenseChoice(); + licenseChoice.addLicense(license); + cycloneComponent.setLicenses(licenseChoice); + } else if (component.getLicense() != null) { + final org.cyclonedx.model.License license = new org.cyclonedx.model.License(); + license.setName(component.getLicense()); + final LicenseChoice licenseChoice = new LicenseChoice(); + licenseChoice.addLicense(license); + cycloneComponent.setLicenses(licenseChoice); + } + */ + + /* + TODO: Assemble child/parent hierarchy. Components come in as flat, resolved dependencies. + */ + /* + if (component.getChildren() != null && component.getChildren().size() > 0) { + final List components = new ArrayList<>(); + final Component[] children = component.getChildren().toArray(new Component[0]); + for (Component child : children) { + components.add(convert(qm, child)); + } + if (children.length > 0) { + cycloneComponent.setComponents(components); + } + } + */ + return cycloneService; + } + + private static List convert(final Collection dtProperties) { + if (dtProperties == null || dtProperties.isEmpty()) { + return Collections.emptyList(); + } + + final List cdxProperties = new ArrayList<>(); + for (final T dtProperty : dtProperties) { + if (dtProperty.getPropertyType() == IConfigProperty.PropertyType.ENCRYPTEDSTRING) { + // We treat encrypted properties as internal. + // They shall not be leaked when exporting. + continue; + } + + final var cdxProperty = new org.cyclonedx.model.Property(); + if (dtProperty.getGroupName() == null) { + cdxProperty.setName(dtProperty.getPropertyName()); + } else { + cdxProperty.setName("%s:%s".formatted(dtProperty.getGroupName(), dtProperty.getPropertyName())); + } + cdxProperty.setValue(dtProperty.getPropertyValue()); + cdxProperties.add(cdxProperty); + } + + return cdxProperties; + } + + private static org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity convertDtSeverityToCdxSeverity(final Severity severity) { + switch (severity) { + case CRITICAL: + return org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.CRITICAL; + case HIGH: + return org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.HIGH; + case MEDIUM: + return org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.MEDIUM; + case LOW: + return org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.LOW; + default: + return org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.UNKNOWN; + } + } + + private static org.cyclonedx.model.vulnerability.Vulnerability.Source convertDtVulnSourceToCdxVulnSource(final Vulnerability.Source vulnSource) { + org.cyclonedx.model.vulnerability.Vulnerability.Source cdxSource = new org.cyclonedx.model.vulnerability.Vulnerability.Source(); + cdxSource.setName(vulnSource.name()); + switch (vulnSource) { + case NVD: + cdxSource.setUrl("https://nvd.nist.gov/"); + break; + case NPM: + cdxSource.setUrl("https://www.npmjs.com/"); + break; + case GITHUB: + cdxSource.setUrl("https://github.com/advisories"); + break; + case VULNDB: + cdxSource.setUrl("https://vulndb.cyberriskanalytics.com/"); + break; + case OSSINDEX: + cdxSource.setUrl("https://ossindex.sonatype.org/"); + break; + case RETIREJS: + cdxSource.setUrl("https://github.com/RetireJS/retire.js"); + break; + } + return cdxSource; + } + + public static String convertContactsToString(List authors) { + if (authors == null || authors.isEmpty()) { + return ""; + } + StringBuilder stringBuilder = new StringBuilder(); + for (OrganizationalContact author : authors) { + if (author != null && author.getName() != null) { + stringBuilder.append(author.getName()).append(", "); + } + } + //remove trailing comma and space + if (stringBuilder.length() > 0) { + stringBuilder.setLength(stringBuilder.length() - 2); + } + return stringBuilder.toString(); + } + + 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(ModelExporter::convert).toList()); + } + if (dtEntity.getUrls() != null && dtEntity.getUrls().length > 0) { + cdxEntity.setUrls(Arrays.stream(dtEntity.getUrls()).toList()); + } + + return cdxEntity; + } + + private static List convertContacts(final List dtContacts) { + if (dtContacts == null) { + return null; + } + + return dtContacts.stream().map(ModelExporter::convert).toList(); + } + + 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; + } + + private static org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Response convertDtVulnAnalysisResponseToCdxAnalysisResponse(final AnalysisResponse analysisResponse) { + if (analysisResponse == null) { + return null; + } + switch (analysisResponse) { + case UPDATE: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Response.UPDATE; + case CAN_NOT_FIX: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Response.CAN_NOT_FIX; + case WILL_NOT_FIX: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Response.WILL_NOT_FIX; + case ROLLBACK: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Response.ROLLBACK; + case WORKAROUND_AVAILABLE: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Response.WORKAROUND_AVAILABLE; + default: + return null; + } + } + + private static org.cyclonedx.model.vulnerability.Vulnerability.Analysis.State convertDtVulnAnalysisStateToCdxAnalysisState(final AnalysisState analysisState) { + if (analysisState == null) { + return null; + } + switch (analysisState) { + case EXPLOITABLE: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.State.EXPLOITABLE; + case FALSE_POSITIVE: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.State.FALSE_POSITIVE; + case IN_TRIAGE: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.State.IN_TRIAGE; + case NOT_AFFECTED: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.State.NOT_AFFECTED; + case RESOLVED: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.State.RESOLVED; + default: + return null; + } + } + + private static org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification convertDtVulnAnalysisJustificationToCdxAnalysisJustification(final AnalysisJustification analysisJustification) { + if (analysisJustification == null) { + return null; + } + switch (analysisJustification) { + case CODE_NOT_PRESENT: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.CODE_NOT_PRESENT; + case CODE_NOT_REACHABLE: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.CODE_NOT_REACHABLE; + case PROTECTED_AT_PERIMETER: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.PROTECTED_AT_PERIMETER; + case PROTECTED_AT_RUNTIME: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.PROTECTED_AT_RUNTIME; + case PROTECTED_BY_COMPILER: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.PROTECTED_BY_COMPILER; + case PROTECTED_BY_MITIGATING_CONTROL: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.PROTECTED_BY_MITIGATING_CONTROL; + case REQUIRES_CONFIGURATION: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.REQUIRES_CONFIGURATION; + case REQUIRES_DEPENDENCY: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.REQUIRES_DEPENDENCY; + case REQUIRES_ENVIRONMENT: + return org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.REQUIRES_ENVIRONMENT; + default: + return null; + } + } + + public static org.cyclonedx.model.vulnerability.Vulnerability convert(final QueryManager qm, final CycloneDXExporter.Variant variant, + final Finding finding) { + final Component component = qm.getObjectByUuid(Component.class, (String) finding.getComponent().get("uuid")); + final Project project = component.getProject(); + final Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, (String) finding.getVulnerability().get("uuid")); + + final org.cyclonedx.model.vulnerability.Vulnerability cdxVulnerability = new org.cyclonedx.model.vulnerability.Vulnerability(); + cdxVulnerability.setBomRef(vulnerability.getUuid().toString()); + cdxVulnerability.setId(vulnerability.getVulnId()); + // Add the vulnerability source + org.cyclonedx.model.vulnerability.Vulnerability.Source cdxSource = new org.cyclonedx.model.vulnerability.Vulnerability.Source(); + cdxSource.setName(vulnerability.getSource()); + cdxVulnerability.setSource(convertDtVulnSourceToCdxVulnSource(Vulnerability.Source.valueOf(vulnerability.getSource()))); + if (vulnerability.getCvssV2BaseScore() != null) { + org.cyclonedx.model.vulnerability.Vulnerability.Rating rating = new org.cyclonedx.model.vulnerability.Vulnerability.Rating(); + rating.setSource(convertDtVulnSourceToCdxVulnSource(Vulnerability.Source.valueOf(vulnerability.getSource()))); + rating.setMethod(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Method.CVSSV2); + rating.setScore(vulnerability.getCvssV2BaseScore().doubleValue()); + rating.setVector(vulnerability.getCvssV2Vector()); + if (rating.getScore() >= 7.0) { + rating.setSeverity(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.HIGH); + } else if (rating.getScore() >= 4.0) { + rating.setSeverity(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.MEDIUM); + } else { + rating.setSeverity(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.LOW); + } + cdxVulnerability.addRating(rating); + } + if (vulnerability.getCvssV3BaseScore() != null) { + org.cyclonedx.model.vulnerability.Vulnerability.Rating rating = new org.cyclonedx.model.vulnerability.Vulnerability.Rating(); + rating.setSource(convertDtVulnSourceToCdxVulnSource(Vulnerability.Source.valueOf(vulnerability.getSource()))); + if (vulnerability.getCvssV3Vector() != null && vulnerability.getCvssV3Vector().contains("CVSS:3.0")) { + rating.setMethod(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Method.CVSSV3); + } else { + rating.setMethod(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Method.CVSSV31); + } + rating.setScore(vulnerability.getCvssV3BaseScore().doubleValue()); + rating.setVector(vulnerability.getCvssV3Vector()); + if (rating.getScore() >= 9.0) { + rating.setSeverity(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.CRITICAL); + } else if (rating.getScore() >= 7.0) { + rating.setSeverity(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.HIGH); + } else if (rating.getScore() >= 4.0) { + rating.setSeverity(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.MEDIUM); + } else { + rating.setSeverity(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity.LOW); + } + cdxVulnerability.addRating(rating); + } + if (vulnerability.getOwaspRRLikelihoodScore() != null && vulnerability.getOwaspRRTechnicalImpactScore() != null && vulnerability.getOwaspRRBusinessImpactScore() != null) { + org.cyclonedx.model.vulnerability.Vulnerability.Rating rating = new org.cyclonedx.model.vulnerability.Vulnerability.Rating(); + rating.setSeverity(convertDtSeverityToCdxSeverity(VulnerabilityUtil.normalizedOwaspRRScore(vulnerability.getOwaspRRLikelihoodScore().doubleValue(), vulnerability.getOwaspRRTechnicalImpactScore().doubleValue(), vulnerability.getOwaspRRBusinessImpactScore().doubleValue()))); + rating.setSource(convertDtVulnSourceToCdxVulnSource(Vulnerability.Source.valueOf(vulnerability.getSource()))); + rating.setMethod(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Method.OWASP); + rating.setVector(vulnerability.getOwaspRRVector()); + cdxVulnerability.addRating(rating); + } + if (vulnerability.getCvssV2BaseScore() == null && vulnerability.getCvssV3BaseScore() == null && vulnerability.getOwaspRRLikelihoodScore() == null) { + org.cyclonedx.model.vulnerability.Vulnerability.Rating rating = new org.cyclonedx.model.vulnerability.Vulnerability.Rating(); + rating.setSeverity(convertDtSeverityToCdxSeverity(vulnerability.getSeverity())); + rating.setSource(convertDtVulnSourceToCdxVulnSource(Vulnerability.Source.valueOf(vulnerability.getSource()))); + rating.setMethod(org.cyclonedx.model.vulnerability.Vulnerability.Rating.Method.OTHER); + cdxVulnerability.addRating(rating); + } + if (vulnerability.getCwes() != null) { + for (final Integer cweId : vulnerability.getCwes()) { + final Cwe cwe = CweResolver.getInstance().lookup(cweId); + if (cwe != null) { + cdxVulnerability.addCwe(cwe.getCweId()); + } + } + } + cdxVulnerability.setDescription(vulnerability.getDescription()); + cdxVulnerability.setRecommendation(vulnerability.getRecommendation()); + cdxVulnerability.setCreated(vulnerability.getCreated()); + cdxVulnerability.setPublished(vulnerability.getPublished()); + cdxVulnerability.setUpdated(vulnerability.getUpdated()); + + if (CycloneDXExporter.Variant.INVENTORY_WITH_VULNERABILITIES == variant || CycloneDXExporter.Variant.VDR == variant) { + final List affects = new ArrayList<>(); + final org.cyclonedx.model.vulnerability.Vulnerability.Affect affect = new org.cyclonedx.model.vulnerability.Vulnerability.Affect(); + affect.setRef(component.getUuid().toString()); + affects.add(affect); + cdxVulnerability.setAffects(affects); + } else if (CycloneDXExporter.Variant.VEX == variant && project != null) { + final List affects = new ArrayList<>(); + final org.cyclonedx.model.vulnerability.Vulnerability.Affect affect = new org.cyclonedx.model.vulnerability.Vulnerability.Affect(); + affect.setRef(project.getUuid().toString()); + affects.add(affect); + cdxVulnerability.setAffects(affects); + } + + if (CycloneDXExporter.Variant.VEX == variant || CycloneDXExporter.Variant.VDR == variant) { + final Analysis analysis = qm.getAnalysis( + qm.getObjectByUuid(Component.class, component.getUuid()), + qm.getObjectByUuid(Vulnerability.class, vulnerability.getUuid()) + ); + if (analysis != null) { + final org.cyclonedx.model.vulnerability.Vulnerability.Analysis cdxAnalysis = new org.cyclonedx.model.vulnerability.Vulnerability.Analysis(); + if (analysis.getAnalysisResponse() != null) { + final org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Response response = convertDtVulnAnalysisResponseToCdxAnalysisResponse(analysis.getAnalysisResponse()); + if (response != null) { + List responses = new ArrayList<>(); + responses.add(response); + cdxAnalysis.setResponses(responses); + } + } + if (analysis.getAnalysisState() != null) { + cdxAnalysis.setState(convertDtVulnAnalysisStateToCdxAnalysisState(analysis.getAnalysisState())); + } + if (analysis.getAnalysisJustification() != null) { + cdxAnalysis.setJustification(convertDtVulnAnalysisJustificationToCdxAnalysisJustification(analysis.getAnalysisJustification())); + } + cdxAnalysis.setDetail(StringUtils.trimToNull(analysis.getAnalysisDetails())); + cdxVulnerability.setAnalysis(cdxAnalysis); + } + } + + return cdxVulnerability; + } + + public static List generateVulnerabilities(final QueryManager qm, final CycloneDXExporter.Variant variant, final List findings) { + if (findings == null) { + return Collections.emptyList(); + } + final var vulnerabilitiesSeen = new HashSet(); + return findings.stream() + .map(finding -> convert(qm, variant, finding)) + .filter(vulnerabilitiesSeen::add) + .toList(); + } + + 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()); + cycloneComponent.setGroup(StringUtils.trimToNull(component.getGroup())); + cycloneComponent.setName(StringUtils.trimToNull(component.getName())); + cycloneComponent.setVersion(StringUtils.trimToNull(component.getVersion())); + cycloneComponent.setDescription(StringUtils.trimToNull(component.getDescription())); + cycloneComponent.setCopyright(StringUtils.trimToNull(component.getCopyright())); + cycloneComponent.setCpe(StringUtils.trimToNull(component.getCpe())); + cycloneComponent.setAuthor(StringUtils.trimToNull(convertContactsToString(component.getAuthors()))); + cycloneComponent.setSupplier(convert(component.getSupplier())); + cycloneComponent.setProperties(convert(component.getProperties())); + + if (component.getSwidTagId() != null) { + final Swid swid = new Swid(); + swid.setTagId(component.getSwidTagId()); + cycloneComponent.setSwid(swid); + } + + if (component.getPurl() != null) { + cycloneComponent.setPurl(component.getPurl().canonicalize()); + } + + if (component.getClassifier() != null) { + cycloneComponent.setType(org.cyclonedx.model.Component.Type.valueOf(component.getClassifier().name())); + } else { + cycloneComponent.setType(org.cyclonedx.model.Component.Type.LIBRARY); + } + + if (component.getMd5() != null) { + cycloneComponent.addHash(new Hash(Hash.Algorithm.MD5, component.getMd5())); + } + if (component.getSha1() != null) { + cycloneComponent.addHash(new Hash(Hash.Algorithm.SHA1, component.getSha1())); + } + if (component.getSha256() != null) { + cycloneComponent.addHash(new Hash(Hash.Algorithm.SHA_256, component.getSha256())); + } + if (component.getSha512() != null) { + cycloneComponent.addHash(new Hash(Hash.Algorithm.SHA_512, component.getSha512())); + } + if (component.getSha3_256() != null) { + cycloneComponent.addHash(new Hash(Hash.Algorithm.SHA3_256, component.getSha3_256())); + } + if (component.getSha3_512() != null) { + cycloneComponent.addHash(new Hash(Hash.Algorithm.SHA3_512, component.getSha3_512())); + } + + final LicenseChoice licenses = new LicenseChoice(); + if (component.getResolvedLicense() != null) { + final org.cyclonedx.model.License license = new org.cyclonedx.model.License(); + if (!component.getResolvedLicense().isCustomLicense()) { + license.setId(component.getResolvedLicense().getLicenseId()); + } else { + license.setName(component.getResolvedLicense().getName()); + } + license.setUrl(component.getLicenseUrl()); + licenses.addLicense(license); + cycloneComponent.setLicenses(licenses); + } else if (component.getLicense() != null) { + final org.cyclonedx.model.License license = new org.cyclonedx.model.License(); + license.setName(component.getLicense()); + license.setUrl(component.getLicenseUrl()); + licenses.addLicense(license); + cycloneComponent.setLicenses(licenses); + } else if (StringUtils.isNotEmpty(component.getLicenseUrl())) { + final org.cyclonedx.model.License license = new org.cyclonedx.model.License(); + license.setUrl(component.getLicenseUrl()); + licenses.addLicense(license); + cycloneComponent.setLicenses(licenses); + } + if (component.getLicenseExpression() != null) { + final var licenseExpression = new Expression(); + licenseExpression.setValue(component.getLicenseExpression()); + licenses.setExpression(licenseExpression); + cycloneComponent.setLicenses(licenses); + } + + if (component.getCryptoAssetProperties() != null) { + CryptoAssetProperties cryptoAssetProperties = component.getCryptoAssetProperties(); + CryptoProperties cryptoProperties = new CryptoProperties(); + + cryptoProperties.setAssetType(cryptoAssetProperties.getAssetType()); + + switch (cryptoAssetProperties.getAssetType()) { + case ALGORITHM: + if (cryptoAssetProperties.getAlgorithmProperties() != null) { + cryptoProperties.setAlgorithmProperties(convert(cryptoAssetProperties.getAlgorithmProperties())); + } + break; + case CERTIFICATE: + if (cryptoAssetProperties.getCertificateProperties() != null) { + cryptoProperties.setCertificateProperties(convert(cryptoAssetProperties.getCertificateProperties())); + } + break; + case RELATED_CRYPTO_MATERIAL: + if (cryptoAssetProperties.getRelatedMaterialProperties() != null) { + cryptoProperties.setRelatedCryptoMaterialProperties(convert(cryptoAssetProperties.getRelatedMaterialProperties())); + } + break; + case PROTOCOL: + if (cryptoAssetProperties.getProtocolProperties() != null) { + cryptoProperties.setProtocolProperties(convert(cryptoAssetProperties.getProtocolProperties())); + } + break; + default: + break; + } + + cryptoProperties.setOid(cryptoAssetProperties.getOid()); + cycloneComponent.setCryptoProperties(cryptoProperties); + } + + if (component.getOccurrences() != null && !component.getOccurrences().isEmpty()) { + org.cyclonedx.model.Evidence evidence = new org.cyclonedx.model.Evidence(); + List occs = new ArrayList<>(); + for (Occurrence o: component.getOccurrences()) { + occs.add(convertOccurrence(o)); + } + evidence.setOccurrences(occs); + cycloneComponent.setEvidence(evidence); + } + + if (component.getExternalReferences() != null && !component.getExternalReferences().isEmpty()) { + List references = new ArrayList<>(); + for (ExternalReference ref : component.getExternalReferences()) { + org.cyclonedx.model.ExternalReference cdxRef = new org.cyclonedx.model.ExternalReference(); + cdxRef.setType(ref.getType()); + cdxRef.setUrl(ref.getUrl()); + cdxRef.setComment(ref.getComment()); + references.add(cdxRef); + } + cycloneComponent.setExternalReferences(references); + } else { + cycloneComponent.setExternalReferences(null); + } + + /* + TODO: Assemble child/parent hierarchy. Components come in as flat, resolved dependencies. + */ + /* + if (component.getChildren() != null && component.getChildren().size() > 0) { + final List components = new ArrayList<>(); + final Component[] children = component.getChildren().toArray(new Component[0]); + for (Component child : children) { + components.add(convert(qm, child)); + } + if (children.length > 0) { + cycloneComponent.setComponents(components); + } + } + */ + + return cycloneComponent; + } + + private static org.cyclonedx.model.component.evidence.Occurrence convertOccurrence(Occurrence o) { + org.cyclonedx.model.component.evidence.Occurrence occ = new org.cyclonedx.model.component.evidence.Occurrence(); + occ.setBomRef(o.getBomRef()); + occ.setLine(o.getLine()); + occ.setLocation(o.getLocation()); + occ.setOffset(o.getOffset()); + occ.setSymbol(o.getSymbol()); + occ.setAdditionalContext(o.getAdditionalContext()); + return occ; + } + + private static AlgorithmProperties convert(CryptoAlgorithmProperties algorithmProperties) { + AlgorithmProperties ap = new AlgorithmProperties(); + ap.setPrimitive(algorithmProperties.getPrimitive()); + ap.setParameterSetIdentifier(algorithmProperties.getParameterSetIdentifier()); + ap.setCurve(algorithmProperties.getCurve()); + ap.setExecutionEnvironment(algorithmProperties.getExecutionEnvironment()); + ap.setImplementationPlatform(algorithmProperties.getImplementationPlatform()); + ap.setCertificationLevel(algorithmProperties.getCertificationLevel()); + ap.setMode(algorithmProperties.getMode()); + ap.setPadding(algorithmProperties.getPadding()); + ap.setCryptoFunctions(algorithmProperties.getCryptoFunctions()); + ap.setClassicalSecurityLevel(algorithmProperties.getClassicalSecurityLevel()); + ap.setNistQuantumSecurityLevel(algorithmProperties.getNistQuantumSecurityLevel()); + return ap; + } + + private static CertificateProperties convert(CryptoCertificateProperties certificateProperties) { + CertificateProperties cp = new CertificateProperties(); + cp.setSubjectName(certificateProperties.getSubjectName()); + cp.setIssuerName(certificateProperties.getIssuerName()); + cp.setNotValidBefore(DateUtil.toISO8601(certificateProperties.getNotValidBefore())); + cp.setNotValidAfter(DateUtil.toISO8601(certificateProperties.getNotValidAfter())); + cp.setSignatureAlgorithmRef(certificateProperties.getSignatureAlgorithmRef()); + cp.setSubjectPublicKeyRef(certificateProperties.getSubjectPublicKeyRef()); + cp.setCertificateFormat(certificateProperties.getCertificateFormat()); + cp.setCertificateExtension(certificateProperties.getCertificateExtension()); + return cp; + } + + private static RelatedCryptoMaterialProperties convert(CryptoRelatedMaterialProperties cryptoMaterialProperties) { + RelatedCryptoMaterialProperties rcmp = new RelatedCryptoMaterialProperties(); + rcmp.setType(cryptoMaterialProperties.getType()); + rcmp.setId(cryptoMaterialProperties.getIdentifier()); + rcmp.setState(cryptoMaterialProperties.getState()); + rcmp.setAlgorithmRef(cryptoMaterialProperties.getAlgorithmRef()); + rcmp.setCreationDate(DateUtil.toISO8601(cryptoMaterialProperties.getCreationDate())); + rcmp.setActivationDate(DateUtil.toISO8601(cryptoMaterialProperties.getActivationDate())); + rcmp.setUpdateDate(DateUtil.toISO8601(cryptoMaterialProperties.getUpdateDate())); + rcmp.setExpirationDate(DateUtil.toISO8601(cryptoMaterialProperties.getExpirationDate())); + rcmp.setValue(cryptoMaterialProperties.getValue()); + rcmp.setSize(cryptoMaterialProperties.getSize()); + rcmp.setFormat(cryptoMaterialProperties.getFormat()); + if (cryptoMaterialProperties.getSecuredByMechanism() != null || cryptoMaterialProperties.getSecuredByAlgorithmRef() != null) { + SecuredBy sb = new SecuredBy(); + sb.setMechanism(cryptoMaterialProperties.getSecuredByMechanism().getName()); + sb.setAlgorithmRef(cryptoMaterialProperties.getAlgorithmRef()); + rcmp.setSecuredBy(sb); + } + return rcmp; + } + + private static ProtocolProperties convert(CryptoProtocolProperties protocolProperties) { + ProtocolProperties pp = new ProtocolProperties(); + pp.setType(protocolProperties.getType()); + pp.setVersion(protocolProperties.getVersion()); + + if (protocolProperties.getCipherSuites() != null && !protocolProperties.getCipherSuites().isEmpty()) { + final var suites = new ArrayList(); + for (final CipherSuite cipherSuite : protocolProperties.getCipherSuites()) { + suites.add(convertCipherSuite(cipherSuite)); + } + pp.setCipherSuites(suites); + } + + if (protocolProperties.getIkev2Types() != null) { + Map cxIkev2Types = new HashMap<>(); + for( Ikev2Type it: protocolProperties.getIkev2Types()) { + CryptoRef cr = new CryptoRef(); + cr.setRef(it.getRefs()); + cxIkev2Types.put(it.getType(), cr); + } + pp.setIkev2TransformTypes(cxIkev2Types); + } + + // TODO: Enable when bug in cyclonedx xsd is fixed + // if (protocolProperties.getCryptoRefs() != null) { + // CryptoRef cr = new CryptoRef(); + // List crs = new ArrayList<>(); + // protocolProperties.getCryptoRefs().forEach(crs::add); + // cr.setRef(crs); + // pp.setCryptoRefArray(cr); + // } + + return pp; + } + + private static org.cyclonedx.model.component.crypto.CipherSuite convertCipherSuite(CipherSuite cs) { + org.cyclonedx.model.component.crypto.CipherSuite ccs = new org.cyclonedx.model.component.crypto.CipherSuite(); + ccs.setName(cs.getName()); + ccs.setAlgorithms(cs.getAlgorithms()); + ccs.setIdentifiers(cs.getIdentifiers()); + return ccs; + } +} diff --git a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java index ad8b15f4f..b1d34a64c 100644 --- a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java @@ -135,7 +135,7 @@ public List getAllComponents() { @SuppressWarnings("unchecked") public List getAllComponents(Project project) { final Query query = pm.newQuery(Component.class, "project == :project"); - query.getFetchPlan().setMaxFetchDepth(2); + query.getFetchPlan().setMaxFetchDepth(3); query.setOrdering("name asc"); return (List) query.execute(project); } @@ -553,6 +553,7 @@ public Component cloneComponent(Component sourceComponent, Project destinationPr component.setSupplier(sourceComponent.getSupplier()); component.setDirectDependencies(sourceComponent.getDirectDependencies()); // TODO Add support for parent component and children components + component.setCryptoAssetProperties(sourceComponent.getCryptoAssetProperties()); component.setProject(destinationProject); return createComponent(component, commitIndex); } @@ -590,10 +591,15 @@ public Component updateComponent(Component transientComponent, boolean commitInd component.setAuthors(transientComponent.getAuthors()); component.setSupplier(transientComponent.getSupplier()); component.setExternalReferences(transientComponent.getExternalReferences()); + component.setClassifier(transientComponent.getClassifier()); + component.setCryptoAssetProperties(transientComponent.getCryptoAssetProperties()); final Component result = persist(component); return result; } + + + /** * Deletes all components for the specified Project. * @@ -764,6 +770,14 @@ private static Pair> buildComponentIdentityQuery(fin filterParts.add("swidTagId == :swidTagId"); params.put("swidTagId", cid.getSwidTagId()); } + if (cid.getAssetType() != null) { + filterParts.add("(cryptoAssetProperties != null && cryptoAssetProperties.assetType == :assetType)"); + params.put("assetType", cid.getAssetType()); + } + if (cid.getOid() != null) { + filterParts.add("(cryptoAssetProperties != null && cryptoAssetProperties.oid == :oid)"); + params.put("oid", cid.getOid()); + } final var coordinatesFilterParts = new ArrayList(); if (cid.getGroup() != null) { @@ -839,6 +853,20 @@ private static Pair> buildExactComponentIdentityQuer coordinatesFilter += ")"; filterParts.add(coordinatesFilter); + if (cid.getAssetType() != null) { + filterParts.add("(cryptoAssetProperties != null && cryptoAssetProperties.assetType == :assetType)"); + params.put("assetType", cid.getAssetType()); + } else { + filterParts.add("cryptoAssetProperties == null"); + } + + if (cid.getOid() != null) { + filterParts.add("(cryptoAssetProperties != null && cryptoAssetProperties.oid == :oid)"); + params.put("oid", cid.getOid()); + } else { + filterParts.add("cryptoAssetProperties != null && cryptoAssetProperties.oid == null"); + } + final var filter = "project == :project && (" + String.join(" && ", filterParts) + ")"; params.put("project", project); diff --git a/src/main/java/org/dependencytrack/persistence/CryptoAssetQueryManager.java b/src/main/java/org/dependencytrack/persistence/CryptoAssetQueryManager.java new file mode 100644 index 000000000..fed975469 --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/CryptoAssetQueryManager.java @@ -0,0 +1,133 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.persistence; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.jdo.PersistenceManager; +import javax.jdo.Query; + +import org.apache.commons.lang3.tuple.Pair; +import org.dependencytrack.model.Classifier; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.ComponentIdentity; +import org.dependencytrack.model.Project; + +import alpine.persistence.PaginatedResult; +import alpine.resources.AlpineRequest; + +public class CryptoAssetQueryManager extends QueryManager implements IQueryManager { + + /** + * Constructs a new QueryManager. + * @param pm a PersistenceManager object + */ + CryptoAssetQueryManager(final PersistenceManager pm) { + super(pm); + } + + /** + * Constructs a new QueryManager. + * @param pm a PersistenceManager object + * @param request an AlpineRequest object + */ + CryptoAssetQueryManager(final PersistenceManager pm, final AlpineRequest request) { + super(pm, request); + } + + private static final String globalFilter = setGlobalFilter(); + private static String setGlobalFilter() { + return "bomReference == null"; + } + + /** + * Returns a complete list of all CryptoAssets + * @return a List of CryptoAssets + */ + @SuppressWarnings("unchecked") + public List getAllCryptoAssets() { + final Query query = pm.newQuery(Component.class, globalFilter + " && (classifier == :asset)"); + query.getFetchPlan().setMaxFetchDepth(3); + return (List) query.execute(Classifier.CRYPTOGRAPHIC_ASSET); + } + + /** + * Returns a List of all CryptoAssets for the specified Project. + * This method is designed NOT to provide paginated results. + * @param project the Project to retrieve dependencies of + * @return a List of Component objects + */ + @SuppressWarnings("unchecked") + public List getAllCryptoAssets(Project project) { + final Query query = pm.newQuery(Component.class, globalFilter + " && (project == :project) && (classifier == :asset)"); + query.getFetchPlan().setMaxFetchDepth(3); + query.setOrdering("name asc"); + return (List)query.execute(project, Classifier.CRYPTOGRAPHIC_ASSET); + } + + /** + * Returns crypto assets by their identity. + * @param identity the asset identity to query against + * @return a list of components + */ + public PaginatedResult getCryptoAssets(ComponentIdentity identity) { + if (identity == null) { + return null; + } + Pair, HashMap> queryProp = buildIdentityQuery(identity); + String filter = String.join(" && ", queryProp.getKey()); + return loadComponents(filter, queryProp.getValue()); + } + + private PaginatedResult loadComponents(String queryFilter, Map params) { + var query = pm.newQuery(Component.class, globalFilter); + query.getFetchPlan().setMaxFetchDepth(3); + if (orderBy == null) { + query.setOrdering("id asc"); + } + preprocessACLs(query, queryFilter, params, false); + return execute(query, params); + } + + private Pair, HashMap> buildIdentityQuery(ComponentIdentity identity) { + if (identity == null) { + return null; + } + + final var queryFilterElements = new ArrayList(); + final var queryParams = new HashMap(); + + queryFilterElements.add(" classifier == :classifier "); + queryParams.put("classifier", Classifier.CRYPTOGRAPHIC_ASSET); + + if (identity.getAssetType() != null) { + try { + queryFilterElements.add("cryptoAssetProperties.assetType == :assetType"); + queryParams.put("assetType", identity.getAssetType()); + } catch (IllegalArgumentException iae) { + // ignore + } + } + + return Pair.of(queryFilterElements, queryParams); + } +} diff --git a/src/main/java/org/dependencytrack/persistence/MetricsQueryManager.java b/src/main/java/org/dependencytrack/persistence/MetricsQueryManager.java index ea96a9618..a2b0bbbe3 100644 --- a/src/main/java/org/dependencytrack/persistence/MetricsQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/MetricsQueryManager.java @@ -21,6 +21,7 @@ import alpine.persistence.PaginatedResult; import alpine.resources.AlpineRequest; import org.dependencytrack.model.Component; +import org.dependencytrack.model.CryptographyMetrics; import org.dependencytrack.model.DependencyMetrics; import org.dependencytrack.model.PortfolioMetrics; import org.dependencytrack.model.Project; @@ -220,4 +221,27 @@ void deleteMetrics(Component component) { final Query query = pm.newQuery(DependencyMetrics.class, "component == :component"); query.deletePersistentAll(component); } + + /** + * Returns a paginated result containing cryptography metrics ordered by last occurrence date. + * + * @return a PaginatedResult containing CryptographyMetrics objects, ordered by last occurrence date. + */ + public PaginatedResult getCryptographyMetrics() { + final Query query = pm.newQuery(CryptographyMetrics.class); + query.setOrdering("lastOccurrence desc"); + return execute(query); + } + + /** + * Returns a list of cryptography metrics that have been updated since the specified date. + * @param since The date to compare against the last occurrence of each metric. Metrics with a last occurrence date greater than or equal to this date will be included in the result. + * @return A list of {@link CryptographyMetrics} objects that have been updated since the specified date. + */ + @SuppressWarnings("unchecked") + public List getCryptographyMetricsSince(Date since) { + final Query query = pm.newQuery(CryptographyMetrics.class, "lastOccurrence >= :since"); + query.setOrdering("lastOccurrence asc"); + return (List) query.execute(since); + } } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 3a921fff3..515d64801 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -56,6 +56,7 @@ import org.dependencytrack.model.ComponentMetaInformation; import org.dependencytrack.model.ComponentProperty; import org.dependencytrack.model.ConfigPropertyConstants; +import org.dependencytrack.model.CryptographyMetrics; import org.dependencytrack.model.DependencyMetrics; import org.dependencytrack.model.Epss; import org.dependencytrack.model.Finding; @@ -147,6 +148,7 @@ public class QueryManager extends AlpineQueryManager { private static final Logger LOGGER = Logger.getLogger(QueryManager.class); private BomQueryManager bomQueryManager; private ComponentQueryManager componentQueryManager; + private CryptoAssetQueryManager cryptoAssetQueryManager; private FindingsQueryManager findingsQueryManager; private FindingsSearchQueryManager findingsSearchQueryManager; private LicenseQueryManager licenseQueryManager; @@ -477,6 +479,13 @@ private IntegrityAnalysisQueryManager getIntegrityAnalysisQueryManager() { return integrityAnalysisQueryManager; } + private CryptoAssetQueryManager getCryptoAssetQueryManager() { + if (cryptoAssetQueryManager == null) { + cryptoAssetQueryManager = (request == null) ? new CryptoAssetQueryManager(getPersistenceManager()) : new CryptoAssetQueryManager(getPersistenceManager(), request); + } + return cryptoAssetQueryManager; + } + private void disableL2Cache() { pm.setProperty(PropertyNames.PROPERTY_CACHE_L2_TYPE, "none"); } @@ -718,6 +727,18 @@ public PaginatedResult getComponents(ComponentIdentity identity, Project project return getComponentQueryManager().getComponents(identity, project, includeMetrics); } + public List getAllCryptoAssets() { + return getCryptoAssetQueryManager().getAllCryptoAssets(); + } + + public List getAllCryptoAssets(Project project) { + return getCryptoAssetQueryManager().getAllCryptoAssets(project); + } + + public PaginatedResult getCryptoAssets(ComponentIdentity identity) { + return getCryptoAssetQueryManager().getCryptoAssets(identity); + } + public Component createComponent(Component component, boolean commitIndex) { return getComponentQueryManager().createComponent(component, commitIndex); } @@ -1300,6 +1321,14 @@ public List getDependencyMetricsSince(Component component, Da return getMetricsQueryManager().getDependencyMetricsSince(component, since); } + public PaginatedResult getCryptographyMetrics() { + return getMetricsQueryManager().getCryptographyMetrics(); + } + + public List getCryptographyMetricsSince(Date since) { + return getMetricsQueryManager().getCryptographyMetricsSince(since); + } + public void synchronizeVulnerabilityMetrics(List metrics) { getMetricsQueryManager().synchronizeVulnerabilityMetrics(metrics); } diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java index 5f4df23a7..a6c5fb078 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java @@ -170,6 +170,7 @@ public Response getComponentByUuid( if (component != null) { final Project project = component.getProject(); if (qm.hasAccess(super.getPrincipal(), project)) { + qm.getPersistenceManager().getFetchPlan().setMaxFetchDepth(3); final Component detachedComponent = qm.detach(Component.class, component.getId()); // TODO: Force project to be loaded. It should be anyway, but JDO seems to be having issues here. if ((includeRepositoryMetaData || includeIntegrityMetaData) && detachedComponent.getPurl() != null) { final RepositoryType type = RepositoryType.resolve(detachedComponent.getPurl()); diff --git a/src/main/java/org/dependencytrack/resources/v1/CryptoAssetsResource.java b/src/main/java/org/dependencytrack/resources/v1/CryptoAssetsResource.java new file mode 100644 index 000000000..6d46db10e --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/CryptoAssetsResource.java @@ -0,0 +1,409 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v1; + +import alpine.persistence.PaginatedResult; +import alpine.server.auth.PermissionRequired; +import alpine.server.resources.AlpineResource; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; + +import org.apache.commons.lang3.StringUtils; +import org.cyclonedx.model.component.crypto.enums.AssetType; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.Classifier; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.ComponentIdentity; +import org.dependencytrack.model.License; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.validation.ValidUuid; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; +import org.dependencytrack.util.InternalComponentIdentifier; + +import jakarta.validation.Validator; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; + +/** + * JAX-RS resources for processing crypto assets. + * + * @author Nicklas Körtge + * @since 4.5.0 + */ + +@Path("/v1/crypto") +@Tag(name = "crypto") +public class CryptoAssetsResource extends AlpineResource { + + @GET + @Path("/project/{uuid}") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns a list of all crypto assets of a specific project", + description = "Returns a list of all crypto assets of a specific project" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "A list of all crypto assets for a given project", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of crypto assets", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Component.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified crypto asset is forbidden"), + @ApiResponse(responseCode = "404", description = "The rypto asset could not be found") + }) + public Response getAllCryptoAssetsOfAProject(@PathParam("uuid") String uuid) { + try (QueryManager qm = new QueryManager(getAlpineRequest())) { + + final Project project = qm.getObjectByUuid(Project.class, uuid); + + if (project != null) { + if (qm.hasAccess(super.getPrincipal(), project)) { + final List cryptoAssets = qm.getAllCryptoAssets(project); + return Response.ok(cryptoAssets).header(TOTAL_COUNT_HEADER, cryptoAssets.size()).build(); + } else { + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + } + } else { + return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); + } + } catch (Error e) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + } + + @GET + @Path("/{uuid}") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns a specific crypto asset", + description = "Returns a specific crypto asset given by its uuid" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "A crypto asset", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of crypto assets", schema = @Schema(format = "integer")), + content = @Content(schema = @Schema(implementation = Component.class)) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified crypto asset is forbidden"), + @ApiResponse(responseCode = "404", description = "The crypto asset could not be found") + }) + @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) + public Response getCryptoAssetByUuid( + @Parameter(description = "The UUID of the component to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid) { + try (QueryManager qm = new QueryManager()) { + final Component component = qm.getObjectByUuid(Component.class, uuid); + if (component != null && component.getClassifier() == Classifier.CRYPTOGRAPHIC_ASSET) { + final Project project = component.getProject(); + if (qm.hasAccess(super.getPrincipal(), project)) { + final Component asset = qm.detach(Component.class, component.getId()); // TODO: Force project to be loaded. It should be anyway, but JDO seems to be having issues here. + return Response.ok(asset).build(); + } else { + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified crypto asset is forbidden").build(); + } + } else { + return Response.status(Response.Status.NOT_FOUND).entity("The crypto asset could not be found").build(); + } + } + } + + @GET + @Path("/identity") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns a a list of crypto asset that have the specified identity.", + description = "Returns a a list of crypto asset that have the specified identity." + ) + @PaginatedApi + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "A list of all components for a given project", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of crypto assets", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Component.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") + }) + @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) + public Response getComponentByIdentity( + @Parameter(description = "The type of the crypto assets to retrieve") + @QueryParam("assetType") String assetType){ + try (QueryManager qm = new QueryManager(getAlpineRequest())) { + String assetTypeStr = StringUtils.trimToNull(assetType); + final ComponentIdentity identity = new ComponentIdentity(assetTypeStr != null ? AssetType.valueOf(assetTypeStr) : null); + final PaginatedResult result = qm.getCryptoAssets(identity); + return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); + } + } + + @PUT + @Path("/project/{uuid}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Creates a new crypto asset", + description = "

Requires permission PORTFOLIO_MANAGEMENT or PORTFOLIO_MANAGEMENT_UPDATE

" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "The created component", + content = @Content(schema = @Schema(implementation = Component.class)) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse(responseCode = "404", description = "The project could not be found") + }) + @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE}) + public Response createComponent(@PathParam("uuid") String uuid, Component jsonComponent) { + final Validator validator = super.getValidator(); + failOnValidationError( + validator.validateProperty(jsonComponent, "author"), + validator.validateProperty(jsonComponent, "publisher"), + validator.validateProperty(jsonComponent, "name"), + validator.validateProperty(jsonComponent, "version"), + validator.validateProperty(jsonComponent, "group"), + validator.validateProperty(jsonComponent, "description"), + validator.validateProperty(jsonComponent, "license"), + validator.validateProperty(jsonComponent, "filename"), + validator.validateProperty(jsonComponent, "classifier"), + validator.validateProperty(jsonComponent, "cpe"), + validator.validateProperty(jsonComponent, "swidTagId"), + validator.validateProperty(jsonComponent, "copyright"), + validator.validateProperty(jsonComponent, "md5"), + validator.validateProperty(jsonComponent, "sha1"), + validator.validateProperty(jsonComponent, "sha256"), + validator.validateProperty(jsonComponent, "sha384"), + validator.validateProperty(jsonComponent, "sha512"), + validator.validateProperty(jsonComponent, "sha3_256"), + validator.validateProperty(jsonComponent, "sha3_384"), + validator.validateProperty(jsonComponent, "sha3_512"), + validator.validateProperty(jsonComponent, "cryptoAssetProperties") + ); + + try (QueryManager qm = new QueryManager()) { + final Project project = qm.getObjectByUuid(Project.class, uuid); + if (project == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); + } + if (! qm.hasAccess(super.getPrincipal(), project)) { + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + } + if (jsonComponent.getClassifier() != Classifier.CRYPTOGRAPHIC_ASSET) { + return Response.status(Response.Status.BAD_REQUEST).entity("The component you provided is not a crypto asset").build(); + } + final License resolvedLicense = qm.getLicense(jsonComponent.getLicense()); + Component component = new Component(); + component.setProject(project); + component.setAuthor(StringUtils.trimToNull(jsonComponent.getAuthor())); + component.setPublisher(StringUtils.trimToNull(jsonComponent.getPublisher())); + component.setName(StringUtils.trimToNull(jsonComponent.getName())); + component.setVersion(StringUtils.trimToNull(jsonComponent.getVersion())); + component.setGroup(StringUtils.trimToNull(jsonComponent.getGroup())); + component.setDescription(StringUtils.trimToNull(jsonComponent.getDescription())); + component.setFilename(StringUtils.trimToNull(jsonComponent.getFilename())); + component.setClassifier(jsonComponent.getClassifier()); + component.setPurl(jsonComponent.getPurl()); + component.setInternal(new InternalComponentIdentifier().isInternal(component)); + component.setCpe(StringUtils.trimToNull(jsonComponent.getCpe())); + component.setSwidTagId(StringUtils.trimToNull(jsonComponent.getSwidTagId())); + component.setCopyright(StringUtils.trimToNull(jsonComponent.getCopyright())); + component.setMd5(StringUtils.trimToNull(jsonComponent.getMd5())); + component.setSha1(StringUtils.trimToNull(jsonComponent.getSha1())); + component.setSha256(StringUtils.trimToNull(jsonComponent.getSha256())); + component.setSha384(StringUtils.trimToNull(jsonComponent.getSha384())); + component.setSha512(StringUtils.trimToNull(jsonComponent.getSha512())); + component.setSha3_256(StringUtils.trimToNull(jsonComponent.getSha3_256())); + component.setSha3_384(StringUtils.trimToNull(jsonComponent.getSha3_384())); + component.setSha3_512(StringUtils.trimToNull(jsonComponent.getSha3_512())); + + if (jsonComponent.getCryptoAssetProperties() != null) { + component.setCryptoAssetProperties(jsonComponent.getCryptoAssetProperties()); + } else { + return Response.status(Response.Status.BAD_REQUEST).entity("No data for crypto asset properties provided").build(); + } + + if (resolvedLicense != null) { + component.setLicense(null); + component.setResolvedLicense(resolvedLicense); + } else { + component.setLicense(StringUtils.trimToNull(jsonComponent.getLicense())); + component.setResolvedLicense(null); + } + component.setNotes(StringUtils.trimToNull(jsonComponent.getNotes())); + + component = qm.createComponent(component, true); + return Response.status(Response.Status.CREATED).entity(component).build(); + } + } + + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Updates a crypto assets", + description = "

Requires permission PORTFOLIO_MANAGEMENT or PORTFOLIO_MANAGEMENT_UPDATE

" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "The updated component", + content = @Content(schema = @Schema(implementation = Component.class)) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse(responseCode = "404", description = "The UUID of the component could not be found"), + }) + @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE}) + public Response updateCryptoAsset(Component jsonComponent) { + final Validator validator = super.getValidator(); + failOnValidationError( + validator.validateProperty(jsonComponent, "name"), + validator.validateProperty(jsonComponent, "version"), + validator.validateProperty(jsonComponent, "group"), + validator.validateProperty(jsonComponent, "description"), + validator.validateProperty(jsonComponent, "license"), + validator.validateProperty(jsonComponent, "filename"), + validator.validateProperty(jsonComponent, "classifier"), + validator.validateProperty(jsonComponent, "cpe"), + validator.validateProperty(jsonComponent, "swidTagId"), + validator.validateProperty(jsonComponent, "copyright"), + validator.validateProperty(jsonComponent, "md5"), + validator.validateProperty(jsonComponent, "sha1"), + validator.validateProperty(jsonComponent, "sha256"), + validator.validateProperty(jsonComponent, "sha512"), + validator.validateProperty(jsonComponent, "sha3_256"), + validator.validateProperty(jsonComponent, "sha3_512"), + validator.validateProperty(jsonComponent, "cryptoAssetProperties") + ); + try (QueryManager qm = new QueryManager()) { + Component component = qm.getObjectByUuid(Component.class, jsonComponent.getUuid()); + if (component != null) { + if (! qm.hasAccess(super.getPrincipal(), component.getProject())) { + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified crypto asset is forbidden").build(); + } + if (jsonComponent.getClassifier() != Classifier.CRYPTOGRAPHIC_ASSET) { + return Response.status(Response.Status.BAD_REQUEST).entity("The component you provided is not a crypto asset").build(); + } + // Name cannot be empty or null - prevent it + final String name = StringUtils.trimToNull(jsonComponent.getName()); + if (name != null) { + component.setName(name); + } + component.setAuthor(StringUtils.trimToNull(jsonComponent.getAuthor())); + component.setPublisher(StringUtils.trimToNull(jsonComponent.getPublisher())); + component.setVersion(StringUtils.trimToNull(jsonComponent.getVersion())); + component.setGroup(StringUtils.trimToNull(jsonComponent.getGroup())); + component.setDescription(StringUtils.trimToNull(jsonComponent.getDescription())); + component.setFilename(StringUtils.trimToNull(jsonComponent.getFilename())); + component.setClassifier(jsonComponent.getClassifier()); + component.setPurl(jsonComponent.getPurl()); + component.setInternal(new InternalComponentIdentifier().isInternal(component)); + component.setCpe(StringUtils.trimToNull(jsonComponent.getCpe())); + component.setSwidTagId(StringUtils.trimToNull(jsonComponent.getSwidTagId())); + component.setCopyright(StringUtils.trimToNull(jsonComponent.getCopyright())); + component.setMd5(StringUtils.trimToNull(jsonComponent.getMd5())); + component.setSha1(StringUtils.trimToNull(jsonComponent.getSha1())); + component.setSha256(StringUtils.trimToNull(jsonComponent.getSha256())); + component.setSha384(StringUtils.trimToNull(jsonComponent.getSha384())); + component.setSha512(StringUtils.trimToNull(jsonComponent.getSha512())); + component.setSha3_256(StringUtils.trimToNull(jsonComponent.getSha3_256())); + component.setSha3_384(StringUtils.trimToNull(jsonComponent.getSha3_384())); + component.setSha3_512(StringUtils.trimToNull(jsonComponent.getSha3_512())); + + if (jsonComponent.getCryptoAssetProperties() != null) { + component.setCryptoAssetProperties(jsonComponent.getCryptoAssetProperties()); + } else { + return Response.status(Response.Status.BAD_REQUEST).entity("No data for crypto asset properties provided").build(); + } + + final License resolvedLicense = qm.getLicense(jsonComponent.getLicense()); + if (resolvedLicense != null) { + component.setLicense(null); + component.setResolvedLicense(resolvedLicense); + } else { + component.setLicense(StringUtils.trimToNull(jsonComponent.getLicense())); + component.setResolvedLicense(null); + } + component.setNotes(StringUtils.trimToNull(jsonComponent.getNotes())); + + component = qm.updateComponent(component, true); + return Response.ok(component).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the component could not be found.").build(); + } + } + } + + @DELETE + @Path("/{uuid}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Deletes a crypto asset", + description = "

Requires permission PORTFOLIO_MANAGEMENT

" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Crypto asset removed successfully"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Access to the specified crypto asset is forbidden"), + @ApiResponse(responseCode = "404", description = "The UUID of the crypto asset could not be found") + }) + @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE}) + public Response deleteComponent( + @Parameter(description = "The UUID of the component to delete", schema = @Schema(format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid) { + try (QueryManager qm = new QueryManager()) { + final Component component = qm.getObjectByUuid(Component.class, uuid, Component.FetchGroup.ALL.name()); + if (component != null && component.getClassifier() == Classifier.CRYPTOGRAPHIC_ASSET) { + if (! qm.hasAccess(super.getPrincipal(), component.getProject())) { + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified crypto asset is forbidden").build(); + } + qm.recursivelyDelete(component, false); + //qm.commitSearchIndex(Component.class); + return Response.status(Response.Status.NO_CONTENT).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the crypto asset could not be found.").build(); + } + } + } +} diff --git a/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java b/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java index f0af0b8cb..aca708856 100644 --- a/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java @@ -43,6 +43,7 @@ import org.dependencytrack.event.PortfolioMetricsUpdateEvent; import org.dependencytrack.event.ProjectMetricsUpdateEvent; import org.dependencytrack.model.Component; +import org.dependencytrack.model.CryptographyMetrics; import org.dependencytrack.model.DependencyMetrics; import org.dependencytrack.model.PortfolioMetrics; import org.dependencytrack.model.Project; @@ -493,4 +494,30 @@ private Response getComponentMetrics(String uuid, Date since) { } } + @GET + @Path("/cryptography/{days}/days") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns X days of historical cryptography metrics for the entire portfolio", + description = "

Requires permission VIEW_PORTFOLIO

" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "X days of historical cryptography metrics for the entire portfolio", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = CryptographyMetrics.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") + }) + @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) + public Response getCryptographyMetricsXDays( + @Parameter(description = "The number of days back to retrieve metrics for", required = true) + @PathParam("days") int days) { + + final Date since = DateUtils.addDays(new Date(), -days); + try (QueryManager qm = new QueryManager()) { + final List metrics = qm.getCryptographyMetricsSince(since); + return Response.ok(metrics).build(); + } + } } diff --git a/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java b/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java index 971d38719..ac1c2b963 100644 --- a/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java +++ b/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java @@ -81,6 +81,8 @@ private void updateMetrics(final boolean forceRefresh) throws Exception { } Metrics.updatePortfolioMetrics(); + // update overall cryptography metrics + Metrics.updateCryptographyMetrics(); } finally { LOGGER.info("Completed portfolio metrics update in " + Duration.ofNanos(System.nanoTime() - startTimeNs)); } diff --git a/src/main/java/org/dependencytrack/util/DateUtil.java b/src/main/java/org/dependencytrack/util/DateUtil.java index c4129517e..fbba8864e 100644 --- a/src/main/java/org/dependencytrack/util/DateUtil.java +++ b/src/main/java/org/dependencytrack/util/DateUtil.java @@ -83,6 +83,9 @@ public static long diff(final Date start, final Date end) { * @since 3.4.0 */ public static String toISO8601(final Date date) { + if (date == null) { + return null; + } final TimeZone tz = TimeZone.getTimeZone("UTC"); final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); // Quoted "Z" to indicate UTC, no timezone offset df.setTimeZone(tz); diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml index f09e8a9e0..3b12a1bfd 100644 --- a/src/main/resources/META-INF/persistence.xml +++ b/src/main/resources/META-INF/persistence.xml @@ -25,8 +25,16 @@ org.dependencytrack.model.Analysis org.dependencytrack.model.AnalysisComment org.dependencytrack.model.Bom + org.dependencytrack.model.CipherSuite org.dependencytrack.model.Component org.dependencytrack.model.ComponentProperty + org.dependencytrack.model.CryptoAssetProperties + org.dependencytrack.model.CryptoAlgorithmProperties + org.dependencytrack.model.CryptoCertificateProperties + org.dependencytrack.model.CryptoRelatedMaterialProperties + org.dependencytrack.model.CryptoProtocolProperties + org.dependencytrack.model.CryptographyMetrics + org.dependencytrack.model.Ikev2Type org.dependencytrack.model.IntegrityMetaComponent org.dependencytrack.model.DependencyMetrics org.dependencytrack.model.Epss @@ -35,6 +43,7 @@ org.dependencytrack.model.LicenseGroup org.dependencytrack.model.NotificationPublisher org.dependencytrack.model.NotificationRule + org.dependencytrack.model.Occurrence org.dependencytrack.model.Policy org.dependencytrack.model.PolicyCondition org.dependencytrack.model.PolicyViolation diff --git a/src/main/resources/migration/changelog-procedures.xml b/src/main/resources/migration/changelog-procedures.xml index 5e7f0a02a..f68fc369f 100644 --- a/src/main/resources/migration/changelog-procedures.xml +++ b/src/main/resources/migration/changelog-procedures.xml @@ -23,4 +23,7 @@ + + + \ No newline at end of file diff --git a/src/main/resources/migration/changelog-v5.6.0.xml b/src/main/resources/migration/changelog-v5.6.0.xml index fc1bc3137..87383d0a9 100644 --- a/src/main/resources/migration/changelog-v5.6.0.xml +++ b/src/main/resources/migration/changelog-v5.6.0.xml @@ -107,4 +107,202 @@ onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="ID" referencedTableName="TAG" validate="true"/> + + + + ALTER TABLE "COMPONENT" DROP CONSTRAINT IF EXISTS "COMPONENT_CLASSIFIER_check"; + ALTER TABLE "COMPONENT" ADD CONSTRAINT "COMPONENT_CLASSIFIER_check" + CHECK ("CLASSIFIER" IS NULL OR "CLASSIFIER"::TEXT = ANY(ARRAY['APPLICATION', 'CONTAINER', 'DEVICE', 'FILE', 'FIRMWARE', 'FRAMEWORK', 'LIBRARY', 'OPERATING_SYSTEM', 'CRYPTOGRAPHIC_ASSET'])); + + ALTER TABLE "PROJECT" DROP CONSTRAINT IF EXISTS "PROJECT_CLASSIFIER_check"; + ALTER TABLE "PROJECT" ADD CONSTRAINT "PROJECT_CLASSIFIER_check" + CHECK ("CLASSIFIER" IS NULL OR "CLASSIFIER"::TEXT = ANY(ARRAY['APPLICATION', 'CONTAINER', 'DEVICE', 'FILE', 'FIRMWARE', 'FRAMEWORK', 'LIBRARY', 'OPERATING_SYSTEM', 'CRYPTOGRAPHIC_ASSET'])); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/migration/procedures/procedure_cryptography_metrics.sql b/src/main/resources/migration/procedures/procedure_cryptography_metrics.sql new file mode 100644 index 000000000..8d8307eba --- /dev/null +++ b/src/main/resources/migration/procedures/procedure_cryptography_metrics.sql @@ -0,0 +1,61 @@ +CREATE OR REPLACE PROCEDURE "UPDATE_CRYPTOGRAPHY_METRICS"() + LANGUAGE "plpgsql" +AS +$$ +DECLARE + "v_number_of_cryptographic_assets" INT; + "v_most_used_algorithm_name" VARCHAR; + "v_most_used_algorithm_percentage" DOUBLE PRECISION; + "v_number_of_keys" INT; + "v_existing_id" BIGINT; -- ID of the existing row that matches the data point calculated in this procedure +BEGIN + SELECT COUNT(*) + FROM "COMPONENT" + WHERE "CLASSIFIER" = 'CRYPTOGRAPHIC_ASSET' + INTO "v_number_of_cryptographic_assets"; + + SELECT + "NAME", + COUNT("NAME") / SUM(COUNT(*)) OVER () * 100 percent + FROM "COMPONENT" INNER JOIN "CRYPTO_PROPERTIES" CP on "COMPONENT"."CRYPTO_PROPERTIES_ID" = CP."ID" + WHERE "CLASSIFIER" = 'CRYPTOGRAPHIC_ASSET' AND "ASSET_TYPE" = 'ALGORITHM' + GROUP BY "NAME" + INTO "v_most_used_algorithm_name", "v_most_used_algorithm_percentage"; + + SELECT + COUNT(*) + FROM "COMPONENT" + INNER JOIN "CRYPTO_PROPERTIES" CP on "COMPONENT"."CRYPTO_PROPERTIES_ID" = CP."ID" + INNER JOIN "RELATED_CRYPTO_MATERIAL_PROPERTIES" RP on CP."RELATED_MATERIAL_PROPERTIES_ID" = RP."ID" + WHERE "CLASSIFIER" = 'CRYPTOGRAPHIC_ASSET' AND "ASSET_TYPE" = 'RELATED_CRYPTO_MATERIAL' AND + ("TYPE" = 'SECRET_KEY' OR "TYPE" = 'PUBLIC_KEY' OR "TYPE" = 'PRIVATE_KEY' OR "TYPE" = 'KEY') + INTO "v_number_of_keys"; + + SELECT "ID" + FROM "CRYPTOGRAPHYMETRICS" + WHERE "NUMBER_OF_CRYPTOGRAPHIC_ASSETS" = "v_number_of_cryptographic_assets" + AND "MOST_USED_ALGORITHM_NAME" = "v_most_used_algorithm_name" + AND "MOST_USED_ALGORITHM_PERCENTAGE" = "v_most_used_algorithm_percentage" + AND "NUMBER_OF_KEYS" = "v_number_of_keys" + ORDER BY "LAST_OCCURRENCE" DESC + LIMIT 1 + INTO "v_existing_id"; + + IF "v_existing_id" IS NOT NULL THEN + UPDATE "CRYPTOGRAPHYMETRICS" SET "LAST_OCCURRENCE" = NOW() WHERE "ID" = "v_existing_id"; + ELSE + INSERT INTO "CRYPTOGRAPHYMETRICS" ("NUMBER_OF_CRYPTOGRAPHIC_ASSETS", + "MOST_USED_ALGORITHM_NAME", + "MOST_USED_ALGORITHM_PERCENTAGE", + "NUMBER_OF_KEYS", + "FIRST_OCCURRENCE", + "LAST_OCCURRENCE") + VALUES ("v_number_of_cryptographic_assets", + "v_most_used_algorithm_name", + "v_most_used_algorithm_percentage", + "v_number_of_keys", + NOW(), + NOW()); + END IF; +END; +$$; \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/parser/cyclonedx/util/ModelConverterTest.java b/src/test/java/org/dependencytrack/parser/cyclonedx/util/ModelConverterTest.java new file mode 100644 index 000000000..390263fcf --- /dev/null +++ b/src/test/java/org/dependencytrack/parser/cyclonedx/util/ModelConverterTest.java @@ -0,0 +1,208 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.parser.cyclonedx.util; + +import java.util.List; +import java.util.UUID; + +import org.cyclonedx.model.Dependency; +import org.cyclonedx.model.Metadata; +import org.cyclonedx.model.Property; +import org.cyclonedx.model.component.crypto.AlgorithmProperties; +import org.cyclonedx.model.component.crypto.CertificateProperties; +import org.cyclonedx.model.component.crypto.CryptoProperties; +import org.cyclonedx.model.component.crypto.enums.CryptoFunction; +import org.cyclonedx.model.component.crypto.enums.Padding; +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.model.Classifier; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Project; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; +import com.github.packageurl.PackageURLBuilder; + +public class ModelConverterTest extends PersistenceCapableTest { + + Project project; + + @Before + public void before() throws Exception { + super.before(); + this.project = new Project(); + project.setName("testProject"); + project.setUuid(UUID.randomUUID()); + qm.persist(project); + } + + @Test + public void testConvertCycloneDX1() throws MalformedPackageURLException { + org.cyclonedx.model.Component component = new org.cyclonedx.model.Component(); + component.setName("testComponent"); + component.setType(org.cyclonedx.model.Component.Type.LIBRARY); + + PackageURL purl = PackageURLBuilder.aPackageURL() + .withType("maven").withNamespace("acme").withName("product").withVersion("1.0").build(); + component.setPurl(purl); + + org.cyclonedx.model.Property property = new Property(); + property.setName("testName"); + property.setValue("testValue"); + component.setProperties(List.of(property)); + + org.cyclonedx.model.Component component2 = new org.cyclonedx.model.Component(); + component2.setName("testComponent"); + component2.setType(org.cyclonedx.model.Component.Type.LIBRARY); + component.setComponents(List.of(component2)); + + CryptoProperties cryptoProperties = new CryptoProperties(); + cryptoProperties.setOid("oid:2.16.840.1.101.3.4.1.6"); + + AlgorithmProperties algorithmProperties = new AlgorithmProperties(); + algorithmProperties.setPadding(Padding.PKCS7); + algorithmProperties.setCryptoFunctions(List.of(CryptoFunction.DECRYPT)); + cryptoProperties.setAlgorithmProperties(algorithmProperties); + + CertificateProperties certificateProperties = new CertificateProperties(); + certificateProperties.setNotValidAfter("2020-01-01T18:00:00.000Z"); + cryptoProperties.setCertificateProperties(certificateProperties); + + component.setCryptoProperties(cryptoProperties); + + Component newComponent = ModelConverter.convertComponent(component); + + Assert.assertEquals("testComponent", newComponent.getName()); + Assert.assertEquals(Classifier.LIBRARY, newComponent.getClassifier()); + + Assert.assertEquals(purl, newComponent.getPurl()); + Assert.assertEquals(purl, newComponent.getPurlCoordinates()); + + // Assert.assertEquals("testName", newComponent.getProperties().get(0).getName()); + // Assert.assertEquals("testValue", newComponent.getProperties().get(0).getValue()); + + Assert.assertEquals(1, newComponent.getChildren().size()); + + Assert.assertEquals("oid:2.16.840.1.101.3.4.1.6", newComponent.getCryptoAssetProperties().getOid()); + Assert.assertEquals(Padding.PKCS7, newComponent.getCryptoAssetProperties().getAlgorithmProperties().getPadding()); + Assert.assertEquals(List.of(CryptoFunction.DECRYPT), newComponent.getCryptoAssetProperties().getAlgorithmProperties().getCryptoFunctions()); + } + + @Test + public void testConvertCycloneDX2() { + org.cyclonedx.model.Component component = new org.cyclonedx.model.Component(); + component.setName("testComponent"); + component.setType(org.cyclonedx.model.Component.Type.LIBRARY); + + Component newComponent = ModelConverter.convertComponent(component); + + Assert.assertNull(newComponent.getCryptoAssetProperties()); + } + + // @Test + // public void testConvertCycloneDXWithToolHash() throws MalformedPackageURLException { + // //Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false); + + // org.cyclonedx.model.Bom bom = new org.cyclonedx.model.Bom(); + // bom.setSerialNumber("urn:uuid:44371afa-c7cf-48cf-b385-17795344811a"); + // bom.setVersion(1); + + // Metadata metadata = new Metadata(); + + // org.cyclonedx.model.Component metaDataComponent = new org.cyclonedx.model.Component(); + // metaDataComponent.setBomRef("alg:fips197/generic"); + // metaDataComponent.setType(org.cyclonedx.model.Component.Type.CRYPTOGRAPHIC_ASSET); + // metaDataComponent.setName("test"); + // metaDataComponent.setVersion("test"); + // metadata.setComponent(metaDataComponent); + + // Tool scanner = new Tool(); + // scanner.setName("testScanner"); + // scanner.setVendor("testVendor"); + // metadata.setTools(List.of(scanner)); + + // bom.setMetadata(metadata); + + // org.cyclonedx.model.Component c1 = new org.cyclonedx.model.Component(); + // c1.setBomRef("alg:fips197/c1"); + // c1.setName("c1"); + // c1.setVersion("c1"); + // c1.setType(org.cyclonedx.model.Component.Type.LIBRARY); + // org.cyclonedx.model.Component c2 = new org.cyclonedx.model.Component(); + // c2.setBomRef("alg:fips197/c2"); + // c2.setName("c2"); + // c2.setVersion("c2"); + // c2.setType(org.cyclonedx.model.Component.Type.CRYPTOGRAPHIC_ASSET); + // bom.setComponents(List.of(c1,c2)); + + // Dependency d1 = new Dependency("alg:fips197/c1"); + // d1.addDependency(new Dependency("alg:fips197/c2")); + // // d1.setType(Dependency.Type.USES); + // bom.setDependencies(List.of(d1)); + + // List components = ModelConverter.convertComponents(bom.getComponents()); + // Set toolHashes = components.stream().map(Component::getToolingHash).collect(Collectors.toSet()); + // // both hashes are equal + // Assert.assertEquals(1, toolHashes.size()); + // } + + + @Test + public void testGenerateDependencies() { + Project acmeProject = qm.createProject("Acme Application", null, null, null, null, null, true, false); + + org.cyclonedx.model.Bom bom = new org.cyclonedx.model.Bom(); + bom.setSerialNumber("urn:uuid:44371afa-c7cf-48cf-b385-17795344811a"); + bom.setVersion(1); + + Metadata metadata = new Metadata(); + org.cyclonedx.model.Component metaDataComponent = new org.cyclonedx.model.Component(); + metaDataComponent.setBomRef("alg:fips197/generic"); + metaDataComponent.setType(org.cyclonedx.model.Component.Type.CRYPTOGRAPHIC_ASSET); + metaDataComponent.setName("test"); + metaDataComponent.setVersion("test"); + metadata.setComponent(metaDataComponent); + bom.setMetadata(metadata); + + org.cyclonedx.model.Component c1 = new org.cyclonedx.model.Component(); + c1.setBomRef("alg:fips197/c1"); + c1.setName("c1"); + c1.setVersion("c1"); + c1.setType(org.cyclonedx.model.Component.Type.LIBRARY); + org.cyclonedx.model.Component c2 = new org.cyclonedx.model.Component(); + c2.setBomRef("alg:fips197/c2"); + c2.setName("c2"); + c2.setVersion("c2"); + c2.setType(org.cyclonedx.model.Component.Type.CRYPTOGRAPHIC_ASSET); + bom.setComponents(List.of(c1,c2)); + + Dependency d1 = new Dependency("alg:fips197/c1"); + d1.addDependency(new Dependency("alg:fips197/c2")); + //d1.setType(Dependency.Type.USES); + bom.setDependencies(List.of(d1)); + + List components = ModelConverter.convertComponents(bom.getComponents()); + this.qm.persist(components); + List dependencies = DependencyUtil.generateDependencies(acmeProject, components); + Assert.assertEquals(dependencies.size(), bom.getDependencies().size()); + } + +}