Skip to content

Commit

Permalink
Add Support for Identification of Aliases
Browse files Browse the repository at this point in the history
... by ignoring them

closes #168
  • Loading branch information
sephiroth-j committed Jan 14, 2024
1 parent 571662b commit 976c7fe
Show file tree
Hide file tree
Showing 11 changed files with 284 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.jenkinsci.plugins.DependencyTrack;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
Expand All @@ -38,7 +39,13 @@ List<Finding> parse(final String jsonResponse) {
return jsonArray.stream()
.map(JSONObject.class::cast)
.map(FindingParser::parseFinding)
.collect(Collectors.toList());
.collect(ArrayList<Finding>::new, (findings, finding) -> {
// filter duplicates based on aliases
// add if is not already included and if it is not an alias of an already present finding/vulnerability
if (!findings.contains(finding) && findings.stream().noneMatch(finding::isAliasOf)) {
findings.add(finding);
}
}, List::addAll);
}

private Finding parseFinding(JSONObject json) {
Expand Down Expand Up @@ -71,7 +78,8 @@ private Vulnerability parseVulnerability(JSONObject json) {
final var cwe = Optional.ofNullable(json.optJSONArray("cwes")).map(a -> a.optJSONObject(0)).filter(Predicate.not(JSONNull.class::isInstance));
final Integer cweId = cwe.map(o -> o.optInt("cweId")).orElse(null);
final String cweName = cwe.map(o -> getKeyOrNull(o, "name")).orElse(null);
return new Vulnerability(uuid, source, vulnId, title, subtitle, description, recommendation, severity, severityRank, cweId, cweName);
final var aliases = parseAliases(json, vulnId);
return new Vulnerability(uuid, source, vulnId, title, subtitle, description, recommendation, severity, severityRank, cweId, cweName, aliases);
}

private Analysis parseAnalysis(JSONObject json) {
Expand All @@ -80,13 +88,26 @@ private Analysis parseAnalysis(JSONObject json) {
return new Analysis(state, isSuppressed);
}

private List<String> parseAliases(JSONObject json, String vulnId) {
final var aliases = json.optJSONArray("aliases");
return aliases != null ? aliases.stream()
.map(JSONObject.class::cast)
.flatMap(alias -> alias.names().stream()
.map(String.class::cast)
.map(alias::getString)
.filter(Predicate.not(vulnId::equalsIgnoreCase)))
.distinct()
.collect(Collectors.toList())
: null;
}

private String getKeyOrNull(JSONObject json, String key) {
// key can be null. but it may also be JSONNull!
// optString and getString do not check if v is JSONNull. instead they return just v.toString() which will be "null"!
Object v = json.opt(key);
if (v instanceof JSONNull) {
v = null;
}
return v == null ? null : StringUtils.trimToNull(v.toString());
return Optional.ofNullable(json.opt(key))
.filter(Predicate.not(JSONNull.class::isInstance))
.map(Object::toString)
.map(StringUtils::trimToNull)
.orElse(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
package org.jenkinsci.plugins.DependencyTrack.model;

import java.io.Serializable;
import lombok.EqualsAndHashCode;
import lombok.Value;

@Value
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Component implements Serializable {

private static final long serialVersionUID = -4825926766668357091L;

@EqualsAndHashCode.Include
private final String uuid;
private final String name;
private final String group;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,47 @@
*/
package org.jenkinsci.plugins.DependencyTrack.model;

import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.Serializable;
import lombok.EqualsAndHashCode;
import lombok.Value;

@Value
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Finding implements Serializable {

private static final long serialVersionUID = 5309487290800777874L;

private final Component component;
private final Vulnerability vulnerability;
private final Analysis analysis;

// includes uuid of project, component and vulnerability delimited by colon
@EqualsAndHashCode.Include
private final String matrix;

/**
* checks whether this finding is an alias of the given other finding
*
* @param other the other finding to check against
* @return {@code true} if the finding {@code other} is for the same
* {@link #component} as this one and this
* {@link #vulnerability} {@link Vulnerability#isAliasOf(org.jenkinsci.plugins.DependencyTrack.model.Vulnerability) is an alias of the other one}
*/
public boolean isAliasOf(@NonNull final Finding other) {
return vulnerability != null && component.equals(other.component) && other.getVulnerability() != null && vulnerability.isAliasOf(other.getVulnerability());
}

/**
* checks whether the given other finding is an alias of this finding
*
* @param alias the possible alias to check
* @return {@code true} if the finding {@code alias} is for the same
* {@link #component} as this one and this
* {@link #vulnerability} {@link Vulnerability#hasAlias(org.jenkinsci.plugins.DependencyTrack.model.Vulnerability) has the others one's vulnerability as an alias}
*/
public boolean hasAlias(@NonNull final Finding alias) {
return vulnerability != null && component.equals(alias.component) && alias.getVulnerability() != null && vulnerability.hasAlias(alias.getVulnerability());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,20 @@
*/
package org.jenkinsci.plugins.DependencyTrack.model;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.io.Serializable;
import java.util.List;
import lombok.EqualsAndHashCode;
import lombok.Value;

@Value
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Vulnerability implements Serializable {

private static final long serialVersionUID = 2921629806625084133L;

@EqualsAndHashCode.Include
private final String uuid;
private final String source;
private final String vulnId;
Expand All @@ -35,4 +41,29 @@ public class Vulnerability implements Serializable {
private final Integer cweId;
private final String cweName;

@Nullable
private List<String> aliases;

/**
* checks whether this vulnerability is an alias of the given other
* vulnerability
*
* @param other the other vulnerability to check against
* @return {@code true} if the {@code other} vulnerability contains this
* vulnerability as an alias
*/
public boolean isAliasOf(@NonNull final Vulnerability other) {
return other.aliases != null && other.aliases.contains(vulnId);
}

/**
* checks whether the given other vulnerability is an alias of this
* vulnerability
*
* @param alias the possible alias to check
* @return {@code true} if this vulnerability contains the other as an alias
*/
public boolean hasAlias(@NonNull final Vulnerability alias) {
return aliases != null && aliases.contains(alias.vulnId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<b-form-checkbox value="component.group">${%filter.value.group}</b-form-checkbox>
<b-form-checkbox value="vulnerability.vulnId">${%filter.value.vuln}</b-form-checkbox>
<b-form-checkbox value="vulnerability.severityRank">${%filter.value.severity}</b-form-checkbox>
<b-form-checkbox value="vulnerability.aliases">${%filter.value.aliases}</b-form-checkbox>
<b-form-checkbox value="vulnerability.cweId">${%filter.value.cwe}</b-form-checkbox>
</b-form-checkbox-group>
</b-form-group>
Expand Down Expand Up @@ -131,6 +132,9 @@
<b-card-footer v-if="row.item.analysis.state"><b>${%analysis.state.title}:</b> {{ row.item.analysis.state }}</b-card-footer>
</b-card>
</template>
<template slot="cell(vulnerability.aliases)" slot-scope="data" v-if="data.value?.length">
<ul class="list-unstyled"><li v-for="alias in data.value">{{alias}}</li></ul>
</template>
</b-table>
<b-pagination
v-model="currentPage"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ filter.value.version=Version
filter.value.group=Group
filter.value.vuln=Vulnerability
filter.value.severity=Severity
filter.value.aliases=Aliases
filter.value.cwe=CWE

link.title=View Details on {0}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ filter.value.version=Version
filter.value.group=Gruppe
filter.value.vuln=Schwachstelle
filter.value.severity=Schweregrad
filter.value.aliases=Aliase
filter.value.cwe=CWE-Nummer

link.title=Details bei {0} aufrufen
Expand Down
2 changes: 2 additions & 0 deletions src/main/webapp/js/result-action.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
{ key: 'component.version', label: 'Version', sortable: true },
{ key: 'component.group', label: 'Group', sortable: true },
{ key: 'vulnerability.vulnId', label: 'Vulnerability', sortable: true },
{ key: 'vulnerability.aliases', label: 'Aliases', sortable: false },
{ key: 'vulnerability.severityRank', label: 'Severity', sortable: true },
{ key: 'vulnerability.cweId', label: 'CWE', sortable: true },
],
Expand Down Expand Up @@ -158,6 +159,7 @@
app.fields.find(field => field.key === 'component.version').label = i18n['filter.value.version'];
app.fields.find(field => field.key === 'component.group').label = i18n['filter.value.group'];
app.fields.find(field => field.key === 'vulnerability.vulnId').label = i18n['filter.value.vuln'];
app.fields.find(field => field.key === 'vulnerability.aliases').label = i18n['filter.value.aliases'];
app.fields.find(field => field.key === 'vulnerability.severityRank').label = i18n['filter.value.severity'];
app.fields.find(field => field.key === 'vulnerability.cweId').label = i18n['filter.value.cwe'];
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.assertj.core.util.Files;
import org.jenkinsci.plugins.DependencyTrack.model.Analysis;
import org.jenkinsci.plugins.DependencyTrack.model.Component;
Expand All @@ -38,10 +39,21 @@ void parseTest() {
assertThat(FindingParser.parse("[]")).isEmpty();

File findings = new File("src/test/resources/findings.json");

Component c1 = new Component("uuid-1", "name-1", "group-1", "version-1", "purl-1");
Vulnerability v1 = new Vulnerability("uuid-1", "source-1", "vulnId-1", "title-1", "subtitle-1", "description-1", "recommendation-1", Severity.CRITICAL, 1, 2, "cweName-1");
var aliases1 = List.of("GHSA-abcd-abcd-abcd", "FOO-12345");
var v1 = new Vulnerability("uuid-1", "NVD", "vulnId-1", "title-1", "subtitle-1", "description-1", "recommendation-1", Severity.CRITICAL, 1, 2, "cweName-1", aliases1);
Analysis a1 = new Analysis("state-1", false);
Finding f1 = new Finding(c1, v1, a1, "matrix-1");
assertThat(FindingParser.parse(Files.contentOf(findings, StandardCharsets.UTF_8))).containsExactly(f1);

var c2 = new Component("uuid-2", "name-2", "group-2", "version-2", "purl-2");
var v2 = new Vulnerability("uuid-2", "GITHUB", "GHSA-abcd-abcd-abcd", "title-1", "subtitle-1", "description-1", "recommendation-1", Severity.CRITICAL, 1, 2, "cweName-1", null);
var f2 = new Finding(c2, v2, a1, "matrix-3");

var c3 = new Component("uuid-3", "name-3", "group-3", "version-3", "purl-3");
var v3 = new Vulnerability("uuid-3", "FOO", "FOO-78945", "title-3", "subtitle-3", "description-3", "recommendation-3", Severity.CRITICAL, 1, 2, "cweName-1", null);
var f3 = new Finding(c3, v3, a1, "matrix-4");

assertThat(FindingParser.parse(Files.contentOf(findings, StandardCharsets.UTF_8))).containsExactly(f1, f2, f3);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2024 OWASP.
*
* 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
*
* https://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.
*/
package org.jenkinsci.plugins.DependencyTrack.model;

import java.util.List;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class FindingTest {

@Test
void testAliases() {
var a1 = new Analysis("state-1", false);
var c1 = new Component("uuid-1", "name-1", "group-1", "version-1", "purl-1");

var aliases1 = List.of("GHSA-abcd-abcd-abcd", "FOO-12345");
var v1 = new Vulnerability("uuid-1", "NVD", "vulnId-1", "title-1", "subtitle-1", "description-1", "recommendation-1", Severity.CRITICAL, 1, 2, "cweName-1", aliases1);
var f1 = new Finding(c1, v1, a1, "matrix-1");

var aliases2 = List.of("vulnId-1", "FOO-12345");
var v2 = new Vulnerability("uuid-2", "GITHUB", "GHSA-abcd-abcd-abcd", "title-1", "subtitle-1", "description-1", "recommendation-1", Severity.CRITICAL, 1, 2, "cweName-1", aliases2);
var f2 = new Finding(c1, v2, a1, "matrix-2");

assertThat(f2.isAliasOf(f1)).isTrue();
assertThat(f1.isAliasOf(f2)).isTrue();
assertThat(f1.hasAlias(f2)).isTrue();
assertThat(f2.hasAlias(f1)).isTrue();

var v3 = new Vulnerability("uuid-2", "GITHUB", "GHSA-abcd-abcd-abcd", "title-1", "subtitle-1", "description-1", "recommendation-1", Severity.CRITICAL, 1, 2, "cweName-1", null);
var f3 = new Finding(c1, v3, a1, "matrix-2");

var c2 = new Component("uuid-2", "name-1", "group-1", "version-1", "purl-1");
var f4 = new Finding(c2, v1, a1, "matrix-2");
assertThat(f2.isAliasOf(f3)).isFalse();
assertThat(f3.isAliasOf(f2)).isFalse();
assertThat(f1.hasAlias(f4)).isFalse();
assertThat(f3.hasAlias(f1)).isFalse();
}
}
Loading

0 comments on commit 976c7fe

Please sign in to comment.