Skip to content

Commit

Permalink
Construct NEW_VULNERABILITY and NEW_VULNERABLE_DEPENDENCY notific…
Browse files Browse the repository at this point in the history
…ation subjects with JDBI

This is a prerequisite for DependencyTrack/hyades#937 in order to make notification dispatching less resource-intensive.

Loading this data via ORM is cumbersome, as we either load too much (due to default fetch groups), or too less (due to lazy loading). Getting the subjects in a single query also reduces database round trips.

Signed-off-by: nscuro <[email protected]>
  • Loading branch information
nscuro committed Dec 6, 2023
1 parent 98e44f7 commit 1a85436
Show file tree
Hide file tree
Showing 11 changed files with 748 additions and 30 deletions.
37 changes: 19 additions & 18 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@
<artifactId>jdbi3-sqlobject</artifactId>
<version>${lib.jdbi.version}</version>
</dependency>
<dependency>
<groupId>org.jdbi</groupId>
<artifactId>jdbi3-postgres</artifactId>
<version>${lib.jdbi.version}</version>
</dependency>
<!-- OWASP Risk Rating calculator -->
<dependency>
<groupId>us.springett</groupId>
Expand Down Expand Up @@ -555,25 +560,21 @@
<filtering>false</filtering>
</testResource>
</testResources>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
<compilerArgs>
<arg>-Xlint:all</arg>
<arg>-Xlint:-processing</arg>
<arg>-Xlint:-serial</arg>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
<compilerArgs>
<arg>-Xlint:all</arg>
<arg>-Xlint:-processing</arg>
<arg>-Xlint:-serial</arg>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.datanucleus.store.rdbms.RDBMSStoreManager;
import org.dependencytrack.persistence.QueryManager;
import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.postgres.PostgresPlugin;
import org.jdbi.v3.sqlobject.SqlObjectPlugin;

