Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: CBOM support #933

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/main/java/org/dependencytrack/metrics/Metrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
88 changes: 88 additions & 0 deletions src/main/java/org/dependencytrack/model/CipherSuite.java
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> identifiers;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List<String> getAlgorithms() {
return algorithms;
}

public void setAlgorithms(List<String> algorithms) {
this.algorithms = algorithms;
}

public List<String> getIdentifiers() {
return identifiers;
}

public void setIdentifiers(List<String> identifiers) {
this.identifiers = identifiers;
}
}

3 changes: 2 additions & 1 deletion src/main/java/org/dependencytrack/model/Classifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ public enum Classifier {
PLATFORM,
DEVICE_DRIVER,
MACHINE_LEARNING_MODEL,
DATA
DATA,
CRYPTOGRAPHIC_ASSET
}
27 changes: 27 additions & 0 deletions src/main/java/org/dependencytrack/model/Component.java
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at a few example CBOMs, and it strikes me that there's very little overlap of fields between "normal" components and crypto components. CycloneDX uses the same object model for both, but I'm wondering if it makes sense for us to follow that approach.

I asked about this in the CycloneDX Slack as well: https://cyclonedx.slack.com/archives/C05E2FLUTNH/p1737221163931089

A few downsides I'm seeing currently:

  1. The object hierarchy of crypto components adds a lot of joins. Those joins will be performed even if a component is not a crypto component. I haven't done any representative testing, but I think it's fair to assume a noticeable performance hit whenever components are queried.
  2. Both API and UI show fields for crypto components that will never be populated (e.g. purl, swidTagId, ...), which is a bit confusing. The UI also offers users to modify those fields.
  3. Validation becomes more complicated. Technically we'd need to ensure that components of type cryptographic-asset only have crypto-related fields set, and that "normal" components do not have any crypto-related fields set. The object model itself allows for invalid states, and we would have to constantly work around preventing those.

IMO keeping Component and, say, CryptoComponent separate would make a lot of things more clear, and it would also perform better. The FetchGroup issue would be easier to resolve as well. WDYT?

CC @n1ckl0sk0rtge

Copy link
Author

@san-zrl san-zrl Feb 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @nscuro - Thanks a lot for your comments and sorry for the late response. I'm currently caught up in some other projects so it may take a little while before I will come back with some code. Without having a deep insight yet, here is my preliminary answer on your thoughts.

The issue with storing refs in DT is that they have no real meaning and could be totally random (e.g. UUIDs). So there's no real benefit of retaining them if they can't be resolved.

Agree. As a first step, I will change the ref fields to Component references (for the time being - see below) and add a post-processing step to resolve the refs when the BOM has been loaded.

IMO keeping Component and, say, CryptoComponent separate would make a lot of things more clear, and it would also perform better.

Makes sense. The CycloneDX Reference specifies components similarly to a union with a type selector. Except the name, components of type cryptographic-asset have not much in common with other component types. Their characteristics are specified in cryptoProperties which don't exist for other types. I'll look into this as a 2nd step.

Both API and UI show fields for crypto components that will never be populated (e.g. purl, swidTagId, ...), which is a bit confusing.

These are component ids. I guess there are so many of them because the spec authors didn't quite know if there is a unique id for components and what this would be. We plan to use the purl as id for a crypto component.

Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
@Persistent(name = "children"),
@Persistent(name = "properties"),
@Persistent(name = "vulnerabilities"),
@Persistent(name = "cryptoAssetProperties")
}),
@FetchGroup(name = "IDENTITY", members = {
@Persistent(name = "id"),
Expand Down Expand Up @@ -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<Occurrence> occurrences;

private transient String bomRef;
private transient List<org.cyclonedx.model.License> licenseCandidates;
private transient DependencyMetrics metrics;
Expand Down Expand Up @@ -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<Occurrence> getOccurrences() {
return occurrences;
}

public void setOccurrences(List<Occurrence> occurrences) {
this.occurrences = occurrences;
}

@Override
public String toString() {
Expand Down
46 changes: 43 additions & 3 deletions src/main/java/org/dependencytrack/model/ComponentIdentity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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() {
Expand Down
Loading
Loading