Skip to content

Commit

Permalink
Ingest metadata.tools from BOMs and make it available in CEL policies
Browse files Browse the repository at this point in the history
The internal model is aligned with CycloneDX v1.5, in that it differentiates between tools that are components, and tools that are services: https://cyclonedx.org/docs/1.5/json/#tab-pane_metadata_tools_oneOf_i0

When ingesting BOMs following v1.4 or older of the CycloneDX specification, `metadata.tools` array items will be converted to `metadata.tools.components`.

For the time being, tools are persisted as JSON column in the `PROJECT_METADATA` table. As such, tools will not be analyzed for vulnerabilities or other kinds of risk.

Tool components and services are treated as subsets of the internal `Component` and `ServiceComponent` models. This subset property is enforced via Jackson's `@JsonView`s, such that only specific fields are considered when serializing and deserializing to and from JSON.

Tools are made available in CEL policy expressions under `project.metadata.tools.components`. Tool components use the existing `v1.Component` type, which means that functions like `matches_version` can be used on them.

Signed-off-by: nscuro <[email protected]>
  • Loading branch information
nscuro committed Feb 28, 2024
1 parent b1c3b0d commit f56c0a3
Show file tree
Hide file tree
Showing 29 changed files with 987 additions and 109 deletions.
24 changes: 24 additions & 0 deletions src/main/java/org/dependencytrack/model/Component.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.github.packageurl.MalformedPackageURLException;
Expand Down Expand Up @@ -112,22 +113,26 @@ public enum FetchGroup {
@Persistent
@Column(name = "AUTHOR", jdbcType = "CLOB")
@Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The author may only contain printable characters")
@JsonView(JsonViews.MetadataTools.class)
private String author;

@Persistent
@Column(name = "PUBLISHER", jdbcType = "CLOB")
@Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The publisher may only contain printable characters")
@JsonView(JsonViews.MetadataTools.class)
private String publisher;

@Persistent(defaultFetchGroup = "true")
@Convert(OrganizationalEntityJsonConverter.class)
@Column(name = "SUPPLIER", jdbcType = "CLOB", allowsNull = "true")
@JsonView(JsonViews.MetadataTools.class)
private OrganizationalEntity supplier;

@Persistent
@Column(name = "GROUP", jdbcType = "CLOB")
@Index(name = "COMPONENT_GROUP_IDX")
@Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The group may only contain printable characters")
@JsonView(JsonViews.MetadataTools.class)
private String group;

@Persistent
Expand All @@ -136,18 +141,21 @@ public enum FetchGroup {
@NotBlank
@JsonDeserialize(using = TrimmedStringDeserializer.class)
@Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The name may only contain printable characters")
@JsonView(JsonViews.MetadataTools.class)
private String name;

@Persistent
@Column(name = "VERSION", jdbcType = "CLOB")
@JsonDeserialize(using = TrimmedStringDeserializer.class)
@Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The version may only contain printable characters")
@JsonView(JsonViews.MetadataTools.class)
private String version;

@Persistent
@Column(name = "CLASSIFIER", jdbcType = "CLOB")
@Index(name = "COMPONENT_CLASSIFIER_IDX")
@Extension(vendorName = "datanucleus", key = "enum-check-constraint", value = "true")
@JsonView(JsonViews.MetadataTools.class)
private Classifier classifier;

@Persistent
Expand All @@ -166,86 +174,100 @@ public enum FetchGroup {
@Index(name = "COMPONENT_MD5_IDX")
@Column(name = "MD5", jdbcType = "VARCHAR", length = 32)
@Pattern(regexp = "^[0-9a-fA-F]{32}$", message = "The MD5 hash must be a valid 32 character HEX number")
@JsonView(JsonViews.MetadataTools.class)
private String md5;

@Persistent
@Index(name = "COMPONENT_SHA1_IDX")
@Column(name = "SHA1", jdbcType = "VARCHAR", length = 40)
@Pattern(regexp = "^[0-9a-fA-F]{40}$", message = "The SHA1 hash must be a valid 40 character HEX number")
@JsonView(JsonViews.MetadataTools.class)
private String sha1;

@Persistent
@Index(name = "COMPONENT_SHA256_IDX")
@Column(name = "SHA_256", jdbcType = "VARCHAR", length = 64)
@Pattern(regexp = "^[0-9a-fA-F]{64}$", message = "The SHA-256 hash must be a valid 64 character HEX number")
@JsonView(JsonViews.MetadataTools.class)
private String sha256;

@Persistent
@Index(name = "COMPONENT_SHA384_IDX")
@Column(name = "SHA_384", jdbcType = "VARCHAR", length = 96)
@Pattern(regexp = "^[0-9a-fA-F]{96}$", message = "The SHA-384 hash must be a valid 96 character HEX number")
@JsonView(JsonViews.MetadataTools.class)
private String sha384;

@Persistent
@Index(name = "COMPONENT_SHA512_IDX")
@Column(name = "SHA_512", jdbcType = "VARCHAR", length = 128)
@Pattern(regexp = "^[0-9a-fA-F]{128}$", message = "The SHA-512 hash must be a valid 128 character HEX number")
@JsonView(JsonViews.MetadataTools.class)
private String sha512;

@Persistent
@Index(name = "COMPONENT_SHA3_256_IDX")
@Column(name = "SHA3_256", jdbcType = "VARCHAR", length = 64)
@Pattern(regexp = "^[0-9a-fA-F]{64}$", message = "The SHA3-256 hash must be a valid 64 character HEX number")
@JsonView(JsonViews.MetadataTools.class)
private String sha3_256;

@Persistent
@Index(name = "COMPONENT_SHA3_384_IDX")
@Column(name = "SHA3_384", jdbcType = "VARCHAR", length = 96)
@Pattern(regexp = "^[0-9a-fA-F]{96}$", message = "The SHA3-384 hash must be a valid 96 character HEX number")
@JsonView(JsonViews.MetadataTools.class)
private String sha3_384;

@Persistent
@Index(name = "COMPONENT_SHA3_512_IDX")
@Column(name = "SHA3_512", jdbcType = "VARCHAR", length = 128)
@Pattern(regexp = "^[0-9a-fA-F]{128}$", message = "The SHA3-512 hash must be a valid 128 character HEX number")
@JsonView(JsonViews.MetadataTools.class)
private String sha3_512;

@Persistent
@Index(name = "COMPONENT_BLAKE2B_256_IDX")
@Column(name = "BLAKE2B_256", jdbcType = "VARCHAR", length = 64)
@Pattern(regexp = RegexSequence.Definition.HASH_SHA256, message = "The BLAKE2b hash must be a valid 64 character HEX number")
@JsonView(JsonViews.MetadataTools.class)
private String blake2b_256;

@Persistent
@Index(name = "COMPONENT_BLAKE2B_384_IDX")
@Column(name = "BLAKE2B_384", jdbcType = "VARCHAR", length = 96)
@Pattern(regexp = RegexSequence.Definition.HASH_SHA384, message = "The BLAKE2b hash must be a valid 96 character HEX number")
@JsonView(JsonViews.MetadataTools.class)
private String blake2b_384;

@Persistent
@Index(name = "COMPONENT_BLAKE2B_512_IDX")
@Column(name = "BLAKE2B_512", jdbcType = "VARCHAR", length = 128)
@Pattern(regexp = RegexSequence.Definition.HASH_SHA512, message = "The BLAKE2b hash must be a valid 128 character HEX number")
@JsonView(JsonViews.MetadataTools.class)
private String blake2b_512;

@Persistent
@Index(name = "COMPONENT_BLAKE3_IDX")
@Column(name = "BLAKE3", jdbcType = "VARCHAR", length = 255)
@Pattern(regexp = RegexSequence.Definition.HEXADECIMAL, message = "The BLAKE3 hash must be a valid HEX number")
@JsonView(JsonViews.MetadataTools.class)
private String blake3;

@Persistent
@Index(name = "COMPONENT_CPE_IDX")
@Column(name = "CPE", jdbcType = "CLOB")
//Patterns obtained from https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd
@Pattern(regexp = "(cpe:2\\.3:[aho\\*\\-](:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!\"#$$%&'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|}~]))+(\\?*|\\*?))|[\\*\\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\\*\\-]))(:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!\"#$$%&'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|}~]))+(\\?*|\\*?))|[\\*\\-])){4})|([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\\._\\-~%]*){0,6})", message = "The CPE must conform to the CPE v2.2 or v2.3 specification defined by NIST")
@JsonView(JsonViews.MetadataTools.class)
private String cpe;

@Persistent(defaultFetchGroup = "true")
@Index(name = "COMPONENT_PURL_IDX")
@Column(name = "PURL", jdbcType = "CLOB")
@com.github.packageurl.validator.PackageURL
@JsonDeserialize(using = TrimmedStringDeserializer.class)
@JsonView(JsonViews.MetadataTools.class)
private String purl;

@Persistent(defaultFetchGroup = "true")
Expand All @@ -259,6 +281,7 @@ public enum FetchGroup {
@Column(name = "SWIDTAGID", jdbcType = "CLOB")
@Index(name = "COMPONENT_SWID_TAGID_IDX")
@Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The SWID tagId may only contain printable characters")
@JsonView(JsonViews.MetadataTools.class)
private String swidTagId;

@Persistent
Expand Down Expand Up @@ -308,6 +331,7 @@ public enum FetchGroup {
@Persistent(defaultFetchGroup = "true")
@Column(name = "EXTERNAL_REFERENCES")
@Serialized
@JsonView(JsonViews.MetadataTools.class)
private List<ExternalReference> externalReferences;

@Persistent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.dependencytrack.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonView;

import java.io.Serializable;

Expand Down Expand Up @@ -50,7 +51,10 @@ private Direction(String name) {
}
}

@JsonView(JsonViews.MetadataTools.class)
private Direction direction;

@JsonView(JsonViews.MetadataTools.class)
private String name;

public Direction getDirection() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import alpine.common.validation.RegexSequence;
import alpine.server.json.TrimmedStringDeserializer;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

import javax.validation.constraints.NotBlank;
Expand All @@ -38,14 +39,17 @@ public class ExternalReference implements Serializable {

private static final long serialVersionUID = -5885851731192037664L;

@JsonView(JsonViews.MetadataTools.class)
private org.cyclonedx.model.ExternalReference.Type type;

@NotBlank
@JsonDeserialize(using = TrimmedStringDeserializer.class)
@JsonView(JsonViews.MetadataTools.class)
private String url;

@JsonDeserialize(using = TrimmedStringDeserializer.class)
@Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The comment may only contain printable characters")
@JsonView(JsonViews.MetadataTools.class)
private String comment;

public org.cyclonedx.model.ExternalReference.Type getType() {
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/org/dependencytrack/model/JsonViews.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.dependencytrack.model;

import com.fasterxml.jackson.annotation.JsonView;

/**
* Marker interfaces to be used in conjunction with Jackson's {@link JsonView} annotation.
*/
public class JsonViews {

/**
* Marks fields to be included when (de-)serializing {@link Tools}.
*/
public interface MetadataTools {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import alpine.server.json.TrimmedStringDeserializer;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

import java.io.Serializable;
Expand All @@ -37,12 +38,15 @@ public class OrganizationalContact implements Serializable {
private static final long serialVersionUID = -1026863376484187244L;

@JsonDeserialize(using = TrimmedStringDeserializer.class)
@JsonView(JsonViews.MetadataTools.class)
private String name;

@JsonDeserialize(using = TrimmedStringDeserializer.class)
@JsonView(JsonViews.MetadataTools.class)
private String email;

@JsonDeserialize(using = TrimmedStringDeserializer.class)
@JsonView(JsonViews.MetadataTools.class)
private String phone;

public String getName() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import alpine.server.json.TrimmedStringArrayDeserializer;
import alpine.server.json.TrimmedStringDeserializer;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

import java.io.Serializable;
Expand All @@ -41,11 +42,14 @@ public class OrganizationalEntity implements Serializable {
private static final long serialVersionUID = 5333594855427723634L;

@JsonDeserialize(using = TrimmedStringDeserializer.class)
@JsonView(JsonViews.MetadataTools.class)
private String name;

@JsonDeserialize(using = TrimmedStringArrayDeserializer.class)
@JsonView(JsonViews.MetadataTools.class)
private String[] urls;

@JsonView(JsonViews.MetadataTools.class)
private List<OrganizationalContact> contacts;

public String getName() {
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/org/dependencytrack/model/ProjectMetadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import org.dependencytrack.persistence.converter.OrganizationalContactsJsonConverter;
import org.dependencytrack.persistence.converter.OrganizationalEntityJsonConverter;
import org.dependencytrack.persistence.converter.ToolsJsonConverter;

import javax.jdo.annotations.Column;
import javax.jdo.annotations.Convert;
Expand Down Expand Up @@ -66,6 +67,11 @@ public class ProjectMetadata {
@Column(name = "AUTHORS", jdbcType = "CLOB", allowsNull = "true")
private List<OrganizationalContact> authors;

@Persistent(defaultFetchGroup = "true")
@Convert(ToolsJsonConverter.class)
@Column(name = "TOOLS", jdbcType = "CLOB", allowsNull = "true")
private Tools tools;

public long getId() {
return id;
}
Expand Down Expand Up @@ -98,4 +104,12 @@ public void setAuthors(final List<OrganizationalContact> authors) {
this.authors = authors;
}

public Tools getTools() {
return tools;
}

public void setTools(final Tools tools) {
this.tools = tools;
}

}
11 changes: 11 additions & 0 deletions src/main/java/org/dependencytrack/model/ServiceComponent.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import alpine.server.json.TrimmedStringDeserializer;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

import javax.jdo.annotations.Column;
Expand Down Expand Up @@ -85,56 +86,66 @@ public enum FetchGroup {
@Persistent(defaultFetchGroup = "true")
@Column(name = "PROVIDER_ID")
@Serialized
@JsonView(JsonViews.MetadataTools.class)
private OrganizationalEntity provider;

@Persistent
@Column(name = "GROUP", jdbcType = "CLOB")
@Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The group may only contain printable characters")
@JsonView(JsonViews.MetadataTools.class)
private String group;

@Persistent
@Column(name = "NAME", allowsNull = "false", jdbcType = "CLOB")
@NotBlank
@JsonDeserialize(using = TrimmedStringDeserializer.class)
@Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The name may only contain printable characters")
@JsonView(JsonViews.MetadataTools.class)
private String name;

@Persistent
@Column(name = "VERSION", jdbcType = "CLOB")
@JsonDeserialize(using = TrimmedStringDeserializer.class)
@Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The version may only contain printable characters")
@JsonView(JsonViews.MetadataTools.class)
private String version;

@Persistent
@Column(name = "DESCRIPTION", jdbcType = "CLOB")
@JsonDeserialize(using = TrimmedStringDeserializer.class)
@Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The description may only contain printable characters")
@JsonView(JsonViews.MetadataTools.class)
private String description;

@Persistent(defaultFetchGroup = "true")
@Serialized
@Column(name = "ENDPOINTS", jdbcType = "LONGVARBINARY")
@JsonDeserialize(using = TrimmedStringArrayDeserializer.class)
@JsonView(JsonViews.MetadataTools.class)
private String[] endpoints;

@Persistent
@Column(name = "AUTHENTICATED")
@JsonView(JsonViews.MetadataTools.class)
private Boolean authenticated;

@Persistent
@Column(name = "X_TRUST_BOUNDARY")
@JsonView(JsonViews.MetadataTools.class)
private Boolean crossesTrustBoundary;

@Persistent(defaultFetchGroup = "true")
@Column(name = "DATA")
@Serialized
@JsonView(JsonViews.MetadataTools.class)
private List<DataClassification> data;

//TODO add license support once Component license support is refactored

@Persistent(defaultFetchGroup = "true")
@Column(name = "EXTERNAL_REFERENCES")
@Serialized
@JsonView(JsonViews.MetadataTools.class)
private List<ExternalReference> externalReferences;

@Persistent
Expand Down
Loading

0 comments on commit f56c0a3

Please sign in to comment.