Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
20 changes: 20 additions & 0 deletions apiserver/src/main/java/org/dependencytrack/model/Analysis.java
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ public class Analysis implements Serializable {
@JsonProperty(value = "owaspScore")
private BigDecimal owaspScore;

/**
* The source that owns this analysis.
* Tracks whether the analysis was set by POLICY, VEX, MANUAL, or NVD.
* Higher-precedence sources prevent lower-precedence sources from overwriting.
*
* @since 5.8.0
*/
@Persistent(defaultFetchGroup = "true")
@Column(name = "SOURCE", jdbcType = "VARCHAR", allowsNull = "true")
@JsonProperty(value = "source")
private RatingSource source;

@Persistent
@Column(name = "VULNERABILITY_POLICY_ID", allowsNull = "true")
@JsonIgnore
Expand Down Expand Up @@ -306,6 +318,14 @@ public void setOwaspScore(BigDecimal owaspScore) {
this.owaspScore = owaspScore;
}

public RatingSource getSource() {
return source;
}

public void setSource(RatingSource source) {
this.source = source;
}

public Long getVulnerabilityPolicyId() {
return vulnerabilityPolicyId;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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;

/**
* Defines the source of an analysis. Precedence: POLICY > VEX > MANUAL > NVD.
*
* @since 5.0.0
*/
public enum RatingSource {

POLICY(4),
VEX(3),
MANUAL(2),
NVD(1);

private final int precedence;

RatingSource(int precedence) {
this.precedence = precedence;
}

public int getPrecedence() {
return precedence;
}

public boolean canOverwrite(RatingSource other) {
return other == null || this.precedence >= other.precedence;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.dependencytrack.model.Component;
import org.dependencytrack.model.ComponentIdentity;
import org.dependencytrack.model.Project;
import org.dependencytrack.model.RatingSource;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.parser.cyclonedx.util.ModelConverter;
import org.dependencytrack.persistence.QueryManager;
Expand Down Expand Up @@ -142,9 +143,9 @@ private static List<org.cyclonedx.model.vulnerability.Vulnerability> getApplicab
vexVulnSource, vexVulnId, vexVulnPos);
continue;
}
if (vexVuln.getAnalysis() == null) {
if (vexVuln.getAnalysis() == null && CollectionUtils.isEmpty(vexVuln.getRatings())) {
LOGGER.debug(
"VEX vulnerability {}/{} at position #{} does not have an analysis; Skipping it",
"VEX vulnerability {}/{} at position #{} does not have an analysis or ratings; Skipping it",
vexVulnSource, vexVulnId, vexVulnPos);
continue;
}
Expand Down Expand Up @@ -195,38 +196,58 @@ private static void indexComponents(

private static void updateAnalysis(final QueryManager qm, final Component component, final Vulnerability vuln,
final org.cyclonedx.model.vulnerability.Vulnerability cdxVuln) {
final AnalysisState state =
convertCdxVulnAnalysisStateToDtAnalysisState(cdxVuln.getAnalysis().getState());
final AnalysisJustification justification =
convertCdxVulnAnalysisJustificationToDtAnalysisJustification(cdxVuln.getAnalysis().getJustification());

// CycloneDX supports multiple responses, DT only one.
// The decision to effectively pick the last one is legacy behavior,
// there's no other particular reason for doing it.
final AnalysisResponse response;
if (cdxVuln.getAnalysis().getResponses() != null
&& !cdxVuln.getAnalysis().getResponses().isEmpty()) {
response = cdxVuln.getAnalysis().getResponses().stream()
.map(ModelConverter::convertCdxVulnAnalysisResponseToDtAnalysisResponse)
.toList()
.getLast();
} else {
response = null;
MakeAnalysisCommand command = new MakeAnalysisCommand(component, vuln)
.withCommenter(COMMENTER)
.withSource(RatingSource.VEX);

if (cdxVuln.getAnalysis() != null) {
final AnalysisState state = convertCdxVulnAnalysisStateToDtAnalysisState(cdxVuln.getAnalysis().getState());
final AnalysisJustification justification = convertCdxVulnAnalysisJustificationToDtAnalysisJustification(cdxVuln.getAnalysis().getJustification());

// CycloneDX supports multiple responses, DT only one.
// The decision to effectively pick the last one is legacy behavior,
// there's no other particular reason for doing it.
final AnalysisResponse response;
if (cdxVuln.getAnalysis().getResponses() != null
&& !cdxVuln.getAnalysis().getResponses().isEmpty()) {
response = cdxVuln.getAnalysis().getResponses().stream()
.map(ModelConverter::convertCdxVulnAnalysisResponseToDtAnalysisResponse)
.toList()
.getLast();
} else {
response = null;
}

final boolean isSuppressed = state == AnalysisState.FALSE_POSITIVE
|| state == AnalysisState.NOT_AFFECTED
|| state == AnalysisState.RESOLVED;

command = command
.withState(state)
.withJustification(justification)
.withResponse(response)
.withDetails(cdxVuln.getAnalysis().getDetail())
.withSuppress(isSuppressed);
}

if (cdxVuln.getRatings() != null && !cdxVuln.getRatings().isEmpty()) {
for (final org.cyclonedx.model.vulnerability.Vulnerability.Rating rating : cdxVuln.getRatings()) {
if (rating.getMethod() == org.cyclonedx.model.vulnerability.Vulnerability.Rating.Method.OWASP) {
if (rating.getVector() == null && rating.getScore() == null) {
LOGGER.warn("VEX OWASP rating has neither vector nor score - skipping");
continue;
}

final java.math.BigDecimal score = rating.getScore() != null
? java.math.BigDecimal.valueOf(rating.getScore())
: null;
Comment on lines +241 to +243
command = command.withOwasp(rating.getVector(), score);
break;
}
}
}

final boolean isSuppressed =
state == AnalysisState.FALSE_POSITIVE
|| state == AnalysisState.NOT_AFFECTED
|| state == AnalysisState.RESOLVED;

qm.makeAnalysis(
new MakeAnalysisCommand(component, vuln)
.withState(state)
.withJustification(justification)
.withResponse(response)
.withDetails(cdxVuln.getAnalysis().getDetail())
.withCommenter(COMMENTER)
.withSuppress(isSuppressed));
qm.makeAnalysis(command);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.dependencytrack.model.AnalysisResponse;
import org.dependencytrack.model.AnalysisState;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.RatingSource;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.notification.JdoNotificationEmitter;
import org.dependencytrack.notification.NotificationModelConverter;
Expand Down Expand Up @@ -102,27 +103,49 @@ public long makeAnalysis(final MakeAnalysisCommand command) {
boolean stateChanged = false;
boolean suppressionChanged = false;

if (command.state() != null && command.state() != analysis.getAnalysisState()) {
auditTrailComments.add("Analysis: %s → %s".formatted(analysis.getAnalysisState(), command.state()));
analysis.setAnalysisState(command.state());
stateChanged = true;
}
if (command.justification() != null && command.justification() != analysis.getAnalysisJustification()) {
auditTrailComments.add("Justification: %s → %s".formatted(analysis.getAnalysisJustification(), command.justification()));
analysis.setAnalysisJustification(command.justification());
}
if (command.response() != null && command.response() != analysis.getAnalysisResponse()) {
auditTrailComments.add("Vendor Response: %s → %s".formatted(analysis.getAnalysisResponse(), command.response()));
analysis.setAnalysisResponse(command.response());
}
if (command.details() != null && !command.details().equals(analysis.getAnalysisDetails())) {
auditTrailComments.add("Details: %s".formatted(command.details()));
analysis.setAnalysisDetails(command.details());
}
if (command.suppress() != null && command.suppress() != analysis.isSuppressed()) {
auditTrailComments.add(command.suppress() ? "Suppressed" : "Unsuppressed");
analysis.setSuppressed(command.suppress());
suppressionChanged = true;
final boolean canUpdate = canUpdateAnalysis(analysis.getSource(), command.source());

if (canUpdate) {
Comment on lines +106 to +108
if (command.state() != null && command.state() != analysis.getAnalysisState()) {
auditTrailComments.add("Analysis: %s → %s".formatted(analysis.getAnalysisState(), command.state()));
analysis.setAnalysisState(command.state());
stateChanged = true;
}
if (command.justification() != null && command.justification() != analysis.getAnalysisJustification()) {
auditTrailComments.add("Justification: %s → %s".formatted(analysis.getAnalysisJustification(), command.justification()));
analysis.setAnalysisJustification(command.justification());
}
if (command.response() != null && command.response() != analysis.getAnalysisResponse()) {
auditTrailComments.add("Vendor Response: %s → %s".formatted(analysis.getAnalysisResponse(), command.response()));
analysis.setAnalysisResponse(command.response());
}
if (command.details() != null && !command.details().equals(analysis.getAnalysisDetails())) {
auditTrailComments.add("Details: %s".formatted(command.details()));
analysis.setAnalysisDetails(command.details());
}
if (command.suppress() != null && command.suppress() != analysis.isSuppressed()) {
auditTrailComments.add(command.suppress() ? "Suppressed" : "Unsuppressed");
analysis.setSuppressed(command.suppress());
suppressionChanged = true;
}

if (command.owaspVector() != null && !command.owaspVector().equals(analysis.getOwaspVector())) {
auditTrailComments.add("OWASP RR Vector: %s → %s".formatted(
analysis.getOwaspVector(), command.owaspVector()));
analysis.setOwaspVector(command.owaspVector());
}
if (command.owaspScore() != null && !command.owaspScore().equals(analysis.getOwaspScore())) {
auditTrailComments.add("OWASP RR Score: %s → %s".formatted(
analysis.getOwaspScore(), command.owaspScore()));
analysis.setOwaspScore(command.owaspScore());
}

if (command.source() != null && analysis.getSource() != command.source()) {
if (analysis.getSource() != null) {
auditTrailComments.add("Source: %s → %s".formatted(analysis.getSource(), command.source()));
}
analysis.setSource(command.source());
}
Comment on lines +143 to +148
}

final List<String> comments =
Expand Down Expand Up @@ -198,4 +221,11 @@ private void createAnalysisComments(
});
}

private boolean canUpdateAnalysis(final RatingSource existingSource, final RatingSource newSource) {
if (newSource == null || existingSource == null) {
return true;
}
return newSource.canOverwrite(existingSource);
}
Comment on lines +224 to +229

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
import org.dependencytrack.model.AnalysisResponse;
import org.dependencytrack.model.AnalysisState;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.RatingSource;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.notification.proto.v1.Group;

import java.math.BigDecimal;
import java.util.Collections;
import java.util.Set;

Expand All @@ -38,8 +40,11 @@
* @param response The vendor response to set
* @param details The details to set
* @param suppress Whether to suppress the finding
* @param source The source that owns this analysis (POLICY, VEX, MANUAL, NVD)
* @param commenter Name of the principal on which behalf audit trail entries will be created
* @param comment The comment to add to the audit trail
* @param owaspVector OWASP RR vector to set
* @param owaspScore OWASP RR score to set
* @param options Additional options
* @since 5.0.0
*/
Expand All @@ -51,8 +56,11 @@ public record MakeAnalysisCommand(
AnalysisResponse response,
String details,
Boolean suppress,
RatingSource source,
String commenter,
String comment,
String owaspVector,
BigDecimal owaspScore,
Set<Option> options) {

public enum Option {
Expand All @@ -77,39 +85,48 @@ public enum Option {
}

public MakeAnalysisCommand(final Component component, final Vulnerability vulnerability) {
this(component, vulnerability, null, null, null, null, null, null, null, Collections.emptySet());
this(component, vulnerability, null, null, null, null, null, null, null, null,
null, null, Collections.emptySet());
}

public MakeAnalysisCommand withState(final AnalysisState state) {
return new MakeAnalysisCommand(this.component, this.vulnerability, state, this.justification, this.response, this.details, this.suppress, this.commenter, this.comment, this.options);
return new MakeAnalysisCommand(this.component, this.vulnerability, state, this.justification, this.response, this.details, this.suppress, this.source, this.commenter, this.comment, this.owaspVector, this.owaspScore, this.options);
}

public MakeAnalysisCommand withJustification(final AnalysisJustification justification) {
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, justification, this.response, this.details, this.suppress, this.commenter, this.comment, this.options);
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, justification, this.response, this.details, this.suppress, this.source, this.commenter, this.comment, this.owaspVector, this.owaspScore, this.options);
}

public MakeAnalysisCommand withResponse(final AnalysisResponse response) {
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, this.justification, response, this.details, this.suppress, this.commenter, this.comment, this.options);
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, this.justification, response, this.details, this.suppress, this.source, this.commenter, this.comment, this.owaspVector, this.owaspScore, this.options);
}

public MakeAnalysisCommand withDetails(final String detail) {
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, this.justification, this.response, detail, this.suppress, this.commenter, this.comment, this.options);
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, this.justification, this.response, detail, this.suppress, this.source, this.commenter, this.comment, this.owaspVector, this.owaspScore, this.options);
}

public MakeAnalysisCommand withSuppress(final Boolean suppress) {
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, this.justification, this.response, this.details, suppress, this.commenter, this.comment, this.options);
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, this.justification, this.response, this.details, suppress, this.source, this.commenter, this.comment, this.owaspVector, this.owaspScore, this.options);
}

