diff --git a/src/main/java/de/rub/nds/scanner/core/probe/result/FaultListResult.java b/src/main/java/de/rub/nds/scanner/core/probe/result/FaultListResult.java new file mode 100644 index 00000000..a105e4ff --- /dev/null +++ b/src/main/java/de/rub/nds/scanner/core/probe/result/FaultListResult.java @@ -0,0 +1,148 @@ +/* + * Scanner Core - A Modular Framework for Probe Definition, Execution, and Result Analysis. + * + * Copyright 2017-2023 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.scanner.core.probe.result; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonIncludeProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import de.rub.nds.scanner.core.probe.AnalyzedProperty; +import java.util.List; + +/** + * Represents {@link TestResult}s which list the features for which a fault has been observed. The + * list holds the affected features only, the summary states whether any fault was found at all: an + * empty list summarizes to {@link TestResults#FALSE}, a non-empty list to {@link TestResults#TRUE}. + * An explicit TestResult can be set, for example, to communicate that the test could not be + * applied. + * + * @param the type of the listed faulty features. + */ +@JsonIncludeProperties({"type", "value", "summary"}) +@JsonPropertyOrder({"type", "value", "summary"}) +public class FaultListResult extends ListResult implements SummarizableTestResult { + + private final TestResults explicitSummary; + + @SuppressWarnings("unused") + private FaultListResult() { + // Default constructor for deserialization + this(null, null, null); + } + + /** + * Constructs a FaultListResult which summarizes the listed faulty features. + * + * @param property the analyzed property associated with this result + * @param faultyFeatures the features for which a fault has been observed, may be empty but + * should not be null + */ + public FaultListResult(AnalyzedProperty property, List faultyFeatures) { + this(property, faultyFeatures, null); + } + + /** + * Constructs a FaultListResult which reports the given summary instead of summarizing listed + * faulty features. Use this if the features could not be examined, e.g. {@link + * TestResults#CANNOT_BE_TESTED} if a precondition of the probe was not met. + * + * @param property the analyzed property associated with this result + * @param explicitSummary the summary to report, must not be null + */ + public FaultListResult(AnalyzedProperty property, TestResults explicitSummary) { + this(property, null, explicitSummary); + } + + /** + * Constructs a FaultListResult with the specified property, list of faulty features and + * explicit summary. If the explicit summary is null, the summary is derived from the listed + * faulty features, otherwise the explicit summary takes precedence. + * + * @param property the analyzed property associated with this result + * @param faultyFeatures the features for which a fault has been observed + * @param explicitSummary the summary to report, or null to derive it from the listed features + */ + public FaultListResult( + AnalyzedProperty property, List faultyFeatures, TestResults explicitSummary) { + super(property, faultyFeatures); + this.explicitSummary = explicitSummary; + } + + /** + * Restores a FaultListResult from its serialized form. The analyzed property is not part of the + * serialized form and is therefore not restored. + * + * @param faultyFeatures the features for which a fault has been observed + * @param explicitSummary the name of the explicitly set summary, or null if the summary is + * derived from the listed faulty features + * @return the deserialized FaultListResult + */ + @JsonCreator + private static FaultListResult fromJson( + @JsonProperty("value") List faultyFeatures, + @JsonProperty("summary") String explicitSummary) { + return new FaultListResult<>( + null, + faultyFeatures, + explicitSummary == null ? null : TestResults.valueOf(explicitSummary)); + } + + /** + * Returns the explicitly set summary of this result. + * + * @return the explicit summary, or null if the summary is derived from the listed faulty + * features + */ + @JsonGetter("summary") + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) + public TestResults getExplicitSummary() { + return explicitSummary; + } + + /** + * Summarizes the listed faults. Returns the explicit summary if one has been set. Otherwise + * returns {@link TestResults#TRUE} if at least one faulty feature has been listed, {@link + * TestResults#FALSE} if the list is empty and {@link TestResults#NOT_TESTED_YET} if no list has + * been set at all. + * + * @return the summarized TestResults value + */ + @Override + public TestResults getSummarizedResult() { + if (explicitSummary != null) { + return explicitSummary; + } + if (collection == null) { + return TestResults.NOT_TESTED_YET; + } + return TestResults.of(!collection.isEmpty()); + } + + /** + * Indicates whether the summary was explicitly set. + * + * @return true if an explicit summary has been set, false if the summary is derived from the + * listed faulty features + */ + @Override + @JsonIgnore + public boolean isExplicitSummary() { + return explicitSummary != null; + } + + @Override + public String getName() { + return SummarizableTestResult.super.getName(); + } +} diff --git a/src/main/java/de/rub/nds/scanner/core/report/rating/RatingInfluencer.java b/src/main/java/de/rub/nds/scanner/core/report/rating/RatingInfluencer.java index 6a003633..1f6c5403 100644 --- a/src/main/java/de/rub/nds/scanner/core/report/rating/RatingInfluencer.java +++ b/src/main/java/de/rub/nds/scanner/core/report/rating/RatingInfluencer.java @@ -109,8 +109,10 @@ public void addPropertyRatingInfluencer(PropertyResultRatingInfluencer ratingInf } /** - * Gets the property rating influencer for a specific test result. If no influencer is found for - * the given result, returns a new influencer with zero influence. + * Gets the property rating influencer for a specific test result. A complex result which + * summarizes to a plain {@link de.rub.nds.scanner.core.probe.result.TestResults} is matched by + * its summary. If no influencer is found for the given result, returns a new influencer with + * zero influence. * * @param result the test result to find an influencer for * @return the matching property rating influencer, or a new one with zero influence if not @@ -118,7 +120,7 @@ public void addPropertyRatingInfluencer(PropertyResultRatingInfluencer ratingInf */ public PropertyResultRatingInfluencer getPropertyRatingInfluencer(TestResult result) { for (PropertyResultRatingInfluencer ri : propertyRatingInfluencers) { - if (ri.getResult().equalsExpectedResult(result)) { + if (ResultMatcher.matches(result, ri.getResult())) { return ri; } } diff --git a/src/main/java/de/rub/nds/scanner/core/report/rating/Recommendation.java b/src/main/java/de/rub/nds/scanner/core/report/rating/Recommendation.java index d92f6faa..dbe6637b 100644 --- a/src/main/java/de/rub/nds/scanner/core/report/rating/Recommendation.java +++ b/src/main/java/de/rub/nds/scanner/core/report/rating/Recommendation.java @@ -308,15 +308,17 @@ public void setPropertyRecommendations( } /** - * Gets the recommendation for a specific test result. If no recommendation is found, returns a - * default recommendation with no information available message. + * Gets the recommendation for a specific test result. A complex result which summarizes to a + * plain {@link de.rub.nds.scanner.core.probe.result.TestResults} is matched by its summary. If + * no recommendation is found, returns a default recommendation with no information available + * message. * * @param result the test result to find a recommendation for * @return the matching recommendation or a default recommendation if not found */ public PropertyResultRecommendation getPropertyResultRecommendation(TestResult result) { for (PropertyResultRecommendation r : propertyRecommendations) { - if (r.getResult() == result) { + if (ResultMatcher.matches(result, r.getResult())) { return r; } } diff --git a/src/main/java/de/rub/nds/scanner/core/report/rating/ResultMatcher.java b/src/main/java/de/rub/nds/scanner/core/report/rating/ResultMatcher.java new file mode 100644 index 00000000..4b375b50 --- /dev/null +++ b/src/main/java/de/rub/nds/scanner/core/report/rating/ResultMatcher.java @@ -0,0 +1,46 @@ +/* + * Scanner Core - A Modular Framework for Probe Definition, Execution, and Result Analysis. + * + * Copyright 2017-2023 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.scanner.core.report.rating; + +import de.rub.nds.scanner.core.probe.result.SummarizableTestResult; +import de.rub.nds.scanner.core.probe.result.TestResult; + +/** + * Matches the {@link TestResult} a scan yielded for a property against the result a {@link + * RatingInfluencer} or {@link Recommendation} has been configured for. + */ +final class ResultMatcher { + + private ResultMatcher() { + // Private constructor to prevent instantiation of utility class + } + + /** + * Determines whether the actual result of a property matches the result a rating influencer or + * recommendation has been configured for. The comparison is performed by the actual result, as + * this may be a {@link SummarizableTestResult}) which compares against its own summary result. + * + * @param actualResult the result the scan yielded, may be null + * @param configuredResult the result the influencer or recommendation is configured for, may be + * null + * @return true if the actual result matches the configured result + */ + static boolean matches(TestResult actualResult, TestResult configuredResult) { + if (actualResult == null || configuredResult == null) { + return false; + } + try { + return actualResult.equalsExpectedResult(configuredResult); + } catch (IllegalArgumentException e) { + // The actual result is a complex result which does not know how to compare itself to + // the configured result. + return false; + } + } +} diff --git a/src/test/java/de/rub/nds/scanner/core/probe/result/FaultListResultTest.java b/src/test/java/de/rub/nds/scanner/core/probe/result/FaultListResultTest.java new file mode 100644 index 00000000..da9563e3 --- /dev/null +++ b/src/test/java/de/rub/nds/scanner/core/probe/result/FaultListResultTest.java @@ -0,0 +1,153 @@ +/* + * Scanner Core - A Modular Framework for Probe Definition, Execution, and Result Analysis. + * + * Copyright 2017-2023 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.scanner.core.probe.result; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.rub.nds.scanner.core.TestAnalyzedProperty; +import java.util.List; +import org.junit.jupiter.api.Test; + +class FaultListResultTest { + + private static FaultListResult faultsFor(List faultyFeatures) { + return new FaultListResult<>(TestAnalyzedProperty.TEST_ANALYZED_PROPERTY, faultyFeatures); + } + + private static FaultListResult summarizedAs(TestResults explicitSummary) { + return new FaultListResult<>(TestAnalyzedProperty.TEST_ANALYZED_PROPERTY, explicitSummary); + } + + @Test + void emptyListSummarizesToFalse() { + assertEquals(TestResults.FALSE, faultsFor(List.of()).getSummarizedResult()); + } + + @Test + void nonEmptyListSummarizesToTrue() { + assertEquals(TestResults.TRUE, faultsFor(List.of("SECP256R1")).getSummarizedResult()); + } + + @Test + void missingListSummarizesToNotTestedYet() { + assertEquals(TestResults.NOT_TESTED_YET, faultsFor(null).getSummarizedResult()); + } + + @Test + void summaryIsNotExplicit() { + assertFalse(faultsFor(List.of()).isExplicitSummary()); + assertNull(faultsFor(List.of()).getExplicitSummary()); + } + + @Test + void explicitSummaryIsReportedAsSet() { + FaultListResult cannotBeTested = summarizedAs(TestResults.CANNOT_BE_TESTED); + + assertTrue(cannotBeTested.isExplicitSummary()); + assertEquals(TestResults.CANNOT_BE_TESTED, cannotBeTested.getExplicitSummary()); + assertEquals(TestResults.CANNOT_BE_TESTED, cannotBeTested.getSummarizedResult()); + assertEquals(TestResults.CANNOT_BE_TESTED.getName(), cannotBeTested.getName()); + } + + @Test + void explicitSummaryCollectsNoFaults() { + assertNull(summarizedAs(TestResults.CANNOT_BE_TESTED).getList()); + } + + @Test + void explicitSummaryTakesPrecedenceOverListedFaults() { + FaultListResult result = + new FaultListResult<>( + TestAnalyzedProperty.TEST_ANALYZED_PROPERTY, + List.of("SECP256R1"), + TestResults.ERROR_DURING_TEST); + + assertTrue(result.isExplicitSummary()); + assertEquals(TestResults.ERROR_DURING_TEST, result.getSummarizedResult()); + assertEquals(List.of("SECP256R1"), result.getList()); + } + + @Test + void explicitSummaryMatchesExpectedTestResults() { + FaultListResult cannotBeTested = summarizedAs(TestResults.CANNOT_BE_TESTED); + + assertTrue(cannotBeTested.equalsExpectedResult(TestResults.CANNOT_BE_TESTED)); + assertFalse(cannotBeTested.equalsExpectedResult(TestResults.FALSE)); + assertFalse(cannotBeTested.equalsExpectedResult(TestResults.TRUE)); + } + + @Test + void nameIsTakenFromSummary() { + assertEquals(TestResults.TRUE.getName(), faultsFor(List.of("SECP256R1")).getName()); + assertEquals(TestResults.FALSE.getName(), faultsFor(List.of()).getName()); + } + + @Test + void matchesExpectedTestResultsOfSummary() { + FaultListResult withFaults = faultsFor(List.of("SECP256R1")); + FaultListResult withoutFaults = faultsFor(List.of()); + + assertTrue(withFaults.equalsExpectedResult(TestResults.TRUE)); + assertFalse(withFaults.equalsExpectedResult(TestResults.FALSE)); + assertTrue(withoutFaults.equalsExpectedResult(TestResults.FALSE)); + assertFalse(withoutFaults.equalsExpectedResult(TestResults.TRUE)); + } + + @Test + void listStaysAccessible() { + assertEquals( + List.of("SECP256R1", "SECP384R1"), + faultsFor(List.of("SECP256R1", "SECP384R1")).getList()); + } + + @Test + void roundTripKeepsFaultsAndSummary() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + String json = mapper.writeValueAsString(faultsFor(List.of("SECP256R1"))); + + FaultListResult restored = mapper.readValue(json, FaultListResult.class); + + assertNotNull(restored.getList()); + assertEquals(List.of("SECP256R1"), restored.getList()); + assertEquals(TestResults.TRUE, restored.getSummarizedResult()); + } + + @Test + void roundTripKeepsExplicitSummary() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + String json = mapper.writeValueAsString(summarizedAs(TestResults.CANNOT_BE_TESTED)); + + FaultListResult restored = mapper.readValue(json, FaultListResult.class); + + assertTrue(restored.isExplicitSummary()); + assertEquals(TestResults.CANNOT_BE_TESTED, restored.getSummarizedResult()); + assertNull(restored.getList()); + } + + @Test + void roundTripKeepsEmptyFaultList() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + String json = mapper.writeValueAsString(faultsFor(List.of())); + + FaultListResult restored = mapper.readValue(json, FaultListResult.class); + + assertNotNull(restored.getList()); + assertTrue(restored.getList().isEmpty()); + assertEquals(TestResults.FALSE, restored.getSummarizedResult()); + } +} diff --git a/src/test/java/de/rub/nds/scanner/core/report/rating/RatingInfluencerTest.java b/src/test/java/de/rub/nds/scanner/core/report/rating/RatingInfluencerTest.java new file mode 100644 index 00000000..84f1268f --- /dev/null +++ b/src/test/java/de/rub/nds/scanner/core/report/rating/RatingInfluencerTest.java @@ -0,0 +1,98 @@ +/* + * Scanner Core - A Modular Framework for Probe Definition, Execution, and Result Analysis. + * + * Copyright 2017-2023 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.scanner.core.report.rating; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import de.rub.nds.scanner.core.TestAnalyzedProperty; +import de.rub.nds.scanner.core.probe.result.FaultListResult; +import de.rub.nds.scanner.core.probe.result.ListResult; +import de.rub.nds.scanner.core.probe.result.TestResult; +import de.rub.nds.scanner.core.probe.result.TestResults; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class RatingInfluencerTest { + + private static final RatingInfluencer INFLUENCER = + new RatingInfluencer( + TestAnalyzedProperty.TEST_ANALYZED_PROPERTY, + new PropertyResultRatingInfluencer(TestResults.TRUE, -200), + new PropertyResultRatingInfluencer(TestResults.FALSE, 50), + new PropertyResultRatingInfluencer(TestResults.CANNOT_BE_TESTED, -10)); + + private static int influenceFor(TestResult result) { + return INFLUENCER.getPropertyRatingInfluencer(result).getInfluence(); + } + + private static FaultListResult faultsFor(List faultyFeatures) { + return new FaultListResult<>(TestAnalyzedProperty.TEST_ANALYZED_PROPERTY, faultyFeatures); + } + + @Test + void plainResultsAreMatched() { + assertEquals(-200, influenceFor(TestResults.TRUE)); + assertEquals(50, influenceFor(TestResults.FALSE)); + } + + @Test + void listedFaultsAreMatchedAsTrue() { + assertEquals(-200, influenceFor(faultsFor(List.of("SECP256R1")))); + } + + @Test + void absentFaultsAreMatchedAsFalse() { + assertEquals(50, influenceFor(faultsFor(List.of()))); + } + + @Test + void explicitSummaryIsMatched() { + assertEquals( + -10, + influenceFor( + new FaultListResult( + TestAnalyzedProperty.TEST_ANALYZED_PROPERTY, + TestResults.CANNOT_BE_TESTED))); + } + + @Test + void unconfiguredResultYieldsNeutralInfluence() { + assertEquals(0, influenceFor(TestResults.PARTIALLY)); + } + + @Test + void resultWithoutSummaryYieldsNeutralInfluence() { + // a plain ListResult cannot be compared to the TestResults of the configuration, which + // must not fail the rating of the report as a whole + assertEquals( + 0, + influenceFor( + new ListResult<>( + TestAnalyzedProperty.TEST_ANALYZED_PROPERTY, + List.of("SECP256R1")))); + } + + @Test + void faultsInfluenceTheScore() { + SiteReportRater rater = + new SiteReportRater( + new RatingInfluencers(new LinkedList<>(List.of(INFLUENCER))), + new Recommendations(List.of())); + + assertEquals( + -200, + rater.getScoreReport( + Map.of( + TestAnalyzedProperty.TEST_ANALYZED_PROPERTY, + faultsFor(List.of("SECP256R1")))) + .getScore()); + } +} diff --git a/src/test/java/de/rub/nds/scanner/core/report/rating/RecommendationTest.java b/src/test/java/de/rub/nds/scanner/core/report/rating/RecommendationTest.java new file mode 100644 index 00000000..1ed5d135 --- /dev/null +++ b/src/test/java/de/rub/nds/scanner/core/report/rating/RecommendationTest.java @@ -0,0 +1,61 @@ +/* + * Scanner Core - A Modular Framework for Probe Definition, Execution, and Result Analysis. + * + * Copyright 2017-2023 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.scanner.core.report.rating; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import de.rub.nds.scanner.core.TestAnalyzedProperty; +import de.rub.nds.scanner.core.probe.result.FaultListResult; +import de.rub.nds.scanner.core.probe.result.TestResult; +import de.rub.nds.scanner.core.probe.result.TestResults; +import java.util.List; +import org.junit.jupiter.api.Test; + +class RecommendationTest { + + private static final Recommendation RECOMMENDATION = + new Recommendation( + TestAnalyzedProperty.TEST_ANALYZED_PROPERTY, + List.of( + new PropertyResultRecommendation( + TestResults.TRUE, "A fault was observed", "Fix it"), + new PropertyResultRecommendation( + TestResults.FALSE, + "No fault was observed", + "Keep it that way"))); + + private static String statusFor(TestResult result) { + return RECOMMENDATION.getPropertyResultRecommendation(result).getShortDescription(); + } + + private static FaultListResult faultsFor(List faultyFeatures) { + return new FaultListResult<>(TestAnalyzedProperty.TEST_ANALYZED_PROPERTY, faultyFeatures); + } + + @Test + void plainResultsAreMatched() { + assertEquals("A fault was observed", statusFor(TestResults.TRUE)); + assertEquals("No fault was observed", statusFor(TestResults.FALSE)); + } + + @Test + void listedFaultsAreMatchedAsTrue() { + assertEquals("A fault was observed", statusFor(faultsFor(List.of("SECP256R1")))); + } + + @Test + void absentFaultsAreMatchedAsFalse() { + assertEquals("No fault was observed", statusFor(faultsFor(List.of()))); + } + + @Test + void unconfiguredResultYieldsDefaultRecommendation() { + assertEquals(Recommendation.NO_INFORMATION_FOUND, statusFor(TestResults.PARTIALLY)); + } +}