diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 9b633d56ba..87e4bdfe92 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -14,6 +14,7 @@ The full list of changes for this release can be found in https://github.com/dev Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/1628[#1628]: Support for OS-specific CVEs * https://github.com/devonfw/IDEasy/issues/1525[#1525]: Document known issue and workaround for lombok plugin in Eclipse * https://github.com/devonfw/IDEasy/issues/1031[#1031]: Added OpenRewrite commandlet * https://github.com/devonfw/IDEasy/issues/2361[#2361]: Improve dotnet installation by setting DOTNET_ROOT diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java index d6b51d7319..8df3ac8174 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java @@ -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; @@ -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()) { @@ -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); diff --git a/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/Cve.java b/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/Cve.java index 19d40777a0..bc1afae6c4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/Cve.java +++ b/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/Cve.java @@ -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; @@ -17,9 +20,11 @@ * @param severity the severity in the range from (0,10.0] where 10.0 is most critical. * @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 versions) implements JsonObject { +public record Cve(String id, double severity, List versions, Map> conditions) implements JsonObject { static final String PROPERTY_ID = "id"; @@ -27,10 +32,50 @@ public record Cve(String id, double severity, List versions) imple 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 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); + } + + private static boolean contains(List ranges, VersionIdentifier version) { + + if (ranges != null) { + for (VersionRange range : ranges) { + if (range.contains(version)) { + return true; + } + } + } + return false; } /** @@ -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> mergeConditions(Map> other) { + + if (this.conditions.isEmpty() && other.isEmpty()) { + return Map.of(); + } + Map> newConditions = new TreeMap<>(); + this.conditions.forEach((os, ranges) -> newConditions.put(os, new ArrayList<>(ranges))); + other.forEach((os, ranges) -> { + List newRanges = newConditions.computeIfAbsent(os, key -> new ArrayList<>()); + ranges.forEach(range -> mergeVersionRage(newRanges, range)); + }); + return newConditions; } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/CveJsonDeserializer.java b/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/CveJsonDeserializer.java index 6036a3f7bc..508406cb31 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/CveJsonDeserializer.java +++ b/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/CveJsonDeserializer.java @@ -2,11 +2,14 @@ import java.io.IOException; import java.util.List; +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; /** @@ -25,6 +28,7 @@ private class CveBuilder extends JsonBuilder { private String id; private Double severity; private List versions; + private Map> conditions; @Override public void setProperty(String property, JsonParser p, DeserializationContext ctxt) throws IOException { @@ -39,16 +43,35 @@ 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> readConditions(JsonParser p) throws IOException { + + if (p.getCurrentToken() != JsonToken.START_OBJECT) { + return null; + } + Map> result = new TreeMap<>(); + JsonToken token = p.nextToken(); + while (token == JsonToken.FIELD_NAME) { + String os = p.currentName(); + p.nextToken(); + result.put(os, readArray(p, VersionRange.class, os, null)); + 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); } } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/CveJsonSerializer.java b/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/CveJsonSerializer.java index 6373dad5a8..156d80e43c 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/CveJsonSerializer.java +++ b/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/CveJsonSerializer.java @@ -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; @@ -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> conditions = cve.conditions(); + if (!conditions.isEmpty()) { + jgen.writeFieldName(Cve.PROPERTY_CONDITIONS); + jgen.writeStartObject(); + for (Map.Entry> condition : conditions.entrySet()) { + jgen.writeFieldName(condition.getKey()); + writeArray(condition.getValue(), jgen); + } + jgen.writeEndObject(); + } } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/ToolSecurity.java b/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/ToolSecurity.java index 0e90f47a73..2bb6ad58fd 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/ToolSecurity.java +++ b/cli/src/main/java/com/devonfw/tools/ide/url/model/file/json/ToolSecurity.java @@ -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; /** @@ -109,22 +109,21 @@ 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 predicate) { + public ToolVulnerabilities findCves(VersionIdentifier version, OperatingSystem os, Predicate predicate) { List 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()); } } } @@ -132,14 +131,15 @@ public ToolVulnerabilities findCves(VersionIdentifier version, Predicate pr } /** - * 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); } /** diff --git a/cli/src/test/java/com/devonfw/tools/ide/url/model/file/json/CveTest.java b/cli/src/test/java/com/devonfw/tools/ide/url/model/file/json/CveTest.java index 2b0a055aee..7791a527df 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/url/model/file/json/CveTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/url/model/file/json/CveTest.java @@ -1,10 +1,13 @@ 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; /** @@ -12,6 +15,21 @@ */ 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 void testMerge() { diff --git a/cli/src/test/java/com/devonfw/tools/ide/url/model/file/json/ToolSecurityMapperTest.java b/cli/src/test/java/com/devonfw/tools/ide/url/model/file/json/ToolSecurityMapperTest.java index c47c36ddd4..4e364b82bb 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/url/model/file/json/ToolSecurityMapperTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/url/model/file/json/ToolSecurityMapperTest.java @@ -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; @@ -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); + } + }