public MakeAnalysisCommand withSource(final RatingSource source) {
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, this.justification, this.response, this.details, this.suppress, source, this.commenter, this.comment, this.owaspVector, this.owaspScore, this.options);
}

public MakeAnalysisCommand withCommenter(final String commenter) {
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, this.justification, this.response, this.details, this.suppress, commenter, this.comment, this.options);
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, this.justification, this.response, this.details, this.suppress, this.source, commenter, this.comment, this.owaspVector, this.owaspScore, this.options);
}

public MakeAnalysisCommand withComment(final String comment) {
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, this.justification, this.response, this.details, this.suppress, this.commenter, comment, this.options);
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, this.justification, this.response, this.details, this.suppress, this.source, this.commenter, comment, this.owaspVector, this.owaspScore, this.options);
}

public MakeAnalysisCommand withOwasp(final String vector, final BigDecimal score) {
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, this.justification, this.response, this.details, this.suppress, this.source, this.commenter, this.comment, vector, score, this.options);
}

public MakeAnalysisCommand withOptions(final Set<Option> options) {
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, this.justification, this.response, this.details, this.suppress, this.commenter, this.comment, options);
return new MakeAnalysisCommand(this.component, this.vulnerability, this.state, this.justification, this.response, this.details, this.suppress, this.source, this.commenter, this.comment, this.owaspVector, this.owaspScore, options);
}

}
Loading
Loading