import javax.jdo.PersistenceManager;
Expand Down Expand Up @@ -87,7 +88,10 @@ private static Jdbi localJdbi(final PersistenceManager pm) {
Use the global instance instead if combining JDBI with JDO transactions is not needed.""");
}

return Jdbi.create(new JdoConnectionFactory(pm));
return Jdbi
.create(new JdoConnectionFactory(pm))
.installPlugin(new SqlObjectPlugin())
.installPlugin(new PostgresPlugin());
}

private record GlobalInstanceHolder(Jdbi jdbi, PersistenceManagerFactory pmf) {
Expand All @@ -102,7 +106,8 @@ && readField(connectionManager, "primaryConnectionFactory", true) instanceof Con
&& readField(connectionFactory, "dataSource", true) instanceof final DataSource dataSource) {
return Jdbi
.create(dataSource)
.installPlugin(new SqlObjectPlugin());
.installPlugin(new SqlObjectPlugin())
.installPlugin(new PostgresPlugin());
}
} catch (IllegalAccessException e) {
throw new IllegalStateException("Failed to access datasource of PMF via reflection", e);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package org.dependencytrack.persistence.jdbi;

import org.dependencytrack.model.VulnerabilityAnalysisLevel;
import org.dependencytrack.persistence.jdbi.mapping.NotificationComponentRowMapper;
import org.dependencytrack.persistence.jdbi.mapping.NotificationProjectRowMapper;
import org.dependencytrack.persistence.jdbi.mapping.NotificationSubjectNewVulnerabilityRowMapper;
import org.dependencytrack.persistence.jdbi.mapping.NotificationSubjectNewVulnerableDependencyRowReducer;
import org.dependencytrack.persistence.jdbi.mapping.NotificationVulnerabilityRowMapper;
import org.dependencytrack.proto.notification.v1.NewVulnerabilitySubject;
import org.dependencytrack.proto.notification.v1.NewVulnerableDependencySubject;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.config.RegisterRowMappers;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.UseRowReducer;

import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@RegisterRowMappers({
@RegisterRowMapper(NotificationComponentRowMapper.class),
@RegisterRowMapper(NotificationProjectRowMapper.class),
@RegisterRowMapper(NotificationVulnerabilityRowMapper.class)
})
public interface NotificationSubjectDao {

@SqlQuery("""
SELECT
"C"."UUID" AS "componentUuid",
"C"."GROUP" AS "componentGroup",
"C"."NAME" AS "componentName",
"C"."VERSION" AS "componentVersion",
"C"."PURL" AS "componentPurl",
"C"."MD5" AS "componentMd5",
"C"."SHA1" AS "componentSha1",
"C"."SHA_256" AS "componentSha256",
"C"."SHA_512" AS "componentSha512",
"P"."UUID" AS "projectUuid",
"P"."NAME" AS "projectName",
"P"."VERSION" AS "projectVersion",
"P"."DESCRIPTION" AS "projectDescription",
"P"."PURL" AS "projectPurl",
(SELECT
STRING_AGG("T"."NAME", ',')
FROM
"TAG" AS "T"
INNER JOIN
"PROJECTS_TAGS" AS "PT" ON "PT"."TAG_ID" = "T"."ID"
WHERE
"PT"."PROJECT_ID" = "P"."ID"
) AS "projectTags",
"V"."UUID" AS "vulnUuid",
"V"."VULNID" AS "vulnId",
"V"."SOURCE" AS "vulnSource",
"V"."TITLE" AS "vulnTitle",
"V"."SUBTITLE" AS "vulnSubTitle",
"V"."DESCRIPTION" AS "vulnDescription",
"V"."RECOMMENDATION" AS "vulnRecommendation",
-- TODO: Select ratings from ANALYSIS table if available.
-- https://github.com/DependencyTrack/hyades/issues/941
"V"."CVSSV2BASESCORE" AS "vulnCvssV2BaseScore",
"V"."CVSSV3BASESCORE" AS "vulnCvssV3BaseScore",
"V"."OWASPRRBUSINESSIMPACTSCORE" AS "vulnOwaspRrBusinessImpactScore",
"V"."OWASPRRLIKELIHOODSCORE" AS "vulnOwaspRrLikelihoodScore",
"V"."OWASPRRTECHNICALIMPACTSCORE" AS "vulnOwaspRrTechnicalImpactScore",
"V"."SEVERITY" AS "vulnSeverity",
"V"."CWES" AS "vulnCwes",
"vulnAliasesJson",
:vulnAnalysisLevel AS "vulnAnalysisLevel"
FROM
"COMPONENT" AS "C"
INNER JOIN
"PROJECT" AS "P" ON "P"."ID" = "C"."PROJECT_ID"
INNER JOIN
"COMPONENTS_VULNERABILITIES" AS "CV" ON "CV"."COMPONENT_ID" = "C"."ID"
INNER JOIN
"VULNERABILITY" AS "V" ON "V"."ID" = "CV"."VULNERABILITY_ID"
LEFT JOIN
"ANALYSIS" AS "A" ON "A"."COMPONENT_ID" = "C"."ID" AND "A"."VULNERABILITY_ID" = "V"."ID"
LEFT JOIN LATERAL (
SELECT
CAST(JSONB_AGG(DISTINCT JSONB_STRIP_NULLS(JSONB_BUILD_OBJECT(
'cveId', "VA"."CVE_ID",
'ghsaId', "VA"."GHSA_ID",
'gsdId', "VA"."GSD_ID",
'internalId', "VA"."INTERNAL_ID",
'osvId', "VA"."OSV_ID",
'sonatypeId', "VA"."SONATYPE_ID",
'snykId', "VA"."SNYK_ID",
'vulnDbId', "VA"."VULNDB_ID"
))) AS TEXT) AS "vulnAliasesJson"
FROM
"VULNERABILITYALIAS" AS "VA"
WHERE
("V"."SOURCE" = 'NVD' AND "VA"."CVE_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'GITHUB' AND "VA"."GHSA_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'GSD' AND "VA"."GSD_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'INTERNAL' AND "VA"."INTERNAL_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'OSV' AND "VA"."OSV_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'SONATYPE' AND "VA"."SONATYPE_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'SNYK' AND "VA"."SNYK_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'VULNDB' AND "VA"."VULNDB_ID" = "V"."VULNID")
) AS "vulnAliases" ON TRUE
WHERE
"C"."UUID" = (:componentUuid)::TEXT AND "V"."UUID" = ANY((:vulnUuids)::TEXT[])
AND ("A"."SUPPRESSED" IS NULL OR NOT "A"."SUPPRESSED")
""")
@RegisterRowMapper(NotificationSubjectNewVulnerabilityRowMapper.class)
List<NewVulnerabilitySubject> getForNewVulnerabilities(final UUID componentUuid, final Collection<UUID> vulnUuids,
final VulnerabilityAnalysisLevel vulnAnalysisLevel);

@SqlQuery("""
SELECT
"C"."UUID" AS "componentUuid",
"C"."GROUP" AS "componentGroup",
"C"."NAME" AS "componentName",
"C"."VERSION" AS "componentVersion",
"C"."PURL" AS "componentPurl",
"C"."MD5" AS "componentMd5",
"C"."SHA1" AS "componentSha1",
"C"."SHA_256" AS "componentSha256",
"C"."SHA_512" AS "componentSha512",
"P"."UUID" AS "projectUuid",
"P"."NAME" AS "projectName",
"P"."VERSION" AS "projectVersion",
"P"."DESCRIPTION" AS "projectDescription",
"P"."PURL" AS "projectPurl",
(SELECT
STRING_AGG("T"."NAME", ',')
FROM
"TAG" AS "T"
INNER JOIN
"PROJECTS_TAGS" AS "PT" ON "PT"."TAG_ID" = "T"."ID"
WHERE
"PT"."PROJECT_ID" = "P"."ID"
) AS "projectTags",
"V"."UUID" AS "vulnUuid",
"V"."VULNID" AS "vulnId",
"V"."SOURCE" AS "vulnSource",
"V"."TITLE" AS "vulnTitle",
"V"."SUBTITLE" AS "vulnSubTitle",
"V"."DESCRIPTION" AS "vulnDescription",
"V"."RECOMMENDATION" AS "vulnRecommendation",
-- TODO: Select ratings from ANALYSIS table if available.
-- https://github.com/DependencyTrack/hyades/issues/941
"V"."CVSSV2BASESCORE" AS "vulnCvssV2BaseScore",
"V"."CVSSV3BASESCORE" AS "vulnCvssV3BaseScore",
"V"."OWASPRRBUSINESSIMPACTSCORE" AS "vulnOwaspRrBusinessImpactScore",
"V"."OWASPRRLIKELIHOODSCORE" AS "vulnOwaspRrLikelihoodScore",
"V"."OWASPRRTECHNICALIMPACTSCORE" AS "vulnOwaspRrTechnicalImpactScore",
"V"."SEVERITY" AS "vulnSeverity",
"V"."CWES" AS "vulnCwes",
"vulnAliasesJson"
FROM
"COMPONENT" AS "C"
INNER JOIN
"PROJECT" AS "P" ON "P"."ID" = "C"."PROJECT_ID"
INNER JOIN
"COMPONENTS_VULNERABILITIES" AS "CV" ON "CV"."COMPONENT_ID" = "C"."ID"
INNER JOIN
"VULNERABILITY" AS "V" ON "V"."ID" = "CV"."VULNERABILITY_ID"
LEFT JOIN
"ANALYSIS" AS "A" ON "A"."COMPONENT_ID" = "C"."ID" AND "A"."VULNERABILITY_ID" = "V"."ID"
LEFT JOIN LATERAL (
SELECT
CAST(JSONB_AGG(DISTINCT JSONB_STRIP_NULLS(JSONB_BUILD_OBJECT(
'cveId', "VA"."CVE_ID",
'ghsaId', "VA"."GHSA_ID",
'gsdId', "VA"."GSD_ID",
'internalId', "VA"."INTERNAL_ID",
'osvId', "VA"."OSV_ID",
'sonatypeId', "VA"."SONATYPE_ID",
'snykId', "VA"."SNYK_ID",
'vulnDbId', "VA"."VULNDB_ID"
))) AS TEXT) AS "vulnAliasesJson"
FROM
"VULNERABILITYALIAS" AS "VA"
WHERE
("V"."SOURCE" = 'NVD' AND "VA"."CVE_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'GITHUB' AND "VA"."GHSA_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'GSD' AND "VA"."GSD_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'INTERNAL' AND "VA"."INTERNAL_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'OSV' AND "VA"."OSV_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'SONATYPE' AND "VA"."SONATYPE_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'SNYK' AND "VA"."SNYK_ID" = "V"."VULNID")
OR ("V"."SOURCE" = 'VULNDB' AND "VA"."VULNDB_ID" = "V"."VULNID")
) AS "vulnAliases" ON TRUE
WHERE
"C"."UUID" = (:componentUuid)::TEXT
AND ("A"."SUPPRESSED" IS NULL OR NOT "A"."SUPPRESSED")
""")
@UseRowReducer(NotificationSubjectNewVulnerableDependencyRowReducer.class)
Optional<NewVulnerableDependencySubject> getForNewVulnerableDependency(final UUID componentUuid);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.dependencytrack.persistence.jdbi.mapping;

import org.dependencytrack.proto.notification.v1.Component;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;

import java.sql.ResultSet;
import java.sql.SQLException;

import static org.apache.commons.lang3.StringUtils.trimToEmpty;

public class NotificationComponentRowMapper implements RowMapper<Component> {

@Override
public Component map(final ResultSet rs, final StatementContext ctx) throws SQLException {
return Component.newBuilder()
.setUuid(trimToEmpty(rs.getString("componentUuid")))
.setGroup(trimToEmpty(rs.getString("componentGroup")))
.setName(trimToEmpty(rs.getString("componentName")))
.setVersion(trimToEmpty(rs.getString("componentVersion")))
.setPurl(trimToEmpty(rs.getString("componentPurl")))
.setMd5(trimToEmpty(rs.getString("componentMd5")))
.setSha1(trimToEmpty(rs.getString("componentSha1")))
.setSha256(trimToEmpty(rs.getString("componentSha256")))
.setSha512(trimToEmpty(rs.getString("componentSha512")))
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.dependencytrack.persistence.jdbi.mapping;

import org.apache.commons.lang3.StringUtils;
import org.dependencytrack.proto.notification.v1.Project;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;

import static org.apache.commons.lang3.StringUtils.trimToEmpty;

public class NotificationProjectRowMapper implements RowMapper<Project> {

@Override
public Project map(final ResultSet rs, final StatementContext ctx) throws SQLException {
return Project.newBuilder()
.setUuid(trimToEmpty(rs.getString("projectUuid")))
.setName(trimToEmpty(rs.getString("projectName")))
.setVersion(trimToEmpty(rs.getString("projectVersion")))
.setDescription(trimToEmpty(rs.getString("projectDescription")))
.setPurl(trimToEmpty(rs.getString("projectPurl")))
.addAllTags(Optional.ofNullable(rs.getString("projectTags")).stream()
.flatMap(tagNames -> Arrays.stream(tagNames.split(",")))
.map(StringUtils::trimToNull)
.filter(Objects::nonNull)
.toList())
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.dependencytrack.persistence.jdbi.mapping;

import org.dependencytrack.proto.notification.v1.Component;
import org.dependencytrack.proto.notification.v1.NewVulnerabilitySubject;
import org.dependencytrack.proto.notification.v1.Project;
import org.dependencytrack.proto.notification.v1.Vulnerability;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;

import java.sql.ResultSet;
import java.sql.SQLException;

public class NotificationSubjectNewVulnerabilityRowMapper implements RowMapper<NewVulnerabilitySubject> {

@Override
public NewVulnerabilitySubject map(final ResultSet rs, final StatementContext ctx) throws SQLException {
final RowMapper<Component> componentRowMapper = ctx.findRowMapperFor(Component.class).orElseThrow();
final RowMapper<Project> projectRowMapper = ctx.findRowMapperFor(Project.class).orElseThrow();
final RowMapper<Vulnerability> vulnRowMapper = ctx.findRowMapperFor(Vulnerability.class).orElseThrow();

return NewVulnerabilitySubject.newBuilder()
.setComponent(componentRowMapper.map(rs, ctx))
.setProject(projectRowMapper.map(rs, ctx))
.setVulnerability(vulnRowMapper.map(rs, ctx))
.setVulnerabilityAnalysisLevel(rs.getString("vulnAnalysisLevel"))
.addAffectedProjects(projectRowMapper.map(rs, ctx))
.build();
}

}
Loading

0 comments on commit 1a85436

Please sign in to comment.