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
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.devonfw.tools.ide.log.IdeLogLevel;
import com.devonfw.tools.ide.nls.NlsBundle;
import com.devonfw.tools.ide.os.MacOsHelper;
import com.devonfw.tools.ide.os.OperatingSystem;
import com.devonfw.tools.ide.process.EnvironmentContext;
import com.devonfw.tools.ide.process.ProcessContext;
import com.devonfw.tools.ide.process.ProcessErrorHandling;
Expand Down Expand Up @@ -714,7 +715,8 @@ protected VersionIdentifier cveCheck(ToolInstallRequest request) {
}
ToolSecurity toolSecurity = this.context.getDefaultToolRepository().findSecurity(this.tool, toolEdition.edition());
double minSeverity = IdeVariables.CVE_MIN_SEVERITY.get(context);
ToolVulnerabilities currentVulnerabilities = toolSecurity.findCves(resolvedVersion, minSeverity);
OperatingSystem os = this.context.getSystemInfo().getOs();
ToolVulnerabilities currentVulnerabilities = toolSecurity.findCves(resolvedVersion, os, minSeverity);
ToolVersionChoice currentChoice = ToolVersionChoice.ofCurrent(requested, currentVulnerabilities);
request.setCveCheckDone();
if (currentChoice.logAndCheckIfEmpty()) {
Expand All @@ -741,7 +743,7 @@ protected VersionIdentifier cveCheck(ToolInstallRequest request) {
}

if (acceptVersion(version, allowedVersions, requireStableVersion)) {
ToolVulnerabilities newVulnerabilities = toolSecurity.findCves(version, minSeverity);
ToolVulnerabilities newVulnerabilities = toolSecurity.findCves(version, os, minSeverity);
if (newVulnerabilities.isSafer(latestVulnerabilities)) {
// we found a better/safer version
ToolEditionAndVersion toolEditionAndVersion = new ToolEditionAndVersion(toolEdition, version);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;

import com.devonfw.tools.ide.json.JsonObject;
import com.devonfw.tools.ide.os.OperatingSystem;
import com.devonfw.tools.ide.version.VersionIdentifier;
import com.devonfw.tools.ide.version.VersionRange;
import com.devonfw.tools.ide.version.VersionRangeRelation;
Expand All @@ -17,20 +20,62 @@
* @param severity the severity in the range from (0,10.0] where 10.0 is most critical.
Comment thread
laert-ll marked this conversation as resolved.
* @param versions the {@link VersionRange}s of the affected versions. Typically one entry but might also affect multiple ranges. E.g. "[1.0,1.2)" and
* "[2.0,2.2)". Should never be {@code null} or {@link List#isEmpty() empty}.
* @param conditions the additional {@link VersionRange}s of affected versions per {@link OperatingSystem#toString() operating system}. Only relevant when the
* end-user runs IDEasy on the matching operating system. Never {@code null} but may be {@link Map#isEmpty() empty}.
* @see ToolSecurity
*/
public record Cve(String id, double severity, List<VersionRange> versions) implements JsonObject {
public record Cve(String id, double severity, List<VersionRange> versions, Map<String, List<VersionRange>> conditions) implements JsonObject {

static final String PROPERTY_ID = "id";

static final String PROPERTY_SEVERITY = "severity";

static final String PROPERTY_VERSIONS = "versions";

static final String PROPERTY_CONDITIONS = "conditions";

public Cve {
Objects.requireNonNull(id);
Objects.requireNonNull(versions);
assert !versions.isEmpty();
if (conditions == null) {
conditions = Map.of();
}
}

/**
* @param id the {@link #id()}.
* @param severity the {@link #severity()}.
* @param versions the {@link #versions()}.
*/
public Cve(String id, double severity, List<VersionRange> versions) {

this(id, severity, versions, Map.of());
}

/**
* @param version the {@link VersionIdentifier} to check.
* @param os the current {@link OperatingSystem} (may be {@code null}).
* @return {@code true} if the given {@link VersionIdentifier} is affected by this CVE on the given {@link OperatingSystem}, {@code false} otherwise.
*/
public boolean isAffected(VersionIdentifier version, OperatingSystem os) {

if (contains(this.versions, version)) {
return true;
}
return (os != null) && contains(this.conditions.get(os.toString()), version);
Comment thread
laert-ll marked this conversation as resolved.
}

private static boolean contains(List<VersionRange> ranges, VersionIdentifier version) {

if (ranges != null) {
for (VersionRange range : ranges) {
if (range.contains(version)) {
return true;
}
}
}
return false;
}

/**
Expand All @@ -49,7 +94,21 @@ public Cve merge(Cve issue) {
for (VersionRange versionRange : issue.versions) {
mergeVersionRage(newVersions, versionRange);
}
return new Cve(this.id, this.severity, newVersions);
return new Cve(this.id, this.severity, newVersions, mergeConditions(issue.conditions));
}

private Map<String, List<VersionRange>> mergeConditions(Map<String, List<VersionRange>> other) {

if (this.conditions.isEmpty() && other.isEmpty()) {
return Map.of();
}
Comment on lines +102 to +104

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Wouldn't this make more sense here?

Suggested change
if (this.conditions.isEmpty() && other.isEmpty()) {
return Map.of();
}
if (this.conditions.isEmpty()) {
return other;
}

Map<String, List<VersionRange>> newConditions = new TreeMap<>();
this.conditions.forEach((os, ranges) -> newConditions.put(os, new ArrayList<>(ranges)));
Comment on lines +105 to +106

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can be simplified and made more efficient:

Suggested change
Map<String, List<VersionRange>> newConditions = new TreeMap<>();
this.conditions.forEach((os, ranges) -> newConditions.put(os, new ArrayList<>(ranges)));
Map<String, List<VersionRange>> newConditions = new TreeMap<>(this.conditions);

other.forEach((os, ranges) -> {
List<VersionRange> newRanges = newConditions.computeIfAbsent(os, key -> new ArrayList<>());
ranges.forEach(range -> mergeVersionRage(newRanges, range));
});
return newConditions;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This Cve is a record and should stay immutable:

Suggested change
return newConditions;
return Map.copyOf(newConditions);

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;

import com.devonfw.tools.ide.json.JsonBuilder;
import com.devonfw.tools.ide.json.JsonObjectDeserializer;
import com.devonfw.tools.ide.version.VersionRange;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;

/**
Expand All @@ -25,6 +29,7 @@ private class CveBuilder extends JsonBuilder<Cve> {
private String id;
private Double severity;
private List<VersionRange> versions;
private Map<String, List<VersionRange>> conditions;

@Override
public void setProperty(String property, JsonParser p, DeserializationContext ctxt) throws IOException {
Expand All @@ -39,16 +44,41 @@ public void setProperty(String property, JsonParser p, DeserializationContext ct
case Cve.PROPERTY_VERSIONS -> {
this.versions = readArray(p, VersionRange.class, property, this.versions);
}
case Cve.PROPERTY_CONDITIONS -> {
this.conditions = readConditions(p);
}
default -> {
super.setProperty(property, p, ctxt);
}
}
}

private Map<String, List<VersionRange>> readConditions(JsonParser p) throws IOException {

JsonToken token = p.getCurrentToken();
if (token == JsonToken.VALUE_NULL) {
return null;
} else if (token != JsonToken.START_OBJECT) {
throw new IllegalStateException("Unexpected token " + token);
}
Map<String, List<VersionRange>> result = new TreeMap<>();
token = p.nextToken();
while (token == JsonToken.FIELD_NAME) {
String os = p.currentName().toLowerCase(Locale.ROOT);
p.nextToken();
List<VersionRange> ranges = readArray(p, VersionRange.class, os, null);
if (ranges != null) {
result.put(os, ranges);
}
token = p.nextToken();
}
return result;
}

@Override
public Cve build() {

return new Cve(this.id, this.severity, this.versions);
return new Cve(this.id, this.severity, this.versions, this.conditions);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.devonfw.tools.ide.url.model.file.json;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import com.devonfw.tools.ide.json.JsonObjectSerializer;
import com.devonfw.tools.ide.version.VersionRange;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;

Expand All @@ -17,5 +20,15 @@ protected void serializeProperties(Cve cve, JsonGenerator jgen, SerializerProvid
jgen.writeNumberField(Cve.PROPERTY_SEVERITY, cve.severity());
jgen.writeFieldName(Cve.PROPERTY_VERSIONS);
writeArray(cve.versions(), jgen);
Map<String, List<VersionRange>> conditions = cve.conditions();
if (!conditions.isEmpty()) {
jgen.writeFieldName(Cve.PROPERTY_CONDITIONS);
jgen.writeStartObject();
for (Map.Entry<String, List<VersionRange>> condition : conditions.entrySet()) {
jgen.writeFieldName(condition.getKey());
writeArray(condition.getValue(), jgen);
Comment thread
laert-ll marked this conversation as resolved.
}
jgen.writeEndObject();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

import com.devonfw.tools.ide.json.JsonMapping;
import com.devonfw.tools.ide.json.JsonObject;
import com.devonfw.tools.ide.os.OperatingSystem;
import com.devonfw.tools.ide.security.ToolVulnerabilities;
import com.devonfw.tools.ide.variable.IdeVariables;
import com.devonfw.tools.ide.version.VersionIdentifier;
import com.devonfw.tools.ide.version.VersionRange;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
Expand Down Expand Up @@ -109,37 +109,37 @@ public void clearIssues() {
}

/**
* Finds all {@link Cve}s for the given {@link VersionIdentifier} that also match the given {@link Predicate}.
* Finds all {@link Cve}s for the given {@link VersionIdentifier} and {@link OperatingSystem} that also match the given {@link Predicate}.
*
* @param version the {@link VersionIdentifier} to check.
* @param os the current {@link OperatingSystem} (may be {@code null}).
* @param predicate the {@link Predicate} deciding which matching {@link Cve}s are {@link Predicate#test(Object) accepted}.
* @return all {@link Cve}s for the given {@link VersionIdentifier}.
*/
public ToolVulnerabilities findCves(VersionIdentifier version, Predicate<Cve> predicate) {
public ToolVulnerabilities findCves(VersionIdentifier version, OperatingSystem os, Predicate<Cve> predicate) {
List<Cve> cvesOfVersion = new ArrayList<>();
for (Cve cve : this.issues) {
for (VersionRange range : cve.versions()) {
if (range.contains(version)) {
if (predicate.test(cve)) {
cvesOfVersion.add(cve);
} else {
LOG.info("Ignoring CVE {} with severity {}", cve.id(), cve.severity());
}
if (cve.isAffected(version, os)) {
if (predicate.test(cve)) {
cvesOfVersion.add(cve);
} else {
LOG.info("Ignoring CVE {} with severity {}", cve.id(), cve.severity());
}
}
}
return ToolVulnerabilities.of(cvesOfVersion);
}

/**
* Finds all {@link Cve}s for the given {@link VersionIdentifier} and {@code minSeverity}.
* Finds all {@link Cve}s for the given {@link VersionIdentifier}, {@link OperatingSystem} and {@code minSeverity}.
*
* @param version the {@link VersionIdentifier} to check.
* @param os the current {@link OperatingSystem} (may be {@code null}).
* @param minSeverity the {@link IdeVariables#CVE_MIN_SEVERITY minimum severity}.
* @return the {@link ToolVulnerabilities} for the given {@link VersionIdentifier}.
*/
public ToolVulnerabilities findCves(VersionIdentifier version, double minSeverity) {
return findCves(version, cve -> cve.severity() >= minSeverity);
public ToolVulnerabilities findCves(VersionIdentifier version, OperatingSystem os, double minSeverity) {
return findCves(version, os, cve -> cve.severity() >= minSeverity);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
package com.devonfw.tools.ide.url.model.file.json;

import java.util.List;
import java.util.Map;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import com.devonfw.tools.ide.os.OperatingSystem;
import com.devonfw.tools.ide.version.VersionIdentifier;
import com.devonfw.tools.ide.version.VersionRange;

/**
* Test of {@link Cve}.
*/
class CveTest extends Assertions {

@Test
void testIsAffected() {

// arrange
Cve cve = new Cve("CVE-2024-99999", 5.0, List.of(VersionRange.of("(,1.0.0)")),
Map.of("windows", List.of(VersionRange.of("[2.0.0,2.0.8]")), "linux", List.of(VersionRange.of("[2.0.0,2.0.5]"))));

// act + assert
assertThat(cve.isAffected(VersionIdentifier.of("0.9.0"), OperatingSystem.LINUX)).isTrue();
assertThat(cve.isAffected(VersionIdentifier.of("2.0.6"), OperatingSystem.WINDOWS)).isTrue();
assertThat(cve.isAffected(VersionIdentifier.of("2.0.6"), OperatingSystem.LINUX)).isFalse();
assertThat(cve.isAffected(VersionIdentifier.of("2.0.6"), OperatingSystem.MAC)).isFalse();
assertThat(cve.isAffected(VersionIdentifier.of("2.0.6"), null)).isFalse();
}

@Test
Comment thread
laert-ll marked this conversation as resolved.
void testMerge() {

Expand All @@ -31,4 +49,29 @@ void testMerge() {
assertThat(merged.versions()).containsExactly(VersionRange.of("(,2.6.9]"), VersionRange.of("[2.8.0,2.8.3]"));
}

@Test
void testMergeConditions() {

// arrange
Cve cve1 = new Cve("CVE-2024-99999", 5.0, List.of(VersionRange.of("(,1.0.0)")),
Map.of("windows", List.of(VersionRange.of("[2.0.0,2.0.5]"), VersionRange.of("[3.0.0,3.0.1]")), "linux", List.of(VersionRange.of("[2.0.0,2.0.5]"))));
Cve cve2 = new Cve("CVE-2024-99999", 5.0, List.of(VersionRange.of("[1.5.0,1.6.0]")),
Map.of("windows", List.of(VersionRange.of("[2.0.4,2.0.9]")), "mac", List.of(VersionRange.of("[4.0.0,4.0.1]"))));

// act
Cve merged = cve1.merge(cve2);

// assert
assertThat(merged.versions()).containsExactly(VersionRange.of("(,1.0.0)"), VersionRange.of("[1.5.0,1.6.0]"));
assertThat(merged.conditions()).containsOnlyKeys("linux", "mac", "windows");
// overlapping ranges of the same operating system are merged, disjoint ones are kept apart
assertThat(merged.conditions().get("windows")).containsExactly(VersionRange.of("[2.0.0,2.0.9]"), VersionRange.of("[3.0.0,3.0.1]"));
// operating systems only present on one side are taken over unchanged
assertThat(merged.conditions().get("linux")).containsExactly(VersionRange.of("[2.0.0,2.0.5]"));
assertThat(merged.conditions().get("mac")).containsExactly(VersionRange.of("[4.0.0,4.0.1]"));
// merging does not modify the merged CVEs
assertThat(cve1.conditions()).containsOnlyKeys("linux", "windows");
assertThat(cve2.conditions()).containsOnlyKeys("mac", "windows");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -35,4 +36,18 @@ void testLoadAndSaveJson(@TempDir Path tmpDir) {
assertThat(tmpDir.resolve(mapper.getStandardFilename())).hasSameTextualContentAs(testPath.resolve("security-normalized.json"));
}

@Test
void testConditionsRoundTrip(@TempDir Path tmpDir) {
// arrange
ToolSecurityMapper mapper = ToolSecurityMapper.get();
Cve cve = new Cve("CVE-2024-99999", 5.0, List.of(VersionRange.of("(,1.0.0)")),
Map.of("windows", List.of(VersionRange.of("[2.0.0,2.0.8]")), "linux", List.of(VersionRange.of("[2.0.0,2.0.5]"))));
ToolSecurity toolSecurity = new ToolSecurity(List.of(cve));
// act
mapper.saveJsonToFolder(toolSecurity, tmpDir);
ToolSecurity loaded = mapper.loadJsonFromFolder(tmpDir);
// assert
assertThat(loaded.getIssues()).containsExactly(cve);
}

}